diff options
Diffstat (limited to 'panels/display')
31 files changed, 9330 insertions, 0 deletions
diff --git a/panels/display/TODO b/panels/display/TODO new file mode 100644 index 0000000..f09425c --- /dev/null +++ b/panels/display/TODO @@ -0,0 +1,837 @@ +Highlevel overview: + +Tablet rotation things +only when there is a tablet attached. + +Here is the OS X Display menu: + + Detect Displays + Turn on mirroring + -------------------------- + SyncMaster + - 1280 x 1024, 60 Hz, Millions + - 1344 x ... + -------------------------------- + Color LCD + - 1024 x 1024 ... + -------------------------- + Displays Preferences + + Color LCD means "laptop panel". + +- GTK+ work. + + Allow applications to be notified whenever monitors are added + or removed. Allow applications to get more detailed + information about the connected monitors. + + The main complication is that XRRGetScreenResources() is very + slow. We could call it only when the X server sends an event, + but it's not desirable to have every application freeze for + half a second. And certainly not desirable to have the X + server block for n * 0.5 seconds. + + With the X server work below we should be fine just calling + XRRGetScreenResources on startup and in response to events. + +- X server work: + + X server needs to poll for whether a monitor is plugged + in. Whenever it detects a change, it should do an EDID query, + and cache the resulting information. That way XRRGetScreenResources() + can be the speed of a normal roundtrip. It's desirable that + normal client requests can still be processed during the EDID + querying, but only a nice-to-have. + + Drivers need to work reliably. There could be substantial work + here. For F9, possibly only the Intel driver can be made to + work. + + Interrupts and events must be generated whenever something changes + about the outputs, if necessary by polling. + + Events must be emitted whenever something changes, including when + the reason for the change is a manual change. + + The maximum framebuffer must be dynamically changable. + +- Control panel work: + Capplet needs to be written. The main complications: + + - It needs to pay attention to events from the X server + and update itself, ie., add show new monitors if they become + available when the applet is shown. + + - It needs to store information under a key computed + from a monitor identifier. The complication here is that + it's not completely clear how to do this in GConf. + + - Would probably be worthwhile to drop libgnome/libgnomeui from + the craplets. + +- Metacity work: + - Metacity is already Xinerama aware, but it needs to update itself + when monitors come and go. + +- GNOME panel work: + - Is already Xinerama aware, but needs to listen and update itself + when monitors change. + +- Evince work: + - Make sure it deals sensibly with multiple monitors + +- OpenOffice work: + - Make sure it deals sensibly with multiple monitors + +- An Xlib call to just return all the available information would be + useful. At the moment we have to do a bunch of roundtrips to + get the information. This is a would-be-nice though. + +- A dbus service could be written that pops up the applet whenever a + monitor. It should only pop up if the new monitor is unknown. This + is at best a nice-to-have, and low priority in my opinion. + + +******************* Metacity + +Havoc: + +> I was just talking to bryan about this and "helping" him design it ;-) + +> But I wanted to be sure and lobby for a fix window managers +> need. Basically right now the WM can't tell "physical" from +> "logical" monitors. + +> A "logical" monitor is a desktop; it has its own panel, windows +> maximize to it, etc. + +> A "physical" monitor is a piece of hardware. + +> Sometimes people want to combine physical monitors into a video wall +> or just two monitors treated as one. Or at least a couple of noisy +> people in bugzilla want to do this. + +> When people talk about a "Xinerama aware" app or WM they usually +> mean that all physical monitors are treated as logical monitors, +> while lack of Xinerama-aware means treating the entire X screen (all +> physical monitors) as one logical monitor. + +> The problem is that the setting for "ignore Xinerama" or "don't be +> Xinerama aware" should be global to the desktop (GTK, all apps, WM) +> and should not be a window manager setting. + +> Bryan thought people who wanted non-Xinerama-aware should just use +> fvwm, which may be right, but what I'd say is that if there is any +> setting for this, it should be desktop-global and in this monitor +> config dialog. + +> It should not be a metacity or Compiz option, but in some way an X +> option in short. The implementation could be either an X server +> feature or an EWMH hint or whatever, but it should be controlled by +> the monitor config dialog and used by apps, GTK, etc. in addition to +> used by the WM. + +> People tend to insist this should be a WM option, but that's just +> busted, since GTK and apps also have Xinerama-awareness features. + + +******************* EDID + +edid-decode enhancements: + +- Rejects years <= 0x0f for all versions, but this should only be done + for monitors claiming conformance to 1.4 (since 1.4 was released in + 2006). A monitor produced in 2005 should have 0x0f - it's the only + reasonable thing to do. + +- Uses 0x80 as the conformance mask for 1.4, should be 0 + +- Should read from stdin + +- Should parse xrandr -verbose output more robustly + +- Color depth computation is wrong. It uses the formula + + (edid[0x14] >> 3) + 2 + + The correct formula to use is + + (edid[0x14] & 0x70) >> 3 + 4 + +- + +-=-=-=- +Computing a display name from EDID information: + + vendor = lookup_vendor (code); + + if (dsc_product && !is_gobbledigook (dsc_product)) + { + if (vendor && !fuzzy_string_search (vendor, dsc_product)) + prepend (vendor); + } + else + { + if (vendor) + append (vendor); + else + append ("Unknown"); + } + + if (has size) + { + convert_to_inches() + + append (" %d\"", inches) + } + +(Does this internationalize at all)? + +We also need the ability to get laptop names. The laptop panel may report +a manufacturer that has nothing to do with the laptop manufacturer. + +Needed XRandr output properties: + +- Modes that the monitor supports, or enough information that the + client can go throught the list of modes for the relevant + CRTC/Outputs and filter those out that the monitor can't support. + +- The preferred mode, if any. Also useful if we could get a "strongly + preferred" indication if it's an LCD with a fixed resolution. + +- Sufficient information that a fairly specific identifier can be + computed. The algorithm the client should use is: + + 1 Have we seen exactly this monitor before? If yes, use + settings for that. + + 2 Have we seen a monitor with similar specs before? If yes, + use settings for that. (But don't save, unless the user + changes the settings). + + 3 Otherwise, use some reasonable default for the monitor and + save it. + + A setting should only be used if the CRTC/Output allows it. Ie,. if + a user has installed a new video card, then previously-used settings + may no longer apply, so this must be checked every time. + + (1) Implies that we really need a globally unique identifier for + monitors. (2) is useful in an enterprise setting, but not absolutely + critical, since (3) would still handle the majority of cases. + + There is a question here: Where are machine specific preferences + stored? Havoc mentions three possibilities here: + + http://mail.gnome.org/archives/gnomecc-list/2001-October/msg00023.html + + I'm not sure if any of them are implementable at this point. Also + (1) may mostly take care of the problem. + + + Usecases: + + 1. Fixed setup with some number of monitors. + - They should be set to the correct mode on login. + Note that this involves setting the right position in the + framebuffer too. + + What if someone swaps two monitors? Users are going to expect + that the images will switch position. + + 2. Laptop being moved between home and work + - Setups should be detected and the correct mode set, at least on + login, but ideally when you put the laptop into the docking + station. + + 3. Laptop gets projector plugged in. + + Note the same model monitor can be used in two different ways. Ie., + at home, it's being used at one resolution, at work the same type of + monitor is used at a different resolution. + + Simple solution: + + - The on-disk database is just a list of monitors. Each monitor has an + associated mode. This has these problems: + - If someone uses the same monitor model in two different ways. + - If someone swaps the monitors around + + Better solution + + - The on-disk database is a list of configurations, where a + configuration is a list of monitors and what outputs they are + connected to, and the position in the framebuffer. + + - Picking a default configuration is then a matter of selecting the + closest existing configuration from the database. + + - If the stored configuration is a subset of the existing, + then use that - then pick the best mode available for the + rest of the monitors + + - If the stored configuration is a superset of the existing, + then use the projection of the configuration onto the monitors. + + - Pick the configuration with the most overlap in monitors. + Although, if a configuration differs only in what outputs + they are connected to, then those outputs should probably + get their original modes set. + + - Or maybe simply: + + - If there is an exact match, use it, if not, pick a default. + + - Picking a new default must never change the mode of any existing + output. + +******************* Capplet + +Somehow the applet will find out that a new monitor is plugged in +(either through notification, or through a refresh button). When this +happens, this monitor is looked up in a database and if it is found, +some suitable mode is set. + +Restrictions on the modes: + +- Monitors that are already plugged in should not get their mode + changed just because a new monitor is plugged in. + +- If the exact configuration of monitors is known, and all the old + monitors have the same mode as the known configuration, then just use + the known configuration. Also do this, if the configuration is a + subset of something known. + +- Otherwise, if the configuration is a subset of a known configuration + where the only difference is that existing monitors have different + modes, then try and convert that mode to something we can know + about. Maybe configurations should be stored in terms of edges that + line up. + +- Otherwise, just pick some good default for the mode, probably based + on the EDID prferred mode if possible. By default cloning is + probably best. + +- How do virtual desktops interact with this? + + +g-s-d: + +- On startup + + - It reads the configuration file into memory + + capplet --configure + + - It gathers the existing configuration from randr + + - If the existing config is in the file, set that mode + +- On changes, including changes to the config file [this is crack] + + - Reread configuration file + + - Compare new configuration to database, if it is there, set the + mode as appropriate + + - If a monitor was added, pop up a bubble + + capplet --show-bubble + + capplet --set-mode + +capplet + +- On changes + + - Update GUI + +- When user changes something, + + - Write configuration to file + + - Signal gsd somehow + +Schemes: + - configuration file changes + - randr code will have to be shared between gcc and gsd + + - binary installed by gcc + - something will still have to listen for changes to pop + up the notification bubble. + +Structure of capplet: + +- There is a database on disk with monitors and their corresponding + settings. + +- On startup, this database is read into memory. When the user accepts + new settings, it is written back to disk. + +- When something changes about the settings + + - If new configuration is in the database, use that mode + + - Else, find all outputs that are now connected but weren't before, + and set a default mode for them. + + - If GUI is running, update graphics. + + + - Notification thing: + - if + + - if the new configuration is found in the database, use it + + and added if they are not already there. Initial settings are + 1 what the output is already doing, if anything + 2 based on an existing sufficiently similar monitor, if possible + 3 some reasonable default. + +- When the user changes settings in the GUI, the corresponding monitor + in the database is updated. + +- Whenever the GUI settings change, for all displayed monitors the + possible modes are recomputed. + +- Whenever a new monitor is selected in the GUI, it first gets all its + possible modes computed based on the selections on other + outputs. Then, if the possible modes include the existing choice of + resolution, that is selected. + + Actually, + + - initially, the settings are copied from the current settings + + - whenever a gui setting changes for a monitor, all the other + monitors get their list of choices set to whatever is possible + given the chocie for the current monitor. A 'desired mode' is + maintained, and the closest choice to that is displayed. Whenever + the user actively selects something, that becomes the desired mode + for that monitor. + +- Required + + - Generate all outputs that are newly connected + + foreach_newly_connected (Configuration *before, Configuration *after, + OutputFunc); + + - A way to generate the best mode for a connected output + + existing best_mode() can probably be used + + - Given a list of modes, pick the one closest to a given mode. + + (a possibility here is: pick an exact match, if that's + impossible, then pick the best one with the same + width/height, if that's impossible, then just pick the + best mode on the list). + + - For a configuation, fix the mode for a subset of the outputs, then + list the combinations for the rest of the outputs. + + An obvious possibility here is to simply list all possibilities, + then weed out those that don't work. Is this too expensive? + It might be. + +Structure of login time program: + +- The configuration database is read + +- The current hardware configuration is generated + +- If the current configuration is found in the database, that mode is set. + +- If it isn't found, then nothing changes. + + This could just be gnome-screen-resolution-capplet --reset + +******************* Things that need to be done to the xrandr.patch: + +=== + +XRRGetScreenResources() is a roundtrip and very slow (~0.5 s). GTK+ +needs to keep information up-to-date by tracking events rather than +calling this function. In fact we probably can't call it at all unless +its performance improves significantly. + +If EDID processing really has to be this slow, and we can't get +interrupts when monitors are plugged in, then we have a problem, +because we can't do anything this expensive once per second. + + +Detailed notes (but most of the patch should be rewritten): + + +=== FIXME in gdkscreen-x11.c in get_width_mm() + +/* monitor pixel width / screen pixel width * screen_physical width */ + + + +=== Check for 1.2 library + +The patch should check that the 1.2 version of the XRandR library is +available before using the functions. A possibility is to not use any +RandR unless 1.2 is available, another is to conditionalize the code. + +The most sane thing is probably to just require 1.2. + +On the other hand, installing a newer gtk+ on a system with older X is +probably not that unusual, so maybe it's better to do the full 1.0, +vs. 1.1 vs 1.2 check. + +For now it just requires 1.2. + +Actually, this might be fine because the only place where we make use +of a 1.1 library is in the _gdk_x11_screen_size_changed() function, +but there we have a fallback that just updates the variables in the +Screen struct itself. + +So, only defining HAVE_RANDR if we detect 1.2 should be ok. + +=== Monitor information available + +- Subpixel information. This should be set automatically for the fonts and + store under the name of the monitor. If the user changes the font + configuration, that change should also be stored under the monitor name. + +- When a monitor we don't know about is plugged in, a configuration should + be generated: + + - Screen size, computed based on the location of the screens + + - RGBA information + + - Whether the screen has a panel on it + + - If there is a conflict between stored information and EDID, + the stored information wins + + + +New API so far: + +(* monitors_changed) signal +gdk_screen_get_monitor_width_mm() +gdk_screen_get_monitor_height_mm() +gdk_screen_get_monitor_name() => Note this is the output (eg. "DVI-0") + +We should probably also have +get_manufacturer() +get_serial() +get_resolutions() + +etc. + +Should there be a GdkMonitor object that would correspond to an +output? Or maybe GdkOutput? + +screen_list_monitors() + + +*************************** Issues XRandR/Xserver + +- We need polling in the X server, whenever something changes, X must + recompute the information and cache it, then send an event. Note the + situation where the user disconnects and reconnects a monitor within + the polling interval. The event could missed in that case since the polling + cannot do a full EDID query. Difficult to see a way around this. + + Actually, DDC allows random access, so it should be possible to just + read theq vendor id and manufacturer codes. This can be done once a + second without a problem. The polling should be turned off in power + saving mode anyway. + + - Driver work: + + - Intel driver: + + - EDID information is not reported for VGA when the output is not + turned on (i945 laptop). + + - Screen size must be dynamically changable. (No xorg.conf changes + should be required). + + - Make use of ACPI information when possible. + + Adam has code on his freedesktop page. + + - i830 laptop can be put in a state where XRandr reports that no + outputs are connected to a CRTC, but the panel is on. + + - Plug in VGA + - xrandr --auto + - xrandr --output VGA --off + - run chk + - xrandr --verbose will now not report any outputs as turned on + - run chk again - all screens will be turned off + + - Small Sun monitor - an 1152x921 mode is generated, but the + monitor doesn't handle that. The monitor itself only claims to + handle 1152x920. It doesn't look to me like there is anything + in the EDID information that would indicate that it could handle + 1152x921. + + This happens with a radeon as wellso it may be a bug in the + generic X server EDID parsing. The X server apparently + interpretes the standard timing 1152x920 as 1152x921. + + This happens because the X server uses + + hsize * 4 / 5 + + which gives 921 for 1152. By using + + (hsize / 5) * 4 + + you get 920. The 66 Hz version can bet set, the 76 Hz mode gets + sync out of range. (Would be interesting to find out whether the + 1152x920 ModeLine would allow the 76 Hz version to be set). + + This is for the ATI driver as shipped in F8: + + - XRRGetScreenResources() takes half a second. + + - Adam has now removed a workaround that caused some of the slowdown. + + - If a DVI monitor is disconnected, you get "Unknown" for connection + status. + + - If a VGA monitor is plugged in, then EDID information is not + available, even after running xrandr --verbose. The monitor has + to be plugged in at driver startup time, apparently. + + - Logging out and logging back in often results in some random mode being + set. We need mode selection to not be completely screwed up. + Currently it is. + + - The set up at server startup needs to be fixed. *If* randr actually works, + then we might be able to do something sensible. + + - We need to revisit the idea that many monitors have broken EDID data. + This may be less widespread than previously believed. + +- It may be useful to return the connector names as identifiers instead + of relying on UTF-8 strings. Ie., have an enum + + { UNKNOWN, OTHER, DVI, VGA, HDMI, ..., } + + in addition to the string. The difference between UNKNOWN and OTHER is that + UNKNOWN means the driver doesn't know, whereas OTHER means it is something + not listed in the enum (which could be listed in a later version). + +- Mouse cursor should be confined to the visible area. (It is already, I think) + +- It looks like EDID information is only available for one output + even though it is actually read according to the log file. + (nv, intel drivers) + + +********************************* + + + +DONE: + +Server work: + + - i830 laptop incorrectly reports BadMatch when you configure the + CRTC to drive both VGA and LVDS with the 1024x768 mode that both + outputs can handle. (It should return 'failed' if it can't do + that). Same for i945 laptop. It seems as if the same CRTC can't + drive more than one output at the same time on Intel. + + This was a client bug, but the documentation for SetCrtcConfig + should say that BadMatch will be returned if the outputs aren't + clones. + +GTK+ patch is in now. + +=== Add helper function + + ++ if (screen_x11->randr12) ++ { ++ XRRScreenResources *sr; ++ XRROutputInfo *output; ++ gchar *retval; ++ ++ sr = XRRGetScreenResources ( screen_x11->xdisplay, ++ screen_x11->xroot_window ); ++ ++ output = XRRGetOutputInfo ( screen_x11->xdisplay, ++ sr, ++ (RROutput)screen_x11->act_outputs[monitor_num] +); + + Might be worthwhile to factor this out into a + gdk_screen_get_output_info (screen, monitor_num) + helper function ? + +Instead of cutting and pasting all over creation + +* Calling XRRGetScreenResources all the time is not going to fly. It + takes hundreds of milliseconds ... Even if it didn't, it wouldn't + be acceptable to do all those roundtrips. + + +=== Some g_prints left + + +=== Version check + +Should be (maj > 1) || (maj == 1 && min >= 2) + + +=== Grep for TODO + + +=== Setup XRRSelectInput() + + You should call XRRSelectInput() at the same place where you are + calling XSelectInput() right now. The right place to handle the + XRandr events is the huge switch in gdkevents-x11.c:gdk_event_translate + Check out how other extension events are handled there, like + XKB, or XFixes. + + +=== Lots of variable naming issues, such as act_output and noutput + +=== Needs to select the input, and hook it up to the signal + +=== Add version markers to API + +=== API to turn monitors on and off? + +- DPMS not exposed through randr, maybe should be + + - DPMS is presumably a property of either an + output or a CRTC. Logically it's an output. + +- Need events when DPMS happens. Exposing the "screen saving on" on + dbus may not be good enough. + +=== Why does init_multihead_support() start by freeing monitors and +outputs? + +=== Do we disable Xinerama support entirely when 1.2 is in use? + +=== We should expose information about what parts of the screen monitors +are viewing. + +=== Make use of the EDID information? + + +-- details for X server -- + +In nv driver SorSetOutputProperty should return TRUE for unknown +properties. (Like the Intel driver does). + +Detecting plugged in + +- Periodically poll + - + + - One ddc probe takes 5 ms, according to a comment in the intel + driver. Running this twice a second would mean spending 1% of + overall time doing ddc polling, which is almost certainly not + acceptable. + + 1) Async I2C: + + void I2CProbeAsync(..., callback, data); + Bool I2CPending() + void I2CUpdate() + + In Dispatch, call I2CUpdate() + Before going idle, do + + while (I2CPending()) + I2CUpdate() + + Would need + RegisterDispatchFunction() (Is this called Wakeup?) + RegisterIdleFunction() + + Note the idle function should have the option of saying: + "check if something else happened; if not, call me again" and + "ok, I'm done - go idle". Otherwise, we would be blocking for + 5 ms whenever the X server went idle. So actually the idle + function should be + + if (I2CPending()) + { + I2CUpdate(); + return TRUE; /* call me again */ + } + else + { + return FALSE; /* I'm done */ + } + + What happens if another I2C requests come in while an async one + is pending? Most likely we simply finish whatever is going on, + then process the new request. + + What happens if an X request takes so long that we get timeouts on + the i2c bus? Good question. Need to read the VESA ddc spec. + + 2) Run the polling in a separate thread. + + Probably crack. + + 3) Run the polling less, maybe once every three seconds. + +-- details for control panel -- +Screen changes + - Currently it is polling via rw_screen_refresh(), which will always emit + a screen-changed event. In reponse to this event the capplet currently + checks whether anything changed physically about the setup. This means + the capplet can't react to external changes to modes. On the other hand + if it didn't +Disallow combinations that would exceed the screen ranges. + - Note rotations + +Give rw objects stable positions in memory so that they can be cached +across screen_changed events. + +Add Clone Mode + +Drag and drop for the monitors + - 2 dimensional layout + +Store make and model in monitors.xml, then if serial numbers don't +match, fall back to a make and model match. Users with an nfs mounted +home directory should not have to reconfigure for each new system they +log in to. + +Make sure text is scaled correctly + +Need to sanitize naming + RWOutput vs Output - should probably be OutputInfo + rate vs. freq - decide on one + +Should probably reconsider the use of null terminated arrays. +Maybe lists would be better. + +Pick a fixed scale, so that two 1024x768 don't look like two 6x4. + - An alternative would be to draw a checkerboard pattern + below the monitors. + + + +done: + +Add rotation + +Disable panel checkbox for now + +Patch into gnome-desktop + +Find out how to share code between gcc and gsd + +Make it assign coordinates correctly + - including computing correct screen size + diff --git a/panels/display/cc-display-arrangement.c b/panels/display/cc-display-arrangement.c new file mode 100644 index 0000000..adbbcbc --- /dev/null +++ b/panels/display/cc-display-arrangement.c @@ -0,0 +1,988 @@ +/* cc-display-arrangement.c + * + * Copyright (C) 2007, 2008, 2017 Red Hat, Inc. + * Copyright (C) 2013 Intel, Inc. + * + * Written by: Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include <math.h> +#include "cc-display-arrangement.h" +#include "cc-display-config.h" + +struct _CcDisplayArrangement +{ + GtkDrawingArea object; + + CcDisplayConfig *config; + + cairo_matrix_t to_widget; + cairo_matrix_t to_actual; + + gboolean drag_active; + CcDisplayMonitor *selected_output; + CcDisplayMonitor *prelit_output; + /* Starting position of cursor inside the monitor. */ + gdouble drag_anchor_x; + gdouble drag_anchor_y; + + guint major_snap_distance; +}; + +typedef struct _CcDisplayArrangement CcDisplayArrangement; + +enum { + PROP_0, + PROP_CONFIG, + PROP_SELECTED_OUTPUT, + PROP_LAST +}; + +typedef enum { + SNAP_DIR_NONE = 0, + SNAP_DIR_X = 1 << 0, + SNAP_DIR_Y = 1 << 1, + SNAP_DIR_BOTH = (SNAP_DIR_X | SNAP_DIR_Y), +} SnapDirection; + +typedef struct { + cairo_matrix_t to_widget; + guint major_snap_distance; + gdouble dist_x; + gdouble dist_y; + gint mon_x; + gint mon_y; + SnapDirection snapped; +} SnapData; + +#define MARGIN_PX 0 +#define MARGIN_MON 0.66 +#define MAJOR_SNAP_DISTANCE 25 +#define MINOR_SNAP_DISTANCE 5 +#define MIN_OVERLAP 25 + +G_DEFINE_TYPE (CcDisplayArrangement, cc_display_arrangement, GTK_TYPE_DRAWING_AREA) + +static GParamSpec *props[PROP_LAST]; + +static void +apply_rotation_to_geometry (CcDisplayMonitor *output, + int *w, + int *h) +{ + CcDisplayRotation rotation; + + rotation = cc_display_monitor_get_rotation (output); + if ((rotation == CC_DISPLAY_ROTATION_90) || (rotation == CC_DISPLAY_ROTATION_270)) + { + int tmp; + tmp = *h; + *h = *w; + *w = tmp; + } +} + +/* get_geometry */ +static void +get_scaled_geometry (CcDisplayConfig *config, + CcDisplayMonitor *output, + int *x, + int *y, + int *w, + int *h) +{ + if (cc_display_monitor_is_active (output)) + { + cc_display_monitor_get_geometry (output, x, y, w, h); + } + else + { + cc_display_monitor_get_geometry (output, x, y, NULL, NULL); + cc_display_mode_get_resolution (cc_display_monitor_get_preferred_mode (output), w, h); + } + + if (cc_display_config_is_layout_logical (config)) + { + double scale = cc_display_monitor_get_scale (output); + *w = round (*w / scale); + *h = round (*h / scale); + } + + apply_rotation_to_geometry (output, w, h); +} + +static void +get_bounding_box (CcDisplayConfig *config, + gint *x1, + gint *y1, + gint *x2, + gint *y2, + gint *max_w, + gint *max_h) +{ + GList *outputs, *l; + + g_assert (x1 && y1 && x2 && y2); + + *x1 = *y1 = G_MAXINT; + *x2 = *y2 = G_MININT; + *max_w = 0; + *max_h = 0; + + outputs = cc_display_config_get_monitors (config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + int x, y, w, h; + + if (!cc_display_monitor_is_useful (output)) + continue; + + get_scaled_geometry (config, output, &x, &y, &w, &h); + + *x1 = MIN (*x1, x); + *y1 = MIN (*y1, y); + *x2 = MAX (*x2, x + w); + *y2 = MAX (*y2, y + h); + *max_w = MAX (*max_w, w); + *max_h = MAX (*max_h, h); + } +} + +static void +monitor_get_drawing_rect (CcDisplayArrangement *self, + CcDisplayMonitor *output, + gint *x1, + gint *y1, + gint *x2, + gint *y2) +{ + gdouble x, y; + + get_scaled_geometry (self->config, output, x1, y1, x2, y2); + + /* get_scaled_geometry returns the width and height */ + *x2 = *x1 + *x2; + *y2 = *y1 + *y2; + + x = *x1; y = *y1; + cairo_matrix_transform_point (&self->to_widget, &x, &y); + *x1 = round (x); + *y1 = round (y); + + x = *x2; y = *y2; + cairo_matrix_transform_point (&self->to_widget, &x, &y); + *x2 = round (x); + *y2 = round (y); +} + + +static void +get_snap_distance (SnapData *snap_data, + gint mon_x, + gint mon_y, + gint new_x, + gint new_y, + gdouble *dist_x, + gdouble *dist_y) +{ + gdouble local_dist_x, local_dist_y; + + local_dist_x = ABS (mon_x - new_x); + local_dist_y = ABS (mon_y - new_y); + + cairo_matrix_transform_distance (&snap_data->to_widget, &local_dist_x, &local_dist_y); + + if (dist_x) + *dist_x = local_dist_x; + if (dist_y) + *dist_y = local_dist_y; +} + +static void +maybe_update_snap (SnapData *snap_data, + gint mon_x, + gint mon_y, + gint new_x, + gint new_y, + SnapDirection snapped, + SnapDirection major_axis, + gint minor_unlimited) +{ + SnapDirection update_snap = SNAP_DIR_NONE; + gdouble dist_x, dist_y; + gdouble dist; + + get_snap_distance (snap_data, mon_x, mon_y, new_x, new_y, &dist_x, &dist_y); + dist = MAX (dist_x, dist_y); + + /* Snap by the variable max snap distance on the major axis, ensure the + * minor axis is below the minimum snapping distance (often just zero). */ + switch (major_axis) + { + case SNAP_DIR_X: + if (dist_x > snap_data->major_snap_distance) + return; + if (dist_y > MINOR_SNAP_DISTANCE) + { + if (new_y > mon_y && minor_unlimited <= 0) + return; + if (new_y < mon_y && minor_unlimited >= 0) + return; + } + break; + + case SNAP_DIR_Y: + if (dist_y > snap_data->major_snap_distance) + return; + if (dist_x > MINOR_SNAP_DISTANCE) + { + if (new_x > mon_x && minor_unlimited <= 0) + return; + if (new_x < mon_x && minor_unlimited >= 0) + return; + } + break; + + default: + g_assert_not_reached(); + } + + if (snapped == SNAP_DIR_BOTH) + { + if (snap_data->snapped == SNAP_DIR_NONE) + update_snap = SNAP_DIR_BOTH; + + /* Update, if this is closer on the main axis. */ + if (((major_axis == SNAP_DIR_X) && (dist_x < snap_data->dist_x)) || + ((major_axis == SNAP_DIR_Y) && (dist_y < snap_data->dist_y))) + { + update_snap = SNAP_DIR_BOTH; + } + + /* Also update if we were only snapping in one direction earlier and it + * is better or equally good. */ + if ((snap_data->snapped == SNAP_DIR_X && (dist <= snap_data->dist_x)) || + (snap_data->snapped == SNAP_DIR_Y && (dist <= snap_data->dist_y))) + { + update_snap = SNAP_DIR_BOTH; + } + + /* Also allow a minor axis to be added if the first axis remains identical. */ + if (((snap_data->snapped == SNAP_DIR_X) && (major_axis == SNAP_DIR_X) && (new_x == snap_data->mon_x)) || + ((snap_data->snapped == SNAP_DIR_Y) && (major_axis == SNAP_DIR_Y) && (new_y == snap_data->mon_y))) + { + update_snap = SNAP_DIR_BOTH; + } + } + else if (snapped == SNAP_DIR_X) + { + if (dist_x < snap_data->dist_x || (snap_data->snapped & SNAP_DIR_X) == SNAP_DIR_NONE) + update_snap = SNAP_DIR_X; + } + else if (snapped == SNAP_DIR_Y) + { + if (dist_y < snap_data->dist_y || (snap_data->snapped & SNAP_DIR_Y) == SNAP_DIR_NONE) + update_snap = SNAP_DIR_Y; + } + else + { + g_assert_not_reached (); + } + + if (update_snap & SNAP_DIR_X) + { + snap_data->dist_x = dist_x; + snap_data->mon_x = new_x; + snap_data->snapped = snap_data->snapped | SNAP_DIR_X; + } + if (update_snap & SNAP_DIR_Y) + { + snap_data->dist_y = dist_y; + snap_data->mon_y = new_y; + snap_data->snapped = snap_data->snapped | SNAP_DIR_Y; + } +} + +static void +find_best_snapping (CcDisplayConfig *config, + CcDisplayMonitor *snap_output, + SnapData *snap_data) +{ + GList *outputs, *l; + gint x1, y1, x2, y2; + gint w, h; + + g_assert (snap_data != NULL); + + get_scaled_geometry (config, snap_output, &x1, &y1, &w, &h); + x2 = x1 + w; + y2 = y1 + h; + +#define OVERLAP(_s1, _s2, _t1, _t2) ((_s1) <= (_t2) && (_t1) <= (_s2)) + + outputs = cc_display_config_get_monitors (config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + gint _x1, _y1, _x2, _y2, _h, _w; + gint bottom_snap_pos; + gint top_snap_pos; + gint left_snap_pos; + gint right_snap_pos; + gdouble dist_x, dist_y; + gdouble tmp; + + if (output == snap_output) + continue; + + if (!cc_display_monitor_is_useful (output)) + continue; + + get_scaled_geometry (config, output, &_x1, &_y1, &_w, &_h); + _x2 = _x1 + _w; + _y2 = _y1 + _h; + + top_snap_pos = _y1 - h; + bottom_snap_pos = _y2; + left_snap_pos = _x1 - w; + right_snap_pos = _x2; + + dist_y = 9999; + /* overlap on the X axis */ + if (OVERLAP (x1, x2, _x1, _x2)) + { + get_snap_distance (snap_data, x1, y1, x1, top_snap_pos, NULL, &dist_y); + get_snap_distance (snap_data, x1, y1, x1, bottom_snap_pos, NULL, &tmp); + dist_y = MIN(dist_y, tmp); + } + + dist_x = 9999; + /* overlap on the Y axis */ + if (OVERLAP (y1, y2, _y1, _y2)) + { + get_snap_distance (snap_data, x1, y1, left_snap_pos, y1, &dist_x, NULL); + get_snap_distance (snap_data, x1, y1, right_snap_pos, y1, &tmp, NULL); + dist_x = MIN(dist_x, tmp); + } + + /* We only snap horizontally or vertically to an edge of the same monitor */ + if (dist_y < dist_x) + { + maybe_update_snap (snap_data, x1, y1, x1, top_snap_pos, SNAP_DIR_Y, SNAP_DIR_Y, 0); + maybe_update_snap (snap_data, x1, y1, x1, bottom_snap_pos, SNAP_DIR_Y, SNAP_DIR_Y, 0); + } + else if (dist_x < 9999) + { + maybe_update_snap (snap_data, x1, y1, left_snap_pos, y1, SNAP_DIR_X, SNAP_DIR_X, 0); + maybe_update_snap (snap_data, x1, y1, right_snap_pos, y1, SNAP_DIR_X, SNAP_DIR_X, 0); + } + + /* Left/right edge identical on the top */ + maybe_update_snap (snap_data, x1, y1, _x1, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0); + maybe_update_snap (snap_data, x1, y1, _x2 - w, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0); + + /* Left/right edge identical on the bottom */ + maybe_update_snap (snap_data, x1, y1, _x1, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0); + maybe_update_snap (snap_data, x1, y1, _x2 - w, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 0); + + /* Top/bottom edge identical on the left */ + maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y1, SNAP_DIR_BOTH, SNAP_DIR_X, 0); + maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y2 - h, SNAP_DIR_BOTH, SNAP_DIR_X, 0); + + /* Top/bottom edge identical on the right */ + maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y1, SNAP_DIR_BOTH, SNAP_DIR_X, 0); + maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y2 - h, SNAP_DIR_BOTH, SNAP_DIR_X, 0); + + /* If snapping is infinite, then add snapping points with minimal overlap + * to prevent detachment. + * This is similar to the above but simply re-defines the snapping pos + * to have only minimal overlap */ + if (snap_data->major_snap_distance == G_MAXUINT) + { + /* Hanging over the left/right edge on the top */ + maybe_update_snap (snap_data, x1, y1, _x1 - w + MIN_OVERLAP, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 1); + maybe_update_snap (snap_data, x1, y1, _x2 - MIN_OVERLAP, top_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, -1); + + /* Left/right edge identical on the bottom */ + maybe_update_snap (snap_data, x1, y1, _x1 - w + MIN_OVERLAP, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, 1); + maybe_update_snap (snap_data, x1, y1, _x2 - MIN_OVERLAP, bottom_snap_pos, SNAP_DIR_BOTH, SNAP_DIR_Y, -1); + + /* Top/bottom edge identical on the left */ + maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y1 - h + MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, 1); + maybe_update_snap (snap_data, x1, y1, left_snap_pos, _y2 - MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, -1); + + /* Top/bottom edge identical on the right */ + maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y1 - h + MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, 1); + maybe_update_snap (snap_data, x1, y1, right_snap_pos, _y2 - MIN_OVERLAP, SNAP_DIR_BOTH, SNAP_DIR_X, -1); + } + } + +#undef OVERLAP +} + +static void +cc_display_arrangement_update_matrices (CcDisplayArrangement *self) +{ + GtkAllocation allocation; + gdouble scale, scale_h, scale_w; + gint x1, y1, x2, y2, max_w, max_h; + + g_assert (self->config); + + /* Do not update the matrices while the user is dragging things around. */ + if (self->drag_active) + return; + + get_bounding_box (self->config, &x1, &y1, &x2, &y2, &max_w, &max_h); + gtk_widget_get_allocation (GTK_WIDGET (self), &allocation); + + scale_h = (gdouble) (allocation.width - 2 * MARGIN_PX) / (x2 - x1 + max_w * 2 * MARGIN_MON); + scale_w = (gdouble) (allocation.height - 2 * MARGIN_PX) / (y2 - y1 + max_h * 2 * MARGIN_MON); + + scale = MIN (scale_h, scale_w); + + cairo_matrix_init_identity (&self->to_widget); + cairo_matrix_translate (&self->to_widget, allocation.width / 2.0, allocation.height / 2.0); + cairo_matrix_scale (&self->to_widget, scale, scale); + cairo_matrix_translate (&self->to_widget, - (x1 + x2) / 2.0, - (y1 + y2) / 2.0); + + self->to_actual = self->to_widget; + cairo_matrix_invert (&self->to_actual); +} + +static CcDisplayMonitor* +cc_display_arrangement_find_monitor_at (CcDisplayArrangement *self, + gint x, + gint y) +{ + g_autoptr(GList) outputs = NULL; + GList *l; + + outputs = g_list_copy (cc_display_config_get_monitors (self->config)); + + if (self->selected_output) + outputs = g_list_prepend (outputs, self->selected_output); + + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + gint x1, y1, x2, y2; + + if (!cc_display_monitor_is_useful (output)) + continue; + + monitor_get_drawing_rect (self, output, &x1, &y1, &x2, &y2); + + if (x >= x1 && x <= x2 && y >= y1 && y <= y2) + return output; + } + + return NULL; +} + +static void +cc_display_arrangement_update_cursor (CcDisplayArrangement *self, + gboolean dragable) +{ + g_autoptr(GdkCursor) cursor = NULL; + GdkWindow *window; + + if (dragable) + cursor = gdk_cursor_new_for_display (gtk_widget_get_display (GTK_WIDGET (self)), GDK_FLEUR); + else + cursor = NULL; + + window = gtk_widget_get_window (GTK_WIDGET (self)); + + if (window) + gdk_window_set_cursor (window, cursor); +} + +static void +on_output_changed_cb (CcDisplayArrangement *self, + CcDisplayMonitor *output) +{ + if (cc_display_config_count_useful_monitors (self->config) > 2) + self->major_snap_distance = MAJOR_SNAP_DISTANCE; + else + self->major_snap_distance = G_MAXUINT; + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static gboolean +cc_display_arrangement_draw (GtkWidget *widget, + cairo_t *cr) +{ + CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget); + GtkStyleContext *context = gtk_widget_get_style_context (widget); + g_autoptr(GList) outputs = NULL; + GList *l; + + if (!self->config) + return FALSE; + + cc_display_arrangement_update_matrices (self); + + gtk_style_context_save (context); + gtk_style_context_add_class (context, "display-arrangement"); + + /* Draw in reverse order so that hit detection matches visual. Also pull + * the selected output to the back. */ + outputs = g_list_copy (cc_display_config_get_monitors (self->config)); + outputs = g_list_remove (outputs, self->selected_output); + if (self->selected_output != NULL) + outputs = g_list_prepend (outputs, self->selected_output); + outputs = g_list_reverse (outputs); + + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + GtkStateFlags state = GTK_STATE_FLAG_NORMAL; + GtkBorder border, padding, margin; + gint x1, y1, x2, y2; + gint w, h; + gint num; + + if (!cc_display_monitor_is_useful (output)) + continue; + + gtk_style_context_save (context); + cairo_save (cr); + + gtk_style_context_add_class (context, "monitor"); + + if (output == self->selected_output) + state |= GTK_STATE_FLAG_SELECTED; + if (output == self->prelit_output) + state |= GTK_STATE_FLAG_PRELIGHT; + + gtk_style_context_set_state (context, state); + if (cc_display_monitor_is_primary (output) || cc_display_config_is_cloning (self->config)) + gtk_style_context_add_class (context, "primary"); + + /* Set in cc-display-panel.c */ + num = cc_display_monitor_get_ui_number (output); + + monitor_get_drawing_rect (self, output, &x1, &y1, &x2, &y2); + w = x2 - x1; + h = y2 - y1; + + cairo_translate (cr, x1, y1); + + gtk_style_context_get_margin (context, state, &margin); + + cairo_translate (cr, margin.left, margin.top); + + w -= margin.left + margin.right; + h -= margin.top + margin.bottom; + + gtk_render_background (context, cr, 0, 0, w, h); + gtk_render_frame (context, cr, 0, 0, w, h); + + gtk_style_context_get_border (context, state, &border); + gtk_style_context_get_padding (context, state, &padding); + + w -= border.left + border.right + padding.left + padding.right; + h -= border.top + border.bottom + padding.top + padding.bottom; + + cairo_translate (cr, border.left + padding.left, border.top + padding.top); + + if (num > 0) + { + PangoLayout *layout; + PangoFontDescription *font = NULL; + g_autofree gchar *number_str = NULL; + PangoRectangle extents; + GdkRGBA color; + gdouble text_width, text_padding; + + gtk_style_context_add_class (context, "monitor-label"); + gtk_style_context_remove_class (context, "monitor"); + + gtk_style_context_get_border (context, state, &border); + gtk_style_context_get_padding (context, state, &padding); + gtk_style_context_get_margin (context, state, &margin); + + cairo_translate (cr, margin.left, margin.top); + + number_str = g_strdup_printf ("%d", num); + gtk_style_context_get (context, state, "font", &font, NULL); + layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), number_str); + pango_layout_set_font_description (layout, font); + pango_layout_get_extents (layout, NULL, &extents); + + h = (extents.height - extents.y) / PANGO_SCALE; + text_width = (extents.width - extents.x) / PANGO_SCALE; + w = MAX (text_width, h - padding.left - padding.right); + text_padding = w - text_width; + + w += border.left + border.right + padding.left + padding.right; + h += border.top + border.bottom + padding.top + padding.bottom; + + gtk_render_background (context, cr, 0, 0, w, h); + gtk_render_frame (context, cr, 0, 0, w, h); + + cairo_translate (cr, border.left + padding.left, border.top + padding.top); + cairo_translate (cr, extents.x + text_padding / 2, 0); + + gtk_style_context_get_color (context, state, &color); + gdk_cairo_set_source_rgba (cr, &color); + + gtk_render_layout (context, cr, 0, 0, layout); + g_object_unref (layout); + } + + gtk_style_context_restore (context); + cairo_restore (cr); + } + + gtk_style_context_restore (context); + + return TRUE; +} + +static gboolean +cc_display_arrangement_button_press_event (GtkWidget *widget, + GdkEventButton *event) +{ + CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget); + CcDisplayMonitor *output; + gdouble event_x, event_y; + gint mon_x, mon_y; + + if (!self->config) + return FALSE; + + /* Only handle normal presses of the left mouse button. */ + if (event->button != 1 || event->type != GDK_BUTTON_PRESS) + return FALSE; + + g_return_val_if_fail (self->drag_active == FALSE, FALSE); + + output = cc_display_arrangement_find_monitor_at (self, event->x, event->y); + if (!output) + return FALSE; + + event_x = event->x; + event_y = event->y; + + cairo_matrix_transform_point (&self->to_actual, &event_x, &event_y); + cc_display_monitor_get_geometry (output, &mon_x, &mon_y, NULL, NULL); + + cc_display_arrangement_set_selected_output (self, output); + + if (cc_display_config_count_useful_monitors (self->config) > 1) + { + self->drag_active = TRUE; + self->drag_anchor_x = event_x - mon_x; + self->drag_anchor_y = event_y - mon_y; + } + + return TRUE; +} + +static gboolean +cc_display_arrangement_button_release_event (GtkWidget *widget, + GdkEventButton *event) +{ + CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget); + CcDisplayMonitor *output; + + if (!self->config) + return FALSE; + + /* Only handle left mouse button */ + if (event->button != 1) + return FALSE; + + if (!self->drag_active) + return FALSE; + + self->drag_active = FALSE; + + output = cc_display_arrangement_find_monitor_at (self, event->x, event->y); + cc_display_arrangement_update_cursor (self, output != NULL); + + /* And queue a redraw to recenter everything */ + gtk_widget_queue_draw (widget); + + g_signal_emit_by_name (G_OBJECT (widget), "updated"); + + return TRUE; +} + +static gboolean +cc_display_arrangement_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (widget); + gdouble event_x, event_y; + gint mon_x, mon_y; + SnapData snap_data; + + if (!self->config) + return FALSE; + + if (cc_display_config_count_useful_monitors (self->config) <= 1) + return FALSE; + + if (!self->drag_active) + { + CcDisplayMonitor *output; + output = cc_display_arrangement_find_monitor_at (self, event->x, event->y); + + cc_display_arrangement_update_cursor (self, output != NULL); + if (self->prelit_output != output) + gtk_widget_queue_draw (widget); + + self->prelit_output = output; + + return FALSE; + } + + g_assert (self->selected_output); + + event_x = event->x; + event_y = event->y; + + cairo_matrix_transform_point (&self->to_actual, &event_x, &event_y); + + mon_x = round (event_x - self->drag_anchor_x); + mon_y = round (event_y - self->drag_anchor_y); + + /* The monitor is now at the location as if there was no snapping whatsoever. */ + snap_data.snapped = SNAP_DIR_NONE; + snap_data.mon_x = mon_x; + snap_data.mon_y = mon_y; + snap_data.dist_x = 0; + snap_data.dist_y = 0; + snap_data.to_widget = self->to_widget; + snap_data.major_snap_distance = self->major_snap_distance; + + cc_display_monitor_set_position (self->selected_output, mon_x, mon_y); + + find_best_snapping (self->config, self->selected_output, &snap_data); + + cc_display_monitor_set_position (self->selected_output, snap_data.mon_x, snap_data.mon_y); + + return TRUE; +} + +static void +cc_display_arrangement_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (object); + + switch (prop_id) + { + case PROP_CONFIG: + g_value_set_object (value, self->config); + break; + + case PROP_SELECTED_OUTPUT: + g_value_set_object (value, self->selected_output); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_display_arrangement_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcDisplayArrangement *obj = CC_DISPLAY_ARRANGEMENT (object); + + switch (prop_id) + { + case PROP_CONFIG: + cc_display_arrangement_set_config (obj, g_value_get_object (value)); + break; + + case PROP_SELECTED_OUTPUT: + cc_display_arrangement_set_selected_output (obj, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_display_arrangement_finalize (GObject *object) +{ + CcDisplayArrangement *self = CC_DISPLAY_ARRANGEMENT (object); + + g_clear_object (&self->config); + + G_OBJECT_CLASS (cc_display_arrangement_parent_class)->finalize (object); +} + +static void +cc_display_arrangement_class_init (CcDisplayArrangementClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gobject_class->finalize = cc_display_arrangement_finalize; + gobject_class->get_property = cc_display_arrangement_get_property; + gobject_class->set_property = cc_display_arrangement_set_property; + + widget_class->draw = cc_display_arrangement_draw; + widget_class->button_press_event = cc_display_arrangement_button_press_event; + widget_class->button_release_event = cc_display_arrangement_button_release_event; + widget_class->motion_notify_event = cc_display_arrangement_motion_notify_event; + + props[PROP_CONFIG] = g_param_spec_object ("config", "Display Config", + "The display configuration to work with", + CC_TYPE_DISPLAY_CONFIG, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_SELECTED_OUTPUT] = g_param_spec_object ("selected-output", "Selected Output", + "The output that is currently selected on the configuration", + CC_TYPE_DISPLAY_MONITOR, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, + PROP_LAST, + props); + + g_signal_new ("updated", + CC_TYPE_DISPLAY_ARRANGEMENT, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +cc_display_arrangement_init (CcDisplayArrangement *self) +{ + /* XXX: Do we need to listen to touch events here? */ + gtk_widget_add_events (GTK_WIDGET (self), + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK); + + self->major_snap_distance = MAJOR_SNAP_DISTANCE; +} + +CcDisplayArrangement* +cc_display_arrangement_new (CcDisplayConfig *config) +{ + return g_object_new (CC_TYPE_DISPLAY_ARRANGEMENT, "config", config, NULL); +} + +CcDisplayConfig* +cc_display_arrangement_get_config (CcDisplayArrangement *self) +{ + return self->config; +} + +void +cc_display_arrangement_set_config (CcDisplayArrangement *self, + CcDisplayConfig *config) +{ + const gchar *signals[] = { "rotation", "mode", "primary", "active", "scale", "position-changed", "is-usable" }; + GList *outputs, *l; + guint i; + + if (self->config) + { + outputs = cc_display_config_get_monitors (self->config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + g_signal_handlers_disconnect_by_data (output, self); + } + } + g_clear_object (&self->config); + + self->drag_active = FALSE; + + /* Listen to all the signals */ + if (config) + { + self->config = g_object_ref (config); + + outputs = cc_display_config_get_monitors (self->config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + for (i = 0; i < G_N_ELEMENTS (signals); ++i) + g_signal_connect_object (output, signals[i], G_CALLBACK (on_output_changed_cb), self, G_CONNECT_SWAPPED); + } + } + + cc_display_arrangement_set_selected_output (self, NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONFIG]); +} + +CcDisplayMonitor* +cc_display_arrangement_get_selected_output (CcDisplayArrangement *self) +{ + return self->selected_output; +} + +void +cc_display_arrangement_set_selected_output (CcDisplayArrangement *self, + CcDisplayMonitor *output) +{ + g_return_if_fail (self->drag_active == FALSE); + + /* XXX: Could check that it actually belongs to the right config object. */ + self->selected_output = output; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_OUTPUT]); +} + +void +cc_display_config_snap_output (CcDisplayConfig *config, + CcDisplayMonitor *output) +{ + SnapData snap_data; + gint x, y, w, h; + + if (!cc_display_monitor_is_useful (output)) + return; + + if (cc_display_config_count_useful_monitors (config) <= 1) + return; + + get_scaled_geometry (config, output, &x, &y, &w, &h); + + snap_data.snapped = SNAP_DIR_NONE; + snap_data.mon_x = x; + snap_data.mon_y = y; + snap_data.dist_x = 0; + snap_data.dist_y = 0; + cairo_matrix_init_identity (&snap_data.to_widget); + snap_data.major_snap_distance = G_MAXUINT; + + find_best_snapping (config, output, &snap_data); + + cc_display_monitor_set_position (output, snap_data.mon_x, snap_data.mon_y); +} diff --git a/panels/display/cc-display-arrangement.h b/panels/display/cc-display-arrangement.h new file mode 100644 index 0000000..9494c48 --- /dev/null +++ b/panels/display/cc-display-arrangement.h @@ -0,0 +1,47 @@ +/* -*- mode: c; style: linux -*- + * + * Copyright (C) 2017 Red Hat, Inc. + * + * Written by: Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <gtk/gtk.h> +#include "cc-display-config.h" + +G_BEGIN_DECLS + +#define CC_TYPE_DISPLAY_ARRANGEMENT cc_display_arrangement_get_type () +G_DECLARE_FINAL_TYPE (CcDisplayArrangement, cc_display_arrangement, CC, DISPLAY_ARRANGEMENT, GtkDrawingArea); + +CcDisplayArrangement* cc_display_arrangement_new (CcDisplayConfig *config); + +CcDisplayConfig* cc_display_arrangement_get_config (CcDisplayArrangement *self); +void cc_display_arrangement_set_config (CcDisplayArrangement *self, + CcDisplayConfig *config); + +CcDisplayMonitor* cc_display_arrangement_get_selected_output (CcDisplayArrangement *arr); +void cc_display_arrangement_set_selected_output (CcDisplayArrangement *arr, + CcDisplayMonitor *output); + +/* This is a bit of an odd-ball, but it currently makes sense to have it with + * the arrangement widget where the snapping code lives. */ +void cc_display_config_snap_output (CcDisplayConfig *config, + CcDisplayMonitor *output); + +G_END_DECLS + diff --git a/panels/display/cc-display-config-dbus.c b/panels/display/cc-display-config-dbus.c new file mode 100644 index 0000000..cdc8f8a --- /dev/null +++ b/panels/display/cc-display-config-dbus.c @@ -0,0 +1,1792 @@ +/* + * Copyright (C) 2017 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include <math.h> +#include <gio/gio.h> + +#include "cc-display-config-dbus.h" + +#define MODE_BASE_FORMAT "siiddad" +#define MODE_FORMAT "(" MODE_BASE_FORMAT "a{sv})" +#define MODES_FORMAT "a" MODE_FORMAT +#define MONITOR_SPEC_FORMAT "(ssss)" +#define MONITOR_FORMAT "(" MONITOR_SPEC_FORMAT MODES_FORMAT "a{sv})" +#define MONITORS_FORMAT "a" MONITOR_FORMAT + +#define LOGICAL_MONITOR_MONITORS_FORMAT "a" MONITOR_SPEC_FORMAT +#define LOGICAL_MONITOR_FORMAT "(iidub" LOGICAL_MONITOR_MONITORS_FORMAT "a{sv})" +#define LOGICAL_MONITORS_FORMAT "a" LOGICAL_MONITOR_FORMAT + +#define CURRENT_STATE_FORMAT "(u" MONITORS_FORMAT LOGICAL_MONITORS_FORMAT "a{sv})" + +typedef enum _CcDisplayModeFlags +{ + MODE_PREFERRED = 1 << 0, + MODE_CURRENT = 1 << 1, + MODE_INTERLACED = 1 << 2, +} CcDisplayModeFlags; + +struct _CcDisplayModeDBus +{ + CcDisplayMode parent_instance; + + char *id; + int width; + int height; + double refresh_rate; + double preferred_scale; + GArray *supported_scales; + guint32 flags; +}; + +G_DEFINE_TYPE (CcDisplayModeDBus, + cc_display_mode_dbus, + CC_TYPE_DISPLAY_MODE) + +static gboolean +cc_display_mode_dbus_equal (const CcDisplayModeDBus *m1, + const CcDisplayModeDBus *m2) +{ + if (!m1 && !m2) + return TRUE; + else if (!m1 || !m2) + return FALSE; + + return m1->width == m2->width && + m1->height == m2->height && + m1->refresh_rate == m2->refresh_rate && + (m1->flags & MODE_INTERLACED) == (m2->flags & MODE_INTERLACED); +} + +static void +cc_display_mode_dbus_get_resolution (CcDisplayMode *pself, + int *w, int *h) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + if (w) + *w = self->width; + if (h) + *h = self->height; +} + +static const double * +cc_display_mode_dbus_get_supported_scales (CcDisplayMode *pself) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + return (const double *) self->supported_scales->data; +} + +static double +cc_display_mode_dbus_get_preferred_scale (CcDisplayMode *pself) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + return self->preferred_scale; +} + +static gboolean +cc_display_mode_dbus_is_supported_scale (CcDisplayMode *pself, + double scale) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + guint i; + for (i = 0; i < self->supported_scales->len; i++) + if (g_array_index (self->supported_scales, double, i) == scale) + return TRUE; + return FALSE; +} + + +static gboolean +cc_display_mode_dbus_is_interlaced (CcDisplayMode *pself) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + return !!(self->flags & MODE_INTERLACED); +} + +static int +cc_display_mode_dbus_get_freq (CcDisplayMode *pself) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + return self->refresh_rate; +} + +static double +cc_display_mode_dbus_get_freq_f (CcDisplayMode *pself) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (pself); + + return self->refresh_rate; +} + +static void +cc_display_mode_dbus_init (CcDisplayModeDBus *self) +{ + self->supported_scales = g_array_new (TRUE, TRUE, sizeof (double)); +} + +static void +cc_display_mode_dbus_finalize (GObject *object) +{ + CcDisplayModeDBus *self = CC_DISPLAY_MODE_DBUS (object); + + g_free (self->id); + g_array_free (self->supported_scales, TRUE); + + G_OBJECT_CLASS (cc_display_mode_dbus_parent_class)->finalize (object); +} + +static void +cc_display_mode_dbus_class_init (CcDisplayModeDBusClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + CcDisplayModeClass *parent_class = CC_DISPLAY_MODE_CLASS (klass); + + gobject_class->finalize = cc_display_mode_dbus_finalize; + + parent_class->get_resolution = cc_display_mode_dbus_get_resolution; + parent_class->get_supported_scales = cc_display_mode_dbus_get_supported_scales; + parent_class->get_preferred_scale = cc_display_mode_dbus_get_preferred_scale; + parent_class->is_interlaced = cc_display_mode_dbus_is_interlaced; + parent_class->get_freq = cc_display_mode_dbus_get_freq; + parent_class->get_freq_f = cc_display_mode_dbus_get_freq_f; +} + +static CcDisplayModeDBus * +cc_display_mode_dbus_new (GVariant *variant) +{ + double d; + g_autoptr(GVariantIter) scales_iter = NULL; + g_autoptr(GVariant) properties_variant = NULL; + gboolean is_current; + gboolean is_preferred; + gboolean is_interlaced; + CcDisplayModeDBus *self = g_object_new (CC_TYPE_DISPLAY_MODE_DBUS, NULL); + + g_variant_get (variant, "(" MODE_BASE_FORMAT "@a{sv})", + &self->id, + &self->width, + &self->height, + &self->refresh_rate, + &self->preferred_scale, + &scales_iter, + &properties_variant); + + while (g_variant_iter_next (scales_iter, "d", &d)) + g_array_append_val (self->supported_scales, d); + + if (!g_variant_lookup (properties_variant, "is-current", "b", &is_current)) + is_current = FALSE; + if (!g_variant_lookup (properties_variant, "is-preferred", "b", &is_preferred)) + is_preferred = FALSE; + if (!g_variant_lookup (properties_variant, "is-interlaced", "b", &is_interlaced)) + is_interlaced = FALSE; + + if (is_current) + self->flags |= MODE_CURRENT; + if (is_preferred) + self->flags |= MODE_PREFERRED; + if (is_interlaced) + self->flags |= MODE_INTERLACED; + + return self; +} + + +#define CC_TYPE_DISPLAY_LOGICAL_MONITOR (cc_display_logical_monitor_get_type ()) +G_DECLARE_FINAL_TYPE (CcDisplayLogicalMonitor, cc_display_logical_monitor, + CC, DISPLAY_LOGICAL_MONITOR, GObject) + +struct _CcDisplayLogicalMonitor +{ + GObject parent_instance; + + int x; + int y; + double scale; + CcDisplayRotation rotation; + gboolean primary; + + GHashTable *monitors; +}; + +G_DEFINE_TYPE (CcDisplayLogicalMonitor, + cc_display_logical_monitor, + G_TYPE_OBJECT) + +static gboolean +cc_display_logical_monitor_equal (const CcDisplayLogicalMonitor *m1, + const CcDisplayLogicalMonitor *m2) +{ + if (!m1 && !m2) + return TRUE; + else if (!m1 || !m2) + return FALSE; + + return m1->x == m2->x && + m1->y == m2->y && + m1->scale == m2->scale && + m1->rotation == m2->rotation && + m1->primary == m2->primary; +} + +static void +cc_display_logical_monitor_init (CcDisplayLogicalMonitor *self) +{ + self->scale = 1.0; + self->monitors = g_hash_table_new (NULL, NULL); +} + +static void +cc_display_logical_monitor_finalize (GObject *object) +{ + CcDisplayLogicalMonitor *self = CC_DISPLAY_LOGICAL_MONITOR (object); + + g_warn_if_fail (g_hash_table_size (self->monitors) == 0); + g_clear_pointer (&self->monitors, g_hash_table_destroy); + + G_OBJECT_CLASS (cc_display_logical_monitor_parent_class)->finalize (object); +} + +static void +cc_display_logical_monitor_class_init (CcDisplayLogicalMonitorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = cc_display_logical_monitor_finalize; +} + + +typedef enum _CcDisplayMonitorUnderscanning +{ + UNDERSCANNING_UNSUPPORTED = 0, + UNDERSCANNING_DISABLED, + UNDERSCANNING_ENABLED +} CcDisplayMonitorUnderscanning; + +struct _CcDisplayMonitorDBus +{ + CcDisplayMonitor parent_instance; + CcDisplayConfigDBus *config; + + gchar *connector_name; + gchar *vendor_name; + gchar *product_name; + gchar *product_serial; + gchar *display_name; + + int width_mm; + int height_mm; + gboolean builtin; + CcDisplayMonitorUnderscanning underscanning; + int max_width; + int max_height; + + GList *modes; + CcDisplayMode *current_mode; + CcDisplayMode *preferred_mode; + + CcDisplayLogicalMonitor *logical_monitor; +}; + +G_DEFINE_TYPE (CcDisplayMonitorDBus, + cc_display_monitor_dbus, + CC_TYPE_DISPLAY_MONITOR) + +static void +register_logical_monitor (CcDisplayConfigDBus *self, + CcDisplayLogicalMonitor *logical_monitor); +static void +cc_display_config_dbus_set_primary (CcDisplayConfigDBus *self, + CcDisplayMonitorDBus *new_primary); +static void +cc_display_config_dbus_unset_primary (CcDisplayConfigDBus *self, + CcDisplayMonitorDBus *old_primary); +static void +cc_display_config_dbus_ensure_non_offset_coords (CcDisplayConfigDBus *self); +static void +cc_display_config_dbus_append_right (CcDisplayConfigDBus *self, + CcDisplayLogicalMonitor *monitor); +static void +cc_display_config_dbus_make_linear (CcDisplayConfigDBus *self); + + +static const char * +cc_display_monitor_dbus_get_display_name (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (self->display_name) + return self->display_name; + + return self->connector_name; +} + +static const char * +cc_display_monitor_dbus_get_connector_name (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->connector_name; +} + +static gboolean +cc_display_monitor_dbus_is_builtin (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->builtin; +} + +static gboolean +cc_display_monitor_dbus_is_primary (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (self->logical_monitor) + return self->logical_monitor->primary; + + return FALSE; +} + +static void +cc_display_monitor_dbus_set_primary (CcDisplayMonitor *pself, + gboolean primary) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (primary) + cc_display_config_dbus_set_primary (self->config, self); + else + cc_display_config_dbus_unset_primary (self->config, self); +} + +static gboolean +cc_display_monitor_dbus_is_active (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->logical_monitor != NULL; +} + +static void +cc_display_monitor_dbus_set_logical_monitor (CcDisplayMonitorDBus *self, + CcDisplayLogicalMonitor *logical_monitor) +{ + gboolean was_primary = FALSE; + + if (self->logical_monitor) + { + was_primary = self->logical_monitor->primary; + if (was_primary) + cc_display_config_dbus_unset_primary (self->config, self); + g_hash_table_remove (self->logical_monitor->monitors, self); + g_object_unref (self->logical_monitor); + } + + self->logical_monitor = logical_monitor; + + if (self->logical_monitor) + { + g_hash_table_add (self->logical_monitor->monitors, self); + g_object_ref (self->logical_monitor); + /* unset primary with NULL will select this monitor if it is the only one.*/ + if (was_primary) + cc_display_config_dbus_set_primary (self->config, self); + else + cc_display_config_dbus_unset_primary (self->config, NULL); + } +} + +static void +cc_display_monitor_dbus_set_active (CcDisplayMonitor *pself, + gboolean active) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (!self->current_mode && active) + { + if (self->preferred_mode) + self->current_mode = self->preferred_mode; + else if (self->modes) + self->current_mode = (CcDisplayMode *) self->modes->data; + else + g_warning ("Couldn't find a mode to activate monitor at %s", self->connector_name); + } + + if (!self->logical_monitor && active) + { + CcDisplayLogicalMonitor *logical_monitor; + logical_monitor = g_object_new (CC_TYPE_DISPLAY_LOGICAL_MONITOR, NULL); + cc_display_monitor_dbus_set_logical_monitor (self, logical_monitor); + cc_display_config_dbus_append_right (self->config, logical_monitor); + register_logical_monitor (self->config, logical_monitor); + } + else if (self->logical_monitor && !active) + { + cc_display_monitor_dbus_set_logical_monitor (self, NULL); + } + + g_signal_emit_by_name (self, "active"); +} + +static CcDisplayRotation +cc_display_monitor_dbus_get_rotation (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (self->logical_monitor) + return self->logical_monitor->rotation; + + return CC_DISPLAY_ROTATION_NONE; +} + +static void +cc_display_monitor_dbus_set_rotation (CcDisplayMonitor *pself, + CcDisplayRotation rotation) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (!self->logical_monitor) + return; + + if (self->logical_monitor->rotation != rotation) + { + self->logical_monitor->rotation = rotation; + + g_signal_emit_by_name (self, "rotation"); + } +} + +static gboolean +cc_display_monitor_dbus_supports_rotation (CcDisplayMonitor *pself, + CcDisplayRotation rotation) +{ + return TRUE; +} + +static void +cc_display_monitor_dbus_get_physical_size (CcDisplayMonitor *pself, + int *w, int *h) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (w) + *w = self->width_mm; + if (h) + *h = self->height_mm; +} + +static void +cc_display_monitor_dbus_get_geometry (CcDisplayMonitor *pself, + int *x, int *y, int *w, int *h) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + CcDisplayMode *mode = NULL; + + if (self->logical_monitor) + { + if (x) + *x = self->logical_monitor->x; + if (y) + *y = self->logical_monitor->y; + } + else + { + if (x) + *x = -1; + if (y) + *y = -1; + } + + if (self->current_mode) + mode = self->current_mode; + else if (self->preferred_mode) + mode = self->preferred_mode; + else if (self->modes) + mode = CC_DISPLAY_MODE (self->modes->data); + + if (mode) + cc_display_mode_get_resolution (mode, w, h); + else + { + g_warning ("Monitor at %s has no modes?", self->connector_name); + if (w) + *w = -1; + if (h) + *h = -1; + } +} + +static CcDisplayMode * +cc_display_monitor_dbus_get_mode (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->current_mode; +} + +static CcDisplayMode * +cc_display_monitor_dbus_get_preferred_mode (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->preferred_mode; +} + +static guint32 +cc_display_monitor_dbus_get_id (CcDisplayMonitor *pself) +{ + return 0; +} + +static GList * +cc_display_monitor_dbus_get_modes (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->modes; +} + +static gboolean +cc_display_monitor_dbus_supports_underscanning (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->underscanning != UNDERSCANNING_UNSUPPORTED; +} + +static gboolean +cc_display_monitor_dbus_get_underscanning (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + return self->underscanning == UNDERSCANNING_ENABLED; +} + +static void +cc_display_monitor_dbus_set_underscanning (CcDisplayMonitor *pself, + gboolean underscanning) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (self->underscanning == UNDERSCANNING_UNSUPPORTED) + return; + + if (underscanning) + self->underscanning = UNDERSCANNING_ENABLED; + else + self->underscanning = UNDERSCANNING_DISABLED; +} + +static CcDisplayMode * +cc_display_monitor_dbus_get_closest_mode (CcDisplayMonitorDBus *self, + CcDisplayModeDBus *mode) +{ + CcDisplayModeDBus *best = NULL; + GList *l; + + for (l = self->modes; l != NULL; l = l->next) + { + CcDisplayModeDBus *similar = l->data; + + if (similar->width != mode->width || + similar->height != mode->height) + continue; + + if (similar->refresh_rate == mode->refresh_rate && + (similar->flags & MODE_INTERLACED) == (mode->flags & MODE_INTERLACED)) + { + best = similar; + break; + } + + /* There might be a better heuristic. */ + if (!best || best->refresh_rate < similar->refresh_rate) + { + best = similar; + continue; + } + } + + return CC_DISPLAY_MODE (best); +} + +static void +cc_display_monitor_dbus_set_mode (CcDisplayMonitor *pself, + CcDisplayMode *new_mode) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + CcDisplayMode *mode; + + g_return_if_fail (new_mode != NULL); + + mode = cc_display_monitor_dbus_get_closest_mode (self, CC_DISPLAY_MODE_DBUS (new_mode)); + + self->current_mode = mode; + + if (!cc_display_mode_dbus_is_supported_scale (mode, cc_display_monitor_get_scale (pself))) + cc_display_monitor_set_scale (pself, cc_display_mode_get_preferred_scale (mode)); + + g_signal_emit_by_name (self, "mode"); +} + +static void +cc_display_monitor_dbus_set_position (CcDisplayMonitor *pself, + int x, int y) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (self->logical_monitor) + { + gboolean notify = FALSE; + + if (self->logical_monitor->x != x || self->logical_monitor->y != y) + notify = TRUE; + + self->logical_monitor->x = x; + self->logical_monitor->y = y; + + if (notify) + g_signal_emit_by_name (self, "position-changed"); + } + +} + +static double +cc_display_monitor_dbus_get_scale (CcDisplayMonitor *pself) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (self->logical_monitor) + return self->logical_monitor->scale; + + return 1.0; +} + +static void +cc_display_monitor_dbus_set_scale (CcDisplayMonitor *pself, + double scale) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (pself); + + if (!self->current_mode) + return; + + if (!cc_display_mode_dbus_is_supported_scale (self->current_mode, scale)) + return; + + if (!self->logical_monitor) + return; + + if (self->logical_monitor->scale != scale) + { + self->logical_monitor->scale = scale; + + g_signal_emit_by_name (self, "scale"); + } +} + +static void +cc_display_monitor_dbus_init (CcDisplayMonitorDBus *self) +{ + self->underscanning = UNDERSCANNING_UNSUPPORTED; + self->max_width = G_MAXINT; + self->max_height = G_MAXINT; +} + +static void +cc_display_monitor_dbus_finalize (GObject *object) +{ + CcDisplayMonitorDBus *self = CC_DISPLAY_MONITOR_DBUS (object); + + g_free (self->connector_name); + g_free (self->vendor_name); + g_free (self->product_name); + g_free (self->product_serial); + g_free (self->display_name); + + g_list_foreach (self->modes, (GFunc) g_object_unref, NULL); + g_clear_pointer (&self->modes, g_list_free); + + if (self->logical_monitor) + { + g_hash_table_remove (self->logical_monitor->monitors, self); + g_object_unref (self->logical_monitor); + } + + G_OBJECT_CLASS (cc_display_monitor_dbus_parent_class)->finalize (object); +} + +static void +cc_display_monitor_dbus_class_init (CcDisplayMonitorDBusClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + CcDisplayMonitorClass *parent_class = CC_DISPLAY_MONITOR_CLASS (klass); + + gobject_class->finalize = cc_display_monitor_dbus_finalize; + + parent_class->get_display_name = cc_display_monitor_dbus_get_display_name; + parent_class->get_connector_name = cc_display_monitor_dbus_get_connector_name; + parent_class->is_builtin = cc_display_monitor_dbus_is_builtin; + parent_class->is_primary = cc_display_monitor_dbus_is_primary; + parent_class->set_primary = cc_display_monitor_dbus_set_primary; + parent_class->is_active = cc_display_monitor_dbus_is_active; + parent_class->set_active = cc_display_monitor_dbus_set_active; + parent_class->get_rotation = cc_display_monitor_dbus_get_rotation; + parent_class->set_rotation = cc_display_monitor_dbus_set_rotation; + parent_class->supports_rotation = cc_display_monitor_dbus_supports_rotation; + parent_class->get_physical_size = cc_display_monitor_dbus_get_physical_size; + parent_class->get_geometry = cc_display_monitor_dbus_get_geometry; + parent_class->get_mode = cc_display_monitor_dbus_get_mode; + parent_class->get_preferred_mode = cc_display_monitor_dbus_get_preferred_mode; + parent_class->get_id = cc_display_monitor_dbus_get_id; + parent_class->get_modes = cc_display_monitor_dbus_get_modes; + parent_class->supports_underscanning = cc_display_monitor_dbus_supports_underscanning; + parent_class->get_underscanning = cc_display_monitor_dbus_get_underscanning; + parent_class->set_underscanning = cc_display_monitor_dbus_set_underscanning; + parent_class->set_mode = cc_display_monitor_dbus_set_mode; + parent_class->set_position = cc_display_monitor_dbus_set_position; + parent_class->get_scale = cc_display_monitor_dbus_get_scale; + parent_class->set_scale = cc_display_monitor_dbus_set_scale; +} + +static void +construct_modes (CcDisplayMonitorDBus *self, + GVariantIter *modes) +{ + CcDisplayModeDBus *mode; + + while (TRUE) + { + g_autoptr(GVariant) variant = NULL; + + if (!g_variant_iter_next (modes, "@"MODE_FORMAT, &variant)) + break; + + mode = cc_display_mode_dbus_new (variant); + self->modes = g_list_prepend (self->modes, mode); + + if (mode->flags & MODE_PREFERRED) + self->preferred_mode = CC_DISPLAY_MODE (mode); + if (mode->flags & MODE_CURRENT) + self->current_mode = CC_DISPLAY_MODE (mode); + } +} + +static CcDisplayMonitorDBus * +cc_display_monitor_dbus_new (GVariant *variant, + CcDisplayConfigDBus *config) +{ + CcDisplayMonitorDBus *self = g_object_new (CC_TYPE_DISPLAY_MONITOR_DBUS, NULL); + gchar *s1, *s2, *s3, *s4; + g_autoptr(GVariantIter) modes = NULL; + g_autoptr(GVariantIter) props = NULL; + + self->config = config; + + g_variant_get (variant, MONITOR_FORMAT, + &s1, &s2, &s3, &s4, &modes, &props); + self->connector_name = s1; + self->vendor_name = s2; + self->product_name = s3; + self->product_serial = s4; + + construct_modes (self, modes); + + while (TRUE) + { + const char *s; + g_autoptr(GVariant) v = NULL; + + if (!g_variant_iter_next (props, "{&sv}", &s, &v)) + break; + + if (g_str_equal (s, "width-mm")) + { + g_variant_get (v, "i", &self->width_mm); + } + else if (g_str_equal (s, "height-mm")) + { + g_variant_get (v, "i", &self->height_mm); + } + else if (g_str_equal (s, "is-underscanning")) + { + gboolean underscanning = FALSE; + g_variant_get (v, "b", &underscanning); + if (underscanning) + self->underscanning = UNDERSCANNING_ENABLED; + else + self->underscanning = UNDERSCANNING_DISABLED; + } + else if (g_str_equal (s, "max-screen-size")) + { + g_variant_get (v, "ii", &self->max_width, &self->max_height); + } + else if (g_str_equal (s, "is-builtin")) + { + g_variant_get (v, "b", &self->builtin); + } + else if (g_str_equal (s, "display-name")) + { + g_variant_get (v, "s", &self->display_name); + } + } + + return self; +} + + +typedef enum _CcDisplayLayoutMode +{ + CC_DISPLAY_LAYOUT_MODE_LOGICAL = 1, + CC_DISPLAY_LAYOUT_MODE_PHYSICAL = 2 +} CcDisplayLayoutMode; + +typedef enum _CcDisplayConfigMethod +{ + CC_DISPLAY_CONFIG_METHOD_VERIFY = 0, + CC_DISPLAY_CONFIG_METHOD_TEMPORARY = 1, + CC_DISPLAY_CONFIG_METHOD_PERSISTENT = 2 +} CcDisplayConfigMethod; + +struct _CcDisplayConfigDBus +{ + CcDisplayConfig parent_instance; + + GVariant *state; + GDBusConnection *connection; + GDBusProxy *proxy; + + int min_width; + int min_height; + + guint panel_orientation_managed; + + guint32 serial; + gboolean supports_mirroring; + gboolean supports_changing_layout_mode; + gboolean global_scale_required; + CcDisplayLayoutMode layout_mode; + + GList *monitors; + CcDisplayMonitorDBus *primary; + + GHashTable *logical_monitors; + + GList *clone_modes; +}; + +G_DEFINE_TYPE (CcDisplayConfigDBus, + cc_display_config_dbus, + CC_TYPE_DISPLAY_CONFIG) + +enum +{ + PROP_0, + PROP_STATE, + PROP_CONNECTION, +}; + +static GList * +cc_display_config_dbus_get_monitors (CcDisplayConfig *pself) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + return self->monitors; +} + +static GVariant * +build_monitors_variant (GHashTable *monitors) +{ + GVariantBuilder builder; + GHashTableIter iter; + CcDisplayMonitorDBus *monitor; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + g_hash_table_iter_init (&iter, monitors); + + while (g_hash_table_iter_next (&iter, (void **) &monitor, NULL)) + { + GVariantBuilder props_builder; + CcDisplayModeDBus *mode_dbus; + + if (!monitor->current_mode) + continue; + + g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&props_builder, "{sv}", + "underscanning", + g_variant_new_boolean (monitor->underscanning == UNDERSCANNING_ENABLED)); + + mode_dbus = CC_DISPLAY_MODE_DBUS (monitor->current_mode); + g_variant_builder_add (&builder, "(ss@*)", + monitor->connector_name, + mode_dbus->id, + g_variant_builder_end (&props_builder)); + } + + return g_variant_builder_end (&builder); +} + +static GVariant * +build_logical_monitors_parameter (CcDisplayConfigDBus *self) +{ + GVariantBuilder builder; + GHashTableIter iter; + CcDisplayLogicalMonitor *logical_monitor; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(iiduba(ssa{sv}))")); + g_hash_table_iter_init (&iter, self->logical_monitors); + + while (g_hash_table_iter_next (&iter, (void **) &logical_monitor, NULL)) + g_variant_builder_add (&builder, "(iidub@*)", + logical_monitor->x, + logical_monitor->y, + logical_monitor->scale, + logical_monitor->rotation, + logical_monitor->primary, + build_monitors_variant (logical_monitor->monitors)); + + return g_variant_builder_end (&builder); +} + +static GVariant * +build_apply_parameters (CcDisplayConfigDBus *self, + CcDisplayConfigMethod method) +{ + GVariantBuilder props_builder; + g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}")); + + if (self->supports_changing_layout_mode) + g_variant_builder_add (&props_builder, "{sv}", + "layout-mode", g_variant_new_uint32 (self->layout_mode)); + + return g_variant_new ("(uu@*@*)", + self->serial, + method, + build_logical_monitors_parameter (self), + g_variant_builder_end (&props_builder)); +} + +static gboolean +config_apply (CcDisplayConfigDBus *self, + CcDisplayConfigMethod method, + GError **error) +{ + g_autoptr(GVariant) retval = NULL; + + cc_display_config_dbus_ensure_non_offset_coords (self); + + retval = g_dbus_proxy_call_sync (self->proxy, + "ApplyMonitorsConfig", + build_apply_parameters (self, method), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + error); + return retval != NULL; +} + +static gboolean +cc_display_config_dbus_is_applicable (CcDisplayConfig *pself) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + g_autoptr(GError) error = NULL; + + if (!config_apply (self, CC_DISPLAY_CONFIG_METHOD_VERIFY, &error)) + { + g_warning ("Config not applicable: %s", error->message); + return FALSE; + } + else + { + return TRUE; + } +} + +static CcDisplayMonitorDBus * +monitor_from_spec (CcDisplayConfigDBus *self, + const gchar *connector, + const gchar *vendor, + const gchar *product, + const gchar *serial) +{ + GList *l; + for (l = self->monitors; l != NULL; l = l->next) + { + CcDisplayMonitorDBus *m = l->data; + if (g_str_equal (m->connector_name, connector) && + g_str_equal (m->vendor_name, vendor) && + g_str_equal (m->product_name, product) && + g_str_equal (m->product_serial, serial)) + return m; + } + return NULL; +} + +static gboolean +cc_display_config_dbus_equal (CcDisplayConfig *pself, + CcDisplayConfig *pother) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + CcDisplayConfigDBus *other = CC_DISPLAY_CONFIG_DBUS (pother); + GList *l; + + g_return_val_if_fail (pself, FALSE); + g_return_val_if_fail (pother, FALSE); + + cc_display_config_dbus_ensure_non_offset_coords (self); + cc_display_config_dbus_ensure_non_offset_coords (other); + + for (l = self->monitors; l != NULL; l = l->next) + { + CcDisplayMonitorDBus *m1 = l->data; + CcDisplayMonitorDBus *m2 = monitor_from_spec (other, + m1->connector_name, + m1->vendor_name, + m1->product_name, + m1->product_serial); + if (!m2) + return FALSE; + + if (m1->underscanning != m2->underscanning) + return FALSE; + + if (!cc_display_logical_monitor_equal (m1->logical_monitor, m2->logical_monitor)) + return FALSE; + + /* Modes should not be compared if both monitors have no logical monitor. */ + if (m1->logical_monitor == NULL && m2->logical_monitor == NULL) + continue; + + if (!cc_display_mode_dbus_equal (CC_DISPLAY_MODE_DBUS (m1->current_mode), + CC_DISPLAY_MODE_DBUS (m2->current_mode))) + return FALSE; + } + + return TRUE; +} + +static void +cc_display_config_dbus_set_primary (CcDisplayConfigDBus *self, + CcDisplayMonitorDBus *new_primary) +{ + if (self->primary == new_primary) + return; + + if (!new_primary->logical_monitor) + return; + + if (self->primary && self->primary->logical_monitor) + { + self->primary->logical_monitor->primary = FALSE; + g_signal_emit_by_name (self->primary, "primary"); + } + + self->primary = new_primary; + self->primary->logical_monitor->primary = TRUE; + + g_signal_emit_by_name (self->primary, "primary"); + g_signal_emit_by_name (self, "primary"); +} + +static void +cc_display_config_dbus_unset_primary (CcDisplayConfigDBus *self, + CcDisplayMonitorDBus *old_primary) +{ + GList *l; + + if (self->primary != old_primary) + return; + + for (l = self->monitors; l != NULL; l = l->next) + { + CcDisplayMonitorDBus *monitor = l->data; + if (monitor->logical_monitor && + monitor != old_primary) + { + cc_display_config_dbus_set_primary (self, monitor); + break; + } + } + + if (self->primary == old_primary) + self->primary = NULL; +} + +static gboolean +cc_display_config_dbus_is_cloning (CcDisplayConfig *pself) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + guint n_active_monitors = 0; + GList *l; + + for (l = self->monitors; l != NULL; l = l->next) + if (cc_display_monitor_is_active (CC_DISPLAY_MONITOR (l->data))) + n_active_monitors += 1; + + return n_active_monitors > 1 && g_hash_table_size (self->logical_monitors) == 1; +} + +static void +cc_display_config_dbus_set_cloning (CcDisplayConfig *pself, + gboolean clone) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + gboolean is_cloning = cc_display_config_is_cloning (pself); + CcDisplayLogicalMonitor *logical_monitor; + GList *l; + + if (clone && !is_cloning) + { + logical_monitor = g_object_new (CC_TYPE_DISPLAY_LOGICAL_MONITOR, NULL); + for (l = self->monitors; l != NULL; l = l->next) + cc_display_monitor_dbus_set_logical_monitor (CC_DISPLAY_MONITOR_DBUS (l->data), + logical_monitor); + register_logical_monitor (self, logical_monitor); + } + else if (!clone && is_cloning) + { + for (l = self->monitors; l != NULL; l = l->next) + { + logical_monitor = g_object_new (CC_TYPE_DISPLAY_LOGICAL_MONITOR, NULL); + cc_display_monitor_dbus_set_logical_monitor (CC_DISPLAY_MONITOR_DBUS (l->data), + logical_monitor); + register_logical_monitor (self, logical_monitor); + } + cc_display_config_dbus_make_linear (self); + } +} + +static GList * +cc_display_config_dbus_get_cloning_modes (CcDisplayConfig *pself) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + return self->clone_modes; +} + +static gboolean +cc_display_config_dbus_apply (CcDisplayConfig *pself, + GError **error) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + return config_apply (self, CC_DISPLAY_CONFIG_METHOD_PERSISTENT, error); +} + +static gboolean +cc_display_config_dbus_is_layout_logical (CcDisplayConfig *pself) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + return self->layout_mode == CC_DISPLAY_LAYOUT_MODE_LOGICAL; +} + +static gboolean +is_scaled_mode_allowed (CcDisplayConfigDBus *self, + CcDisplayMode *pmode, + double scale) +{ + gint width, height; + CcDisplayModeDBus *mode = CC_DISPLAY_MODE_DBUS (pmode); + + if (!cc_display_mode_dbus_is_supported_scale (pmode, scale)) + return FALSE; + + /* Do the math as if the monitor is always in landscape mode. */ + width = round (mode->width / scale); + height = round (mode->height / scale); + + return (MAX (width, height) >= self->min_width && + MIN (width, height) >= self->min_height); +} + +static gboolean +is_scale_allowed_by_active_monitors (CcDisplayConfigDBus *self, + CcDisplayMode *mode, + double scale) +{ + GList *l; + + for (l = self->monitors; l != NULL; l = l->next) + { + CcDisplayMonitorDBus *m = CC_DISPLAY_MONITOR_DBUS (l->data); + + if (!cc_display_monitor_is_active (CC_DISPLAY_MONITOR (m))) + continue; + + if (!is_scaled_mode_allowed (self, mode, scale)) + return FALSE; + } + + return TRUE; +} + +static void +cc_display_config_dbus_set_minimum_size (CcDisplayConfig *pself, + int width, + int height) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + g_assert (width >= 0 && height >= 0); + + self->min_width = width; + self->min_height = height; +} + +static gboolean +cc_display_config_dbus_is_scaled_mode_valid (CcDisplayConfig *pself, + CcDisplayMode *mode, + double scale) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + if (self->global_scale_required || cc_display_config_is_cloning (pself)) + return is_scale_allowed_by_active_monitors (self, mode, scale); + + return is_scaled_mode_allowed (self, mode, scale); +} + +static gboolean +cc_display_config_dbus_get_panel_orientation_managed (CcDisplayConfig *pself) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself); + + return self->panel_orientation_managed; +} + +static void +cc_display_config_dbus_init (CcDisplayConfigDBus *self) +{ + self->serial = 0; + self->supports_mirroring = TRUE; + self->supports_changing_layout_mode = FALSE; + self->global_scale_required = FALSE; + self->layout_mode = CC_DISPLAY_LAYOUT_MODE_LOGICAL; + self->logical_monitors = g_hash_table_new (NULL, NULL); +} + +static void +gather_clone_modes (CcDisplayConfigDBus *self) +{ + guint n_monitors = g_list_length (self->monitors); + CcDisplayMonitorDBus *monitor; + GList *l; + + if (n_monitors < 2) + return; + + monitor = self->monitors->data; + for (l = monitor->modes; l != NULL; l = l->next) + { + CcDisplayModeDBus *mode = l->data; + gboolean valid = TRUE; + GList *ll; + for (ll = self->monitors->next; ll != NULL; ll = ll->next) + { + CcDisplayMonitorDBus *other_monitor = ll->data; + if (!cc_display_monitor_dbus_get_closest_mode (other_monitor, mode)) + { + valid = FALSE; + break; + } + } + if (valid) + self->clone_modes = g_list_prepend (self->clone_modes, mode); + } +} + +static void +remove_logical_monitor (gpointer data, + GObject *object) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (data); + + g_hash_table_remove (self->logical_monitors, object); +} + +static void +register_logical_monitor (CcDisplayConfigDBus *self, + CcDisplayLogicalMonitor *logical_monitor) +{ + g_hash_table_add (self->logical_monitors, logical_monitor); + g_object_weak_ref (G_OBJECT (logical_monitor), remove_logical_monitor, self); + g_object_unref (logical_monitor); +} + +static void +apply_global_scale_requirement (CcDisplayConfigDBus *self, + CcDisplayMonitor *monitor) +{ + GList *l; + double scale = cc_display_monitor_get_scale (monitor); + + for (l = self->monitors; l != NULL; l = l->next) + { + CcDisplayMonitor *m = l->data; + if (m != monitor) + cc_display_monitor_set_scale (m, scale); + } +} + +static void +construct_monitors (CcDisplayConfigDBus *self, + GVariantIter *monitors, + GVariantIter *logical_monitors) +{ + while (TRUE) + { + CcDisplayMonitorDBus *monitor; + g_autoptr(GVariant) variant = NULL; + + if (!g_variant_iter_next (monitors, "@"MONITOR_FORMAT, &variant)) + break; + + monitor = cc_display_monitor_dbus_new (variant, self); + self->monitors = g_list_prepend (self->monitors, monitor); + + if (self->global_scale_required) + g_signal_connect_object (monitor, "scale", + G_CALLBACK (apply_global_scale_requirement), + self, G_CONNECT_SWAPPED); + } + + while (TRUE) + { + g_autoptr(GVariant) variant = NULL; + CcDisplayLogicalMonitor *logical_monitor; + g_autoptr(GVariantIter) monitor_specs = NULL; + const gchar *s1, *s2, *s3, *s4; + gboolean primary; + + if (!g_variant_iter_next (logical_monitors, "@"LOGICAL_MONITOR_FORMAT, &variant)) + break; + + logical_monitor = g_object_new (CC_TYPE_DISPLAY_LOGICAL_MONITOR, NULL); + g_variant_get (variant, LOGICAL_MONITOR_FORMAT, + &logical_monitor->x, + &logical_monitor->y, + &logical_monitor->scale, + &logical_monitor->rotation, + &primary, + &monitor_specs, + NULL); + + while (g_variant_iter_next (monitor_specs, "(&s&s&s&s)", &s1, &s2, &s3, &s4)) + { + CcDisplayMonitorDBus *m = monitor_from_spec (self, s1, s2, s3, s4); + if (!m) + { + g_warning ("Couldn't find monitor given spec: %s, %s, %s, %s", + s1, s2, s3, s4); + continue; + } + + cc_display_monitor_dbus_set_logical_monitor (m, logical_monitor); + } + + if (g_hash_table_size (logical_monitor->monitors) > 0) + { + if (primary) + { + CcDisplayMonitorDBus *m = NULL; + GHashTableIter iter; + g_hash_table_iter_init (&iter, logical_monitor->monitors); + g_hash_table_iter_next (&iter, (void **) &m, NULL); + + cc_display_config_dbus_set_primary (self, m); + } + } + else + { + g_warning ("Got an empty logical monitor, ignoring"); + } + + register_logical_monitor (self, logical_monitor); + } + + gather_clone_modes (self); +} + +static void +update_panel_orientation_managed (CcDisplayConfigDBus *self) +{ + g_autoptr(GVariant) v = NULL; + gboolean panel_orientation_managed = FALSE; + + if (self->proxy != NULL) + { + v = g_dbus_proxy_get_cached_property (self->proxy, "PanelOrientationManaged"); + if (v) + { + panel_orientation_managed = g_variant_get_boolean (v); + } + } + + if (panel_orientation_managed == self->panel_orientation_managed) + return; + + self->panel_orientation_managed = panel_orientation_managed; + g_signal_emit_by_name (self, "panel-orientation-managed", self->panel_orientation_managed); +} + +static void +proxy_properties_changed_cb (GDBusProxy *proxy, + GVariant *changed_properties, + GStrv invalidated_properties, + CcDisplayConfigDBus *self) +{ + GVariantDict dict; + + g_variant_dict_init (&dict, changed_properties); + + if (g_variant_dict_contains (&dict, "PanelOrientationManaged")) + update_panel_orientation_managed (self); +} + +static void +cc_display_config_dbus_constructed (GObject *object) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (object); + g_autoptr(GVariantIter) monitors = NULL; + g_autoptr(GVariantIter) logical_monitors = NULL; + g_autoptr(GVariantIter) props = NULL; + g_autoptr(GError) error = NULL; + + g_variant_get (self->state, + CURRENT_STATE_FORMAT, + &self->serial, + &monitors, + &logical_monitors, + &props); + + while (TRUE) + { + const char *s; + g_autoptr(GVariant) v = NULL; + + if (!g_variant_iter_next (props, "{&sv}", &s, &v)) + break; + + if (g_str_equal (s, "supports-mirroring")) + { + g_variant_get (v, "b", &self->supports_mirroring); + } + else if (g_str_equal (s, "supports-changing-layout-mode")) + { + g_variant_get (v, "b", &self->supports_changing_layout_mode); + } + else if (g_str_equal (s, "global-scale-required")) + { + g_variant_get (v, "b", &self->global_scale_required); + } + else if (g_str_equal (s, "layout-mode")) + { + guint32 u = 0; + g_variant_get (v, "u", &u); + if (u >= CC_DISPLAY_LAYOUT_MODE_LOGICAL && + u <= CC_DISPLAY_LAYOUT_MODE_PHYSICAL) + self->layout_mode = u; + } + } + + construct_monitors (self, monitors, logical_monitors); + + self->proxy = g_dbus_proxy_new_sync (self->connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + NULL, + &error); + if (error) + g_warning ("Could not create DisplayConfig proxy: %s", error->message); + + g_signal_connect (self->proxy, "g-properties-changed", + G_CALLBACK (proxy_properties_changed_cb), self); + update_panel_orientation_managed (self); + + G_OBJECT_CLASS (cc_display_config_dbus_parent_class)->constructed (object); +} + +static void +cc_display_config_dbus_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (object); + + switch (prop_id) + { + case PROP_STATE: + self->state = g_value_dup_variant (value); + break; + case PROP_CONNECTION: + self->connection = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_display_config_dbus_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (object); + + switch (prop_id) + { + case PROP_STATE: + g_value_set_variant (value, self->state); + break; + case PROP_CONNECTION: + g_value_set_object (value, self->connection); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_display_config_dbus_finalize (GObject *object) +{ + CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (object); + + g_clear_pointer (&self->state, g_variant_unref); + g_clear_object (&self->connection); + g_clear_object (&self->proxy); + + g_list_foreach (self->monitors, (GFunc) g_object_unref, NULL); + g_clear_pointer (&self->monitors, g_list_free); + g_clear_pointer (&self->logical_monitors, g_hash_table_destroy); + g_clear_pointer (&self->clone_modes, g_list_free); + + G_OBJECT_CLASS (cc_display_config_dbus_parent_class)->finalize (object); +} + +static void +cc_display_config_dbus_class_init (CcDisplayConfigDBusClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + CcDisplayConfigClass *parent_class = CC_DISPLAY_CONFIG_CLASS (klass); + GParamSpec *pspec; + + gobject_class->constructed = cc_display_config_dbus_constructed; + gobject_class->set_property = cc_display_config_dbus_set_property; + gobject_class->get_property = cc_display_config_dbus_get_property; + gobject_class->finalize = cc_display_config_dbus_finalize; + + parent_class->get_monitors = cc_display_config_dbus_get_monitors; + parent_class->is_applicable = cc_display_config_dbus_is_applicable; + parent_class->equal = cc_display_config_dbus_equal; + parent_class->apply = cc_display_config_dbus_apply; + parent_class->is_cloning = cc_display_config_dbus_is_cloning; + parent_class->set_cloning = cc_display_config_dbus_set_cloning; + parent_class->get_cloning_modes = cc_display_config_dbus_get_cloning_modes; + parent_class->is_layout_logical = cc_display_config_dbus_is_layout_logical; + parent_class->is_scaled_mode_valid = cc_display_config_dbus_is_scaled_mode_valid; + parent_class->set_minimum_size = cc_display_config_dbus_set_minimum_size; + parent_class->get_panel_orientation_managed = + cc_display_config_dbus_get_panel_orientation_managed; + + pspec = g_param_spec_variant ("state", + "GVariant", + "GVariant", + G_VARIANT_TYPE (CURRENT_STATE_FORMAT), + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_property (gobject_class, PROP_STATE, pspec); + + pspec = g_param_spec_object ("connection", + "GDBusConnection", + "GDBusConnection", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_property (gobject_class, PROP_CONNECTION, pspec); +} + +static gint +sort_x_axis (gconstpointer a, gconstpointer b) +{ + const CcDisplayLogicalMonitor *ma = a; + const CcDisplayLogicalMonitor *mb = b; + return ma->x - mb->x; +} + +static gint +sort_y_axis (gconstpointer a, gconstpointer b) +{ + const CcDisplayLogicalMonitor *ma = a; + const CcDisplayLogicalMonitor *mb = b; + return ma->y - mb->y; +} + +static void +add_x_delta (gpointer d1, gpointer d2) +{ + CcDisplayLogicalMonitor *m = d1; + int delta = GPOINTER_TO_INT (d2); + m->x += delta; +} + +static gboolean +logical_monitor_is_rotated (CcDisplayLogicalMonitor *lm) +{ + switch (lm->rotation) + { + case CC_DISPLAY_ROTATION_90: + case CC_DISPLAY_ROTATION_270: + case CC_DISPLAY_ROTATION_90_FLIPPED: + case CC_DISPLAY_ROTATION_270_FLIPPED: + return TRUE; + default: + return FALSE; + } +} + +static int +logical_monitor_width (CcDisplayLogicalMonitor *lm) +{ + CcDisplayMonitorDBus *monitor; + CcDisplayModeDBus *mode; + GHashTableIter iter; + int width; + + g_hash_table_iter_init (&iter, lm->monitors); + g_hash_table_iter_next (&iter, (void **) &monitor, NULL); + mode = CC_DISPLAY_MODE_DBUS (monitor->current_mode); + if (logical_monitor_is_rotated (lm)) + width = mode ? mode->height : 0; + else + width = mode ? mode->width : 0; + + if (monitor->config->layout_mode == CC_DISPLAY_LAYOUT_MODE_LOGICAL) + return round (width / lm->scale); + else + return width; +} + +static void +add_y_delta (gpointer d1, gpointer d2) +{ + CcDisplayLogicalMonitor *m = d1; + int delta = GPOINTER_TO_INT (d2); + m->y += delta; +} + +static void +cc_display_config_dbus_ensure_non_offset_coords (CcDisplayConfigDBus *self) +{ + GList *x_axis, *y_axis; + CcDisplayLogicalMonitor *m; + + if (g_hash_table_size (self->logical_monitors) == 0) + return; + + x_axis = g_hash_table_get_keys (self->logical_monitors); + x_axis = g_list_sort (x_axis, sort_x_axis); + y_axis = g_hash_table_get_keys (self->logical_monitors); + y_axis = g_list_sort (y_axis, sort_y_axis); + + m = x_axis->data; + if (m->x != 0) + g_list_foreach (x_axis, add_x_delta, GINT_TO_POINTER (- m->x)); + + m = y_axis->data; + if (m->y != 0) + g_list_foreach (y_axis, add_y_delta, GINT_TO_POINTER (- m->y)); + + g_list_free (x_axis); + g_list_free (y_axis); +} + +static void +cc_display_config_dbus_append_right (CcDisplayConfigDBus *self, + CcDisplayLogicalMonitor *monitor) +{ + GList *x_axis; + CcDisplayLogicalMonitor *last; + + if (g_hash_table_size (self->logical_monitors) == 0) + { + monitor->x = 0; + monitor->y = 0; + return; + } + + x_axis = g_hash_table_get_keys (self->logical_monitors); + x_axis = g_list_sort (x_axis, sort_x_axis); + last = g_list_last (x_axis)->data; + monitor->x = last->x + logical_monitor_width (last); + monitor->y = last->y; + + g_list_free (x_axis); +} + +static void +cc_display_config_dbus_make_linear (CcDisplayConfigDBus *self) +{ + CcDisplayLogicalMonitor *primary; + GList *logical_monitors, *l; + int x; + + if (self->primary && self->primary->logical_monitor) + { + primary = self->primary->logical_monitor; + primary->x = primary->y = 0; + x = logical_monitor_width (primary); + } + else + { + primary = NULL; + x = 0; + } + + logical_monitors = g_hash_table_get_keys (self->logical_monitors); + for (l = logical_monitors; l != NULL; l = l->next) + { + CcDisplayLogicalMonitor *m = l->data; + + if (m == primary) + continue; + + m->x = x; + m->y = 0; + x += logical_monitor_width (m); + } + + g_list_free (logical_monitors); +} diff --git a/panels/display/cc-display-config-dbus.h b/panels/display/cc-display-config-dbus.h new file mode 100644 index 0000000..8609087 --- /dev/null +++ b/panels/display/cc-display-config-dbus.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#include <glib-object.h> + +#include "cc-display-config.h" + +G_BEGIN_DECLS + +#define CC_TYPE_DISPLAY_MODE_DBUS (cc_display_mode_dbus_get_type ()) +G_DECLARE_FINAL_TYPE (CcDisplayModeDBus, cc_display_mode_dbus, + CC, DISPLAY_MODE_DBUS, CcDisplayMode) + +#define CC_TYPE_DISPLAY_MONITOR_DBUS (cc_display_monitor_dbus_get_type ()) +G_DECLARE_FINAL_TYPE (CcDisplayMonitorDBus, cc_display_monitor_dbus, + CC, DISPLAY_MONITOR_DBUS, CcDisplayMonitor) + +#define CC_TYPE_DISPLAY_CONFIG_DBUS (cc_display_config_dbus_get_type ()) +G_DECLARE_FINAL_TYPE (CcDisplayConfigDBus, cc_display_config_dbus, + CC, DISPLAY_CONFIG_DBUS, CcDisplayConfig) + +G_END_DECLS diff --git a/panels/display/cc-display-config-manager-dbus.c b/panels/display/cc-display-config-manager-dbus.c new file mode 100644 index 0000000..653bea0 --- /dev/null +++ b/panels/display/cc-display-config-manager-dbus.c @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "cc-display-config-dbus.h" +#include "cc-display-config-manager-dbus.h" + +#include <gio/gio.h> + +struct _CcDisplayConfigManagerDBus +{ + CcDisplayConfigManager parent_instance; + + GCancellable *cancellable; + GDBusConnection *connection; + guint monitors_changed_id; + + GVariant *current_state; +}; + +G_DEFINE_TYPE (CcDisplayConfigManagerDBus, + cc_display_config_manager_dbus, + CC_TYPE_DISPLAY_CONFIG_MANAGER) + +static CcDisplayConfig * +cc_display_config_manager_dbus_get_current (CcDisplayConfigManager *pself) +{ + CcDisplayConfigManagerDBus *self = CC_DISPLAY_CONFIG_MANAGER_DBUS (pself); + + if (!self->current_state) + return NULL; + + return g_object_new (CC_TYPE_DISPLAY_CONFIG_DBUS, + "state", self->current_state, + "connection", self->connection, NULL); +} + +static void +got_current_state (GObject *object, + GAsyncResult *result, + gpointer data) +{ + CcDisplayConfigManagerDBus *self; + GVariant *variant; + g_autoptr(GError) error = NULL; + + variant = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), + result, &error); + if (!variant) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + self = CC_DISPLAY_CONFIG_MANAGER_DBUS (data); + g_clear_pointer (&self->current_state, g_variant_unref); + _cc_display_config_manager_emit_changed (CC_DISPLAY_CONFIG_MANAGER (data)); + g_warning ("Error calling GetCurrentState: %s", error->message); + } + return; + } + + self = CC_DISPLAY_CONFIG_MANAGER_DBUS (data); + g_clear_pointer (&self->current_state, g_variant_unref); + self->current_state = variant; + + _cc_display_config_manager_emit_changed (CC_DISPLAY_CONFIG_MANAGER (self)); +} + +static void +get_current_state (CcDisplayConfigManagerDBus *self) +{ + g_dbus_connection_call (self->connection, + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + "GetCurrentState", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + self->cancellable, + got_current_state, + self); +} + +static void +monitors_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer data) +{ + CcDisplayConfigManagerDBus *self = CC_DISPLAY_CONFIG_MANAGER_DBUS (data); + get_current_state (self); +} + +static void +bus_gotten (GObject *object, + GAsyncResult *result, + gpointer data) +{ + CcDisplayConfigManagerDBus *self; + GDBusConnection *connection; + g_autoptr(GError) error = NULL; + + connection = g_bus_get_finish (result, &error); + if (!connection) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + _cc_display_config_manager_emit_changed (CC_DISPLAY_CONFIG_MANAGER (data)); + g_warning ("Error obtaining DBus connection: %s", error->message); + } + return; + } + + self = CC_DISPLAY_CONFIG_MANAGER_DBUS (data); + self->connection = connection; + self->monitors_changed_id = + g_dbus_connection_signal_subscribe (self->connection, + "org.gnome.Mutter.DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + "MonitorsChanged", + "/org/gnome/Mutter/DisplayConfig", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + monitors_changed, + self, + NULL); + get_current_state (self); +} + +static void +cc_display_config_manager_dbus_init (CcDisplayConfigManagerDBus *self) +{ + self->cancellable = g_cancellable_new (); + g_bus_get (G_BUS_TYPE_SESSION, self->cancellable, bus_gotten, self); +} + +static void +cc_display_config_manager_dbus_finalize (GObject *object) +{ + CcDisplayConfigManagerDBus *self = CC_DISPLAY_CONFIG_MANAGER_DBUS (object); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + if (self->monitors_changed_id && self->connection) + g_dbus_connection_signal_unsubscribe (self->connection, + self->monitors_changed_id); + g_clear_object (&self->connection); + g_clear_pointer (&self->current_state, g_variant_unref); + + G_OBJECT_CLASS (cc_display_config_manager_dbus_parent_class)->finalize (object); +} + +static void +cc_display_config_manager_dbus_class_init (CcDisplayConfigManagerDBusClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + CcDisplayConfigManagerClass *parent_class = CC_DISPLAY_CONFIG_MANAGER_CLASS (klass); + + gobject_class->finalize = cc_display_config_manager_dbus_finalize; + + parent_class->get_current = cc_display_config_manager_dbus_get_current; +} + +CcDisplayConfigManager * +cc_display_config_manager_dbus_new (void) +{ + return g_object_new (CC_TYPE_DISPLAY_CONFIG_MANAGER_DBUS, NULL); +} diff --git a/panels/display/cc-display-config-manager-dbus.h b/panels/display/cc-display-config-manager-dbus.h new file mode 100644 index 0000000..a099598 --- /dev/null +++ b/panels/display/cc-display-config-manager-dbus.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#include <glib-object.h> + +#include "cc-display-config-manager.h" + +G_BEGIN_DECLS + +#define CC_TYPE_DISPLAY_CONFIG_MANAGER_DBUS (cc_display_config_manager_dbus_get_type ()) +G_DECLARE_FINAL_TYPE (CcDisplayConfigManagerDBus, cc_display_config_manager_dbus, + CC, DISPLAY_CONFIG_MANAGER_DBUS, CcDisplayConfigManager) + +CcDisplayConfigManager * cc_display_config_manager_dbus_new (void); + +G_END_DECLS diff --git a/panels/display/cc-display-config-manager.c b/panels/display/cc-display-config-manager.c new file mode 100644 index 0000000..0da298a --- /dev/null +++ b/panels/display/cc-display-config-manager.c @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "cc-display-config-manager.h" + +G_DEFINE_TYPE (CcDisplayConfigManager, + cc_display_config_manager, + G_TYPE_OBJECT) + +enum +{ + CONFIG_MANAGER_CHANGED, + N_CONFIG_MANAGER_SIGNALS, +}; + +static guint config_manager_signals[N_CONFIG_MANAGER_SIGNALS] = { 0 }; + +static void +cc_display_config_manager_init (CcDisplayConfigManager *self) +{ +} + +static void +cc_display_config_manager_class_init (CcDisplayConfigManagerClass *klass) +{ + config_manager_signals[CONFIG_MANAGER_CHANGED] = + g_signal_new ("changed", + CC_TYPE_DISPLAY_CONFIG_MANAGER, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +void +_cc_display_config_manager_emit_changed (CcDisplayConfigManager *self) +{ + g_signal_emit (self, config_manager_signals[CONFIG_MANAGER_CHANGED], 0); +} + +CcDisplayConfig * +cc_display_config_manager_get_current (CcDisplayConfigManager *self) +{ + return CC_DISPLAY_CONFIG_MANAGER_GET_CLASS (self)->get_current (self); +} diff --git a/panels/display/cc-display-config-manager.h b/panels/display/cc-display-config-manager.h new file mode 100644 index 0000000..1e1b363 --- /dev/null +++ b/panels/display/cc-display-config-manager.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#include <glib-object.h> + +#include "cc-display-config.h" + +G_BEGIN_DECLS + +#define CC_TYPE_DISPLAY_CONFIG_MANAGER (cc_display_config_manager_get_type ()) +G_DECLARE_DERIVABLE_TYPE (CcDisplayConfigManager, cc_display_config_manager, + CC, DISPLAY_CONFIG_MANAGER, GObject) + +struct _CcDisplayConfigManagerClass +{ + GObjectClass parent_class; + + CcDisplayConfig * (*get_current) (CcDisplayConfigManager *self); +}; + +CcDisplayConfig * cc_display_config_manager_get_current (CcDisplayConfigManager *self); + +void _cc_display_config_manager_emit_changed (CcDisplayConfigManager *self); + +G_END_DECLS diff --git a/panels/display/cc-display-config.c b/panels/display/cc-display-config.c new file mode 100644 index 0000000..08b4c48 --- /dev/null +++ b/panels/display/cc-display-config.c @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2016 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include <gio/gio.h> +#include <math.h> +#include "cc-display-config.h" + +static const double known_diagonals[] = { + 12.1, + 13.3, + 15.6 +}; + +static char * +diagonal_to_str (double d) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (known_diagonals); i++) + { + double delta; + + delta = fabs(known_diagonals[i] - d); + if (delta < 0.1) + return g_strdup_printf ("%0.1lf\"", known_diagonals[i]); + } + + return g_strdup_printf ("%d\"", (int) (d + 0.5)); +} + +static char * +make_display_size_string (int width_mm, + int height_mm) +{ + char *inches = NULL; + + if (width_mm > 0 && height_mm > 0) + { + double d = sqrt (width_mm * width_mm + height_mm * height_mm); + + inches = diagonal_to_str (d / 25.4); + } + + return inches; +} + +static char * +make_output_ui_name (CcDisplayMonitor *output) +{ + int width_mm, height_mm; + g_autofree char *size = NULL; + + cc_display_monitor_get_physical_size (output, &width_mm, &height_mm); + size = make_display_size_string (width_mm, height_mm); + if (size) + return g_strdup_printf ("%s (%s)", cc_display_monitor_get_display_name (output), size); + else + return g_strdup_printf ("%s", cc_display_monitor_get_display_name (output)); +} + + + +G_DEFINE_TYPE (CcDisplayMode, + cc_display_mode, + G_TYPE_OBJECT) + +static void +cc_display_mode_init (CcDisplayMode *self) +{ +} + +static void +cc_display_mode_class_init (CcDisplayModeClass *klass) +{ +} + +void +cc_display_mode_get_resolution (CcDisplayMode *self, int *w, int *h) +{ + return CC_DISPLAY_MODE_GET_CLASS (self)->get_resolution (self, w, h); +} + +const double * +cc_display_mode_get_supported_scales (CcDisplayMode *self) +{ + return CC_DISPLAY_MODE_GET_CLASS (self)->get_supported_scales (self); +} + +double +cc_display_mode_get_preferred_scale (CcDisplayMode *self) +{ + return CC_DISPLAY_MODE_GET_CLASS (self)->get_preferred_scale (self); +} + +gboolean +cc_display_mode_is_interlaced (CcDisplayMode *self) +{ + return CC_DISPLAY_MODE_GET_CLASS (self)->is_interlaced (self); +} + +int +cc_display_mode_get_freq (CcDisplayMode *self) +{ + return CC_DISPLAY_MODE_GET_CLASS (self)->get_freq (self); +} + +double +cc_display_mode_get_freq_f (CcDisplayMode *self) +{ + return CC_DISPLAY_MODE_GET_CLASS (self)->get_freq_f (self); +} + + +struct _CcDisplayMonitorPrivate { + int ui_number; + gchar *ui_name; + gchar *ui_number_name; + gboolean is_usable; +}; +typedef struct _CcDisplayMonitorPrivate CcDisplayMonitorPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (CcDisplayMonitor, + cc_display_monitor, + G_TYPE_OBJECT) + +static void +cc_display_monitor_init (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + priv->ui_number = 0; + priv->ui_name = NULL; + priv->ui_number_name = NULL; + priv->is_usable = TRUE; +} + +static void +cc_display_monitor_finalize (GObject *object) +{ + CcDisplayMonitor *self = CC_DISPLAY_MONITOR (object); + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + g_clear_pointer (&priv->ui_name, g_free); + g_clear_pointer (&priv->ui_number_name, g_free); + + G_OBJECT_CLASS (cc_display_monitor_parent_class)->finalize (object); +} + +static void +cc_display_monitor_class_init (CcDisplayMonitorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = cc_display_monitor_finalize; + + g_signal_new ("rotation", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("mode", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("primary", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("active", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("scale", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("position-changed", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("is-usable", + CC_TYPE_DISPLAY_MONITOR, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +const char * +cc_display_monitor_get_display_name (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_display_name (self); +} + +const char * +cc_display_monitor_get_connector_name (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_connector_name (self); +} + +gboolean +cc_display_monitor_is_builtin (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->is_builtin (self); +} + +gboolean +cc_display_monitor_is_primary (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->is_primary (self); +} + +void +cc_display_monitor_set_primary (CcDisplayMonitor *self, gboolean primary) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_primary (self, primary); +} + +gboolean +cc_display_monitor_is_active (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->is_active (self); +} + +void +cc_display_monitor_set_active (CcDisplayMonitor *self, gboolean active) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_active (self, active); +} + +CcDisplayRotation +cc_display_monitor_get_rotation (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_rotation (self); +} + +void +cc_display_monitor_set_rotation (CcDisplayMonitor *self, + CcDisplayRotation rotation) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_rotation (self, rotation); +} + +gboolean +cc_display_monitor_supports_rotation (CcDisplayMonitor *self, CcDisplayRotation r) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->supports_rotation (self, r); +} + +void +cc_display_monitor_get_physical_size (CcDisplayMonitor *self, int *w, int *h) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_physical_size (self, w, h); +} + +void +cc_display_monitor_get_geometry (CcDisplayMonitor *self, int *x, int *y, int *w, int *h) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_geometry (self, x, y, w, h); +} + +CcDisplayMode * +cc_display_monitor_get_mode (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_mode (self); +} + +CcDisplayMode * +cc_display_monitor_get_preferred_mode (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_preferred_mode (self); +} + +guint32 +cc_display_monitor_get_id (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_id (self); +} + +GList * +cc_display_monitor_get_modes (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_modes (self); +} + +gboolean +cc_display_monitor_supports_underscanning (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->supports_underscanning (self); +} + +gboolean +cc_display_monitor_get_underscanning (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_underscanning (self); +} + +void +cc_display_monitor_set_underscanning (CcDisplayMonitor *self, + gboolean underscanning) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_underscanning (self, underscanning); +} + +void +cc_display_monitor_set_mode (CcDisplayMonitor *self, CcDisplayMode *m) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_mode (self, m); +} + +void +cc_display_monitor_set_position (CcDisplayMonitor *self, int x, int y) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_position (self, x, y); +} + +double +cc_display_monitor_get_scale (CcDisplayMonitor *self) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->get_scale (self); +} + +void +cc_display_monitor_set_scale (CcDisplayMonitor *self, double s) +{ + return CC_DISPLAY_MONITOR_GET_CLASS (self)->set_scale (self, s); +} + +gboolean +cc_display_monitor_is_useful (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + return priv->is_usable && + cc_display_monitor_is_active (self); +} + +gboolean +cc_display_monitor_is_usable (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + return priv->is_usable; +} + +void +cc_display_monitor_set_usable (CcDisplayMonitor *self, gboolean is_usable) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + priv->is_usable = is_usable; + + g_signal_emit_by_name (self, "is-usable"); +} + +gint +cc_display_monitor_get_ui_number (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + return priv->ui_number; +} + +const char * +cc_display_monitor_get_ui_name (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + return priv->ui_name; +} + +const char * +cc_display_monitor_get_ui_number_name (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + return priv->ui_number_name; +} + +char * +cc_display_monitor_dup_ui_number_name (CcDisplayMonitor *self) +{ + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + return g_strdup (priv->ui_number_name); +} + +static void +cc_display_monitor_set_ui_info (CcDisplayMonitor *self, gint ui_number, gchar *ui_name) +{ + + CcDisplayMonitorPrivate *priv = cc_display_monitor_get_instance_private (self); + + priv->ui_number = ui_number; + g_free (priv->ui_name); + priv->ui_name = ui_name; + priv->ui_number_name = g_strdup_printf ("%d\u2003%s", ui_number, ui_name); +} + +struct _CcDisplayConfigPrivate { + GList *ui_sorted_monitors; +}; +typedef struct _CcDisplayConfigPrivate CcDisplayConfigPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (CcDisplayConfig, + cc_display_config, + G_TYPE_OBJECT) + +static void +cc_display_config_init (CcDisplayConfig *self) +{ + CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self); + + priv->ui_sorted_monitors = NULL; +} + +static void +cc_display_config_constructed (GObject *object) +{ + CcDisplayConfig *self = CC_DISPLAY_CONFIG (object); + CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self); + GList *monitors = cc_display_config_get_monitors (self); + GList *item; + gint ui_number = 1; + + for (item = monitors; item != NULL; item = item->next) + { + CcDisplayMonitor *monitor = item->data; + + if (cc_display_monitor_is_builtin (monitor)) + priv->ui_sorted_monitors = g_list_prepend (priv->ui_sorted_monitors, monitor); + else + priv->ui_sorted_monitors = g_list_append (priv->ui_sorted_monitors, monitor); + } + + for (item = priv->ui_sorted_monitors; item != NULL; item = item->next) + { + CcDisplayMonitor *monitor = item->data; + char *ui_name; + ui_name = make_output_ui_name (monitor); + + cc_display_monitor_set_ui_info (monitor, ui_number, ui_name); + + ui_number += 1; + } +} + +static void +cc_display_config_finalize (GObject *object) +{ + CcDisplayConfig *self = CC_DISPLAY_CONFIG (object); + CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self); + + g_list_free (priv->ui_sorted_monitors); + + G_OBJECT_CLASS (cc_display_config_parent_class)->finalize (object); +} + +static void +cc_display_config_class_init (CcDisplayConfigClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + g_signal_new ("primary", + CC_TYPE_DISPLAY_CONFIG, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + g_signal_new ("panel-orientation-managed", + CC_TYPE_DISPLAY_CONFIG, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); + + gobject_class->constructed = cc_display_config_constructed; + gobject_class->finalize = cc_display_config_finalize; +} + +GList * +cc_display_config_get_monitors (CcDisplayConfig *self) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), NULL); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->get_monitors (self); +} + +GList * +cc_display_config_get_ui_sorted_monitors (CcDisplayConfig *self) +{ + CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self); + + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), NULL); + return priv->ui_sorted_monitors; +} + +int +cc_display_config_count_useful_monitors (CcDisplayConfig *self) +{ + CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self); + GList *outputs, *l; + guint count = 0; + + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), 0); + + outputs = priv->ui_sorted_monitors; + for (l = outputs; l != NULL; l = l->next) + { + CcDisplayMonitor *output = l->data; + if (!cc_display_monitor_is_useful (output)) + continue; + else + count++; + } + return count; + +} + +gboolean +cc_display_config_is_applicable (CcDisplayConfig *self) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->is_applicable (self); +} + +void +cc_display_config_set_mode_on_all_outputs (CcDisplayConfig *config, + CcDisplayMode *mode) +{ + GList *outputs, *l; + + g_return_if_fail (CC_IS_DISPLAY_CONFIG (config)); + + outputs = cc_display_config_get_monitors (config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + cc_display_monitor_set_mode (output, mode); + cc_display_monitor_set_position (output, 0, 0); + } +} + +gboolean +cc_display_config_equal (CcDisplayConfig *self, + CcDisplayConfig *other) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE); + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (other), FALSE); + + return CC_DISPLAY_CONFIG_GET_CLASS (self)->equal (self, other); +} + +gboolean +cc_display_config_apply (CcDisplayConfig *self, + GError **error) +{ + if (!CC_IS_DISPLAY_CONFIG (self)) + { + g_warning ("Cannot apply invalid configuration"); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Cannot apply invalid configuration"); + return FALSE; + } + + return CC_DISPLAY_CONFIG_GET_CLASS (self)->apply (self, error); +} + +gboolean +cc_display_config_is_cloning (CcDisplayConfig *self) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->is_cloning (self); +} + +void +cc_display_config_set_cloning (CcDisplayConfig *self, + gboolean clone) +{ + g_return_if_fail (CC_IS_DISPLAY_CONFIG (self)); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->set_cloning (self, clone); +} + +GList * +cc_display_config_get_cloning_modes (CcDisplayConfig *self) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), NULL); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->get_cloning_modes (self); +} + +gboolean +cc_display_config_is_layout_logical (CcDisplayConfig *self) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->is_layout_logical (self); +} + +void +cc_display_config_set_minimum_size (CcDisplayConfig *self, + int width, + int height) +{ + g_return_if_fail (CC_IS_DISPLAY_CONFIG (self)); + CC_DISPLAY_CONFIG_GET_CLASS (self)->set_minimum_size (self, width, height); +} + +gboolean +cc_display_config_is_scaled_mode_valid (CcDisplayConfig *self, + CcDisplayMode *mode, + double scale) +{ + g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE); + g_return_val_if_fail (CC_IS_DISPLAY_MODE (mode), FALSE); + return CC_DISPLAY_CONFIG_GET_CLASS (self)->is_scaled_mode_valid (self, mode, scale); +} + +gboolean +cc_display_config_get_panel_orientation_managed (CcDisplayConfig *self) +{ + return CC_DISPLAY_CONFIG_GET_CLASS (self)->get_panel_orientation_managed (self); +} diff --git a/panels/display/cc-display-config.h b/panels/display/cc-display-config.h new file mode 100644 index 0000000..fa5b3a5 --- /dev/null +++ b/panels/display/cc-display-config.h @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2016 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +/* + * GNOME Control Center display configuration system: + * + * The display configuration system consists of multiple concepts: + * + * CcDisplayConfig: + * + * Configuration instance, read from mutter using the + * org.gnome.Mutter.DisplayConfig D-Bus API. Contains information about the + * current configuration. Can be copied, to create a representation of a + * configuration at a given time, and applied, applying any changes that has + * been made to the objects associated with the configuration. + * + * CcDisplayConfig provides a list of all known "monitors" known to the + * compositor. It does not know about ports without any monitors connected, + * nor low level details about monitors, such as tiling etc. + * + * CcDisplayMonitor: + * + * A high level representation of a connected monitor. A monitor have details + * associated with it, some which can be altered. Each CcDisplayMonitor + * instance is associated with a single CcDisplayConfig instance. All + * alteration to a monitor is cached and not applied until + * cc_display_config_apply() is called on the corresponding CcDisplayConfig + * object. + * + * CcDisplayMode: + * + * A monitor mode, including resolution, refresh rate, and scale. Each monitor + * will have a list of possible modes. + * + */ + +typedef enum _CcDisplayRotation +{ + CC_DISPLAY_ROTATION_NONE, + CC_DISPLAY_ROTATION_90, + CC_DISPLAY_ROTATION_180, + CC_DISPLAY_ROTATION_270, + CC_DISPLAY_ROTATION_FLIPPED, + CC_DISPLAY_ROTATION_90_FLIPPED, + CC_DISPLAY_ROTATION_180_FLIPPED, + CC_DISPLAY_ROTATION_270_FLIPPED, +} CcDisplayRotation; + + +#define CC_TYPE_DISPLAY_MODE (cc_display_mode_get_type ()) +G_DECLARE_DERIVABLE_TYPE (CcDisplayMode, cc_display_mode, + CC, DISPLAY_MODE, GObject) + +struct _CcDisplayModeClass +{ + GObjectClass parent_class; + + void (*get_resolution) (CcDisplayMode *self, int *w, int *h); + const double* (*get_supported_scales) (CcDisplayMode *self); + double (*get_preferred_scale) (CcDisplayMode *self); + gboolean (*is_interlaced) (CcDisplayMode *self); + int (*get_freq) (CcDisplayMode *self); + double (*get_freq_f) (CcDisplayMode *self); +}; + + +#define CC_TYPE_DISPLAY_MONITOR (cc_display_monitor_get_type ()) +G_DECLARE_DERIVABLE_TYPE (CcDisplayMonitor, cc_display_monitor, + CC, DISPLAY_MONITOR, GObject) + +struct _CcDisplayMonitorClass +{ + GObjectClass parent_class; + + guint32 (*get_id) (CcDisplayMonitor *self); + const char* (*get_display_name) (CcDisplayMonitor *self); + const char* (*get_connector_name) (CcDisplayMonitor *self); + gboolean (*is_builtin) (CcDisplayMonitor *self); + gboolean (*is_primary) (CcDisplayMonitor *self); + void (*set_primary) (CcDisplayMonitor *self, + gboolean primary); + gboolean (*is_active) (CcDisplayMonitor *self); + void (*set_active) (CcDisplayMonitor *self, + gboolean a); + CcDisplayRotation (*get_rotation) (CcDisplayMonitor *self); + void (*set_rotation) (CcDisplayMonitor *self, + CcDisplayRotation r); + gboolean (*supports_rotation) (CcDisplayMonitor *self, + CcDisplayRotation r); + void (*get_physical_size) (CcDisplayMonitor *self, + int *w, + int *h); + void (*get_geometry) (CcDisplayMonitor *self, + int *x, + int *y, + int *w, + int *h); + gboolean (*supports_underscanning) (CcDisplayMonitor *self); + gboolean (*get_underscanning) (CcDisplayMonitor *self); + void (*set_underscanning) (CcDisplayMonitor *self, + gboolean u); + CcDisplayMode* (*get_mode) (CcDisplayMonitor *self); + CcDisplayMode* (*get_preferred_mode) (CcDisplayMonitor *self); + GList* (*get_modes) (CcDisplayMonitor *self); + void (*set_mode) (CcDisplayMonitor *self, + CcDisplayMode *m); + void (*set_position) (CcDisplayMonitor *self, + int x, + int y); + double (*get_scale) (CcDisplayMonitor *self); + void (*set_scale) (CcDisplayMonitor *self, + double s); +}; + + +#define CC_TYPE_DISPLAY_CONFIG (cc_display_config_get_type ()) +G_DECLARE_DERIVABLE_TYPE (CcDisplayConfig, cc_display_config, + CC, DISPLAY_CONFIG, GObject) + +struct _CcDisplayConfigClass +{ + GObjectClass parent_class; + + GList* (*get_monitors) (CcDisplayConfig *self); + gboolean (*is_applicable) (CcDisplayConfig *self); + gboolean (*equal) (CcDisplayConfig *self, + CcDisplayConfig *other); + gboolean (*apply) (CcDisplayConfig *self, + GError **error); + gboolean (*is_cloning) (CcDisplayConfig *self); + void (*set_cloning) (CcDisplayConfig *self, + gboolean clone); + GList* (*get_cloning_modes) (CcDisplayConfig *self); + gboolean (*is_layout_logical) (CcDisplayConfig *self); + void (*set_minimum_size) (CcDisplayConfig *self, + int width, + int height); + gboolean (*is_scaled_mode_valid) (CcDisplayConfig *self, + CcDisplayMode *mode, + double scale); + gboolean (* get_panel_orientation_managed) (CcDisplayConfig *self); +}; + + +GList* cc_display_config_get_monitors (CcDisplayConfig *config); +GList* cc_display_config_get_ui_sorted_monitors (CcDisplayConfig *config); +int cc_display_config_count_useful_monitors (CcDisplayConfig *config); +gboolean cc_display_config_is_applicable (CcDisplayConfig *config); +gboolean cc_display_config_equal (CcDisplayConfig *config, + CcDisplayConfig *other); +gboolean cc_display_config_apply (CcDisplayConfig *config, + GError **error); +gboolean cc_display_config_is_cloning (CcDisplayConfig *config); +void cc_display_config_set_cloning (CcDisplayConfig *config, + gboolean clone); +GList* cc_display_config_get_cloning_modes (CcDisplayConfig *config); + +void cc_display_config_set_mode_on_all_outputs (CcDisplayConfig *config, + CcDisplayMode *mode); + +gboolean cc_display_config_is_layout_logical (CcDisplayConfig *self); +void cc_display_config_set_minimum_size (CcDisplayConfig *self, + int width, + int height); +gboolean cc_display_config_is_scaled_mode_valid (CcDisplayConfig *self, + CcDisplayMode *mode, + double scale); +gboolean cc_display_config_get_panel_orientation_managed + (CcDisplayConfig *self); + +const char* cc_display_monitor_get_display_name (CcDisplayMonitor *monitor); +gboolean cc_display_monitor_is_active (CcDisplayMonitor *monitor); +void cc_display_monitor_set_active (CcDisplayMonitor *monitor, + gboolean active); +const char* cc_display_monitor_get_connector_name (CcDisplayMonitor *monitor); +CcDisplayRotation cc_display_monitor_get_rotation (CcDisplayMonitor *monitor); +void cc_display_monitor_set_rotation (CcDisplayMonitor *monitor, + CcDisplayRotation r); +gboolean cc_display_monitor_supports_rotation (CcDisplayMonitor *monitor, + CcDisplayRotation rotation); +void cc_display_monitor_get_physical_size (CcDisplayMonitor *monitor, + int *w, + int *h); +gboolean cc_display_monitor_is_builtin (CcDisplayMonitor *monitor); +gboolean cc_display_monitor_is_primary (CcDisplayMonitor *monitor); +void cc_display_monitor_set_primary (CcDisplayMonitor *monitor, + gboolean primary); +guint32 cc_display_monitor_get_id (CcDisplayMonitor *monitor); + +gboolean cc_display_monitor_supports_underscanning (CcDisplayMonitor *monitor); +gboolean cc_display_monitor_get_underscanning (CcDisplayMonitor *monitor); +void cc_display_monitor_set_underscanning (CcDisplayMonitor *monitor, + gboolean underscanning); + +CcDisplayMode* cc_display_monitor_get_mode (CcDisplayMonitor *monitor); +void cc_display_monitor_get_geometry (CcDisplayMonitor *monitor, + int *x, + int *y, + int *width, + int *height); +GList* cc_display_monitor_get_modes (CcDisplayMonitor *monitor); +CcDisplayMode* cc_display_monitor_get_preferred_mode (CcDisplayMonitor *monitor); +double cc_display_monitor_get_scale (CcDisplayMonitor *monitor); +void cc_display_monitor_set_scale (CcDisplayMonitor *monitor, + double s); + +void cc_display_monitor_set_mode (CcDisplayMonitor *monitor, + CcDisplayMode *mode); +void cc_display_monitor_set_position (CcDisplayMonitor *monitor, + int x, + int y); + +gboolean cc_display_monitor_is_useful (CcDisplayMonitor *monitor); +gboolean cc_display_monitor_is_usable (CcDisplayMonitor *monitor); +void cc_display_monitor_set_usable (CcDisplayMonitor *monitor, + gboolean is_usable); +int cc_display_monitor_get_ui_number (CcDisplayMonitor *monitor); +const char* cc_display_monitor_get_ui_name (CcDisplayMonitor *monitor); +const char* cc_display_monitor_get_ui_number_name (CcDisplayMonitor *monitor); +char* cc_display_monitor_dup_ui_number_name (CcDisplayMonitor *monitor); + +void cc_display_mode_get_resolution (CcDisplayMode *mode, + int *width, + int *height); +const double* cc_display_mode_get_supported_scales (CcDisplayMode *self); +double cc_display_mode_get_preferred_scale (CcDisplayMode *self); +gboolean cc_display_mode_is_interlaced (CcDisplayMode *mode); +int cc_display_mode_get_freq (CcDisplayMode *mode); +double cc_display_mode_get_freq_f (CcDisplayMode *mode); + +G_END_DECLS diff --git a/panels/display/cc-display-panel.c b/panels/display/cc-display-panel.c new file mode 100644 index 0000000..f58aaa8 --- /dev/null +++ b/panels/display/cc-display-panel.c @@ -0,0 +1,1183 @@ +/* + * Copyright (C) 2007, 2008 Red Hat, Inc. + * Copyright (C) 2013 Intel, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "cc-display-panel.h" + +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <stdlib.h> +#include <gdesktop-enums.h> +#include <math.h> +#include <handy.h> + +#include "shell/cc-object-storage.h" +#include "list-box-helper.h" +#include <libupower-glib/upower.h> + +#include "cc-display-config-manager-dbus.h" +#include "cc-display-config.h" +#include "cc-display-arrangement.h" +#include "cc-night-light-page.h" +#include "cc-display-resources.h" +#include "cc-display-settings.h" + +/* The minimum supported size for the panel + * Note that WIDTH is assumed to be the larger size and we accept portrait + * mode too effectively (in principle we should probably restrict the rotation + * setting in that case). */ +#define MINIMUM_WIDTH 740 +#define MINIMUM_HEIGHT 530 + +#define PANEL_PADDING 32 +#define SECTION_PADDING 32 +#define HEADING_PADDING 12 + +typedef enum { + CC_DISPLAY_CONFIG_SINGLE, + CC_DISPLAY_CONFIG_JOIN, + CC_DISPLAY_CONFIG_CLONE, + + CC_DISPLAY_CONFIG_INVALID_NONE, +} CcDisplayConfigType; + +#define CC_DISPLAY_CONFIG_LAST_VALID CC_DISPLAY_CONFIG_CLONE + +struct _CcDisplayPanel +{ + CcPanel parent_instance; + + CcDisplayConfigManager *manager; + CcDisplayConfig *current_config; + CcDisplayMonitor *current_output; + + gint rebuilding_counter; + + CcDisplayArrangement *arrangement; + CcDisplaySettings *settings; + + guint focus_id; + + CcNightLightPage *night_light_page; + GtkDialog *night_light_dialog; + + UpClient *up_client; + gboolean lid_is_closed; + + GDBusProxy *shell_proxy; + + gchar *main_title; + GtkWidget *main_titlebar; + GtkWidget *apply_titlebar; + GtkWidget *apply_titlebar_apply; + GtkWidget *apply_titlebar_warning; + + GListStore *primary_display_list; + GtkListStore *output_selection_list; + + GtkWidget *arrangement_frame; + GtkAlignment *arrangement_bin; + GtkRadioButton *config_type_join; + GtkRadioButton *config_type_mirror; + GtkRadioButton *config_type_single; + GtkWidget *config_type_switcher_frame; + GtkLabel *current_output_label; + GtkWidget *display_settings_frame; + GtkBox *multi_selection_box; + GtkSwitch *output_enabled_switch; + GtkComboBox *output_selection_combo; + GtkStack *output_selection_stack; + GtkButtonBox *output_selection_two_buttonbox; + GtkRadioButton *output_selection_two_first; + GtkRadioButton *output_selection_two_second; + HdyComboRow *primary_display_row; + GtkWidget *stack_switcher; +}; + +CC_PANEL_REGISTER (CcDisplayPanel, cc_display_panel) + +static void +update_apply_button (CcDisplayPanel *panel); +static void +apply_current_configuration (CcDisplayPanel *self); +static void +reset_current_config (CcDisplayPanel *panel); +static void +rebuild_ui (CcDisplayPanel *panel); +static void +set_current_output (CcDisplayPanel *panel, + CcDisplayMonitor *output, + gboolean force); + + +static CcDisplayConfigType +config_get_current_type (CcDisplayPanel *panel) +{ + guint n_active_outputs; + GList *outputs, *l; + + outputs = cc_display_config_get_ui_sorted_monitors (panel->current_config); + n_active_outputs = 0; + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + if (cc_display_monitor_is_useful (output)) + n_active_outputs += 1; + } + + if (n_active_outputs == 0) + return CC_DISPLAY_CONFIG_INVALID_NONE; + + if (n_active_outputs == 1) + return CC_DISPLAY_CONFIG_SINGLE; + + if (cc_display_config_is_cloning (panel->current_config)) + return CC_DISPLAY_CONFIG_CLONE; + + return CC_DISPLAY_CONFIG_JOIN; +} + +static CcDisplayConfigType +cc_panel_get_selected_type (CcDisplayPanel *panel) +{ + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (panel->config_type_join))) + return CC_DISPLAY_CONFIG_JOIN; + else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (panel->config_type_mirror))) + return CC_DISPLAY_CONFIG_CLONE; + else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (panel->config_type_single))) + return CC_DISPLAY_CONFIG_SINGLE; + else + g_assert_not_reached (); +} + +static void +config_ensure_of_type (CcDisplayPanel *panel, CcDisplayConfigType type) +{ + CcDisplayConfigType current_type = config_get_current_type (panel); + GList *outputs, *l; + + /* Do not do anything if the current detected configuration type is + * identitcal to what we expect. */ + if (type == current_type) + return; + + reset_current_config (panel); + + outputs = cc_display_config_get_ui_sorted_monitors (panel->current_config); + + switch (type) + { + case CC_DISPLAY_CONFIG_SINGLE: + g_debug ("Creating new single config"); + /* Disable all but the current primary output */ + cc_display_config_set_cloning (panel->current_config, FALSE); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + /* Select the current primary output as the active one */ + if (cc_display_monitor_is_primary (output)) + { + cc_display_monitor_set_active (output, TRUE); + cc_display_monitor_set_mode (output, cc_display_monitor_get_preferred_mode (output)); + set_current_output (panel, output, FALSE); + } + else + { + cc_display_monitor_set_active (output, FALSE); + cc_display_monitor_set_mode (output, cc_display_monitor_get_preferred_mode (output)); + } + } + break; + + case CC_DISPLAY_CONFIG_JOIN: + g_debug ("Creating new join config"); + /* Enable all usable outputs + * Note that this might result in invalid configurations as we + * we might not be able to drive all attached monitors. */ + cc_display_config_set_cloning (panel->current_config, FALSE); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + cc_display_monitor_set_active (output, cc_display_monitor_is_usable (output)); + cc_display_monitor_set_mode (output, cc_display_monitor_get_preferred_mode (output)); + } + break; + + case CC_DISPLAY_CONFIG_CLONE: + { + g_debug ("Creating new clone config"); + GList *modes = cc_display_config_get_cloning_modes (panel->current_config); + gint bw, bh; + CcDisplayMode *best = NULL; + + /* Turn on cloning and select the best mode we can find by default */ + cc_display_config_set_cloning (panel->current_config, TRUE); + + while (modes) + { + CcDisplayMode *mode = modes->data; + gint w, h; + + cc_display_mode_get_resolution (mode, &w, &h); + if (best == NULL || (bw*bh < w*h)) + { + best = mode; + cc_display_mode_get_resolution (best, &bw, &bh); + } + + modes = modes->next; + } + cc_display_config_set_mode_on_all_outputs (panel->current_config, best); + } + break; + + default: + g_assert_not_reached (); + } + + rebuild_ui (panel); +} + +static void +cc_panel_set_selected_type (CcDisplayPanel *panel, CcDisplayConfigType type) +{ + switch (type) + { + case CC_DISPLAY_CONFIG_JOIN: + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (panel->config_type_join), TRUE); + break; + case CC_DISPLAY_CONFIG_CLONE: + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (panel->config_type_mirror), TRUE); + break; + case CC_DISPLAY_CONFIG_SINGLE: + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (panel->config_type_single), TRUE); + break; + default: + g_assert_not_reached (); + } + + config_ensure_of_type (panel, type); +} + +static void +monitor_labeler_hide (CcDisplayPanel *self) +{ + if (!self->shell_proxy) + return; + + g_dbus_proxy_call (self->shell_proxy, + "HideMonitorLabels", + NULL, G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); +} + +static void +monitor_labeler_show (CcDisplayPanel *self) +{ + GList *outputs, *l; + GVariantBuilder builder; + gint number = 0; + + if (!self->shell_proxy || !self->current_config) + return; + + outputs = cc_display_config_get_ui_sorted_monitors (self->current_config); + if (!outputs) + return; + + if (cc_display_config_is_cloning (self->current_config)) + return monitor_labeler_hide (self); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE); + g_variant_builder_open (&builder, G_VARIANT_TYPE_ARRAY); + + for (l = outputs; l != NULL; l = l->next) + { + CcDisplayMonitor *output = l->data; + + number = cc_display_monitor_get_ui_number (output); + if (number == 0) + continue; + + g_variant_builder_add (&builder, "{sv}", + cc_display_monitor_get_connector_name (output), + g_variant_new_int32 (number)); + } + + g_variant_builder_close (&builder); + + if (number < 2) + return monitor_labeler_hide (self); + + g_dbus_proxy_call (self->shell_proxy, + "ShowMonitorLabels", + g_variant_builder_end (&builder), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, NULL, NULL); +} + +static void +ensure_monitor_labels (CcDisplayPanel *self) +{ + g_autoptr(GList) windows = NULL; + GList *w; + + windows = gtk_window_list_toplevels (); + + for (w = windows; w; w = w->next) + { + if (gtk_window_has_toplevel_focus (GTK_WINDOW (w->data))) + { + monitor_labeler_show (self); + break; + } + } + + if (!w) + monitor_labeler_hide (self); +} + +static void +dialog_toplevel_focus_changed (CcDisplayPanel *self) +{ + ensure_monitor_labels (self); +} + +static void +reset_titlebar (CcDisplayPanel *self) +{ + GtkWidget *toplevel = cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (self))); + + if (self->main_titlebar) + { + gtk_window_set_titlebar (GTK_WINDOW (toplevel), self->main_titlebar); + g_clear_object (&self->main_titlebar); + + /* The split header bar will not reset the window title, so do that here. */ + gtk_window_set_title (GTK_WINDOW (toplevel), self->main_title); + g_clear_pointer (&self->main_title, g_free); + } + + g_clear_object (&self->apply_titlebar); + g_clear_object (&self->apply_titlebar_apply); + g_clear_object (&self->apply_titlebar_warning); +} + +static void +active_panel_changed (CcPanel *self) +{ + CcShell *shell; + g_autoptr(CcPanel) panel = NULL; + + shell = cc_panel_get_shell (CC_PANEL (self)); + g_object_get (shell, "active-panel", &panel, NULL); + if (panel != self) + reset_titlebar (CC_DISPLAY_PANEL (self)); +} + +static void +cc_display_panel_dispose (GObject *object) +{ + CcDisplayPanel *self = CC_DISPLAY_PANEL (object); + + reset_titlebar (CC_DISPLAY_PANEL (object)); + + if (self->focus_id) + { + self->focus_id = 0; + monitor_labeler_hide (CC_DISPLAY_PANEL (object)); + } + + g_clear_object (&self->manager); + g_clear_object (&self->current_config); + g_clear_object (&self->up_client); + + g_clear_object (&self->shell_proxy); + + g_clear_pointer ((GtkWidget **) &self->night_light_dialog, gtk_widget_destroy); + + G_OBJECT_CLASS (cc_display_panel_parent_class)->dispose (object); +} + +static void +on_arrangement_selected_ouptut_changed_cb (CcDisplayPanel *panel) +{ + set_current_output (panel, cc_display_arrangement_get_selected_output (panel->arrangement), FALSE); +} + +static void +on_monitor_settings_updated_cb (CcDisplayPanel *panel, + CcDisplayMonitor *monitor, + CcDisplaySettings *settings) +{ + if (monitor) + cc_display_config_snap_output (panel->current_config, monitor); + update_apply_button (panel); +} + +static void +on_config_type_toggled_cb (CcDisplayPanel *panel, + GtkRadioButton *btn) +{ + CcDisplayConfigType type; + + if (panel->rebuilding_counter > 0) + return; + + if (!panel->current_config) + return; + + if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (btn))) + return; + + type = cc_panel_get_selected_type (panel); + config_ensure_of_type (panel, type); +} + +static void +on_night_light_list_box_row_activated_cb (CcDisplayPanel *panel) +{ + GtkWindow *toplevel; + toplevel = GTK_WINDOW (cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (panel)))); + + if (!panel->night_light_dialog) + { + GtkWidget *content_area; + + panel->night_light_dialog = (GtkDialog *)gtk_dialog_new (); + + content_area = gtk_dialog_get_content_area (panel->night_light_dialog); + gtk_container_add (GTK_CONTAINER (content_area), + GTK_WIDGET (panel->night_light_page)); + gtk_widget_show (GTK_WIDGET (panel->night_light_page)); + } + + gtk_window_set_transient_for (GTK_WINDOW (panel->night_light_dialog), toplevel); + gtk_window_present (GTK_WINDOW (panel->night_light_dialog)); +} + +static void +on_output_enabled_active_changed_cb (CcDisplayPanel *panel) +{ + gboolean active; + + if (!panel->current_output) + return; + + active = gtk_switch_get_active (panel->output_enabled_switch); + + if (cc_display_monitor_is_active (panel->current_output) == active) + return; + + cc_display_monitor_set_active (panel->current_output, active); + + /* Prevent the invalid configuration of disabling the last monitor + * by switching on a different one. */ + if (config_get_current_type (panel) == CC_DISPLAY_CONFIG_INVALID_NONE) + { + GList *outputs, *l; + + outputs = cc_display_config_get_ui_sorted_monitors (panel->current_config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = CC_DISPLAY_MONITOR (l->data); + + if (output == panel->current_output) + continue; + + if (!cc_display_monitor_is_usable (output)) + continue; + + cc_display_monitor_set_active (output, TRUE); + cc_display_monitor_set_primary (output, TRUE); + break; + } + } + + /* Changing the active state requires a UI rebuild. */ + rebuild_ui (panel); +} + +static void +on_output_selection_combo_changed_cb (CcDisplayPanel *panel) +{ + GtkTreeIter iter; + g_autoptr(CcDisplayMonitor) output = NULL; + + if (!panel->current_config) + return; + + if (!gtk_combo_box_get_active_iter (panel->output_selection_combo, &iter)) + return; + + gtk_tree_model_get (GTK_TREE_MODEL (panel->output_selection_list), &iter, + 1, &output, + -1); + + set_current_output (panel, output, FALSE); +} + +static void +on_output_selection_two_toggled_cb (CcDisplayPanel *panel, GtkRadioButton *btn) +{ + CcDisplayMonitor *output; + + if (panel->rebuilding_counter > 0) + return; + + if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (btn))) + return; + + output = g_object_get_data (G_OBJECT (btn), "display"); + + /* Stay in single mode when we are in single mode. + * This UI must never cause a switch between the configuration type. + * this is in contrast to the combobox monitor selection, which may + * switch to a disabled output both in SINGLE/MULTI mode without + * anything changing. + */ + if (cc_panel_get_selected_type (panel) == CC_DISPLAY_CONFIG_SINGLE) + { + if (panel->current_output) + cc_display_monitor_set_active (panel->current_output, FALSE); + if (output) + cc_display_monitor_set_active (output, TRUE); + + update_apply_button (panel); + } + + set_current_output (panel, g_object_get_data (G_OBJECT (btn), "display"), FALSE); +} + +static void +on_primary_display_selected_index_changed_cb (CcDisplayPanel *panel) +{ + gint idx = hdy_combo_row_get_selected_index (panel->primary_display_row); + g_autoptr(CcDisplayMonitor) output = NULL; + + if (idx < 0 || panel->rebuilding_counter > 0) + return; + + output = g_list_model_get_item (G_LIST_MODEL (panel->primary_display_list), idx); + + if (cc_display_monitor_is_primary (output)) + return; + + cc_display_monitor_set_primary (output, TRUE); + update_apply_button (panel); +} + +static void +cc_display_panel_constructed (GObject *object) +{ + g_signal_connect_object (cc_panel_get_shell (CC_PANEL (object)), "notify::active-panel", + G_CALLBACK (active_panel_changed), object, G_CONNECT_SWAPPED); + + G_OBJECT_CLASS (cc_display_panel_parent_class)->constructed (object); +} + +static const char * +cc_display_panel_get_help_uri (CcPanel *panel) +{ + return "help:gnome-help/prefs-display"; +} + +static GtkWidget * +cc_display_panel_get_title_widget (CcPanel *panel) +{ + CcDisplayPanel *self = CC_DISPLAY_PANEL (panel); + + return self->stack_switcher; +} + +static void +cc_display_panel_class_init (CcDisplayPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + CcPanelClass *panel_class = CC_PANEL_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + g_type_ensure (CC_TYPE_NIGHT_LIGHT_PAGE); + + panel_class->get_help_uri = cc_display_panel_get_help_uri; + panel_class->get_title_widget = cc_display_panel_get_title_widget; + + object_class->constructed = cc_display_panel_constructed; + object_class->dispose = cc_display_panel_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/display/cc-display-panel.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, arrangement_frame); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, arrangement_bin); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, config_type_switcher_frame); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, config_type_join); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, config_type_mirror); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, config_type_single); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, current_output_label); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, display_settings_frame); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, multi_selection_box); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, night_light_page); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, output_enabled_switch); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, output_selection_combo); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, output_selection_stack); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, output_selection_two_buttonbox); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, output_selection_two_first); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, output_selection_two_second); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, primary_display_row); + gtk_widget_class_bind_template_child (widget_class, CcDisplayPanel, stack_switcher); + + gtk_widget_class_bind_template_callback (widget_class, on_config_type_toggled_cb); + gtk_widget_class_bind_template_callback (widget_class, on_night_light_list_box_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_output_enabled_active_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_output_selection_combo_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_output_selection_two_toggled_cb); + gtk_widget_class_bind_template_callback (widget_class, on_primary_display_selected_index_changed_cb); +} + +static void +set_current_output (CcDisplayPanel *panel, + CcDisplayMonitor *output, + gboolean force) +{ + GtkTreeIter iter; + gboolean changed; + + /* Note, this function is also called if the internal UI needs updating after a rebuild. */ + changed = (output != panel->current_output); + + if (!changed && !force) + return; + + panel->rebuilding_counter++; + + panel->current_output = output; + + if (panel->current_output) + { + gtk_label_set_text (panel->current_output_label, cc_display_monitor_get_ui_name (panel->current_output)); + gtk_switch_set_active (panel->output_enabled_switch, cc_display_monitor_is_active (panel->current_output)); + gtk_widget_set_sensitive (GTK_WIDGET (panel->output_enabled_switch), cc_display_monitor_is_usable (panel->current_output)); + } + else + { + gtk_label_set_text (panel->current_output_label, ""); + gtk_switch_set_active (panel->output_enabled_switch, FALSE); + gtk_widget_set_sensitive (GTK_WIDGET (panel->output_enabled_switch), FALSE); + } + + if (g_object_get_data (G_OBJECT (panel->output_selection_two_first), "display") == output) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (panel->output_selection_two_first), TRUE); + if (g_object_get_data (G_OBJECT (panel->output_selection_two_second), "display") == output) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (panel->output_selection_two_second), TRUE); + + gtk_tree_model_get_iter_first (GTK_TREE_MODEL (panel->output_selection_list), &iter); + while (gtk_list_store_iter_is_valid (panel->output_selection_list, &iter)) + { + g_autoptr(CcDisplayMonitor) o = NULL; + + gtk_tree_model_get (GTK_TREE_MODEL (panel->output_selection_list), &iter, + 1, &o, + -1); + + if (o == panel->current_output) + { + gtk_combo_box_set_active_iter (panel->output_selection_combo, &iter); + break; + } + + gtk_tree_model_iter_next (GTK_TREE_MODEL (panel->output_selection_list), &iter); + } + + if (changed) + { + cc_display_settings_set_selected_output (panel->settings, panel->current_output); + cc_display_arrangement_set_selected_output (panel->arrangement, panel->current_output); + } + + panel->rebuilding_counter--; +} + +static void +rebuild_ui (CcDisplayPanel *panel) +{ + guint n_outputs, n_active_outputs, n_usable_outputs; + GList *outputs, *l; + CcDisplayConfigType type; + + panel->rebuilding_counter++; + + g_list_store_remove_all (panel->primary_display_list); + gtk_list_store_clear (panel->output_selection_list); + + if (!panel->current_config) + { + panel->rebuilding_counter--; + return; + } + + n_active_outputs = 0; + n_usable_outputs = 0; + outputs = cc_display_config_get_ui_sorted_monitors (panel->current_config); + for (l = outputs; l; l = l->next) + { + GtkTreeIter iter; + CcDisplayMonitor *output = l->data; + + gtk_list_store_append (panel->output_selection_list, &iter); + gtk_list_store_set (panel->output_selection_list, + &iter, + 0, cc_display_monitor_get_ui_number_name (output), + 1, output, + -1); + + if (!cc_display_monitor_is_usable (output)) + continue; + + n_usable_outputs += 1; + + if (n_usable_outputs == 1) + { + gtk_button_set_label (GTK_BUTTON (panel->output_selection_two_first), + cc_display_monitor_get_ui_name (output)); + g_object_set_data (G_OBJECT (panel->output_selection_two_first), + "display", + output); + } + else if (n_usable_outputs == 2) + { + gtk_button_set_label (GTK_BUTTON (panel->output_selection_two_second), + cc_display_monitor_get_ui_name (output)); + g_object_set_data (G_OBJECT (panel->output_selection_two_second), + "display", + output); + } + + if (cc_display_monitor_is_active (output)) + { + n_active_outputs += 1; + + g_list_store_append (panel->primary_display_list, output); + if (cc_display_monitor_is_primary (output)) + hdy_combo_row_set_selected_index (panel->primary_display_row, + g_list_model_get_n_items (G_LIST_MODEL (panel->primary_display_list)) - 1); + + /* Ensure that an output is selected; note that this doesn't ensure + * the selected output is any useful (i.e. when switching types). + */ + if (!panel->current_output) + set_current_output (panel, output, FALSE); + } + } + + /* Sync the rebuild lists/buttons */ + set_current_output (panel, panel->current_output, TRUE); + + n_outputs = g_list_length (outputs); + type = config_get_current_type (panel); + + if (n_outputs == 2 && n_usable_outputs == 2) + { + /* We only show the top chooser with two monitors that are + * both usable (i.e. two monitors incl. internal and lid not closed). + * In this case, the arrangement widget is shown if we are in JOIN mode. + */ + if (type > CC_DISPLAY_CONFIG_LAST_VALID) + type = CC_DISPLAY_CONFIG_JOIN; + + gtk_widget_set_visible (panel->config_type_switcher_frame, TRUE); + gtk_widget_set_visible (panel->arrangement_frame, type == CC_DISPLAY_CONFIG_JOIN); + + /* We need a switcher except in CLONE mode */ + if (type == CC_DISPLAY_CONFIG_CLONE) + gtk_stack_set_visible_child (panel->output_selection_stack, GTK_WIDGET (panel->current_output_label)); + else + gtk_stack_set_visible_child (panel->output_selection_stack, GTK_WIDGET (panel->output_selection_two_buttonbox)); + } + else if (n_usable_outputs > 1) + { + /* We have more than one usable monitor. In this case there is no chooser, + * and we always show the arrangement widget even if we are in SINGLE mode. + */ + gtk_widget_set_visible (panel->config_type_switcher_frame, FALSE); + gtk_widget_set_visible (panel->arrangement_frame, TRUE); + + /* Mirror is also invalid as it cannot be configured using this UI. */ + if (type == CC_DISPLAY_CONFIG_CLONE || type > CC_DISPLAY_CONFIG_LAST_VALID) + type = CC_DISPLAY_CONFIG_JOIN; + + gtk_stack_set_visible_child (panel->output_selection_stack, GTK_WIDGET (panel->multi_selection_box)); + } + else + { + /* We only have a single usable monitor, show neither configuration type + * switcher nor arrangement widget and ensure we really are in SINGLE + * mode (and not e.g. mirroring across one display) */ + type = CC_DISPLAY_CONFIG_SINGLE; + + gtk_widget_set_visible (panel->config_type_switcher_frame, FALSE); + gtk_widget_set_visible (panel->arrangement_frame, FALSE); + + gtk_stack_set_visible_child (panel->output_selection_stack, GTK_WIDGET (panel->current_output_label)); + } + + cc_panel_set_selected_type (panel, type); + + panel->rebuilding_counter--; + update_apply_button (panel); +} + +static void +update_panel_orientation_managed (CcDisplayPanel *panel, + gboolean managed) +{ + cc_display_settings_set_has_accelerometer (panel->settings, managed); +} + +static void +reset_current_config (CcDisplayPanel *panel) +{ + CcDisplayConfig *current; + CcDisplayConfig *old; + GList *outputs, *l; + + g_debug ("Resetting current config!"); + + /* We need to hold on to the config until all display references are dropped. */ + old = panel->current_config; + panel->current_output = NULL; + + current = cc_display_config_manager_get_current (panel->manager); + + if (!current) + return; + + cc_display_config_set_minimum_size (current, MINIMUM_WIDTH, MINIMUM_HEIGHT); + panel->current_config = current; + + g_signal_connect_object (current, "panel-orientation-managed", + G_CALLBACK (update_panel_orientation_managed), panel, + G_CONNECT_SWAPPED); + update_panel_orientation_managed (panel, + cc_display_config_get_panel_orientation_managed (current)); + + g_list_store_remove_all (panel->primary_display_list); + gtk_list_store_clear (panel->output_selection_list); + + if (panel->current_config) + { + outputs = cc_display_config_get_ui_sorted_monitors (panel->current_config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + /* Mark any builtin monitor as unusable if the lid is closed. */ + if (cc_display_monitor_is_builtin (output) && panel->lid_is_closed) + cc_display_monitor_set_usable (output, FALSE); + } + } + + cc_display_arrangement_set_config (panel->arrangement, panel->current_config); + cc_display_settings_set_config (panel->settings, panel->current_config); + set_current_output (panel, NULL, FALSE); + + g_clear_object (&old); + + update_apply_button (panel); +} + +static void +on_screen_changed (CcDisplayPanel *panel) +{ + if (!panel->manager) + return; + + reset_titlebar (panel); + + reset_current_config (panel); + rebuild_ui (panel); + + if (!panel->current_config) + return; + + ensure_monitor_labels (panel); +} + +static gboolean +on_toplevel_key_press (GtkWidget *button, + GdkEventKey *event) +{ + if (event->keyval != GDK_KEY_Escape) + return GDK_EVENT_PROPAGATE; + + g_signal_emit_by_name (button, "activate"); + return GDK_EVENT_STOP; +} + +static void +show_apply_titlebar (CcDisplayPanel *panel, gboolean is_applicable) +{ + if (!panel->apply_titlebar) + { + g_autoptr(GtkSizeGroup) size_group = NULL; + GtkWidget *header, *button, *toplevel; + panel->apply_titlebar = header = gtk_header_bar_new (); + gtk_widget_show (header); + + size_group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); + + button = gtk_button_new_with_mnemonic (_("_Cancel")); + gtk_widget_show (button); + gtk_header_bar_pack_start (GTK_HEADER_BAR (header), button); + gtk_size_group_add_widget (size_group, button); + g_signal_connect_object (button, "clicked", G_CALLBACK (on_screen_changed), + panel, G_CONNECT_SWAPPED); + + toplevel = cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (panel))); + g_signal_connect_object (toplevel, "key-press-event", G_CALLBACK (on_toplevel_key_press), + button, G_CONNECT_SWAPPED); + + panel->apply_titlebar_apply = button = gtk_button_new_with_mnemonic (_("_Apply")); + gtk_widget_show (button); + gtk_header_bar_pack_end (GTK_HEADER_BAR (header), button); + gtk_size_group_add_widget (size_group, button); + g_signal_connect_object (button, "clicked", G_CALLBACK (apply_current_configuration), + panel, G_CONNECT_SWAPPED); + gtk_style_context_add_class (gtk_widget_get_style_context (button), + GTK_STYLE_CLASS_SUGGESTED_ACTION); + + header = gtk_window_get_titlebar (GTK_WINDOW (toplevel)); + if (header) + panel->main_titlebar = g_object_ref (header); + panel->main_title = g_strdup (gtk_window_get_title (GTK_WINDOW (toplevel))); + + gtk_window_set_titlebar (GTK_WINDOW (toplevel), panel->apply_titlebar); + g_object_ref (panel->apply_titlebar); + g_object_ref (panel->apply_titlebar_apply); + } + + if (is_applicable) + { + gtk_header_bar_set_title (GTK_HEADER_BAR (panel->apply_titlebar), _("Apply Changes?")); + gtk_header_bar_set_subtitle (GTK_HEADER_BAR (panel->apply_titlebar), NULL); + } + else + { + gtk_header_bar_set_title (GTK_HEADER_BAR (panel->apply_titlebar), _("Changes Cannot be Applied")); + gtk_header_bar_set_subtitle (GTK_HEADER_BAR (panel->apply_titlebar), _("This could be due to hardware limitations.")); + } + gtk_widget_set_sensitive (panel->apply_titlebar_apply, is_applicable); +} + +static void +update_apply_button (CcDisplayPanel *panel) +{ + gboolean config_equal; + g_autoptr(CcDisplayConfig) applied_config = NULL; + + if (!panel->current_config) + { + reset_titlebar (panel); + return; + } + + applied_config = cc_display_config_manager_get_current (panel->manager); + + config_equal = cc_display_config_equal (panel->current_config, + applied_config); + + if (config_equal) + reset_titlebar (panel); + else + show_apply_titlebar (panel, cc_display_config_is_applicable (panel->current_config)); +} + +static void +apply_current_configuration (CcDisplayPanel *self) +{ + g_autoptr(GError) error = NULL; + + cc_display_config_apply (self->current_config, &error); + + /* re-read the configuration */ + on_screen_changed (self); + + if (error) + g_warning ("Error applying configuration: %s", error->message); +} + +static void +mapped_cb (CcDisplayPanel *panel) +{ + CcShell *shell; + GtkWidget *toplevel; + + shell = cc_panel_get_shell (CC_PANEL (panel)); + toplevel = cc_shell_get_toplevel (shell); + if (toplevel && !panel->focus_id) + panel->focus_id = g_signal_connect_object (toplevel, "notify::has-toplevel-focus", + G_CALLBACK (dialog_toplevel_focus_changed), panel, G_CONNECT_SWAPPED); +} + +static void +cc_display_panel_up_client_changed (CcDisplayPanel *self) +{ + gboolean lid_is_closed; + + lid_is_closed = up_client_get_lid_is_closed (self->up_client); + + if (lid_is_closed != self->lid_is_closed) + { + self->lid_is_closed = lid_is_closed; + + on_screen_changed (self); + } +} + +static void +shell_proxy_ready (GObject *source, + GAsyncResult *res, + CcDisplayPanel *self) +{ + GDBusProxy *proxy; + g_autoptr(GError) error = NULL; + + proxy = cc_object_storage_create_dbus_proxy_finish (res, &error); + if (!proxy) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to contact gnome-shell: %s", error->message); + return; + } + + self->shell_proxy = proxy; + + ensure_monitor_labels (self); +} + +static void +session_bus_ready (GObject *source, + GAsyncResult *res, + CcDisplayPanel *self) +{ + GDBusConnection *bus; + g_autoptr(GError) error = NULL; + + bus = g_bus_get_finish (res, &error); + if (!bus) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Failed to get session bus: %s", error->message); + } + return; + } + + self->manager = cc_display_config_manager_dbus_new (); + g_signal_connect_object (self->manager, "changed", + G_CALLBACK (on_screen_changed), + self, + G_CONNECT_SWAPPED); +} + +static void +cc_display_panel_init (CcDisplayPanel *self) +{ + g_autoptr(GtkCssProvider) provider = NULL; + GtkCellRenderer *renderer; + + g_resources_register (cc_display_get_resource ()); + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->arrangement = cc_display_arrangement_new (NULL); + + gtk_widget_show (GTK_WIDGET (self->arrangement)); + gtk_widget_set_size_request (GTK_WIDGET (self->arrangement), 400, 175); + gtk_container_add (GTK_CONTAINER (self->arrangement_bin), GTK_WIDGET (self->arrangement)); + + g_signal_connect_object (self->arrangement, "updated", + G_CALLBACK (update_apply_button), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->arrangement, "notify::selected-output", + G_CALLBACK (on_arrangement_selected_ouptut_changed_cb), self, + G_CONNECT_SWAPPED); + + self->settings = cc_display_settings_new (); + gtk_widget_show (GTK_WIDGET (self->settings)); + gtk_container_add (GTK_CONTAINER (self->display_settings_frame), GTK_WIDGET (self->settings)); + g_signal_connect_object (self->settings, "updated", + G_CALLBACK (on_monitor_settings_updated_cb), self, + G_CONNECT_SWAPPED); + + self->primary_display_list = g_list_store_new (CC_TYPE_DISPLAY_MONITOR); + hdy_combo_row_bind_name_model (self->primary_display_row, + G_LIST_MODEL (self->primary_display_list), + (HdyComboRowGetNameFunc) cc_display_monitor_dup_ui_number_name, + NULL, NULL); + + self->output_selection_list = gtk_list_store_new (2, G_TYPE_STRING, CC_TYPE_DISPLAY_MONITOR); + gtk_combo_box_set_model (self->output_selection_combo, GTK_TREE_MODEL (self->output_selection_list)); + gtk_cell_layout_clear (GTK_CELL_LAYOUT (self->output_selection_combo)); + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (self->output_selection_combo), + renderer, + TRUE); + gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (self->output_selection_combo), + renderer, + "text", + 0); + gtk_cell_renderer_set_visible (renderer, TRUE); + + self->up_client = up_client_new (); + if (up_client_get_lid_is_present (self->up_client)) + { + g_signal_connect_object (self->up_client, "notify::lid-is-closed", + G_CALLBACK (cc_display_panel_up_client_changed), self, G_CONNECT_SWAPPED); + cc_display_panel_up_client_changed (self); + } + else + g_clear_object (&self->up_client); + + g_signal_connect (self, "map", G_CALLBACK (mapped_cb), NULL); + + cc_object_storage_create_dbus_proxy (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + "org.gnome.Shell", + "/org/gnome/Shell", + "org.gnome.Shell", + cc_panel_get_cancellable (CC_PANEL (self)), + (GAsyncReadyCallback) shell_proxy_ready, + self); + + g_bus_get (G_BUS_TYPE_SESSION, + cc_panel_get_cancellable (CC_PANEL (self)), + (GAsyncReadyCallback) session_bus_ready, + self); + + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (provider, "/org/gnome/control-center/display/display-arrangement.css"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} diff --git a/panels/display/cc-display-panel.h b/panels/display/cc-display-panel.h new file mode 100644 index 0000000..a7ade28 --- /dev/null +++ b/panels/display/cc-display-panel.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2010 Intel, Inc + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Author: Thomas Wood <thomas.wood@intel.com> + * + */ + +#pragma once + +#include <shell/cc-panel.h> + +G_BEGIN_DECLS + +#define CC_TYPE_DISPLAY_PANEL (cc_display_panel_get_type ()) +G_DECLARE_FINAL_TYPE (CcDisplayPanel, cc_display_panel, CC, DISPLAY_PANEL, CcPanel) + +G_END_DECLS diff --git a/panels/display/cc-display-panel.ui b/panels/display/cc-display-panel.ui new file mode 100644 index 0000000..855b348 --- /dev/null +++ b/panels/display/cc-display-panel.ui @@ -0,0 +1,458 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <requires lib="libhandy" version="0.0"/> + <object class="GtkImage" id="image-join-displays"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="icon_name">video-joined-displays-symbolic</property> + <property name="icon_size">3</property> + </object> + <object class="GtkImage" id="image-mirror"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="icon_name">view-mirror-symbolic</property> + <property name="icon_size">3</property> + </object> + <object class="GtkImage" id="image-single-display"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <property name="margin_top">12</property> + <property name="margin_bottom">12</property> + <property name="icon_name">video-single-display-symbolic</property> + <property name="icon_size">3</property> + </object> + <object class="GtkStackSwitcher" id="stack_switcher"> + <property name="visible">True</property> + <property name="stack">stack</property> + </object> + <template class="CcDisplayPanel" parent="CcPanel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="transition-type">crossfade</property> + + <!-- Displays page --> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hscrollbar_policy">never</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="margin_top">32</property> + <property name="margin_bottom">32</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">32</property> + <child> + <object class="GtkFrame" id="config_type_switcher_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">True</property> + <property name="layout_style">expand</property> + <child> + <object class="GtkRadioButton" id="config_type_single"> + <property name="label" translatable="yes">Single Display</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="image">image-single-display</property> + <property name="always_show_image">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <property name="group">config_type_join</property> + <signal name="toggled" handler="on_config_type_toggled_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="config_type_join"> + <property name="label" translatable="yes">Join Displays</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="image">image-join-displays</property> + <property name="always_show_image">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <signal name="toggled" handler="on_config_type_toggled_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="config_type_mirror"> + <property name="label" translatable="yes">Mirror</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="image">image-mirror</property> + <property name="always_show_image">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <property name="group">config_type_join</property> + <signal name="toggled" handler="on_config_type_toggled_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_bottom">12</property> + <property name="label" translatable="yes">Display Mode</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="arrangement_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="label_yalign">1</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkAlignment" id="arrangement_bin"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + </child> + <child type="label_item"> + <placeholder/> + </child> + <style> + <class name="view"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="selection_mode">none</property> + <child> + <object class="HdyComboRow" id="primary_display_row"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="subtitle" translatable="yes">Contains top bar and Activities</property> + <property name="title" translatable="yes">Primary Display</property> + <signal name="notify::selected-index" handler="on_primary_display_selected_index_changed_cb" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="label_item"> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Drag displays to match your physical display setup. Select a display to change its settings.</property> + <property name="wrap">True</property> + <property name="margin_bottom">12</property> + </object> + </child> + <child internal-child="accessible"> + <object class="AtkObject" id="frame_arrangement-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes">Display Arrangement</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkStack" id="output_selection_stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hhomogeneous">False</property> + <property name="vhomogeneous">False</property> + <child> + <object class="GtkLabel" id="current_output_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButtonBox" id="output_selection_two_buttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">expand</property> + <child> + <object class="GtkRadioButton" id="output_selection_two_first"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">False</property> + <property name="group">output_selection_two_second</property> + <signal name="toggled" handler="on_output_selection_two_toggled_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="output_selection_two_second"> + <property name="label" translatable="yes">Join Displays</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">False</property> + <signal name="toggled" handler="on_output_selection_two_toggled_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="multi_selection_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkComboBox" id="output_selection_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <signal name="changed" handler="on_output_selection_combo_changed_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="output_enabled_switch"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="valign">center</property> + <signal name="notify::active" handler="on_output_enabled_active_changed_cb" swapped="yes"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="selection_mode">none</property> + <child> + <object class="HdyComboRow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="title" translatable="yes">Active Display</property> + </object> + </child> + </object> + </child> + <child type="label_item"> + <placeholder/> + </child> + </object> + <packing> + <property name="name">single-select</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="display_settings_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + <child type="label_item"> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label_item"> + <placeholder/> + </child> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="AtkObject::accessible-name" translatable="yes">Display Configuration</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">displays</property> + <property name="title" translatable="yes">Displays</property> + </packing> + </child> + + <!-- Night Light page --> + <child> + <object class="CcNightLightPage" id="night_light_page"> + <property name="visible">True</property> + </object> + <packing> + <property name="name">night-light</property> + <property name="title" translatable="yes" comments="This is the redshift functionality where we suppress blue light when the sun has gone down">Night Light</property> + </packing> + </child> + + </object> + </child> + </template> +</interface> diff --git a/panels/display/cc-display-settings.c b/panels/display/cc-display-settings.c new file mode 100644 index 0000000..49ddcf0 --- /dev/null +++ b/panels/display/cc-display-settings.c @@ -0,0 +1,798 @@ +/* cc-display-settings.c + * + * Copyright (C) 2007, 2008, 2018, 2019 Red Hat, Inc. + * Copyright (C) 2013 Intel, Inc. + * + * Written by: Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include <handy.h> +#include <glib/gi18n.h> +#include <math.h> +#include "list-box-helper.h" +#include "cc-display-settings.h" +#include "cc-display-config.h" + +#define MAX_SCALE_BUTTONS 5 + +struct _CcDisplaySettings +{ + GtkDrawingArea object; + + gboolean updating; + guint idle_udpate_id; + + gboolean has_accelerometer; + CcDisplayConfig *config; + CcDisplayMonitor *selected_output; + + GListStore *orientation_list; + GListStore *refresh_rate_list; + GListStore *resolution_list; + + GtkWidget *orientation_row; + GtkWidget *refresh_rate_row; + GtkWidget *resolution_row; + GtkWidget *scale_bbox; + GtkWidget *scale_row; + GtkWidget *underscanning_row; + GtkWidget *underscanning_switch; +}; + +typedef struct _CcDisplaySettings CcDisplaySettings; + +enum { + PROP_0, + PROP_HAS_ACCELEROMETER, + PROP_CONFIG, + PROP_SELECTED_OUTPUT, + PROP_LAST +}; + +G_DEFINE_TYPE (CcDisplaySettings, cc_display_settings, GTK_TYPE_LIST_BOX) + +static GParamSpec *props[PROP_LAST]; + +static void on_scale_btn_active_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + CcDisplaySettings *self); + + +static gboolean +should_show_rotation (CcDisplaySettings *self) +{ + gboolean supports_rotation; + + supports_rotation = cc_display_monitor_supports_rotation (self->selected_output, + CC_DISPLAY_ROTATION_90 | + CC_DISPLAY_ROTATION_180 | + CC_DISPLAY_ROTATION_270); + + /* Doesn't support rotation at all */ + if (!supports_rotation) + return FALSE; + + /* We can always rotate displays that aren't builtin */ + if (!cc_display_monitor_is_builtin (self->selected_output)) + return TRUE; + + /* Only offer rotation if there's no accelerometer */ + return !self->has_accelerometer; +} + +static const gchar * +string_for_rotation (CcDisplayRotation rotation) +{ + switch (rotation) + { + case CC_DISPLAY_ROTATION_NONE: + case CC_DISPLAY_ROTATION_180_FLIPPED: + return C_("Display rotation", "Landscape"); + case CC_DISPLAY_ROTATION_90: + case CC_DISPLAY_ROTATION_270_FLIPPED: + return C_("Display rotation", "Portrait Right"); + case CC_DISPLAY_ROTATION_270: + case CC_DISPLAY_ROTATION_90_FLIPPED: + return C_("Display rotation", "Portrait Left"); + case CC_DISPLAY_ROTATION_180: + case CC_DISPLAY_ROTATION_FLIPPED: + return C_("Display rotation", "Landscape (flipped)"); + } + return ""; +} + +static const gchar * +make_aspect_string (gint width, + gint height) +{ + int ratio; + const gchar *aspect = NULL; + + /* We use a number of Unicode characters below: + * ∶ is U+2236 RATIO + *   is U+2009 THIN SPACE, + * × is U+00D7 MULTIPLICATION SIGN + */ + if (width && height) { + if (width > height) + ratio = width * 10 / height; + else + ratio = height * 10 / width; + + switch (ratio) { + case 13: + aspect = "4∶3"; + break; + case 16: + aspect = "16∶10"; + break; + case 17: + aspect = "16∶9"; + break; + case 23: + aspect = "21∶9"; + break; + case 12: + aspect = "5∶4"; + break; + /* This catches 1.5625 as well (1600x1024) when maybe it shouldn't. */ + case 15: + aspect = "3∶2"; + break; + case 18: + aspect = "9∶5"; + break; + case 10: + aspect = "1∶1"; + break; + } + } + + return aspect; +} + +static char * +make_resolution_string (CcDisplayMode *mode) +{ + const char *interlaced = cc_display_mode_is_interlaced (mode) ? "i" : ""; + const char *aspect; + int width, height; + + cc_display_mode_get_resolution (mode, &width, &height); + aspect = make_aspect_string (width, height); + + if (aspect != NULL) + return g_strdup_printf ("%d × %d%s (%s)", width, height, interlaced, aspect); + else + return g_strdup_printf ("%d × %d%s", width, height, interlaced); +} + +static gchar * +get_frequency_string (CcDisplayMode *mode) +{ + return g_strdup_printf (_("%.2lf Hz"), cc_display_mode_get_freq_f (mode)); +} + +static double +round_scale_for_ui (double scale) +{ + /* Keep in sync with mutter */ + return round (scale*4)/4; +} + +static gchar * +make_scale_string (gdouble scale) +{ + return g_strdup_printf ("%d %%", (int) (round_scale_for_ui (scale)*100)); +} + +static gint +sort_modes_by_area_desc (CcDisplayMode *a, CcDisplayMode *b) +{ + gint wa, ha, wb, hb; + gint res; + + cc_display_mode_get_resolution (a, &wa, &ha); + cc_display_mode_get_resolution (b, &wb, &hb); + + /* Prefer wide screen if the size is equal */ + res = wb*hb - wa*ha; + if (res == 0) + return wb - wa; + return res; +} + +static gint +sort_modes_by_freq_desc (CcDisplayMode *a, CcDisplayMode *b) +{ + double delta = (cc_display_mode_get_freq_f (b) - cc_display_mode_get_freq_f (a))*1000.; + return delta; +} + +static gboolean +cc_display_settings_rebuild_ui (CcDisplaySettings *self) +{ + GList *modes; + GList *item; + gint width, height; + CcDisplayMode *current_mode; + GtkRadioButton *group = NULL; + gint buttons = 0; + const gdouble *scales, *scale; + + self->idle_udpate_id = 0; + + if (!self->config || !self->selected_output) + { + gtk_widget_set_visible (self->orientation_row, FALSE); + gtk_widget_set_visible (self->refresh_rate_row, FALSE); + gtk_widget_set_visible (self->resolution_row, FALSE); + gtk_widget_set_visible (self->scale_row, FALSE); + gtk_widget_set_visible (self->underscanning_row, FALSE); + + return G_SOURCE_REMOVE; + } + + g_object_freeze_notify ((GObject*) self->orientation_row); + g_object_freeze_notify ((GObject*) self->refresh_rate_row); + g_object_freeze_notify ((GObject*) self->resolution_row); + g_object_freeze_notify ((GObject*) self->underscanning_switch); + + cc_display_monitor_get_geometry (self->selected_output, NULL, NULL, &width, &height); + + /* Selecte the first mode we can find if the monitor is disabled. */ + current_mode = cc_display_monitor_get_mode (self->selected_output); + if (current_mode == NULL) + current_mode = cc_display_monitor_get_preferred_mode (self->selected_output); + if (current_mode == NULL) { + modes = cc_display_monitor_get_modes (self->selected_output); + /* Lets assume that a monitor always has at least one mode. */ + g_assert (modes); + current_mode = CC_DISPLAY_MODE (modes->data); + } + + if (should_show_rotation (self)) + { + guint i; + CcDisplayRotation rotations[] = { CC_DISPLAY_ROTATION_NONE, + CC_DISPLAY_ROTATION_90, + CC_DISPLAY_ROTATION_270, + CC_DISPLAY_ROTATION_180 }; + + gtk_widget_set_visible (self->orientation_row, TRUE); + + g_list_store_remove_all (self->orientation_list); + for (i = 0; i < G_N_ELEMENTS (rotations); i++) + { + g_autoptr(HdyValueObject) obj = NULL; + + if (!cc_display_monitor_supports_rotation (self->selected_output, rotations[i])) + continue; + + obj = hdy_value_object_new_collect (G_TYPE_STRING, string_for_rotation (rotations[i])); + g_list_store_append (self->orientation_list, obj); + g_object_set_data (G_OBJECT (obj), "rotation-value", GINT_TO_POINTER (rotations[i])); + + if (cc_display_monitor_get_rotation (self->selected_output) == rotations[i]) + hdy_combo_row_set_selected_index (HDY_COMBO_ROW (self->orientation_row), + g_list_model_get_n_items (G_LIST_MODEL (self->orientation_list)) - 1); + } + } + else + { + gtk_widget_set_visible (self->orientation_row, FALSE); + } + + /* Only show refresh rate if we are not in cloning mode. */ + if (!cc_display_config_is_cloning (self->config)) + { + GList *item; + gdouble freq; + + freq = cc_display_mode_get_freq_f (current_mode); + + modes = cc_display_monitor_get_modes (self->selected_output); + + g_list_store_remove_all (self->refresh_rate_list); + + for (item = modes; item != NULL; item = item->next) + { + gint w, h; + guint new; + CcDisplayMode *mode = CC_DISPLAY_MODE (item->data); + + cc_display_mode_get_resolution (mode, &w, &h); + if (w != width || h != height) + continue; + + /* At some point we used to filter very close resolutions, + * but we don't anymore these days. + */ + new = g_list_store_insert_sorted (self->refresh_rate_list, + mode, + (GCompareDataFunc) sort_modes_by_freq_desc, + NULL); + if (freq == cc_display_mode_get_freq_f (mode)) + hdy_combo_row_set_selected_index (HDY_COMBO_ROW (self->refresh_rate_row), new); + } + + /* Show if we have more than one frequency to choose from. */ + gtk_widget_set_visible (self->refresh_rate_row, + g_list_model_get_n_items (G_LIST_MODEL (self->refresh_rate_list)) > 1); + } + else + { + gtk_widget_set_visible (self->refresh_rate_row, FALSE); + } + + + /* Resolutions are always shown. */ + gtk_widget_set_visible (self->resolution_row, TRUE); + if (cc_display_config_is_cloning (self->config)) + modes = cc_display_config_get_cloning_modes (self->config); + else + modes = cc_display_monitor_get_modes (self->selected_output); + + g_list_store_remove_all (self->resolution_list); + g_list_store_append (self->resolution_list, current_mode); + hdy_combo_row_set_selected_index (HDY_COMBO_ROW (self->resolution_row), 0); + for (item = modes; item != NULL; item = item->next) + { + gint ins; + gint w, h; + CcDisplayMode *mode = CC_DISPLAY_MODE (item->data); + + /* Exclude unusable low resolutions */ + if (!cc_display_config_is_scaled_mode_valid (self->config, mode, 1.0)) + continue; + + cc_display_mode_get_resolution (mode, &w, &h); + + /* Find the appropriate insertion point. */ + for (ins = 0; ins < g_list_model_get_n_items (G_LIST_MODEL (self->resolution_list)); ins++) + { + g_autoptr(CcDisplayMode) m = NULL; + gint cmp; + + m = g_list_model_get_item (G_LIST_MODEL (self->resolution_list), ins); + + cmp = sort_modes_by_area_desc (mode, m); + /* Next item is smaller, insert at this point. */ + if (cmp < 0) + break; + + /* Don't insert if it is already in the list */ + if (cmp == 0) + { + ins = -1; + break; + } + } + + if (ins >= 0) + g_list_store_insert (self->resolution_list, ins, mode); + } + + + /* Scale row is usually shown. */ + gtk_container_foreach (GTK_CONTAINER (self->scale_bbox), (GtkCallback) gtk_widget_destroy, NULL); + scales = cc_display_mode_get_supported_scales (current_mode); + for (scale = scales; *scale != 0.0; scale++) + { + g_autofree gchar *scale_str = NULL; + GtkWidget *scale_btn; + + if (!cc_display_config_is_scaled_mode_valid (self->config, + current_mode, + *scale) && + cc_display_monitor_get_scale (self->selected_output) != *scale) + continue; + + scale_str = make_scale_string (*scale); + + scale_btn = gtk_radio_button_new_with_label_from_widget (group, scale_str); + if (!group) + group = GTK_RADIO_BUTTON (scale_btn); + gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (scale_btn), FALSE); + g_object_set_data_full (G_OBJECT (scale_btn), + "scale", + g_memdup (scale, sizeof (gdouble)), + g_free); + gtk_widget_show (scale_btn); + gtk_container_add (GTK_CONTAINER (self->scale_bbox), scale_btn); + g_signal_connect_object (scale_btn, + "notify::active", + G_CALLBACK (on_scale_btn_active_changed_cb), + self, 0); + + if (cc_display_monitor_get_scale (self->selected_output) == *scale) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (scale_btn), TRUE); + + buttons += 1; + if (buttons >= MAX_SCALE_BUTTONS) + break; + } + + gtk_widget_set_visible (self->scale_row, buttons > 1); + + gtk_widget_set_visible (self->underscanning_row, + cc_display_monitor_supports_underscanning (self->selected_output) && + !cc_display_config_is_cloning (self->config)); + gtk_switch_set_active (GTK_SWITCH (self->underscanning_switch), + cc_display_monitor_get_underscanning (self->selected_output)); + + self->updating = TRUE; + g_object_thaw_notify ((GObject*) self->orientation_row); + g_object_thaw_notify ((GObject*) self->refresh_rate_row); + g_object_thaw_notify ((GObject*) self->resolution_row); + g_object_thaw_notify ((GObject*) self->underscanning_switch); + self->updating = FALSE; + + return G_SOURCE_REMOVE; +} + +static void +on_output_changed_cb (CcDisplaySettings *self, + GParamSpec *pspec, + CcDisplayMonitor *output) +{ + /* Do this frmo an idle handler, because otherwise we may create an + * infinite loop triggering the notify::selected-index from the + * combo rows. */ + if (self->idle_udpate_id) + return; + + self->idle_udpate_id = g_idle_add ((GSourceFunc) cc_display_settings_rebuild_ui, self); +} + +static void +on_orientation_selection_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + CcDisplaySettings *self) +{ + gint idx; + g_autoptr(HdyValueObject) obj = NULL; + + if (self->updating) + return; + + idx = hdy_combo_row_get_selected_index (HDY_COMBO_ROW (self->orientation_row)); + obj = g_list_model_get_item (G_LIST_MODEL (self->orientation_list), idx); + + cc_display_monitor_set_rotation (self->selected_output, + GPOINTER_TO_INT (g_object_get_data (G_OBJECT (obj), "rotation-value"))); + + g_signal_emit_by_name (G_OBJECT (self), "updated", self->selected_output); +} + +static void +on_refresh_rate_selection_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + CcDisplaySettings *self) +{ + gint idx; + g_autoptr(CcDisplayMode) mode = NULL; + + if (self->updating) + return; + + idx = hdy_combo_row_get_selected_index (HDY_COMBO_ROW (self->refresh_rate_row)); + mode = g_list_model_get_item (G_LIST_MODEL (self->refresh_rate_list), idx); + + cc_display_monitor_set_mode (self->selected_output, mode); + + g_signal_emit_by_name (G_OBJECT (self), "updated", self->selected_output); +} + +static void +on_resolution_selection_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + CcDisplaySettings *self) +{ + gint idx; + g_autoptr(CcDisplayMode) mode = NULL; + + if (self->updating) + return; + + idx = hdy_combo_row_get_selected_index (HDY_COMBO_ROW (self->resolution_row)); + mode = g_list_model_get_item (G_LIST_MODEL (self->resolution_list), idx); + + /* This is the only row that can be changed when in cloning mode. */ + if (!cc_display_config_is_cloning (self->config)) + cc_display_monitor_set_mode (self->selected_output, mode); + else + cc_display_config_set_mode_on_all_outputs (self->config, mode); + + g_signal_emit_by_name (G_OBJECT (self), "updated", self->selected_output); +} + +static void +on_scale_btn_active_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + CcDisplaySettings *self) +{ + gdouble scale; + if (self->updating) + return; + + if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget))) + return; + + scale = *(gdouble*) g_object_get_data (G_OBJECT (widget), "scale"); + cc_display_monitor_set_scale (self->selected_output, + scale); + + g_signal_emit_by_name (G_OBJECT (self), "updated", self->selected_output); +} + +static void +on_underscanning_switch_active_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + CcDisplaySettings *self) +{ + if (self->updating) + return; + + cc_display_monitor_set_underscanning (self->selected_output, + gtk_switch_get_active (GTK_SWITCH (self->underscanning_switch))); + + g_signal_emit_by_name (G_OBJECT (self), "updated", self->selected_output); +} + +static void +cc_display_settings_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcDisplaySettings *self = CC_DISPLAY_SETTINGS (object); + + switch (prop_id) + { + case PROP_HAS_ACCELEROMETER: + g_value_set_boolean (value, cc_display_settings_get_has_accelerometer (self)); + break; + + case PROP_CONFIG: + g_value_set_object (value, self->config); + break; + + case PROP_SELECTED_OUTPUT: + g_value_set_object (value, self->selected_output); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_display_settings_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcDisplaySettings *self = CC_DISPLAY_SETTINGS (object); + + switch (prop_id) + { + case PROP_HAS_ACCELEROMETER: + cc_display_settings_set_has_accelerometer (self, g_value_get_boolean (value)); + break; + + case PROP_CONFIG: + cc_display_settings_set_config (self, g_value_get_object (value)); + break; + + case PROP_SELECTED_OUTPUT: + cc_display_settings_set_selected_output (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_display_settings_finalize (GObject *object) +{ + CcDisplaySettings *self = CC_DISPLAY_SETTINGS (object); + + g_clear_object (&self->config); + + g_clear_object (&self->orientation_list); + g_clear_object (&self->refresh_rate_list); + g_clear_object (&self->resolution_list); + + if (self->idle_udpate_id) + g_source_remove (self->idle_udpate_id); + self->idle_udpate_id = 0; + + G_OBJECT_CLASS (cc_display_settings_parent_class)->finalize (object); +} + +static void +cc_display_settings_class_init (CcDisplaySettingsClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gobject_class->finalize = cc_display_settings_finalize; + gobject_class->get_property = cc_display_settings_get_property; + gobject_class->set_property = cc_display_settings_set_property; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/display/cc-display-settings.ui"); + + props[PROP_HAS_ACCELEROMETER] = + g_param_spec_boolean ("has-accelerometer", "Has Accelerometer", + "If an accelerometre is available for the builtin display", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_CONFIG] = + g_param_spec_object ("config", "Display Config", + "The display configuration to work with", + CC_TYPE_DISPLAY_CONFIG, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_SELECTED_OUTPUT] = + g_param_spec_object ("selected-output", "Selected Output", + "The output that is currently selected on the configuration", + CC_TYPE_DISPLAY_MONITOR, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, + PROP_LAST, + props); + + g_signal_new ("updated", + CC_TYPE_DISPLAY_SETTINGS, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, CC_TYPE_DISPLAY_MONITOR); + + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, orientation_row); + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, refresh_rate_row); + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, resolution_row); + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, scale_bbox); + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, scale_row); + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, underscanning_row); + gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, underscanning_switch); + + gtk_widget_class_bind_template_callback (widget_class, on_orientation_selection_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_refresh_rate_selection_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_resolution_selection_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_underscanning_switch_active_changed_cb); +} + +static void +cc_display_settings_init (CcDisplaySettings *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self), + cc_list_box_update_header_func, + NULL, NULL); + + self->orientation_list = g_list_store_new (HDY_TYPE_VALUE_OBJECT); + self->refresh_rate_list = g_list_store_new (CC_TYPE_DISPLAY_MODE); + self->resolution_list = g_list_store_new (CC_TYPE_DISPLAY_MODE); + + self->updating = TRUE; + + hdy_combo_row_bind_name_model (HDY_COMBO_ROW (self->orientation_row), + G_LIST_MODEL (self->orientation_list), + (HdyComboRowGetNameFunc) hdy_value_object_dup_string, + NULL, NULL); + hdy_combo_row_bind_name_model (HDY_COMBO_ROW (self->refresh_rate_row), + G_LIST_MODEL (self->refresh_rate_list), + (HdyComboRowGetNameFunc) get_frequency_string, + NULL, NULL); + hdy_combo_row_bind_name_model (HDY_COMBO_ROW (self->resolution_row), + G_LIST_MODEL (self->resolution_list), + (HdyComboRowGetNameFunc) make_resolution_string, + NULL, NULL); + + self->updating = FALSE; +} + +CcDisplaySettings* +cc_display_settings_new (void) +{ + return g_object_new (CC_TYPE_DISPLAY_SETTINGS, + NULL); +} + +gboolean +cc_display_settings_get_has_accelerometer (CcDisplaySettings *settings) +{ + return settings->has_accelerometer; +} + +void +cc_display_settings_set_has_accelerometer (CcDisplaySettings *self, + gboolean has_accelerometer) +{ + self->has_accelerometer = has_accelerometer; + + cc_display_settings_rebuild_ui (self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONFIG]); +} + +CcDisplayConfig* +cc_display_settings_get_config (CcDisplaySettings *self) +{ + return self->config; +} + +void +cc_display_settings_set_config (CcDisplaySettings *self, + CcDisplayConfig *config) +{ + const gchar *signals[] = { "rotation", "mode", "scale", "is-usable", "active" }; + GList *outputs, *l; + guint i; + + if (self->config) + { + outputs = cc_display_config_get_monitors (self->config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + g_signal_handlers_disconnect_by_data (output, self); + } + } + g_clear_object (&self->config); + + self->config = g_object_ref (config); + + /* Listen to all the signals */ + if (self->config) + { + outputs = cc_display_config_get_monitors (self->config); + for (l = outputs; l; l = l->next) + { + CcDisplayMonitor *output = l->data; + + for (i = 0; i < G_N_ELEMENTS (signals); ++i) + g_signal_connect_object (output, signals[i], G_CALLBACK (on_output_changed_cb), self, G_CONNECT_SWAPPED); + } + } + + cc_display_settings_set_selected_output (self, NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONFIG]); +} + +CcDisplayMonitor* +cc_display_settings_get_selected_output (CcDisplaySettings *self) +{ + return self->selected_output; +} + +void +cc_display_settings_set_selected_output (CcDisplaySettings *self, + CcDisplayMonitor *output) +{ + self->selected_output = output; + + cc_display_settings_rebuild_ui (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_OUTPUT]); +} + diff --git a/panels/display/cc-display-settings.h b/panels/display/cc-display-settings.h new file mode 100644 index 0000000..58709dd --- /dev/null +++ b/panels/display/cc-display-settings.h @@ -0,0 +1,44 @@ +/* -*- mode: c; style: linux -*- + * + * Copyright (C) 2019 Red Hat, Inc. + * + * Written by: Benjamin Berg <bberg@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <gtk/gtk.h> +#include "cc-display-config.h" + +G_BEGIN_DECLS + +#define CC_TYPE_DISPLAY_SETTINGS cc_display_settings_get_type () +G_DECLARE_FINAL_TYPE (CcDisplaySettings, cc_display_settings, CC, DISPLAY_SETTINGS, GtkListBox); + +CcDisplaySettings* cc_display_settings_new (void); + +gboolean cc_display_settings_get_has_accelerometer (CcDisplaySettings *settings); +void cc_display_settings_set_has_accelerometer (CcDisplaySettings *settings, + gboolean has_accelerometer); +CcDisplayConfig* cc_display_settings_get_config (CcDisplaySettings *settings); +void cc_display_settings_set_config (CcDisplaySettings *settings, + CcDisplayConfig *config); +CcDisplayMonitor* cc_display_settings_get_selected_output (CcDisplaySettings *settings); +void cc_display_settings_set_selected_output (CcDisplaySettings *settings, + CcDisplayMonitor *output); + +G_END_DECLS + diff --git a/panels/display/cc-display-settings.ui b/panels/display/cc-display-settings.ui new file mode 100644 index 0000000..7715ba3 --- /dev/null +++ b/panels/display/cc-display-settings.ui @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <requires lib="libhandy" version="0.0"/> + <template class="CcDisplaySettings" parent="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + <child> + <object class="HdyComboRow" id="orientation_row"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="title" translatable="yes" context="display setting">Orientation</property> + <signal name="notify::selected-index" handler="on_orientation_selection_changed_cb" swapped="no"/> + </object> + </child> + <child> + <object class="HdyComboRow" id="resolution_row"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="title" translatable="yes" context="display setting">Resolution</property> + <signal name="notify::selected-index" handler="on_resolution_selection_changed_cb" swapped="no"/> + </object> + </child> + <child> + <object class="HdyComboRow" id="refresh_rate_row"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="title" translatable="yes">Refresh Rate</property> + <signal name="notify::selected-index" handler="on_refresh_rate_selection_changed_cb" swapped="no"/> + </object> + </child> + <child> + <object class="HdyActionRow" id="underscanning_row"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="title" translatable="yes">Adjust for TV</property> + <child> + <object class="GtkSwitch" id="underscanning_switch"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="valign">center</property> + <signal name="notify::active" handler="on_underscanning_switch_active_changed_cb" swapped="no"/> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow" id="scale_row"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="title" translatable="yes" context="display setting">Scale</property> + <child> + <object class="GtkButtonBox" id="scale_bbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="valign">center</property> + <property name="layout_style">expand</property> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/panels/display/cc-night-light-page.c b/panels/display/cc-night-light-page.c new file mode 100644 index 0000000..f51b0ba --- /dev/null +++ b/panels/display/cc-night-light-page.c @@ -0,0 +1,712 @@ +/* + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <gdesktop-enums.h> +#include <gtk/gtk.h> +#include <glib/gi18n.h> +#include <math.h> + +#include "cc-night-light-page.h" +#include "list-box-helper.h" + +#include "shell/cc-object-storage.h" + +struct _CcNightLightPage { + GtkBin parent; + + GtkWidget *box_manual; + GtkButton *button_from_am; + GtkButton *button_from_pm; + GtkButton *button_to_am; + GtkButton *button_to_pm; + GtkWidget *infobar_disabled; + GtkListBox *listbox; + GtkWidget *scale_color_temperature; + GtkWidget *night_light_toggle_switch; + GtkComboBox *schedule_type_combo; + GtkWidget *spinbutton_from_hours; + GtkWidget *spinbutton_from_minutes; + GtkWidget *spinbutton_to_hours; + GtkWidget *spinbutton_to_minutes; + GtkStack *stack_from; + GtkStack *stack_to; + + GtkAdjustment *adjustment_from_hours; + GtkAdjustment *adjustment_from_minutes; + GtkAdjustment *adjustment_to_hours; + GtkAdjustment *adjustment_to_minutes; + GtkAdjustment *adjustment_color_temperature; + + GSettings *settings_display; + GSettings *settings_clock; + GDBusProxy *proxy_color; + GDBusProxy *proxy_color_props; + GCancellable *cancellable; + gboolean ignore_value_changed; + guint timer_id; + GDesktopClockFormat clock_format; +}; + +G_DEFINE_TYPE (CcNightLightPage, cc_night_light_page, GTK_TYPE_BIN); + +#define CLOCK_SCHEMA "org.gnome.desktop.interface" +#define DISPLAY_SCHEMA "org.gnome.settings-daemon.plugins.color" +#define CLOCK_FORMAT_KEY "clock-format" +#define NIGHT_LIGHT_PREVIEW_TIMEOUT_SECONDS 5 + +static void +dialog_adjustments_set_frac_hours (CcNightLightPage *self, + gdouble value, + GtkAdjustment *adj_hours, + GtkAdjustment *adj_mins, + GtkStack *stack, + GtkButton *button_am, + GtkButton *button_pm) +{ + gdouble hours; + gdouble mins = 0.f; + gboolean is_pm = FALSE; + gboolean is_24h; + + /* display the right thing for AM/PM */ + is_24h = self->clock_format == G_DESKTOP_CLOCK_FORMAT_24H; + mins = modf (value, &hours) * 60.f; + if (!is_24h) + { + if (hours > 12) + { + hours -= 12; + is_pm = TRUE; + } + else if (hours < 1.0) + { + hours += 12; + is_pm = FALSE; + } + else if (hours == 12.f) + { + is_pm = TRUE; + } + } + + g_debug ("setting adjustment %.3f to %.0f:%02.0f", value, hours, mins); + + self->ignore_value_changed = TRUE; + gtk_adjustment_set_value (GTK_ADJUSTMENT (adj_hours), hours); + gtk_adjustment_set_value (GTK_ADJUSTMENT (adj_mins), mins); + self->ignore_value_changed = FALSE; + + gtk_widget_set_visible (GTK_WIDGET (stack), !is_24h); + gtk_stack_set_visible_child (stack, is_pm ? GTK_WIDGET (button_pm) : GTK_WIDGET (button_am)); +} + +static void +dialog_update_state (CcNightLightPage *self) +{ + gboolean automatic; + gboolean disabled_until_tomorrow = FALSE; + gboolean enabled; + gdouble value = 0.f; + + /* only show the infobar if we are disabled */ + if (self->proxy_color != NULL) + { + g_autoptr(GVariant) disabled = NULL; + disabled = g_dbus_proxy_get_cached_property (self->proxy_color, + "DisabledUntilTomorrow"); + if (disabled != NULL) + disabled_until_tomorrow = g_variant_get_boolean (disabled); + } + gtk_widget_set_visible (self->infobar_disabled, disabled_until_tomorrow); + + /* make things insensitive if the switch is disabled */ + enabled = g_settings_get_boolean (self->settings_display, "night-light-enabled"); + automatic = g_settings_get_boolean (self->settings_display, "night-light-schedule-automatic"); + + gtk_widget_set_sensitive (self->box_manual, enabled && !automatic); + + gtk_combo_box_set_active_id (self->schedule_type_combo, automatic ? "automatic" : "manual"); + + /* set from */ + if (automatic && self->proxy_color != NULL) + { + g_autoptr(GVariant) sunset = NULL; + sunset = g_dbus_proxy_get_cached_property (self->proxy_color, "Sunset"); + if (sunset != NULL) + { + value = g_variant_get_double (sunset); + } + else + { + value = 16.0f; + g_warning ("no sunset data, using %02.2f", value); + } + } + else + { + value = g_settings_get_double (self->settings_display, "night-light-schedule-from"); + value = fmod (value, 24.f); + } + dialog_adjustments_set_frac_hours (self, value, + self->adjustment_from_hours, + self->adjustment_from_minutes, + self->stack_from, + self->button_from_am, + self->button_from_pm); + + /* set to */ + if (automatic && self->proxy_color != NULL) + { + g_autoptr(GVariant) sunset = NULL; + sunset = g_dbus_proxy_get_cached_property (self->proxy_color, "Sunrise"); + if (sunset != NULL) + { + value = g_variant_get_double (sunset); + } + else + { + value = 8.0f; + g_warning ("no sunrise data, using %02.2f", value); + } + } + else + { + value = g_settings_get_double (self->settings_display, "night-light-schedule-to"); + value = fmod (value, 24.f); + } + dialog_adjustments_set_frac_hours (self, value, + self->adjustment_to_hours, + self->adjustment_to_minutes, + self->stack_to, + self->button_to_am, + self->button_to_pm); + + self->ignore_value_changed = TRUE; + value = (gdouble) g_settings_get_uint (self->settings_display, "night-light-temperature"); + gtk_adjustment_set_value (self->adjustment_color_temperature, value); + self->ignore_value_changed = FALSE; +} + +static void +build_schedule_combo_row (CcNightLightPage *self) +{ + gboolean automatic; + gboolean enabled; + + self->ignore_value_changed = TRUE; + + + enabled = g_settings_get_boolean (self->settings_display, "night-light-enabled"); + automatic = g_settings_get_boolean (self->settings_display, "night-light-schedule-automatic"); + + gtk_widget_set_sensitive (self->box_manual, enabled && !automatic); + + gtk_combo_box_set_active_id (self->schedule_type_combo, automatic ? "automatic" : "manual"); + + self->ignore_value_changed = FALSE; +} + +static void +on_schedule_type_combo_active_changed_cb (GtkComboBox *combo_box, + GParamSpec *pspec, + CcNightLightPage *self) +{ + const gchar *active_id; + gboolean automatic; + + if (self->ignore_value_changed) + return; + + active_id = gtk_combo_box_get_active_id (combo_box); + automatic = g_str_equal (active_id, "automatic"); + + g_settings_set_boolean (self->settings_display, "night-light-schedule-automatic", automatic); +} + +static gboolean +dialog_tick_cb (gpointer user_data) +{ + CcNightLightPage *self = (CcNightLightPage *) user_data; + dialog_update_state (self); + return G_SOURCE_CONTINUE; +} + +static void +dialog_enabled_notify_cb (GtkSwitch *sw, + GParamSpec *pspec, + CcNightLightPage *self) +{ + g_settings_set_boolean (self->settings_display, "night-light-enabled", + gtk_switch_get_active (sw)); +} + +static void +dialog_undisable_call_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) val = NULL; + g_autoptr(GError) error = NULL; + + val = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, &error); + if (val == NULL) + { + g_warning ("failed to undisable: %s", error->message); + return; + } +} + +static void +dialog_undisable_clicked_cb (GtkButton *button, + CcNightLightPage *self) +{ + g_dbus_proxy_call (self->proxy_color_props, + "Set", + g_variant_new ("(ssv)", + "org.gnome.SettingsDaemon.Color", + "DisabledUntilTomorrow", + g_variant_new_boolean (FALSE)), + G_DBUS_CALL_FLAGS_NONE, + 5000, + self->cancellable, + dialog_undisable_call_cb, + self); +} + +static gdouble +dialog_adjustments_get_frac_hours (CcNightLightPage *self, + GtkAdjustment *adj_hours, + GtkAdjustment *adj_mins, + GtkStack *stack) +{ + gdouble value; + + value = gtk_adjustment_get_value (adj_hours); + value += gtk_adjustment_get_value (adj_mins) / 60.0f; + + if (g_strcmp0 (gtk_stack_get_visible_child_name (stack), "pm") == 0) + value += 12.f; + + return value; +} + +static void +dialog_time_from_value_changed_cb (GtkAdjustment *adjustment, + CcNightLightPage *self) +{ + gdouble value; + + if (self->ignore_value_changed) + return; + + value = dialog_adjustments_get_frac_hours (self, + self->adjustment_from_hours, + self->adjustment_from_minutes, + self->stack_from); + + if (value >= 24.f) + value = fmod (value, 24); + + g_debug ("new value = %.3f", value); + + g_settings_set_double (self->settings_display, "night-light-schedule-from", value); +} + +static void +dialog_time_to_value_changed_cb (GtkAdjustment *adjustment, + CcNightLightPage *self) +{ + gdouble value; + + if (self->ignore_value_changed) + return; + + value = dialog_adjustments_get_frac_hours (self, + self->adjustment_to_hours, + self->adjustment_to_minutes, + self->stack_to); + if (value >= 24.f) + value = fmod (value, 24); + + g_debug ("new value = %.3f", value); + + g_settings_set_double (self->settings_display, "night-light-schedule-to", value); +} + +static void +dialog_color_temperature_value_changed_cb (GtkAdjustment *adjustment, + CcNightLightPage *self) +{ + gdouble value; + + if (self->ignore_value_changed) + return; + + value = gtk_adjustment_get_value (adjustment); + + g_debug ("new value = %.0f", value); + + if (self->proxy_color != NULL) + g_dbus_proxy_call (self->proxy_color, + "NightLightPreview", + g_variant_new ("(u)", NIGHT_LIGHT_PREVIEW_TIMEOUT_SECONDS), + G_DBUS_CALL_FLAGS_NONE, + 5000, + NULL, + NULL, + NULL); + + g_settings_set_uint (self->settings_display, "night-light-temperature", (guint) value); +} + +static void +dialog_color_properties_changed_cb (CcNightLightPage *self) +{ + dialog_update_state (self); +} + +static void +dialog_got_proxy_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CcNightLightPage *self = (CcNightLightPage *) user_data; + GDBusProxy *proxy; + g_autoptr(GError) error = NULL; + + proxy = cc_object_storage_create_dbus_proxy_finish (res, &error); + if (proxy == NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to connect to g-s-d: %s", error->message); + return; + } + + self->proxy_color = proxy; + + g_signal_connect_object (self->proxy_color, "g-properties-changed", + G_CALLBACK (dialog_color_properties_changed_cb), self, G_CONNECT_SWAPPED); + dialog_update_state (self); + self->timer_id = g_timeout_add_seconds (10, dialog_tick_cb, self); +} + +static void +dialog_got_proxy_props_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CcNightLightPage *self = (CcNightLightPage *) user_data; + GDBusProxy *proxy; + g_autoptr(GError) error = NULL; + + proxy = cc_object_storage_create_dbus_proxy_finish (res, &error); + if (proxy == NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("failed to connect to g-s-d: %s", error->message); + return; + } + + self->proxy_color_props = proxy; +} + +static gboolean +dialog_format_minutes_combobox (GtkSpinButton *spin, + CcNightLightPage *self) +{ + GtkAdjustment *adjustment; + g_autofree gchar *text = NULL; + adjustment = gtk_spin_button_get_adjustment (spin); + text = g_strdup_printf ("%02.0f", gtk_adjustment_get_value (adjustment)); + gtk_entry_set_text (GTK_ENTRY (spin), text); + return TRUE; +} + +static gboolean +dialog_format_hours_combobox (GtkSpinButton *spin, + CcNightLightPage *self) +{ + GtkAdjustment *adjustment; + g_autofree gchar *text = NULL; + adjustment = gtk_spin_button_get_adjustment (spin); + if (self->clock_format == G_DESKTOP_CLOCK_FORMAT_12H) + text = g_strdup_printf ("%.0f", gtk_adjustment_get_value (adjustment)); + else + text = g_strdup_printf ("%02.0f", gtk_adjustment_get_value (adjustment)); + gtk_entry_set_text (GTK_ENTRY (spin), text); + return TRUE; +} + +static void +dialog_update_adjustments (CcNightLightPage *self) +{ + /* from */ + if (self->clock_format == G_DESKTOP_CLOCK_FORMAT_24H) + { + gtk_adjustment_set_lower (self->adjustment_from_hours, 0); + gtk_adjustment_set_upper (self->adjustment_from_hours, 23); + } + else + { + if (gtk_adjustment_get_value (self->adjustment_from_hours) > 12) + gtk_stack_set_visible_child (self->stack_from, GTK_WIDGET (self->button_from_pm)); + + gtk_adjustment_set_lower (self->adjustment_from_hours, 1); + gtk_adjustment_set_upper (self->adjustment_from_hours, 12); + } + + /* to */ + if (self->clock_format == G_DESKTOP_CLOCK_FORMAT_24H) + { + gtk_adjustment_set_lower (self->adjustment_to_hours, 0); + gtk_adjustment_set_upper (self->adjustment_to_hours, 23); + } + else + { + if (gtk_adjustment_get_value (self->adjustment_to_hours) > 12) + gtk_stack_set_visible_child (self->stack_to, GTK_WIDGET (self->button_to_pm)); + + gtk_adjustment_set_lower (self->adjustment_to_hours, 1); + gtk_adjustment_set_upper (self->adjustment_to_hours, 12); + } +} + +static void +dialog_settings_changed_cb (CcNightLightPage *self) +{ + dialog_update_state (self); +} + +static void +dialog_clock_settings_changed_cb (CcNightLightPage *self) +{ + self->clock_format = g_settings_get_enum (self->settings_clock, CLOCK_FORMAT_KEY); + + /* uncontionally widen this to avoid truncation */ + gtk_adjustment_set_lower (self->adjustment_from_hours, 0); + gtk_adjustment_set_upper (self->adjustment_from_hours, 23); + gtk_adjustment_set_lower (self->adjustment_to_hours, 0); + gtk_adjustment_set_upper (self->adjustment_to_hours, 23); + + /* update spinbuttons */ + gtk_spin_button_update (GTK_SPIN_BUTTON (self->spinbutton_from_hours)); + gtk_spin_button_update (GTK_SPIN_BUTTON (self->spinbutton_to_hours)); + + /* update UI */ + dialog_update_state (self); + dialog_update_adjustments (self); +} + +static void +dialog_am_pm_from_button_clicked_cb (GtkButton *button, + CcNightLightPage *self) +{ + gdouble value; + value = g_settings_get_double (self->settings_display, "night-light-schedule-from"); + if (value > 12.f) + value -= 12.f; + else + value += 12.f; + if (value >= 24.f) + value = fmod (value, 24); + g_settings_set_double (self->settings_display, "night-light-schedule-from", value); + g_debug ("new value = %.3f", value); +} + +static void +dialog_am_pm_to_button_clicked_cb (GtkButton *button, + CcNightLightPage *self) +{ + gdouble value; + value = g_settings_get_double (self->settings_display, "night-light-schedule-to"); + if (value > 12.f) + value -= 12.f; + else + value += 12.f; + if (value >= 24.f) + value = fmod (value, 24); + g_settings_set_double (self->settings_display, "night-light-schedule-to", value); + g_debug ("new value = %.3f", value); +} + +/* GObject overrides */ +static void +cc_night_light_page_finalize (GObject *object) +{ + CcNightLightPage *self = CC_NIGHT_LIGHT_PAGE (object); + + g_cancellable_cancel (self->cancellable); + + g_clear_object (&self->cancellable); + g_clear_object (&self->proxy_color); + g_clear_object (&self->proxy_color_props); + g_clear_object (&self->settings_display); + g_clear_object (&self->settings_clock); + if (self->timer_id > 0) + g_source_remove (self->timer_id); + + G_OBJECT_CLASS (cc_night_light_page_parent_class)->finalize (object); +} + +static void +cc_night_light_page_class_init (CcNightLightPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = cc_night_light_page_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/display/cc-night-light-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, adjustment_from_hours); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, adjustment_from_minutes); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, adjustment_to_hours); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, adjustment_to_minutes); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, adjustment_color_temperature); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, box_manual); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, button_from_am); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, button_from_pm); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, button_to_am); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, button_to_pm); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, infobar_disabled); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, listbox); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, night_light_toggle_switch); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, schedule_type_combo); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, scale_color_temperature); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, spinbutton_from_hours); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, spinbutton_from_minutes); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, spinbutton_to_hours); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, spinbutton_to_minutes); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, stack_from); + gtk_widget_class_bind_template_child (widget_class, CcNightLightPage, stack_to); + + gtk_widget_class_bind_template_callback (widget_class, dialog_am_pm_from_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_am_pm_to_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_enabled_notify_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_format_hours_combobox); + gtk_widget_class_bind_template_callback (widget_class, dialog_format_minutes_combobox); + gtk_widget_class_bind_template_callback (widget_class, dialog_time_from_value_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_time_to_value_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_color_temperature_value_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_undisable_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_schedule_type_combo_active_changed_cb); + +} + +static void +cc_night_light_page_init (CcNightLightPage *self) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_header_func (self->listbox, cc_list_box_update_header_func, NULL, NULL); + + gtk_scale_add_mark (GTK_SCALE (self->scale_color_temperature), + 1700, GTK_POS_BOTTOM, + _("More Warm")); + + gtk_scale_add_mark (GTK_SCALE (self->scale_color_temperature), + 2700, GTK_POS_BOTTOM, + NULL); + + gtk_scale_add_mark (GTK_SCALE (self->scale_color_temperature), + 3700, GTK_POS_BOTTOM, + NULL); + + gtk_scale_add_mark (GTK_SCALE (self->scale_color_temperature), + 4700, GTK_POS_BOTTOM, + _("Less Warm")); + + self->cancellable = g_cancellable_new (); + self->settings_display = g_settings_new (DISPLAY_SCHEMA); + + g_signal_connect_object (self->settings_display, "changed", G_CALLBACK (dialog_settings_changed_cb), self, G_CONNECT_SWAPPED); + + build_schedule_combo_row (self); + + g_settings_bind (self->settings_display, "night-light-enabled", + self->night_light_toggle_switch, "active", + G_SETTINGS_BIND_DEFAULT); + + g_settings_bind_writable (self->settings_display, "night-light-enabled", + self->night_light_toggle_switch, "sensitive", + FALSE); + + g_settings_bind_writable (self->settings_display, "night-light-schedule-from", + self->spinbutton_from_hours, "sensitive", + FALSE); + g_settings_bind_writable (self->settings_display, "night-light-schedule-from", + self->spinbutton_from_minutes, "sensitive", + FALSE); + g_settings_bind_writable (self->settings_display, "night-light-schedule-to", + self->spinbutton_to_minutes, "sensitive", + FALSE); + g_settings_bind_writable (self->settings_display, "night-light-schedule-to", + self->spinbutton_to_minutes, "sensitive", + FALSE); + + /* use custom CSS */ + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (provider, "/org/gnome/control-center/display/night-light.css"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + cc_object_storage_create_dbus_proxy (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.gnome.SettingsDaemon.Color", + "/org/gnome/SettingsDaemon/Color", + "org.gnome.SettingsDaemon.Color", + self->cancellable, + dialog_got_proxy_cb, + self); + + cc_object_storage_create_dbus_proxy (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.gnome.SettingsDaemon.Color", + "/org/gnome/SettingsDaemon/Color", + "org.freedesktop.DBus.Properties", + self->cancellable, + dialog_got_proxy_props_cb, + self); + + /* clock settings_display */ + self->settings_clock = g_settings_new (CLOCK_SCHEMA); + self->clock_format = g_settings_get_enum (self->settings_clock, CLOCK_FORMAT_KEY); + dialog_update_adjustments (self); + g_signal_connect_object (self->settings_clock, + "changed::" CLOCK_FORMAT_KEY, + G_CALLBACK (dialog_clock_settings_changed_cb), + self, G_CONNECT_SWAPPED); + + dialog_update_state (self); +} + +CcNightLightPage * +cc_night_light_page_new (void) +{ + return g_object_new (CC_TYPE_NIGHT_LIGHT_PAGE, + NULL); +} + diff --git a/panels/display/cc-night-light-page.h b/panels/display/cc-night-light-page.h new file mode 100644 index 0000000..79422bb --- /dev/null +++ b/panels/display/cc-night-light-page.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_NIGHT_LIGHT_PAGE (cc_night_light_page_get_type ()) +G_DECLARE_FINAL_TYPE (CcNightLightPage, cc_night_light_page, CC, NIGHT_LIGHT_PAGE, GtkBin) + +CcNightLightPage* cc_night_light_page_new (void); + +G_END_DECLS diff --git a/panels/display/cc-night-light-page.ui b/panels/display/cc-night-light-page.ui new file mode 100644 index 0000000..02b14f7 --- /dev/null +++ b/panels/display/cc-night-light-page.ui @@ -0,0 +1,443 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="CcNightLightPage" parent="GtkBin"> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">start</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkInfoBar" id="infobar_disabled"> + <property name="name">infobar_disabled</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">0</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child internal-child="action_area"> + <object class="GtkButtonBox"> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_undisable"> + <property name="label" translatable="yes" comments="This cancels the redshift inhibit.">Restart Filter</property> + <property name="name">button_undisable</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="dialog_undisable_clicked_cb" object="CcNightLightPage" swapped="no" /> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child internal-child="content_area"> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="spacing">16</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_left">12</property> + <property name="hexpand">False</property> + <property name="label" translatable="yes" comments="Inhibit the redshift functionality until the next day starts">Temporarily Disabled Until Tomorrow</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">30</property> + <property name="margin_end">12</property> + <property name="margin_start">12</property> + <property name="margin_bottom">36</property> + <property name="orientation">vertical</property> + <property name="spacing">26</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="valign">start</property> + <property name="label" translatable="yes">Night light makes the screen color warmer. This can help to prevent eye strain and sleeplessness.</property> + <property name="wrap">True</property> + <property name="max_width_chars">60</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkListBox" id="listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="selection-mode">none</property> + + <!-- Night Light --> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="title" translatable="yes">Night Light</property> + <property name="activatable-widget">night_light_toggle_switch</property> + + <child> + <object class="GtkSwitch" id="night_light_toggle_switch"> + <property name="visible">True</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + + <!-- Schedule --> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="title" translatable="yes">Schedule</property> + <property name="sensitive" bind-source="night_light_toggle_switch" bind-property="active" bind-flags="default|sync-create" /> + + <child> + <object class="GtkComboBoxText" id="schedule_type_combo"> + <property name="visible">True</property> + <property name="valign">center</property> + <signal name="notify::active" handler="on_schedule_type_combo_active_changed_cb" object="CcNightLightPage" swapped="no" /> + <items> + <item translatable="yes" id="automatic">Sunset to Sunrise</item> + <item translatable="yes" id="manual">Manual Schedule</item> + </items> + </object> + </child> + </object> + </child> + + <!-- Time --> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="title" translatable="yes">Times</property> + <property name="sensitive" bind-source="night_light_toggle_switch" bind-property="active" bind-flags="default|sync-create" /> + + <child> + <object class="GtkBox" id="box_manual"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <style> + <class name="time-widget" /> + </style> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">From</property> + <property name="mnemonic_widget">spinbutton_from_hours</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkSpinButton" id="spinbutton_from_hours"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_width_chars">2</property> + <property name="text">4</property> + <property name="orientation">vertical</property> + <property name="adjustment">adjustment_from_hours</property> + <property name="numeric">True</property> + <property name="wrap">True</property> + <property name="value">4</property> + <signal name="output" handler="dialog_format_hours_combobox" object="CcNightLightPage" swapped="no" /> + <style> + <class name="padded-spinbutton"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="from_h_spinbutton-atkobject"> + <property name="AtkObject::accessible-description" translatable="yes">Hour</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">:</property> + </object> + </child> + <child> + <object class="GtkSpinButton" id="spinbutton_from_minutes"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_width_chars">2</property> + <property name="text">0</property> + <property name="orientation">vertical</property> + <property name="adjustment">adjustment_from_minutes</property> + <property name="numeric">True</property> + <property name="wrap">True</property> + <signal name="output" handler="dialog_format_minutes_combobox" object="CcNightLightPage" swapped="no" /> + <style> + <class name="padded-spinbutton"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="from_m_spinbutton-atkobject"> + <property name="AtkObject::accessible-description" translatable="yes">Minute</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="stack_from"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">False</property> + <child> + <object class="GtkButton" id="button_from_am"> + <property name="label" translatable="yes" comments="This is the short form for the time period in the morning">AM</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="dialog_am_pm_from_button_clicked_cb" object="CcNightLightPage" swapped="no" /> + <style> + <class name="unpadded-button"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_from_pm"> + <property name="label" translatable="yes" comments="This is the short form for the time period in the afternoon">PM</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="dialog_am_pm_from_button_clicked_cb" object="CcNightLightPage" swapped="no" /> + <style> + <class name="unpadded-button"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin-start">6</property> + <property name="label" translatable="yes">To</property> + <property name="mnemonic_widget">spinbutton_to_hours</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkSpinButton" id="spinbutton_to_hours"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_width_chars">2</property> + <property name="text">4</property> + <property name="input_purpose">number</property> + <property name="orientation">vertical</property> + <property name="adjustment">adjustment_to_hours</property> + <property name="numeric">True</property> + <property name="wrap">True</property> + <property name="value">4</property> + <signal name="output" handler="dialog_format_hours_combobox" object="CcNightLightPage" swapped="no" /> + <style> + <class name="padded-spinbutton"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="to_h_spinbutton-atkobject"> + <property name="AtkObject::accessible-description" translatable="yes">Hour</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">:</property> + </object> + </child> + <child> + <object class="GtkSpinButton" id="spinbutton_to_minutes"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="max_width_chars">2</property> + <property name="text">0</property> + <property name="orientation">vertical</property> + <property name="adjustment">adjustment_to_minutes</property> + <property name="numeric">True</property> + <property name="wrap">True</property> + <signal name="output" handler="dialog_format_minutes_combobox" object="CcNightLightPage" swapped="no" /> + <style> + <class name="padded-spinbutton"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="to_m_spinbutton-atkobject"> + <property name="AtkObject::accessible-description" translatable="yes">Minute</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="stack_to"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">False</property> + <child> + <object class="GtkButton" id="button_to_am"> + <property name="label" translatable="yes">AM</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="dialog_am_pm_to_button_clicked_cb" object="CcNightLightPage" swapped="no" /> + <style> + <class name="unpadded-button"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="button_to_pm"> + <property name="label" translatable="yes">PM</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="dialog_am_pm_to_button_clicked_cb" object="CcNightLightPage" swapped="no" /> + <style> + <class name="unpadded-button"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + + <!-- Color Temperature --> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="title" translatable="yes">Color Temperature</property> + <property name="sensitive" bind-source="night_light_toggle_switch" bind-property="active" bind-flags="default|sync-create" /> + + <child> + <object class="GtkScale" id="scale_color_temperature"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="width-request">280</property> + <property name="adjustment">adjustment_color_temperature</property> + <property name="inverted">True</property> + <property name="restrict_to_fill_level">False</property> + <property name="fill_level">1</property> + <property name="digits">0</property> + <property name="draw_value">False</property> + <property name="has_origin">False</property> + <property name="value_pos">bottom</property> + <style> + <class name="night-light-temperature"/> + </style> + </object> + </child> + </object> + </child> + + </object> + </child> + </object> + </child> + + </object> + </child> + </object> + </child> + </template> + + <object class="GtkAdjustment" id="adjustment_from_hours"> + <property name="upper">23</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="dialog_time_from_value_changed_cb" object="CcNightLightPage" swapped="no" /> + </object> + <object class="GtkAdjustment" id="adjustment_from_minutes"> + <property name="upper">59</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="dialog_time_from_value_changed_cb" object="CcNightLightPage" swapped="no" /> + </object> + <object class="GtkAdjustment" id="adjustment_to_hours"> + <property name="upper">23</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="dialog_time_to_value_changed_cb" object="CcNightLightPage" swapped="no" /> + </object> + <object class="GtkAdjustment" id="adjustment_to_minutes"> + <property name="upper">59</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + <signal name="value-changed" handler="dialog_time_to_value_changed_cb" object="CcNightLightPage" swapped="no" /> + </object> + <object class="GtkAdjustment" id="adjustment_color_temperature"> + <property name="lower">1700</property> + <property name="upper">4700</property> + <property name="step_increment">100</property> + <property name="page_increment">500</property> + <signal name="value-changed" handler="dialog_color_temperature_value_changed_cb" object="CcNightLightPage" swapped="no" /> + </object> +</interface> diff --git a/panels/display/display-arrangement.css b/panels/display/display-arrangement.css new file mode 100644 index 0000000..90bce87 --- /dev/null +++ b/panels/display/display-arrangement.css @@ -0,0 +1,26 @@ + +.display-arrangement.monitor { + border: solid 1px @borders; + margin: 0px 0px 1px 1px; + background: @theme_bg_color; + padding: 0.4em; +} + +.display-arrangement.monitor.primary { + border-top: 0.4em solid #000000; +} + +.display-arrangement.monitor:selected { + background: @theme_selected_bg_color; +} + +.display-arrangement.monitor-label { + font-size: larger; + font-weight: bold; + border-radius: 0.3em; + padding-right: 0.2em; + padding-left: 0.2em; + color: #fff; + background: #000; +} + diff --git a/panels/display/display.gresource.xml b/panels/display/display.gresource.xml new file mode 100644 index 0000000..fcac1a2 --- /dev/null +++ b/panels/display/display.gresource.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/control-center/display"> + <file preprocess="xml-stripblanks">cc-display-panel.ui</file> + <file preprocess="xml-stripblanks">cc-display-settings.ui</file> + <file preprocess="xml-stripblanks">cc-night-light-page.ui</file> + <file>display-arrangement.css</file> + <file>night-light.css</file> + </gresource> +</gresources> diff --git a/panels/display/gnome-display-panel.desktop.in.in b/panels/display/gnome-display-panel.desktop.in.in new file mode 100644 index 0000000..9c4744d --- /dev/null +++ b/panels/display/gnome-display-panel.desktop.in.in @@ -0,0 +1,18 @@ +[Desktop Entry] +Name=Displays +Comment=Choose how to use connected monitors and projectors +Exec=gnome-control-center display +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=preferences-desktop-display +Terminal=false +Type=Application +NoDisplay=true +StartupNotify=true +Categories=GNOME;GTK;Settings;HardwareSettings;X-GNOME-Settings-Panel;X-GNOME-DevicesSettings; +OnlyShowIn=GNOME;Unity; +X-GNOME-Bugzilla-Bugzilla=GNOME +X-GNOME-Bugzilla-Product=gnome-control-center +X-GNOME-Bugzilla-Component=Screen resolution +X-GNOME-Bugzilla-Version=@VERSION@ +# Translators: Search terms to find the Displays panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Panel;Projector;xrandr;Screen;Resolution;Refresh;Monitor;Night;Light;Blue;redshift;color;sunset;sunrise; diff --git a/panels/display/icons/16x16/preferences-desktop-display.png b/panels/display/icons/16x16/preferences-desktop-display.png Binary files differnew file mode 100644 index 0000000..f996ddf --- /dev/null +++ b/panels/display/icons/16x16/preferences-desktop-display.png diff --git a/panels/display/icons/22x22/preferences-desktop-display.png b/panels/display/icons/22x22/preferences-desktop-display.png Binary files differnew file mode 100644 index 0000000..cc47eec --- /dev/null +++ b/panels/display/icons/22x22/preferences-desktop-display.png diff --git a/panels/display/icons/24x24/preferences-desktop-display.png b/panels/display/icons/24x24/preferences-desktop-display.png Binary files differnew file mode 100644 index 0000000..49b4e12 --- /dev/null +++ b/panels/display/icons/24x24/preferences-desktop-display.png diff --git a/panels/display/icons/32x32/preferences-desktop-display.png b/panels/display/icons/32x32/preferences-desktop-display.png Binary files differnew file mode 100644 index 0000000..95de3ea --- /dev/null +++ b/panels/display/icons/32x32/preferences-desktop-display.png diff --git a/panels/display/icons/meson.build b/panels/display/icons/meson.build new file mode 100644 index 0000000..7cabe54 --- /dev/null +++ b/panels/display/icons/meson.build @@ -0,0 +1,18 @@ +icon_sizes = [ + '16x16', + '22x22', + '24x24', + '32x32' +] + +foreach icon_size: icon_sizes + install_data( + join_paths(icon_size, 'preferences-desktop-display.png'), + install_dir: join_paths(control_center_icondir, 'hicolor', icon_size, 'apps') + ) +endforeach + +install_data( + 'scalable/preferences-desktop-display.svg', + install_dir: join_paths(control_center_icondir, 'hicolor', 'scalable', 'apps') +) diff --git a/panels/display/icons/scalable/preferences-desktop-display.svg b/panels/display/icons/scalable/preferences-desktop-display.svg new file mode 100644 index 0000000..0679b6b --- /dev/null +++ b/panels/display/icons/scalable/preferences-desktop-display.svg @@ -0,0 +1,470 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="48.000000px" + height="48.000000px" + id="svg3304" + sodipodi:version="0.32" + inkscape:version="0.44+devel" + sodipodi:docbase="/home/jimmac/gfx/ximian/art/icons/control-center/scalable" + sodipodi:docname="change-resolution.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true"> + <defs + id="defs3306"> + <linearGradient + id="linearGradient2804"> + <stop + style="stop-color:black;stop-opacity:0;" + offset="0" + id="stop2806" /> + <stop + id="stop2812" + offset="0.5" + style="stop-color:black;stop-opacity:1;" /> + <stop + style="stop-color:black;stop-opacity:0;" + offset="1" + id="stop2808" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient2804" + id="linearGradient2831" + gradientUnits="userSpaceOnUse" + x1="21.875" + y1="48.000977" + x2="21.875" + y2="40" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient2781" + id="radialGradient2829" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2,0,0,0.8,36,8.8)" + cx="1" + cy="44" + fx="1" + fy="44" + r="5" /> + <linearGradient + inkscape:collect="always" + id="linearGradient2781"> + <stop + style="stop-color:black;stop-opacity:1;" + offset="0" + id="stop2783" /> + <stop + style="stop-color:black;stop-opacity:0;" + offset="1" + id="stop2785" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient2781" + id="radialGradient2827" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2,0,0,0.8,-13,-79.2)" + cx="1" + cy="44" + fx="1" + fy="44" + r="5" /> + <linearGradient + id="linearGradient5137"> + <stop + style="stop-color:#eeeeec;stop-opacity:1;" + offset="0" + id="stop5139" /> + <stop + style="stop-color:#e6e6e3;stop-opacity:1;" + offset="1" + id="stop5141" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient6240"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop6242" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop6244" /> + </linearGradient> + <linearGradient + id="linearGradient11400"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop11402" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop11404" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient11400" + id="linearGradient11406" + x1="23.154902" + y1="34.572548" + x2="23.529411" + y2="40.219608" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5137" + id="linearGradient5147" + gradientUnits="userSpaceOnUse" + x1="17.247635" + y1="6.3760414" + x2="39.904388" + y2="38.876041" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5137" + id="linearGradient5223" + gradientUnits="userSpaceOnUse" + x1="31.743324" + y1="37.842293" + x2="31.86105" + y2="43.82579" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + id="linearGradient7025" + inkscape:collect="always"> + <stop + id="stop7027" + offset="0" + style="stop-color:#e6ce46;stop-opacity:1" /> + <stop + id="stop7029" + offset="1" + style="stop-color:#d6ba1c;stop-opacity:1" /> + </linearGradient> + <linearGradient + id="linearGradient22122" + inkscape:collect="always"> + <stop + id="stop22124" + offset="0" + style="stop-color:black;stop-opacity:1;" /> + <stop + id="stop22126" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient22140"> + <stop + id="stop22142" + offset="0" + style="stop-color:black;stop-opacity:0;" /> + <stop + style="stop-color:black;stop-opacity:1;" + offset="0.5" + id="stop22148" /> + <stop + id="stop22144" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient22122" + id="radialGradient4770" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.000001,1.142856,0,-41.10259,45.50001)" + cx="7" + cy="39.464806" + fx="7" + fy="39.464806" + r="3.5" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient22122" + id="radialGradient4772" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.000001,1.142856,0,-89.10259,-31.49999)" + cx="7" + cy="39.464806" + fx="7" + fy="39.464806" + r="3.5" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient22140" + id="linearGradient4774" + gradientUnits="userSpaceOnUse" + x1="18.142136" + y1="35" + x2="18.142136" + y2="42.040661" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7025" + id="linearGradient4776" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(53,1.428571)" + x1="13.630114" + y1="28.5" + x2="25.208096" + y2="41.180992" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient6240" + id="linearGradient4778" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(7.843e-3,0.7954955)" + x1="20.156862" + y1="5.0996137" + x2="20.156862" + y2="26.039215" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#849193" + borderopacity="1.0000000" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1" + inkscape:cx="39.697787" + inkscape:cy="26.598514" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:showpageshadow="false" + showborder="true" + inkscape:window-width="923" + inkscape:window-height="937" + inkscape:window-x="2004" + inkscape:window-y="169" + showguides="true" + inkscape:guide-bbox="true" + inkscape:grid-points="true" /> + <metadata + id="metadata3309"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>Change Resolution</dc:title> + <dc:creator> + <cc:Agent> + <dc:title>Jakub Steiner</dc:title> + </cc:Agent> + </dc:creator> + <cc:license + rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" /> + <dc:subject> + <rdf:Bag> + <rdf:li>display</rdf:li> + <rdf:li>resolution</rdf:li> + <rdf:li>video</rdf:li> + </rdf:Bag> + </dc:subject> + <dc:contributor> + <cc:Agent> + <dc:title>Andreas Nilsson +Luca Ferretti <elle.uca@libero.it></dc:title> + </cc:Agent> + </dc:contributor> + <dc:date></dc:date> + <dc:source>http://www.gnome.org</dc:source> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/GPL/2.0/"> + <cc:permits + rdf:resource="http://web.resource.org/cc/Reproduction" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/Distribution" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/Notice" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/DerivativeWorks" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/ShareAlike" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/SourceCode" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + id="layer1" + inkscape:label="Layer 1" + inkscape:groupmode="layer"> + <g + id="g2822" + style="opacity:0.3" + transform="matrix(0.9308511,0,0,1.037397,1.6941489,-1.795056)"> + <rect + transform="scale(-1,-1)" + y="-48" + x="-11" + height="8" + width="10" + id="rect1892" + style="opacity:1;color:#000000;fill:url(#radialGradient2827);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + <rect + y="40" + x="38" + height="8" + width="10" + id="rect2789" + style="opacity:1;color:#000000;fill:url(#radialGradient2829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + <rect + y="40" + x="11" + height="8" + width="27" + id="rect2793" + style="opacity:1;color:#000000;fill:url(#linearGradient2831);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + </g> + <rect + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="rect4784" + width="48" + height="48" + x="0" + y="0.79549509" /> + <path + style="opacity:1;color:#000000;fill:url(#linearGradient5223);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + d="M 14.375479,36.328843 C 14.375479,36.328843 15.592355,41.263443 10.51915,41.309767 C 8.0888743,41.331672 8.5866723,45.344918 8.5866723,45.344918 L 39.433139,45.313545 C 39.433139,45.313545 39.851577,41.418182 37.410922,41.372513 C 32.423455,41.280374 33.600393,36.266098 33.600393,36.266098 L 14.375479,36.328843 z " + id="path9222" + sodipodi:nodetypes="csccscc" /> + <path + style="fill:url(#linearGradient5147);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" + d="M 4.8886262,4.2739318 L 43.01746,4.2739318 C 45.042579,4.2739318 46.518438,5.7015863 46.518438,7.9000726 L 46.529388,34.103908 C 46.529388,35.795975 46.001041,36.319137 44.494732,36.319137 L 3.5320635,36.300202 C 2.3527922,36.271409 1.513468,35.805541 1.4976345,34.280899 L 1.5128113,7.7123281 C 1.5128113,5.9385022 3.0522187,4.2739318 4.8886262,4.2739318 z " + id="rect5040" + sodipodi:nodetypes="ccccccccc" /> + <rect + style="fill:#555753;fill-opacity:1;fill-rule:evenodd;stroke:#2e3436;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="rect9208" + width="37.00032" + height="22.996691" + x="5.5059438" + y="8.2973185" /> + <path + sodipodi:type="inkscape:offset" + inkscape:radius="-0.875" + inkscape:original="M 4.875 4.28125 C 3.0385925 4.28125 1.5 5.9449242 1.5 7.71875 L 1.5 34.28125 C 1.5158335 35.805892 2.3519787 36.283708 3.53125 36.3125 L 44.5 36.3125 C 46.006309 36.3125 46.53125 35.785816 46.53125 34.09375 L 46.53125 7.90625 C 46.53125 5.7077637 45.056369 4.2812498 43.03125 4.28125 L 4.875 4.28125 z " + xlink:href="#rect5040" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" + id="path5145" + inkscape:href="#rect5040" + d="M 4.875,5.0625 C 3.5670573,5.0625 2.375,6.3571498 2.375,7.625 L 2.375,34.1875 C 2.3812657,34.79084 2.515801,34.970853 2.65625,35.09375 C 2.796699,35.216647 3.0858905,35.332113 3.5625,35.34375 L 44.5,35.34375 C 45.151173,35.34375 45.356981,35.24273 45.4375,35.15625 C 45.518019,35.06977 45.65625,34.755549 45.65625,34 L 45.65625,7.8125 C 45.65625,6.0053499 44.645463,5.0624999 43.03125,5.0625 L 4.875,5.0625 z " /> + <path + style="opacity:0.6;fill:url(#linearGradient11406);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 13.992156,36.834708 C 14.075817,39.007257 13.708803,39.746151 12.358532,40.509939 L 35.973934,41.807257 C 35.024915,40.638629 33.644523,38.873923 34.020993,36.819022 L 13.992156,36.834708 z " + id="path10672" + sodipodi:nodetypes="ccccc" /> + <path + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#8d8d8f;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:0.43902438" + d="M 9.9921262,42.291555 C 16.698819,42.293524 37.78891,42.291555 37.78891,42.291555" + id="path6575" + sodipodi:nodetypes="cc" /> + <path + sodipodi:nodetypes="cc" + id="path8029" + d="M 9.647928,43.299429 C 16.354621,43.301398 38.367789,43.299429 38.367789,43.299429" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="opacity:0.4;fill:url(#linearGradient4778);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 6.031372,8.8268676 L 6.031372,26.834708 C 22.475817,25.480459 28.630065,16.722289 41.999999,15.807256 L 42,8.7954956 L 6.031372,8.8268676 z " + id="path4073" + sodipodi:nodetypes="ccccc" /> + <g + id="g4754" + transform="translate(-46.77135,-7.3370294)"> + <g + transform="matrix(0.916667,0,0,0.714282,49.771334,11.132681)" + style="opacity:0.3" + id="g22150"> + <rect + style="opacity:1;fill:url(#radialGradient4770);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1" + id="rect22120" + width="4" + height="7" + x="0" + y="35" /> + <rect + style="opacity:1;fill:url(#radialGradient4772);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1" + id="rect22134" + width="4" + height="7" + x="-48" + y="-42" + transform="scale(-1,-1)" /> + <rect + style="opacity:1;fill:url(#linearGradient4774);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000057;stroke-opacity:1" + id="rect22138" + width="40" + height="7" + x="4" + y="35" /> + </g> + <g + id="g4743"> + <g + id="g10824" + transform="translate(-5.228666,-6.29601)"> + <path + id="path4319" + d="M 57.5,14.928571 L 57.5,44.928571 L 96.5,44.928571 L 57.5,14.928571 z M 63.5,27.928571 L 78.5,38.928571 L 63.5,38.928571 L 63.5,27.928571 z " + style="fill:url(#linearGradient4776);fill-opacity:1;fill-rule:evenodd;stroke:#a38503;stroke-width:1.00000024px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cc" + id="path4326" + d="M 61.5,44.928571 L 61.5,41.928571" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cc" + id="path4328" + d="M 67.5,44.928571 L 67.5,41.928571" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cc" + id="path4330" + d="M 73.5,44.928571 L 73.5,41.964285" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cc" + id="path4332" + d="M 79.5,44.928571 L 79.5,41.928571" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cc" + id="path4334" + d="M 85.5,44.928571 L 85.5,41.928571" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path6126" + d="M 58.500002,16.928575 L 58.500002,43.928586 L 93.500014,43.928586 L 58.500002,16.928575 z " + style="opacity:0.4;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000006px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <path + style="opacity:0.4;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 9.5017474,38.542446 L 9.4575532,24.532894 L 28.541592,38.563262 L 9.5017474,38.542446 z " + id="path12221" + sodipodi:nodetypes="cccc" + transform="translate(47.771334,-4.867439)" /> + </g> + </g> + </g> +</svg> diff --git a/panels/display/meson.build b/panels/display/meson.build new file mode 100644 index 0000000..12f35cc --- /dev/null +++ b/panels/display/meson.build @@ -0,0 +1,65 @@ +panels_list += cappletname +desktop = 'gnome-@0@-panel.desktop'.format(cappletname) + +desktop_in = configure_file( + input: desktop + '.in.in', + output: desktop + '.in', + configuration: desktop_conf +) + +i18n.merge_file( + desktop, + type: 'desktop', + input: desktop_in, + output: desktop, + po_dir: po_dir, + install: true, + install_dir: control_center_desktopdir +) + +sources = files( + 'cc-display-panel.c', + 'cc-display-arrangement.c', + 'cc-display-config.c', + 'cc-display-config-dbus.c', + 'cc-display-config-manager-dbus.c', + 'cc-display-config-manager.c', + 'cc-display-settings.c', + 'cc-night-light-page.c', +) + +resource_data = files( + 'cc-display-panel.ui', + 'cc-display-settings.ui', + 'cc-night-light-page.ui', +) + +sources += gnome.compile_resources( + 'cc-' + cappletname + '-resources', + cappletname + '.gresource.xml', + source_dir: ['.', 'icons'], + c_name: 'cc_' + cappletname, + dependencies: resource_data, + export: true +) + +deps = common_deps + [ + colord_dep, + gnome_desktop_dep, + m_dep, + upower_glib_dep +] + +cflags += [ + '-DDATADIR="@0@"'.format(control_center_datadir) +] + +panels_libs += static_library( + cappletname, + sources: sources, + include_directories: [ top_inc, common_inc ], + dependencies: deps, + c_args: cflags +) + +subdir('icons') diff --git a/panels/display/night-light.css b/panels/display/night-light.css new file mode 100644 index 0000000..b73f510 --- /dev/null +++ b/panels/display/night-light.css @@ -0,0 +1,28 @@ +/* color selection by Daniel Foré and elementary OS */ +@define-color ORANGE_100 #ffc27d; +@define-color ORANGE_500 #f37329; +@define-color base_color white; +@define-color bg_color shade (@base_color, 0.96); + +/* Hide the marks at the beginning and the end */ +.night-light-temperature mark indicator:nth-child(even) { + color:transparent; +} + +.night-light-temperature trough { + padding-top: 2px; + padding-bottom: 2px; + background-image: linear-gradient(to right, mix(@bg_color, @ORANGE_100, 0.5), @ORANGE_500); +} + +.night-light-temperature:dir(rtl) trough { + background-image: linear-gradient(to left, mix(@bg_color, @ORANGE_100, 0.5), @ORANGE_500); +} + +.padded-spinbutton { + min-width: 40px; +} + +.unpadded-button { + padding: 6px; +} |