commit b278730ddd99831b3d47d9b0d5372290ca2407c3 Author: Jason Francis Date: Fri Jul 5 22:51:52 2019 -0400 initialize repository diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67ab5ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2019 cyclopsian + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..eca3fd3 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# wdisplay + +wdisplay is a graphical application for configuring displays in wlroots +compositors. It borrows some code from [kanshi]. + +## License + +MIT + +[kanshi]: https://github.com/emersion/kanshi diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..2c3b214 --- /dev/null +++ b/meson.build @@ -0,0 +1,5 @@ +project('wdisplay', 'c') + +subdir('protocol') +subdir('resources') +subdir('src') diff --git a/protocol/meson.build b/protocol/meson.build new file mode 100644 index 0000000..bde9b23 --- /dev/null +++ b/protocol/meson.build @@ -0,0 +1,38 @@ +wayland_scanner = find_program('wayland-scanner') +wayland_client = dependency('wayland-client') + +wayland_scanner_code = generator( + wayland_scanner, + output: '@BASENAME@-protocol.c', + arguments: ['private-code', '@INPUT@', '@OUTPUT@'], +) + +wayland_scanner_client = generator( + wayland_scanner, + output: '@BASENAME@-client-protocol.h', + arguments: ['client-header', '@INPUT@', '@OUTPUT@'], +) + +client_protocols = [ + ['wlr-output-management-unstable-v1.xml'], +] + +client_protos_src = [] +client_protos_headers = [] + +foreach p : client_protocols + xml = join_paths(p) + client_protos_src += wayland_scanner_code.process(xml) + client_protos_headers += wayland_scanner_client.process(xml) +endforeach + +lib_client_protos = static_library( + 'client_protos', + client_protos_src + client_protos_headers, + dependencies: [wayland_client] +) + +client_protos = declare_dependency( + link_with: lib_client_protos, + sources: client_protos_headers, +) diff --git a/protocol/wlr-output-management-unstable-v1.xml b/protocol/wlr-output-management-unstable-v1.xml new file mode 100644 index 0000000..35f7ca4 --- /dev/null +++ b/protocol/wlr-output-management-unstable-v1.xml @@ -0,0 +1,483 @@ + + + + Copyright © 2019 Purism SPC + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol exposes interfaces to obtain and modify output device + configuration. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows reading and writing the current + output device configuration. + + Output devices that display pixels (e.g. a physical monitor or a virtual + output in a window) are represented as heads. Heads cannot be created nor + destroyed by the client, but they can be enabled or disabled and their + properties can be changed. Each head may have one or more available modes. + + Whenever a head appears (e.g. a monitor is plugged in), it will be + advertised via the head event. Immediately after the output manager is + bound, all current heads are advertised. + + Whenever a head's properties change, the relevant wlr_output_head events + will be sent. Not all head properties will be sent: only properties that + have changed need to. + + Whenever a head disappears (e.g. a monitor is unplugged), a + wlr_output_head.finished event will be sent. + + After one or more heads appear, change or disappear, the done event will + be sent. It carries a serial which can be used in a create_configuration + request to update heads properties. + + The information obtained from this protocol should only be used for output + configuration purposes. This protocol is not designed to be a generic + output property advertisement protocol for regular clients. Instead, + protocols such as xdg-output should be used. + + + + + This event introduces a new head. This happens whenever a new head + appears (e.g. a monitor is plugged in) or after the output manager is + bound. + + + + + + + This event is sent after all information has been sent after binding to + the output manager object and after any subsequent changes. This applies + to child head and mode objects as well. In other words, this event is + sent whenever a head or mode is created or destroyed and whenever one of + their properties has been changed. Not all state is re-sent each time + the current configuration changes: only the actual changes are sent. + + This allows changes to the output configuration to be seen as atomic, + even if they happen via multiple events. + + A serial is sent to be used in a future create_configuration request. + + + + + + + Create a new output configuration object. This allows to update head + properties. + + + + + + + + Indicates the client no longer wishes to receive events for output + configuration changes. However the compositor may emit further events, + until the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending manager events. + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + A head is an output device. The difference between a wl_output object and + a head is that heads are advertised even if they are turned off. A head + object only advertises properties and cannot be used directly to change + them. + + A head has some read-only properties: modes, name, description and + physical_size. These cannot be changed by clients. + + Other properties can be updated via a wlr_output_configuration object. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + + + + + This event describes the head name. + + The naming convention is compositor defined, but limited to alphanumeric + characters and dashes (-). Each name is unique among all wlr_output_head + objects, but if a wlr_output_head object is destroyed the same name may + be reused later. The names will also remain consistent across sessions + with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + If the compositor implements the xdg-output protocol and this head is + enabled, the xdg_output.name event must report the same name. + + The name event is sent after a wlr_output_head object is created. This + event is only sent once per object, and the name does not change over + the lifetime of the wlr_output_head object. + + + + + + + This event describes a human-readable description of the head. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. However, do not assume that the name is a reflection of + the make, model, serial of the underlying DRM connector or the display + name of the underlying X11 connection, etc. + + If the compositor implements xdg-output and this head is enabled, + the xdg_output.description must report the same description. + + The description event is sent after a wlr_output_head object is created. + This event is only sent once per object, and the description does not + change over the lifetime of the wlr_output_head object. + + + + + + + This event describes the physical size of the head. This event is only + sent if the head has a physical size (e.g. is not a projector or a + virtual device). + + + + + + + + This event introduces a mode for this head. It is sent once per + supported mode. + + + + + + + This event describes whether the head is enabled. A disabled head is not + mapped to a region of the global compositor space. + + When a head is disabled, some properties (current_mode, position, + transform and scale) are irrelevant. + + + + + + + This event describes the mode currently in use for this head. It is only + sent if the output is enabled. + + + + + + + This events describes the position of the head in the global compositor + space. It is only sent if the output is enabled. + + + + + + + + This event describes the transformation currently applied to the head. + It is only sent if the output is enabled. + + + + + + + This events describes the scale of the head in the global compositor + space. It is only sent if the output is enabled. + + + + + + + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + This object describes an output mode. + + Some heads don't support output modes, in which case modes won't be + advertised. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + + + + + This event describes the mode size. The size is given in physical + hardware units of the output device. This is not necessarily the same as + the output size in the global compositor space. For instance, the output + may be scaled or transformed. + + + + + + + + This event describes the mode's fixed vertical refresh rate. It is only + sent if the mode has a fixed refresh rate. + + + + + + + This event advertises this mode as preferred. + + + + + + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + This object is used by the client to describe a full output configuration. + + First, the client needs to setup the output configuration. Each head can + be either enabled (and configured) or disabled. It is a protocol error to + send two enable_head or disable_head requests with the same head. It is a + protocol error to omit a head in a configuration. + + Then, the client can apply or test the configuration. The compositor will + then reply with a succeeded, failed or cancelled event. Finally the client + should destroy the configuration object. + + + + + + + + + + + Enable a head. This request creates a head configuration object that can + be used to change the head's properties. + + + + + + + + Disable a head. + + + + + + + Apply the new output configuration. + + In case the configuration is successfully applied, there is no guarantee + that the new output state matches completely the requested + configuration. For instance, a compositor might round the scale if it + doesn't support fractional scaling. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + + + + + + Test the new output configuration. The configuration won't be applied, + but will only be validated. + + Even if the compositor succeeds to test a configuration, applying it may + fail. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + + + + + + Sent after the compositor has successfully applied the changes or + tested them. + + Upon receiving this event, the client should destroy this object. + + If the current configuration has changed, events to describe the changes + will be sent followed by a wlr_output_manager.done event. + + + + + + Sent if the compositor rejects the changes or failed to apply them. The + compositor should revert any changes made by the apply request that + triggered this event. + + Upon receiving this event, the client should destroy this object. + + + + + + Sent if the compositor cancels the configuration because the state of an + output changed and the client has outdated information (e.g. after an + output has been hotplugged). + + The client can create a new configuration with a newer serial and try + again. + + Upon receiving this event, the client should destroy this object. + + + + + + Using this request a client can tell the compositor that it is not going + to use the configuration object anymore. Any changes to the outputs + that have not been applied will be discarded. + + This request also destroys wlr_output_configuration_head objects created + via this object. + + + + + + + This object is used by the client to update a single head's configuration. + + It is a protocol error to set the same property twice. + + + + + + + + + + + + + This request sets the head's mode. + + + + + + + This request assigns a custom mode to the head. The size is given in + physical hardware units of the output device. If set to zero, the + refresh rate is unspecified. + + It is a protocol error to set both a mode and a custom mode. + + + + + + + + + This request sets the head's position in the global compositor space. + + + + + + + + This request sets the head's transform. + + + + + + + This request sets the head's scale. + + + + + diff --git a/resources/head.ui b/resources/head.ui new file mode 100644 index 0000000..70f5d42 --- /dev/null +++ b/resources/head.ui @@ -0,0 +1,464 @@ + + + + + + 99999999999999 + 0.1 + 0.5 + + + True + False + 8 + 8 + 8 + 8 + 8 + 16 + True + + + _Enabled + True + True + False + start + True + True + + + + 1 + 0 + + + + + True + False + True + word-char + end + 0 + + + 1 + 1 + + + + + True + True + start + 6 + scal + 2 + 1 + + + + 1 + 3 + + + + + True + False + _Scale + True + scale + 1 + + + 0 + 3 + + + + + True + False + Si_ze + True + width + 1 + + + 0 + 5 + + + + + True + False + _Position + True + pos_x + 1 + + + 0 + 4 + + + + + True + False + _Refresh + True + refresh + 1 + + + 0 + 6 + + + + + True + False + start + 8 + + + True + True + 10 + number + + + + False + False + 0 + + + + + True + False + Hz + + + False + True + 1 + + + + + 1 + 6 + + + + + True + False + start + + + True + True + 6 + number + + + + False + True + 0 + + + + + 20 + True + False + , + + + False + True + 1 + + + + + True + True + 6 + number + + + + False + True + 2 + + + + + 1 + 4 + + + + + True + False + start + + + True + True + 6 + number + + + + False + True + 0 + + + + + 20 + True + False + × + + + False + True + 1 + + + + + True + True + 6 + number + + + + False + True + 2 + + + + + True + True + True + Select Mode Preset + 8 + modes + + + True + False + view-more-symbolic + + + + + False + True + 3 + + + + + 1 + 5 + + + + + True + False + Description + 1 + + + 0 + 1 + + + + + True + False + True + word-char + end + 0 + + + 1 + 2 + + + + + True + False + Physical Size + 1 + + + 0 + 2 + + + + + True + False + _Transform + True + 1 + + + 0 + 7 + + + + + True + True + True + transforms + + + + + + 1 + 7 + + + + + _Flipped + True + True + False + start + True + True + + + + 1 + 8 + + + + + + + + + + + False + mode_button + + + True + False + 10 + 10 + 10 + 10 + vertical + + + + + + + + False + rotate_button + + + True + False + 10 + 10 + 10 + 10 + vertical + + + True + True + True + transform.rotate_0 + Don't Rotate + + + False + True + 0 + + + + + True + True + True + transform.rotate_90 + Rotate 90° + + + False + True + 1 + + + + + True + True + True + transform.rotate_180 + Rotate 180° + + + False + True + 2 + + + + + True + True + True + transform.rotate_270 + Rotate 270° + + + False + True + 3 + + + + + + diff --git a/resources/meson.build b/resources/meson.build new file mode 100644 index 0000000..f4f3f55 --- /dev/null +++ b/resources/meson.build @@ -0,0 +1,7 @@ + +gnome = import('gnome') +resources = gnome.compile_resources( + 'waydisplay-resources', 'resources.xml', + source_dir : '.', + c_name : 'waydisplay_resources') + diff --git a/resources/resources.xml b/resources/resources.xml new file mode 100644 index 0000000..80556eb --- /dev/null +++ b/resources/resources.xml @@ -0,0 +1,8 @@ + + + + waydisplay.ui + head.ui + style.css + + diff --git a/resources/style.css b/resources/style.css new file mode 100644 index 0000000..566ac6e --- /dev/null +++ b/resources/style.css @@ -0,0 +1,9 @@ +spinner { + opacity: 0; + transition: opacity 200ms ease-in-out; + background-color: rgba(64, 64, 64, 0.5); +} + +spinner.visible { + opacity: 1; +} diff --git a/resources/waydisplay.ui b/resources/waydisplay.ui new file mode 100644 index 0000000..b6f32c4 --- /dev/null +++ b/resources/waydisplay.ui @@ -0,0 +1,287 @@ + + + + + + False + Waydisplay + + + True + False + + + True + True + 400 + True + + + True + True + + + True + False + 400 + + + + + + True + False + + + + + True + False + vertical + + + True + False + center + 8 + 8 + 8 + True + heads_stack + + + False + True + 0 + + + + + True + False + crossfade + + + + + + False + True + 1 + + + + + False + False + + + + + -1 + + + + + True + False + True + True + True + True + + + True + + + + + False + True + start + error + True + False + + + + False + 6 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + True + 0 + + + True + True + 2 + + + + + True + True + 0 + + + + + 1 + + + + + + + True + False + crossfade + + + True + False + Waydisplay + False + True + + + True + False + expand + + + True + True + True + Zoom Out + + + + True + False + zoom-out-symbolic + + + + + + True + True + 0 + True + + + + + True + True + True + Zoom Reset + + + + + True + True + 1 + True + + + + + True + True + True + Zoom In + + + + True + False + zoom-in-symbolic + + + + + + True + True + 2 + True + + + + + + + title + + + + + True + False + + + True + False + Apply Changes? + + + + + _Apply + True + True + True + True + + + + + end + + + + + _Cancel + True + True + True + True + + + + 1 + + + + + apply + 1 + + + + + + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1ac45da --- /dev/null +++ b/src/main.c @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2019 cyclopsian + + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include "wdisplay.h" + +__attribute__((noreturn)) void wd_fatal_error(int status, const char *message) { + GtkWindow *parent = gtk_application_get_active_window(GTK_APPLICATION(g_application_get_default())); + GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", message); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + exit(status); +} + +#define DEFAULT_ZOOM 0.1 +#define MIN_ZOOM (1./1000.) +#define MAX_ZOOM 1000. +#define CANVAS_MARGIN 100 + +static const char *MODE_PREFIX = "mode"; +static const char *TRANSFORM_PREFIX = "transform"; + +#define NUM_ROTATIONS 4 +static const char *ROTATE_IDS[NUM_ROTATIONS] = { + "rotate_0", "rotate_90", "rotate_180", "rotate_270" +}; + +static int get_rotate_index(enum wl_output_transform transform) { + if (transform == WL_OUTPUT_TRANSFORM_90 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_90) { + return 1; + } else if (transform == WL_OUTPUT_TRANSFORM_180 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_180) { + return 2; + } else if (transform == WL_OUTPUT_TRANSFORM_270 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_270) { + return 3; + } + return 0; +} + +static bool has_changes(const struct wd_state *state) { + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); + const struct wd_head *head = g_object_get_data(G_OBJECT(form_iter->data), "head"); + if (head->enabled != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled")))) { + return TRUE; + } + if (head->scale != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale")))) { + return TRUE; + } + if (head->x != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_x"))))) { + return TRUE; + } + if (head->y != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_y"))))) { + return TRUE; + } + int w = head->mode != NULL ? head->mode->width : head->custom_mode.width; + if (w != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "width"))))) { + return TRUE; + } + int h = head->mode != NULL ? head->mode->height : head->custom_mode.height; + if (h != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "height"))))) { + return TRUE; + } + int r = head->mode != NULL ? head->mode->refresh : head->custom_mode.refresh; + if (r / 1000. != atof(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "refresh"))))) { + return TRUE; + } + for (int i = 0; i < NUM_ROTATIONS; i++) { + GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); + gboolean selected; + g_object_get(rotate, "active", &selected, NULL); + if (selected) { + if (i != get_rotate_index(head->transform)) { + return TRUE; + } + break; + } + } + bool flipped = head->transform == WL_OUTPUT_TRANSFORM_FLIPPED + || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 + || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180 + || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270; + if (flipped != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "flipped")))) { + return TRUE; + } + } + return FALSE; +} + +// BEGIN FORM CALLBACKS +static void show_apply(struct wd_state *state) { + bool changed = has_changes(state); + gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), changed ? "apply" : "title"); + gtk_widget_queue_draw(state->canvas); +} + +static void update_sensitivity(GtkWidget *form) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + GtkWidget *enabled = GTK_WIDGET(gtk_builder_get_object(builder, "enabled")); + bool enabled_toggled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(enabled)); + + g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(form)); + for (GList *child = children; child != NULL; child = child->next) { + GtkWidget *widget = GTK_WIDGET(child->data); + if (widget != enabled) { + gtk_widget_set_sensitive(widget, enabled_toggled); + } + } +} + +static void select_rotate_option(GtkWidget *form, GtkWidget *model_button) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + GtkWidget *rotate_button = GTK_WIDGET(gtk_builder_get_object(builder, "rotate_button")); + for (int i = 0; i < NUM_ROTATIONS; i++) { + GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); + gboolean selected = model_button == rotate; + g_object_set(rotate, "active", selected, NULL); + if (selected) { + g_autofree gchar *rotate_text = NULL; + g_object_get(rotate, "text", &rotate_text, NULL); + gtk_button_set_label(GTK_BUTTON(rotate_button), rotate_text); + } + } +} + +static void rotate_selected(GSimpleAction *action, GVariant *param, gpointer data) { + select_rotate_option(GTK_WIDGET(data), g_object_get_data(G_OBJECT(action), "widget")); + const struct wd_head *head = g_object_get_data(G_OBJECT(data), "head"); + show_apply(head->state); +} + +static void select_mode_option(GtkWidget *form, int32_t w, int32_t h, int32_t r) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + GtkWidget *mode_box = GTK_WIDGET(gtk_builder_get_object(builder, "mode_box")); + g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(mode_box)); + for (GList *child = children; child != NULL; child = child->next) { + const struct wd_mode *mode = g_object_get_data(G_OBJECT(child->data), "mode"); + g_object_set(child->data, "active", w == mode->width && h == mode->height && r == mode->refresh, NULL); + } +} + +static void update_mode_entries(GtkWidget *form, int32_t w, int32_t h, int32_t r) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + GtkWidget *width = GTK_WIDGET(gtk_builder_get_object(builder, "width")); + GtkWidget *height = GTK_WIDGET(gtk_builder_get_object(builder, "height")); + GtkWidget *refresh = GTK_WIDGET(gtk_builder_get_object(builder, "refresh")); + + g_autofree gchar *widthstr = g_strdup_printf("%d", w); + gtk_entry_set_text(GTK_ENTRY(width), widthstr); + g_autofree gchar *heightstr = g_strdup_printf("%d", h); + gtk_entry_set_text(GTK_ENTRY(height), heightstr); + g_autofree gchar *refreshstr = g_strdup_printf("%0.3f", r / 1000.0); + gtk_entry_set_text(GTK_ENTRY(refresh), refreshstr); +} + +static void mode_selected(GSimpleAction *action, GVariant *param, gpointer data) { + GtkWidget *form = data; + const struct wd_head *head = g_object_get_data(G_OBJECT(form), "head"); + const struct wd_mode *mode = g_object_get_data(G_OBJECT(action), "mode"); + + update_mode_entries(form, mode->width, mode->height, mode->refresh); + select_mode_option(form, mode->width, mode->height, mode->refresh); + show_apply(head->state); +} +// END FORM CALLBACKS + +/* + * Recalculates the desired canvas size, accounting for zoom + margins. + */ +static void update_canvas_size(struct wd_state *state) { + int xmin = 0; + int xmax = 0; + int ymin = 0; + int ymax = 0; + + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + int w = head->custom_mode.width; + int h = head->custom_mode.height; + if (head->enabled && head->mode != NULL) { + w = head->mode->width; + h = head->mode->height; + } + + int x2 = head->x + w; + int y2 = head->y + h; + xmin = MIN(xmin, head->x); + xmax = MAX(xmax, x2); + ymin = MIN(ymin, head->y); + ymax = MAX(ymax, y2); + } + // update canvas sizings + state->xorigin = floor(xmin * state->zoom) - CANVAS_MARGIN; + state->yorigin = floor(ymin * state->zoom) - CANVAS_MARGIN; + int heads_width = ceil((xmax - xmin) * state->zoom) + CANVAS_MARGIN * 2; + int heads_height = ceil((ymax - ymin) * state->zoom) + CANVAS_MARGIN * 2; + gtk_layout_set_size(GTK_LAYOUT(state->canvas), heads_width, heads_height); +} + +static void clear_menu(GtkWidget *box, GActionMap *action_map) { + g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(box)); + for (GList *child = children; child != NULL; child = child->next) { + g_action_map_remove_action(action_map, strchr(gtk_actionable_get_action_name(GTK_ACTIONABLE(child->data)), '.') + 1); + gtk_container_remove(GTK_CONTAINER(box), GTK_WIDGET(child->data)); + } +} +static void update_head_form(GtkWidget *form, unsigned int fields) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + GtkWidget *description = GTK_WIDGET(gtk_builder_get_object(builder, "description")); + GtkWidget *physical_size = GTK_WIDGET(gtk_builder_get_object(builder, "physical_size")); + GtkWidget *enabled = GTK_WIDGET(gtk_builder_get_object(builder, "enabled")); + GtkWidget *scale = GTK_WIDGET(gtk_builder_get_object(builder, "scale")); + GtkWidget *pos_x = GTK_WIDGET(gtk_builder_get_object(builder, "pos_x")); + GtkWidget *pos_y = GTK_WIDGET(gtk_builder_get_object(builder, "pos_y")); + GtkWidget *mode_box = GTK_WIDGET(gtk_builder_get_object(builder, "mode_box")); + GtkWidget *flipped = GTK_WIDGET(gtk_builder_get_object(builder, "flipped")); + const struct wd_head *head = g_object_get_data(G_OBJECT(form), "head"); + + if (fields & WD_FIELD_NAME) { + gtk_container_child_set(GTK_CONTAINER(head->state->stack), form, "name", head->name, "title", head->name, NULL); + } + if (fields & WD_FIELD_DESCRIPTION) { + gtk_label_set_text(GTK_LABEL(description), head->description); + } + if (fields & WD_FIELD_PHYSICAL_SIZE) { + g_autofree gchar *physical_str = g_strdup_printf("%dmm × %dmm", head->phys_width, head->phys_height); + gtk_label_set_text(GTK_LABEL(physical_size), physical_str); + } + if (fields & WD_FIELD_ENABLED) { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enabled), head->enabled); + } + if (fields & WD_FIELD_SCALE) { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(scale), head->scale); + } + if (fields & WD_FIELD_POSITION) { + g_autofree gchar *xstr = g_strdup_printf("%d", head->x); + gtk_entry_set_text(GTK_ENTRY(pos_x), xstr); + g_autofree gchar *ystr = g_strdup_printf("%d", head->y); + gtk_entry_set_text(GTK_ENTRY(pos_y), ystr); + } + + if (fields & WD_FIELD_MODE) { + GActionMap *mode_actions = G_ACTION_MAP(g_object_get_data(G_OBJECT(form), "mode-group")); + clear_menu(mode_box, mode_actions); + struct wd_mode *mode; + wl_list_for_each(mode, &head->modes, link) { + g_autofree gchar *name = g_strdup_printf("%d×%d@%0.3fHz", mode->width, mode->height, mode->refresh / 1000.); + GSimpleAction *action = g_simple_action_new(name, NULL); + g_action_map_add_action(G_ACTION_MAP(mode_actions), G_ACTION(action)); + g_signal_connect(action, "activate", G_CALLBACK(mode_selected), form); + g_object_set_data(G_OBJECT(action), "mode", mode); + g_object_unref(action); + + GtkWidget *button = gtk_model_button_new(); + g_autoptr(GString) prefixed_name = g_string_new(MODE_PREFIX); + g_string_append(prefixed_name, "."); + g_string_append(prefixed_name, name); + gtk_actionable_set_action_name(GTK_ACTIONABLE(button), prefixed_name->str); + g_object_set(button, "role", GTK_BUTTON_ROLE_RADIO, "text", name, NULL); + gtk_box_pack_start(GTK_BOX(mode_box), button, FALSE, FALSE, 0); + g_object_set_data(G_OBJECT(button), "mode", mode); + gtk_widget_show_all(button); + } + // Mode entries + int w = head->custom_mode.width; + int h = head->custom_mode.height; + int r = head->custom_mode.refresh; + if (head->enabled && head->mode != NULL) { + w = head->mode->width; + h = head->mode->height; + r = head->mode->refresh; + } + update_mode_entries(form, w, h, r); + select_mode_option(form, w, h, r); + gtk_widget_show_all(mode_box); + } + + if (fields & WD_FIELD_TRANSFORM) { + int active_rotate = get_rotate_index(head->transform); + select_rotate_option(form, GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[active_rotate]))); + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(flipped), + head->transform == WL_OUTPUT_TRANSFORM_FLIPPED + || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 + || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180 + || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270); + } + + // Sync state + if (fields & WD_FIELD_ENABLED) { + update_sensitivity(form); + } + show_apply(head->state); + gtk_widget_queue_draw(head->state->canvas); +} + +void wd_ui_reset_heads(struct wd_state *state) { + if (state->stack == NULL) { + return; + } + + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + GList *form_iter = forms; + struct wd_head *head; + wl_list_for_each(head, &state->heads, link) { + GtkBuilder *builder; + GtkWidget *form; + if (form_iter == NULL) { + builder = gtk_builder_new_from_resource("/head.ui"); + form = GTK_WIDGET(gtk_builder_get_object(builder, "form")); + g_object_set_data(G_OBJECT(form), "builder", builder); + g_object_set_data(G_OBJECT(form), "head", head); + gtk_stack_add_titled(GTK_STACK(state->stack), form, head->name, head->name); + + GtkWidget *mode_button = GTK_WIDGET(gtk_builder_get_object(builder, "mode_button")); + GtkWidget *rotate_button = GTK_WIDGET(gtk_builder_get_object(builder, "rotate_button")); + + GSimpleActionGroup *mode_actions = g_simple_action_group_new(); + gtk_widget_insert_action_group(mode_button, MODE_PREFIX, G_ACTION_GROUP(mode_actions)); + g_object_set_data(G_OBJECT(form), "mode-group", mode_actions); + g_object_unref(mode_actions); + + GSimpleActionGroup *transform_actions = g_simple_action_group_new(); + gtk_widget_insert_action_group(rotate_button, TRANSFORM_PREFIX, G_ACTION_GROUP(transform_actions)); + g_object_unref(transform_actions); + + for (int i = 0; i < NUM_ROTATIONS; i++) { + GtkWidget *button = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); + g_object_set(button, "role", GTK_BUTTON_ROLE_RADIO, NULL); + GSimpleAction *action = g_simple_action_new(ROTATE_IDS[i], NULL); + g_action_map_add_action(G_ACTION_MAP(transform_actions), G_ACTION(action)); + g_signal_connect(action, "activate", G_CALLBACK(rotate_selected), form); + g_object_set_data(G_OBJECT(action), "widget", button); + g_object_unref(action); + } + update_head_form(form, WD_FIELDS_ALL); + + gtk_widget_show_all(form); + + g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(update_sensitivity), form); + g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "scale"), "value-changed", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_x"), "changed", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_y"), "changed", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "width"), "changed", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "height"), "changed", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "refresh"), "changed", G_CALLBACK(show_apply), state); + g_signal_connect_swapped(gtk_builder_get_object(builder, "flipped"), "toggled", G_CALLBACK(show_apply), state); + + } else { + form = form_iter->data; + g_object_set_data(G_OBJECT(form), "head", head); + form_iter = form_iter->next; + } + } + // remove everything else + for (; form_iter != NULL; form_iter = form_iter->next) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); + g_object_unref(builder); + gtk_container_remove(GTK_CONTAINER(state->stack), GTK_WIDGET(form_iter->data)); + } + gtk_widget_queue_draw(state->canvas); +} + +/* + * Updates the UI form for a single head. Useful for when the compositor notifies us of + * updated configuration caused by another program. + */ +void wd_ui_reset_head(const struct wd_head *head, unsigned int fields) { + if (head->state->stack == NULL) { + return; + } + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(head->state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); + if (head == other) { + update_head_form(GTK_WIDGET(form_iter->data), fields); + } + } +} + +void wd_ui_reset_all(struct wd_state *state) { + wd_ui_reset_heads(state); + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + update_head_form(GTK_WIDGET(form_iter->data), WD_FIELDS_ALL); + } +} + +void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs) { + gtk_style_context_remove_class(gtk_widget_get_style_context(state->spinner), "visible"); + gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(state->overlay), state->spinner, TRUE); + + gtk_widget_set_sensitive(state->stack_switcher, TRUE); + gtk_widget_set_sensitive(state->stack, TRUE); + gtk_widget_set_sensitive(state->zoom_in, TRUE); + gtk_widget_set_sensitive(state->zoom_reset, TRUE); + gtk_widget_set_sensitive(state->zoom_out, TRUE); + show_apply(state); +} + +void wd_ui_show_error(struct wd_state *state, const char *message) { + gtk_label_set_text(GTK_LABEL(state->info_label), message); + gtk_widget_show(state->info_bar); + gtk_info_bar_set_revealed(GTK_INFO_BAR(state->info_bar), TRUE); +} + +void fill_output_from_form(struct wd_head_config *output, GtkWidget *form) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); + output->head = g_object_get_data(G_OBJECT(form), "head"); + output->enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); + output->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); + output->x = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_x")))); + output->y = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_y")))); + output->width = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "width")))); + output->height = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "height")))); + output->refresh = atof(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "refresh")))) * 1000.; + gboolean flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "flipped"))); + for (int i = 0; i < NUM_ROTATIONS; i++) { + GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); + gboolean selected; + g_object_get(rotate, "active", &selected, NULL); + if (selected) { + switch (i) { + case 0: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED : WL_OUTPUT_TRANSFORM_NORMAL; break; + case 1: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_90 : WL_OUTPUT_TRANSFORM_90; break; + case 2: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_180 : WL_OUTPUT_TRANSFORM_180; break; + case 3: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_270 : WL_OUTPUT_TRANSFORM_270; break; + } + break; + } + } +} + +// BEGIN GLOBAL CALLBACKS +static void cleanup(GtkWidget *window, gpointer state) { + g_free(state); +} + +gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer data) { + struct wd_state *state = data; + update_canvas_size(state); + GtkStyleContext *style_ctx = gtk_widget_get_style_context(widget); + GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); + double scroll_x = gtk_adjustment_get_value(scroll_x_adj); + double scroll_y = gtk_adjustment_get_value(scroll_y_adj); + int width = gtk_widget_get_allocated_width(widget); + int height = gtk_widget_get_allocated_height(widget); + + GdkRGBA border; + gtk_style_context_lookup_color(style_ctx, "borders", &border); + + gdk_cairo_set_source_rgba(cr, &border); + cairo_set_line_width(cr, .5); + + gtk_render_background(style_ctx, cr, 0, 0, width, height); + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); + gboolean enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); + if (enabled) { + int x = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_x")))); + int y = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_y")))); + int w = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "width")))); + int h = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "height")))); + cairo_rectangle(cr, + x * state->zoom + .5 - scroll_x - state->xorigin, + y * state->zoom + .5 - scroll_y - state->yorigin, + w * state->zoom, + h * state->zoom); + cairo_stroke(cr); + } + } + + return TRUE; +} + +static void cancel_changes(GtkButton *button, gpointer data) { + struct wd_state *state = data; + gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title"); + wd_ui_reset_all(state); +} + +static void apply_changes(GtkButton *button, gpointer data) { + struct wd_state *state = data; + gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title"); + gtk_style_context_add_class(gtk_widget_get_style_context(state->spinner), "visible"); + gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(state->overlay), state->spinner, FALSE); + + gtk_widget_set_sensitive(state->stack_switcher, FALSE); + gtk_widget_set_sensitive(state->stack, FALSE); + gtk_widget_set_sensitive(state->zoom_in, FALSE); + gtk_widget_set_sensitive(state->zoom_reset, FALSE); + gtk_widget_set_sensitive(state->zoom_out, FALSE); + + struct wl_list *outputs = calloc(1, sizeof(*outputs)); + wl_list_init(outputs); + g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); + for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { + struct wd_head_config *output = calloc(1, sizeof(*output)); + wl_list_insert(outputs, &output->link); + fill_output_from_form(output, GTK_WIDGET(form_iter->data)); + } + wd_apply_state(state, outputs); +} + +static void update_zoom(struct wd_state *state) { + g_autofree gchar *zoom_percent = g_strdup_printf("%.f%%", state->zoom * 100.); + gtk_button_set_label(GTK_BUTTON(state->zoom_reset), zoom_percent); + gtk_widget_set_sensitive(state->zoom_in, state->zoom < MAX_ZOOM); + gtk_widget_set_sensitive(state->zoom_out, state->zoom > MIN_ZOOM); + gtk_widget_queue_draw(state->canvas); +} + +static void zoom_out(GtkButton *button, gpointer data) { + struct wd_state *state = data; + state->zoom *= 0.75; + state->zoom = MAX(state->zoom, MIN_ZOOM); + update_zoom(state); +} + +static void zoom_reset(GtkButton *button, gpointer data) { + struct wd_state *state = data; + state->zoom = DEFAULT_ZOOM; + update_zoom(state); +} + +static void zoom_in(GtkButton *button, gpointer data) { + struct wd_state *state = data; + state->zoom /= 0.75; + state->zoom = MIN(state->zoom, MAX_ZOOM); + update_zoom(state); +} + +static void info_response(GtkInfoBar *info_bar, gint response_id, gpointer data) { + gtk_info_bar_set_revealed(info_bar, FALSE); +} + +static void info_bar_animation_done(GObject *object, GParamSpec *pspec, gpointer data) { + gboolean done = gtk_revealer_get_child_revealed(GTK_REVEALER(object)); + if (!done) { + struct wd_state *state = data; + gtk_widget_set_visible(state->info_bar, gtk_revealer_get_reveal_child(GTK_REVEALER(object))); + } +} + +static void activate(GtkApplication* app, gpointer user_data) { + GdkDisplay *gdk_display = gdk_display_get_default(); + if (!GDK_IS_WAYLAND_DISPLAY(gdk_display)) { + wd_fatal_error(1, "This program is only usable on Wayland sessions."); + } + + struct wd_state *state = g_new0(struct wd_state, 1); + state->zoom = DEFAULT_ZOOM; + wl_list_init(&state->heads); + + GtkCssProvider *css_provider = gtk_css_provider_new(); + gtk_css_provider_load_from_resource(css_provider, "/style.css"); + gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + GtkBuilder *builder = gtk_builder_new_from_resource("/wd.ui"); + GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "heads_window")); + state->header_stack = GTK_WIDGET(gtk_builder_get_object(builder, "header_stack")); + state->stack_switcher = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack_switcher")); + state->stack = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack")); + state->scroller = GTK_WIDGET(gtk_builder_get_object(builder, "heads_scroll")); + state->canvas = GTK_WIDGET(gtk_builder_get_object(builder, "heads_layout")); + state->spinner = GTK_WIDGET(gtk_builder_get_object(builder, "spinner")); + state->zoom_out = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_out")); + state->zoom_reset = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_reset")); + state->zoom_in = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_in")); + state->overlay = GTK_WIDGET(gtk_builder_get_object(builder, "overlay")); + state->info_bar = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info")); + state->info_label = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info_label")); + gtk_builder_add_callback_symbol(builder, "heads_draw", G_CALLBACK(draw)); + gtk_builder_add_callback_symbol(builder, "apply_changes", G_CALLBACK(apply_changes)); + gtk_builder_add_callback_symbol(builder, "cancel_changes", G_CALLBACK(cancel_changes)); + gtk_builder_add_callback_symbol(builder, "zoom_out", G_CALLBACK(zoom_out)); + gtk_builder_add_callback_symbol(builder, "zoom_reset", G_CALLBACK(zoom_reset)); + gtk_builder_add_callback_symbol(builder, "zoom_in", G_CALLBACK(zoom_in)); + gtk_builder_add_callback_symbol(builder, "info_response", G_CALLBACK(info_response)); + gtk_builder_connect_signals(builder, state); + gtk_box_set_homogeneous(GTK_BOX(gtk_builder_get_object(builder, "zoom_box")), FALSE); + update_zoom(state); + + /* first child of GtkInfoBar is always GtkRevealer */ + g_autoptr(GList) info_children = gtk_container_get_children(GTK_CONTAINER(state->info_bar)); + g_signal_connect(info_children->data, "notify::child-revealed", G_CALLBACK(info_bar_animation_done), state); + + struct wl_display *display = gdk_wayland_display_get_wl_display(gdk_display); + wd_add_output_management_listener(state, display); + + if (state->output_manager == NULL) { + wd_fatal_error(1, "Compositor doesn't support wlr-output-management-unstable-v1"); + } + + gtk_application_add_window(app, GTK_WINDOW(window)); + gtk_widget_show_all(window); + g_signal_connect(window, "destroy", G_CALLBACK(cleanup), state); + g_object_unref(builder); +} +// END GLOBAL CALLBACKS + +int main(int argc, char *argv[]) { + GtkApplication *app = gtk_application_new("org.swaywm.sway-outputs", G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + int status = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + + return status; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..326fd5f --- /dev/null +++ b/src/meson.build @@ -0,0 +1,21 @@ + +cc = meson.get_compiler('c') +m_dep = cc.find_library('m', required : false) +gdk = dependency('gdk-3.0') +gtk = dependency('gtk+-3.0') +assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') + +executable( + 'wdisplay', + [ + 'main.c', + 'outputs.c', + resources, + ], + dependencies : [ + m_dep, + wayland_client, + client_protos, + gtk + ] +) diff --git a/src/outputs.c b/src/outputs.c new file mode 100644 index 0000000..cc78b90 --- /dev/null +++ b/src/outputs.c @@ -0,0 +1,349 @@ + +/* + * Copyright (C) 2019 cyclopsian + * Copyright (C) 2017-2019 emersion + + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Parts of this file are taken from emersion/kanshi: + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c + */ + +#include +#include +#include +#include + +#include "wdisplay.h" +#include "wlr-output-management-unstable-v1-client-protocol.h" + +#define HEADS_MAX 64 + +struct wd_pending_config { + struct wd_state *state; + struct wl_list *outputs; +}; + +static void destroy_pending(struct wd_pending_config *pending) { + struct wd_head_config *output, *tmp; + wl_list_for_each_safe(output, tmp, pending->outputs, link) { + wl_list_remove(&output->link); + free(output); + } + free(pending->outputs); + free(pending); +} + +static void config_handle_succeeded(void *data, + struct zwlr_output_configuration_v1 *config) { + struct wd_pending_config *pending = data; + zwlr_output_configuration_v1_destroy(config); + wd_ui_apply_done(pending->state, pending->outputs); + destroy_pending(pending); +} + +static void config_handle_failed(void *data, + struct zwlr_output_configuration_v1 *config) { + struct wd_pending_config *pending = data; + zwlr_output_configuration_v1_destroy(config); + wd_ui_reset_all(pending->state); + wd_ui_apply_done(pending->state, NULL); + wd_ui_show_error(pending->state, + "The display server was not able to process your changes."); + destroy_pending(pending); +} + +static void config_handle_cancelled(void *data, + struct zwlr_output_configuration_v1 *config) { + struct wd_pending_config *pending = data; + zwlr_output_configuration_v1_destroy(config); + wd_ui_reset_all(pending->state); + wd_ui_apply_done(pending->state, NULL); + wd_ui_show_error(pending->state, + "The display configuration was modified by the server before updates were processed. " + "Please check the configuration and apply the changes again."); + destroy_pending(pending); +} + +static const struct zwlr_output_configuration_v1_listener config_listener = { + .succeeded = config_handle_succeeded, + .failed = config_handle_failed, + .cancelled = config_handle_cancelled, +}; + +void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs) { + struct zwlr_output_configuration_v1 *config = + zwlr_output_manager_v1_create_configuration(state->output_manager, state->serial); + + struct wd_pending_config *pending = calloc(1, sizeof(*pending)); + pending->state = state; + pending->outputs = new_outputs; + + zwlr_output_configuration_v1_add_listener(config, &config_listener, pending); + + ssize_t i = -1; + struct wd_head_config *output; + wl_list_for_each(output, new_outputs, link) { + i++; + struct wd_head *head = output->head; + + if (!output->enabled && output->enabled != head->enabled) { + zwlr_output_configuration_v1_disable_head(config, head->wlr_head); + continue; + } + + struct zwlr_output_configuration_head_v1 *config_head = zwlr_output_configuration_v1_enable_head(config, head->wlr_head); + + const struct wd_mode *selected_mode = NULL; + const struct wd_mode *mode; + wl_list_for_each(mode, &head->modes, link) { + if (mode->width == output->width && mode->height == output->height && mode->refresh == output->refresh) { + selected_mode = mode; + break; + } + } + if (selected_mode != NULL) { + if (selected_mode != head->mode) { + zwlr_output_configuration_head_v1_set_mode(config_head, selected_mode->wlr_mode); + } + } else if (output->width != head->custom_mode.width + || output->height != head->custom_mode.height + || output->refresh != head->custom_mode.refresh) { + zwlr_output_configuration_head_v1_set_custom_mode(config_head, + output->width, output->height, output->refresh); + } + if (output->x != head->x || output->y != head->y) { + zwlr_output_configuration_head_v1_set_position(config_head, output->x, output->y); + } + if (output->scale != head->scale) { + zwlr_output_configuration_head_v1_set_scale(config_head, wl_fixed_from_double(output->scale)); + } + if (output->transform != head->transform) { + zwlr_output_configuration_head_v1_set_transform(config_head, output->transform); + } + } + + zwlr_output_configuration_v1_apply(config); +} + +static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode, + int32_t width, int32_t height) { + struct wd_mode *mode = data; + mode->width = width; + mode->height = height; +} + +static void mode_handle_refresh(void *data, + struct zwlr_output_mode_v1 *wlr_mode, int32_t refresh) { + struct wd_mode *mode = data; + mode->refresh = refresh; +} + +static void mode_handle_preferred(void *data, + struct zwlr_output_mode_v1 *wlr_mode) { + struct wd_mode *mode = data; + mode->preferred = true; +} + +static void mode_handle_finished(void *data, + struct zwlr_output_mode_v1 *wlr_mode) { + struct wd_mode *mode = data; + wl_list_remove(&mode->link); + zwlr_output_mode_v1_destroy(mode->wlr_mode); + free(mode); +} + +static const struct zwlr_output_mode_v1_listener mode_listener = { + .size = mode_handle_size, + .refresh = mode_handle_refresh, + .preferred = mode_handle_preferred, + .finished = mode_handle_finished, +}; + +static void head_handle_name(void *data, + struct zwlr_output_head_v1 *wlr_head, const char *name) { + struct wd_head *head = data; + head->name = strdup(name); + wd_ui_reset_head(head, WD_FIELD_NAME); +} + +static void head_handle_description(void *data, + struct zwlr_output_head_v1 *wlr_head, const char *description) { + struct wd_head *head = data; + head->description = strdup(description); + wd_ui_reset_head(head, WD_FIELD_DESCRIPTION); +} + +static void head_handle_physical_size(void *data, + struct zwlr_output_head_v1 *wlr_head, int32_t width, int32_t height) { + struct wd_head *head = data; + head->phys_width = width; + head->phys_height = height; + wd_ui_reset_head(head, WD_FIELD_PHYSICAL_SIZE); +} + +static void head_handle_mode(void *data, + struct zwlr_output_head_v1 *wlr_head, + struct zwlr_output_mode_v1 *wlr_mode) { + struct wd_head *head = data; + + struct wd_mode *mode = calloc(1, sizeof(*mode)); + mode->head = head; + mode->wlr_mode = wlr_mode; + wl_list_insert(head->modes.prev, &mode->link); + + zwlr_output_mode_v1_add_listener(wlr_mode, &mode_listener, mode); +} + +static void head_handle_enabled(void *data, + struct zwlr_output_head_v1 *wlr_head, int32_t enabled) { + struct wd_head *head = data; + head->enabled = !!enabled; + if (!enabled) { + head->mode = NULL; + } + wd_ui_reset_head(head, WD_FIELD_ENABLED); +} + +static void head_handle_current_mode(void *data, + struct zwlr_output_head_v1 *wlr_head, + struct zwlr_output_mode_v1 *wlr_mode) { + struct wd_head *head = data; + struct wd_mode *mode; + wl_list_for_each(mode, &head->modes, link) { + if (mode->wlr_mode == wlr_mode) { + head->mode = mode; + wd_ui_reset_head(head, WD_FIELD_MODE); + return; + } + } + fprintf(stderr, "received unknown current_mode\n"); + head->mode = NULL; +} + +static void head_handle_position(void *data, + struct zwlr_output_head_v1 *wlr_head, int32_t x, int32_t y) { + struct wd_head *head = data; + head->x = x; + head->y = y; + wd_ui_reset_head(head, WD_FIELD_POSITION); +} + +static void head_handle_transform(void *data, + struct zwlr_output_head_v1 *wlr_head, int32_t transform) { + struct wd_head *head = data; + head->transform = transform; + wd_ui_reset_head(head, WD_FIELD_TRANSFORM); +} + +static void head_handle_scale(void *data, + struct zwlr_output_head_v1 *wlr_head, wl_fixed_t scale) { + struct wd_head *head = data; + head->scale = wl_fixed_to_double(scale); + wd_ui_reset_head(head, WD_FIELD_SCALE); +} + +static void head_handle_finished(void *data, + struct zwlr_output_head_v1 *wlr_head) { + struct wd_head *head = data; + wl_list_remove(&head->link); + zwlr_output_head_v1_destroy(head->wlr_head); + free(head->name); + free(head->description); + free(head); +} + +static const struct zwlr_output_head_v1_listener head_listener = { + .name = head_handle_name, + .description = head_handle_description, + .physical_size = head_handle_physical_size, + .mode = head_handle_mode, + .enabled = head_handle_enabled, + .current_mode = head_handle_current_mode, + .position = head_handle_position, + .transform = head_handle_transform, + .scale = head_handle_scale, + .finished = head_handle_finished, +}; + +static void output_manager_handle_head(void *data, + struct zwlr_output_manager_v1 *manager, + struct zwlr_output_head_v1 *wlr_head) { + struct wd_state *state = data; + + struct wd_head *head = calloc(1, sizeof(*head)); + head->state = state; + head->wlr_head = wlr_head; + head->scale = 1.0; + wl_list_init(&head->modes); + wl_list_insert(&state->heads, &head->link); + + zwlr_output_head_v1_add_listener(wlr_head, &head_listener, head); +} + +static void output_manager_handle_done(void *data, + struct zwlr_output_manager_v1 *manager, uint32_t serial) { + struct wd_state *state = data; + state->serial = serial; + + assert(wl_list_length(&state->heads) <= HEADS_MAX); + wd_ui_reset_heads(state); +} + +static void output_manager_handle_finished(void *data, + struct zwlr_output_manager_v1 *manager) { + // This space is intentionally left blank +} + +static const struct zwlr_output_manager_v1_listener output_manager_listener = { + .head = output_manager_handle_head, + .done = output_manager_handle_done, + .finished = output_manager_handle_finished, +}; + +static void registry_handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) { + struct wd_state *state = data; + + if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) { + state->output_manager = wl_registry_bind(registry, name, &zwlr_output_manager_v1_interface, 1); + zwlr_output_manager_v1_add_listener(state->output_manager, &output_manager_listener, state); + } +} + +static void registry_handle_global_remove(void *data, + struct wl_registry *registry, uint32_t name) { + // This space is intentionally left blank +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_handle_global, + .global_remove = registry_handle_global_remove, +}; + +void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display) { + struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, state); + + wl_display_dispatch(display); + wl_display_roundtrip(display); +} diff --git a/src/wdisplay.h b/src/wdisplay.h new file mode 100644 index 0000000..303ac3d --- /dev/null +++ b/src/wdisplay.h @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2019 cyclopsian + * Copyright (C) 2017-2019 emersion + + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Parts of this file are taken from emersion/kanshi: + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/kanshi.h + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h + */ + +#ifndef WDISPLAY_WDISPLAY_H +#define WDISPLAY_WDISPLAY_H + +#include +#include + +struct zwlr_output_mode_v1; +struct zwlr_output_head_v1; +struct zwlr_output_manager_v1; +struct _GtkWidget; +typedef struct _GtkWidget GtkWidget; +struct _GtkBuilder; +typedef struct _GtkBuilder GtkBuilder; + +enum wd_head_fields { + WD_FIELD_NAME = 1 << 0, + WD_FIELD_ENABLED = 1 << 1, + WD_FIELD_DESCRIPTION = 1 << 2, + WD_FIELD_PHYSICAL_SIZE = 1 << 3, + WD_FIELD_SCALE = 1 << 4, + WD_FIELD_POSITION = 1 << 5, + WD_FIELD_MODE = 1 << 6, + WD_FIELD_TRANSFORM = 1 << 7, + WD_FIELDS_ALL = (1 << 8) - 1 +}; + +struct wd_head_config { + struct wl_list link; + + struct wd_head *head; + bool enabled; + int32_t width; + int32_t height; + int32_t refresh; // mHz + int32_t x; + int32_t y; + double scale; + enum wl_output_transform transform; +}; + +struct wd_mode { + struct wd_head *head; + struct zwlr_output_mode_v1 *wlr_mode; + struct wl_list link; + + int32_t width, height; + int32_t refresh; // mHz + bool preferred; +}; + +struct wd_head { + struct wd_state *state; + struct zwlr_output_head_v1 *wlr_head; + struct wl_list link; + + char *name, *description; + int32_t phys_width, phys_height; // mm + struct wl_list modes; + + bool enabled; + struct wd_mode *mode; + struct { + int32_t width, height; + int32_t refresh; + } custom_mode; + int32_t x, y; + enum wl_output_transform transform; + double scale; +}; + +struct wd_state { + struct zwlr_output_manager_v1 *output_manager; + struct wl_list heads; + uint32_t serial; + + double zoom; + int xorigin; + int yorigin; + + GtkWidget *header_stack; + GtkWidget *stack_switcher; + GtkWidget *stack; + GtkWidget *scroller; + GtkWidget *canvas; + GtkWidget *spinner; + GtkWidget *zoom_out; + GtkWidget *zoom_reset; + GtkWidget *zoom_in; + GtkWidget *overlay; + GtkWidget *info_bar; + GtkWidget *info_label; +}; + +/* + * Displays an error message and then exits the program. + */ +void wd_fatal_error(int status, const char *message); + +/* + * Starts listening for output management events from the compositor. + */ +void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display); + +/* + * Sends updated display configuration back to the compositor. + */ +void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs); + +/* + * Updates the UI stack of all heads. Does not update individual head forms. + * Useful for when a display is plugged/unplugged and we want to add/remove + * a page, but we don't want to wipe out user's changes on the other pages. + */ +void wd_ui_reset_heads(struct wd_state *state); + +/* + * Updates a form with head configuration from the server. Only updates specified fields. + */ +void wd_ui_reset_head(const struct wd_head *head, unsigned int fields); + +/* + * Updates the stack and all forms to the last known server state. + */ +void wd_ui_reset_all(struct wd_state *state); + +/* + * Reactivates the GUI after the display configuration updates. + */ +void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs); + +/* + * Reactivates the GUI after the display configuration updates. + */ +void wd_ui_show_error(struct wd_state *state, const char *message); + +#endif