wdisplays_github/src/main.c

1260 lines
52 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <gtk/gtk.h>
#include <gdk/gdkwayland.h>
#include "wdisplays.h"
#include "glviewport.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";
static const char *APP_PREFIX = "app";
#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;
}
double old_scale = round(head->scale * 100.) / 100.;
double new_scale = round(gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))) * 100.) / 100.;
if (old_scale != new_scale) {
return TRUE;
}
if (head->x != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x")))) {
return TRUE;
}
if (head->y != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y")))) {
return TRUE;
}
int w = head->mode != NULL ? head->mode->width : head->custom_mode.width;
if (w != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width")))) {
return TRUE;
}
int h = head->mode != NULL ? head->mode->height : head->custom_mode.height;
if (h != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height")))) {
return TRUE;
}
int r = head->mode != NULL ? head->mode->refresh : head->custom_mode.refresh;
if (r / 1000. != gtk_spin_button_get_value(GTK_SPIN_BUTTON(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;
}
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 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x")));
output->y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y")));
output->width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width")));
output->height = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height")));
output->refresh = gtk_spin_button_get_value(GTK_SPIN_BUTTON(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;
}
}
}
static gboolean send_apply(gpointer data) {
struct wd_state *state = data;
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);
state->apply_pending = false;
return FALSE;
}
static void apply_state(struct wd_state *state) {
gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title");
if (!state->autoapply) {
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);
gtk_widget_set_sensitive(state->menu_button, FALSE);
}
/* queue this once per iteration in order to prevent duplicate updates */
if (!state->apply_pending) {
state->apply_pending = true;
g_idle_add(send_apply, state);
}
}
static gboolean apply_done_reset(gpointer data) {
wd_ui_reset_all(data);
return FALSE;
}
static void update_scroll_size(struct wd_state *state) {
state->render.viewport_width = gtk_widget_get_allocated_width(state->canvas);
state->render.viewport_height = gtk_widget_get_allocated_height(state->canvas);
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));
int scroll_x_upper = state->render.width;
int scroll_y_upper = state->render.height;
gtk_adjustment_set_upper(scroll_x_adj, MAX(0, scroll_x_upper));
gtk_adjustment_set_upper(scroll_y_adj, MAX(0, scroll_y_upper));
gtk_adjustment_set_page_size(scroll_x_adj, state->render.viewport_width);
gtk_adjustment_set_page_size(scroll_y_adj, state->render.viewport_height);
gtk_adjustment_set_page_increment(scroll_x_adj, state->render.viewport_width);
gtk_adjustment_set_page_increment(scroll_y_adj, state->render.viewport_height);
gtk_adjustment_set_step_increment(scroll_x_adj, state->render.viewport_width / 10);
gtk_adjustment_set_step_increment(scroll_y_adj, state->render.viewport_height / 10);
}
/*
* 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;
}
if (head->scale > 0.) {
w /= head->scale;
h /= head->scale;
}
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->render.x_origin = floor(xmin * state->zoom) - CANVAS_MARGIN;
state->render.y_origin = floor(ymin * state->zoom) - CANVAS_MARGIN;
state->render.width = ceil((xmax - xmin) * state->zoom) + CANVAS_MARGIN * 2;
state->render.height = ceil((ymax - ymin) * state->zoom) + CANVAS_MARGIN * 2;
update_scroll_size(state);
}
static void cache_scroll(struct wd_state *state) {
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));
state->render.scroll_x = gtk_adjustment_get_value(scroll_x_adj);
state->render.scroll_y = gtk_adjustment_get_value(scroll_y_adj);
}
static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data);
static void update_tick_callback(struct wd_state *state) {
bool any_animate = false;
for (int i = 0; i < state->render.head_count; i++) {
struct wd_render_head_data *head = &state->render.heads[i];
if (state->render.updated_at < head->transition_begin + HOVER_USECS) {
any_animate = true;
break;
}
}
if (!any_animate && !state->capture) {
if (state->canvas_tick != -1) {
gtk_widget_remove_tick_callback(state->canvas, state->canvas_tick);
state->canvas_tick = -1;
}
} else if (state->canvas_tick == -1) {
state->canvas_tick =
gtk_widget_add_tick_callback(state->canvas, redraw_canvas, state, NULL);
}
gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas));
gtk_gl_area_set_auto_render(GTK_GL_AREA(state->canvas), state->capture);
}
static void update_cursor(struct wd_state *state) {
bool any_hovered = false;
struct wd_head *head;
wl_list_for_each(head, &state->heads, link) {
struct wd_render_head_data *render = head->render;
if (render != NULL && render->hovered) {
any_hovered = true;
break;
}
}
GdkWindow *window = gtk_widget_get_window(state->canvas);
if (any_hovered) {
gdk_window_set_cursor(window, state->grab_cursor);
} else if (state->clicked != NULL) {
gdk_window_set_cursor(window, state->grabbing_cursor);
} else if (state->panning) {
gdk_window_set_cursor(window, state->move_cursor);
} else {
gdk_window_set_cursor(window, NULL);
}
}
static void update_hovered(struct wd_state *state) {
GdkDisplay *display = gdk_display_get_default();
GdkWindow *window = gtk_widget_get_window(state->canvas);
if (!gtk_widget_get_realized(state->canvas)) {
return;
}
GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas);
uint64_t tick = gdk_frame_clock_get_frame_time(clock);
g_autoptr(GList) seats = gdk_display_list_seats(display);
struct wd_head *head;
wl_list_for_each(head, &state->heads, link) {
struct wd_render_head_data *render = head->render;
if (render != NULL) {
bool init_hovered = render->hovered;
render->hovered = false;
if (state->clicked == head) {
render->hovered = true;
} else if (state->clicked == NULL) {
for (GList *iter = seats; iter != NULL; iter = iter->next) {
double mouse_x;
double mouse_y;
GdkDevice *pointer = gdk_seat_get_pointer(GDK_SEAT(iter->data));
gdk_window_get_device_position_double(window, pointer, &mouse_x, &mouse_y, NULL);
if (mouse_x >= render->x1 && mouse_x < render->x2 &&
mouse_y >= render->y1 && mouse_y < render->y2) {
render->hovered = true;
break;
}
}
}
if (init_hovered != render->hovered) {
render->transition_begin = tick;
}
}
}
update_cursor(state);
update_tick_callback(state);
}
static inline void color_to_float_array(GtkStyleContext *ctx,
const char *color_name, float out[4]) {
GdkRGBA color;
gtk_style_context_lookup_color(ctx, color_name, &color);
out[0] = color.red;
out[1] = color.green;
out[2] = color.blue;
out[3] = color.alpha;
}
static void queue_canvas_draw(struct wd_state *state) {
GtkStyleContext *style_ctx = gtk_widget_get_style_context(state->canvas);
color_to_float_array(style_ctx,
"theme_fg_color", state->render.fg_color);
color_to_float_array(style_ctx,
"theme_bg_color", state->render.bg_color);
color_to_float_array(style_ctx,
"borders", state->render.border_color);
color_to_float_array(style_ctx,
"theme_selected_bg_color", state->render.selection_color);
cache_scroll(state);
state->render.head_count = 0;
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 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x")));
int y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y")));
int w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width")));
int h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height")));
double scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale")));
if (scale <= 0.)
scale = 1.;
struct wd_head *head = g_object_get_data(G_OBJECT(form_iter->data), "head");
struct wd_render_head_data *render = &state->render.heads[state->render.head_count];
render->x1 = floor(x * state->zoom - state->render.scroll_x - state->render.x_origin);
render->y1 = floor(y * state->zoom - state->render.scroll_y - state->render.y_origin);
render->x2 = floor(render->x1 + w * state->zoom / scale);
render->y2 = floor(render->y1 + h * state->zoom / scale);
head->render = render;
state->render.head_count++;
if (state->render.head_count >= HEADS_MAX)
break;
}
}
gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas));
}
// BEGIN FORM CALLBACKS
static void show_apply(struct wd_state *state) {
const gchar *page = "title";
if (has_changes(state)) {
if (state->autoapply) {
apply_state(state);
} else {
page = "apply";
}
}
gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), page);
}
static void update_ui(struct wd_state *state) {
show_apply(state);
update_canvas_size(state);
queue_canvas_draw(state);
}
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");
update_ui(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"));
gtk_spin_button_set_value(GTK_SPIN_BUTTON(width), w);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(height), h);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(refresh), r / 1000.);
}
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);
update_ui(head->state);
}
// END FORM CALLBACKS
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) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_x), head->x);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_y), head->y);
}
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;
} else if (!head->enabled && w == 0 && h == 0) {
struct wd_mode *mode;
wl_list_for_each(mode, &head->modes, link) {
if (mode->preferred) {
w = mode->width;
h = mode->height;
r = mode->refresh;
break;
}
}
}
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);
}
update_ui(head->state);
}
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(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "scale"), "value-changed", G_CALLBACK(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_x"), "value-changed", G_CALLBACK(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_y"), "value-changed", G_CALLBACK(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "width"), "value-changed", G_CALLBACK(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "height"), "value-changed", G_CALLBACK(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "refresh"), "value-changed", G_CALLBACK(update_ui), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "flipped"), "toggled", G_CALLBACK(update_ui), 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));
}
update_canvas_size(state);
queue_canvas_draw(state);
}
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);
break;
}
}
update_canvas_size(head->state);
queue_canvas_draw(head->state);
}
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);
}
update_canvas_size(state);
queue_canvas_draw(state);
}
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);
gtk_widget_set_sensitive(state->menu_button, TRUE);
if (!state->autoapply) {
show_apply(state);
}
g_idle_add(apply_done_reset, 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);
}
// BEGIN GLOBAL CALLBACKS
static void cleanup(GtkWidget *window, gpointer data) {
struct wd_state *state = data;
g_object_unref(state->grab_cursor);
g_object_unref(state->grabbing_cursor);
g_object_unref(state->move_cursor);
wd_state_destroy(state);
}
static void monitor_added(GdkDisplay *display, GdkMonitor *monitor, gpointer data) {
wd_add_output(data, gdk_wayland_monitor_get_wl_output(monitor));
}
static void monitor_removed(GdkDisplay *display, GdkMonitor *monitor, gpointer data) {
struct wl_display *wl_display = gdk_wayland_display_get_wl_display(display);
wd_remove_output(data, gdk_wayland_monitor_get_wl_output(monitor), wl_display);
}
static void canvas_realize(GtkWidget *widget, gpointer data) {
gtk_gl_area_make_current(GTK_GL_AREA(widget));
if (gtk_gl_area_get_error(GTK_GL_AREA(widget)) != NULL) {
return;
}
struct wd_state *state = data;
state->gl_data = wd_gl_setup();
}
static inline bool size_changed(const struct wd_render_head_data *render) {
return render->x2 - render->x1 != render->tex_width ||
render->y2 - render->y1 != render->tex_height;
}
static inline void cairo_set_source_color(cairo_t *cr, float color[4]) {
cairo_set_source_rgba(cr, color[0], color[1], color[2], color[3]);
}
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);
update_canvas_size(state);
queue_canvas_draw(state);
}
static void zoom_to(struct wd_state *state, double zoom) {
state->zoom = zoom;
state->zoom = MAX(state->zoom, MIN_ZOOM);
state->zoom = MIN(state->zoom, MAX_ZOOM);
update_zoom(state);
}
static void zoom_out(struct wd_state *state) {
zoom_to(state, state->zoom * 0.75);
}
static void zoom_reset(struct wd_state *state) {
zoom_to(state, DEFAULT_ZOOM);
}
static void zoom_in(struct wd_state *state) {
zoom_to(state, state->zoom / 0.75);
}
#define TEXT_MARGIN 5
static cairo_surface_t *draw_head(PangoContext *pango,
struct wd_render_data *info, const char *name,
unsigned width, unsigned height) {
cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
width, height);
cairo_t *cr = cairo_create(surface);
cairo_rectangle(cr, 0., 0., width, height);
cairo_set_source_color(cr, info->border_color);
cairo_fill(cr);
cairo_set_line_width(cr, 1.);
cairo_rectangle(cr, 0, 0, width, height);
cairo_set_source_color(cr, info->fg_color);
cairo_stroke(cr);
PangoLayout *layout = pango_layout_new(pango);
pango_layout_set_text(layout, name, -1);
int text_width = pango_units_from_double(width - TEXT_MARGIN * 2);
int text_height = pango_units_from_double(height - TEXT_MARGIN * 2);
pango_layout_set_width(layout, MAX(text_width, 0));
pango_layout_set_height(layout, MAX(text_height, 0));
pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
cairo_set_source_color(cr, info->fg_color);
pango_layout_get_size(layout, &text_width, &text_height);
cairo_move_to(cr, TEXT_MARGIN, (height - PANGO_PIXELS(text_height)) / 2);
pango_cairo_show_layout(cr, layout);
g_object_unref(layout);
cairo_destroy(cr);
cairo_surface_flush(surface);
return surface;
}
static void canvas_render(GtkGLArea *area, GdkGLContext *context, gpointer data) {
struct wd_state *state = data;
PangoContext *pango = gtk_widget_get_pango_context(state->canvas);
GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas);
uint64_t tick = gdk_frame_clock_get_frame_time(clock);
wd_capture_frame(state);
struct wd_head *head;
wl_list_for_each(head, &state->heads, link) {
struct wd_render_head_data *render = head->render;
struct wd_output *output = wd_find_output(state, head);
struct wd_frame *frame = NULL;
if (output != NULL && !wl_list_empty(&output->frames)) {
frame = wl_container_of(output->frames.prev, frame, link);
}
if (render != NULL) {
if (state->capture && frame != NULL && frame->pixels != NULL) {
if (frame->tick > render->updated_at) {
render->tex_stride = frame->stride;
render->tex_width = frame->width;
render->tex_height = frame->height;
render->pixels = frame->pixels;
render->preview = true;
render->updated_at = tick;
render->y_invert = frame->y_invert;
render->swap_rgb = frame->swap_rgb;
}
} else if (render->preview
|| render->pixels == NULL || size_changed(render)) {
render->tex_width = render->x2 - render->x1;
render->tex_height = render->y2 - render->y1;
render->preview = false;
if (head->surface != NULL) {
cairo_surface_destroy(head->surface);
}
head->surface = draw_head(pango, &state->render, head->name,
render->tex_width, render->tex_height);
render->pixels = cairo_image_surface_get_data(head->surface);
render->tex_stride = cairo_image_surface_get_stride(head->surface);
render->updated_at = tick;
render->y_invert = false;
render->swap_rgb = false;
}
}
}
wd_gl_render(state->gl_data, &state->render, tick);
state->render.updated_at = tick;
}
static void canvas_unrealize(GtkWidget *widget, gpointer data) {
gtk_gl_area_make_current(GTK_GL_AREA(widget));
if (gtk_gl_area_get_error(GTK_GL_AREA(widget)) != NULL) {
return;
}
struct wd_state *state = data;
GdkDisplay *gdk_display = gdk_display_get_default();
struct wl_display *display = gdk_wayland_display_get_wl_display(gdk_display);
wd_capture_wait(state, display);
wd_gl_cleanup(state->gl_data);
state->gl_data = NULL;
}
static gboolean canvas_click(GtkWidget *widget, GdkEvent *event,
gpointer data) {
struct wd_state *state = data;
if (event->button.type == GDK_BUTTON_PRESS) {
if (event->button.button == 1) {
int i = 0;
struct wd_head *head;
wl_list_for_each(head, &state->heads, link) {
struct wd_render_head_data *render = head->render;
if (render != NULL) {
double mouse_x = event->button.x;
double mouse_y = event->button.y;
if (mouse_x >= render->x1 && mouse_x < render->x2 &&
mouse_y >= render->y1 && mouse_y < render->y2) {
state->clicked = head;
state->click_offset.x = event->button.x - render->x1;
state->click_offset.y = event->button.y - render->y1;
break;
}
}
i++;
}
if (state->clicked != NULL) {
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) {
const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head");
if (state->clicked == other) {
gtk_stack_set_visible_child(GTK_STACK(state->stack), form_iter->data);
break;
}
}
}
} else if (event->button.button == 2) {
state->panning = true;
state->pan_last.x = event->button.x;
state->pan_last.y = event->button.y;
}
}
return TRUE;
}
static gboolean canvas_release(GtkWidget *widget, GdkEvent *event,
gpointer data) {
struct wd_state *state = data;
if (event->button.button == 1) {
state->clicked = NULL;
}
if (event->button.button == 2) {
state->panning = false;
}
update_cursor(state);
return TRUE;
}
#define SNAP_DIST 6.
static gboolean canvas_motion(GtkWidget *widget, GdkEvent *event,
gpointer data) {
struct wd_state *state = data;
if (event->motion.state & GDK_BUTTON2_MASK) {
GtkAdjustment *xadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller));
GtkAdjustment *yadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller));
double delta_x = event->motion.x - state->pan_last.x;
double delta_y = event->motion.y - state->pan_last.y;
gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + delta_x);
gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + delta_y);
state->pan_last.x = event->motion.x;
state->pan_last.y = event->motion.y;
queue_canvas_draw(state);
}
if ((event->motion.state & GDK_BUTTON1_MASK) && state->clicked != NULL) {
GtkWidget *form = NULL;
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) {
const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head");
if (state->clicked == other) {
form = form_iter->data;
break;
}
}
if (form != NULL) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
struct wd_point size = {
.x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))),
.y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))),
};
struct wd_point tl = {
.x = (event->motion.x - state->click_offset.x
+ state->render.x_origin + state->render.scroll_x) / state->zoom,
.y = (event->motion.y - state->click_offset.y
+ state->render.y_origin + state->render.scroll_y) / state->zoom
};
const struct wd_point br = {
.x = tl.x + size.x,
.y = tl.y + size.y
};
struct wd_point new_pos = tl;
float snap = SNAP_DIST / state->zoom;
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 (other != state->clicked && !(event->motion.state & GDK_SHIFT_MASK)) {
GtkBuilder *other_builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder"));
double x1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "pos_x")));
double y1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "pos_y")));
double x2 = x1 + gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "width")));
double y2 = y1 + gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "height")));
if (fabs(br.x) <= snap)
new_pos.x = -size.x;
if (fabs(br.y) <= snap)
new_pos.y = -size.y;
if (fabs(br.x - x1) <= snap)
new_pos.x = x1 - size.x;
if (fabs(br.x - x2) <= snap)
new_pos.x = x2 - size.x;
if (fabs(br.y - y1) <= snap)
new_pos.y = y1 - size.y;
if (fabs(br.y - y2) <= snap)
new_pos.y = y2 - size.y;
if (fabs(tl.x) <= snap)
new_pos.x = 0.;
if (fabs(tl.y) <= snap)
new_pos.y = 0.;
if (fabs(tl.x - x1) <= snap)
new_pos.x = x1;
if (fabs(tl.x - x2) <= snap)
new_pos.x = x2;
if (fabs(tl.y - y1) <= snap)
new_pos.y = y1;
if (fabs(tl.y - y2) <= snap)
new_pos.y = y2;
}
}
GtkWidget *pos_x = GTK_WIDGET(gtk_builder_get_object(builder, "pos_x"));
GtkWidget *pos_y = GTK_WIDGET(gtk_builder_get_object(builder, "pos_y"));
gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_x), new_pos.x);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_y), new_pos.y);
}
}
update_hovered(state);
return TRUE;
}
static gboolean canvas_enter(GtkWidget *widget, GdkEvent *event,
gpointer data) {
struct wd_state *state = data;
if (!(event->crossing.state & GDK_BUTTON1_MASK)) {
state->clicked = NULL;
}
if (!(event->crossing.state & GDK_BUTTON2_MASK)) {
state->panning = false;
}
update_cursor(state);
return TRUE;
}
static gboolean canvas_leave(GtkWidget *widget, GdkEvent *event,
gpointer data) {
struct wd_state *state = data;
for (int i = 0; i < state->render.head_count; i++) {
struct wd_render_head_data *head = &state->render.heads[i];
head->hovered = false;
}
update_tick_callback(state);
return TRUE;
}
static gboolean canvas_scroll(GtkWidget *widget, GdkEvent *event,
gpointer data) {
struct wd_state *state = data;
if (event->scroll.state & GDK_CONTROL_MASK) {
switch (event->scroll.direction) {
case GDK_SCROLL_UP:
zoom_in(state);
break;
case GDK_SCROLL_DOWN:
zoom_out(state);
break;
case GDK_SCROLL_SMOOTH:
if (event->scroll.delta_y)
zoom_to(state, state->zoom * pow(0.75, event->scroll.delta_y));
break;
default:
break;
}
} else {
GtkAdjustment *xadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller));
GtkAdjustment *yadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller));
double xstep = gtk_adjustment_get_step_increment(xadj);
double ystep = gtk_adjustment_get_step_increment(yadj);
switch (event->scroll.direction) {
case GDK_SCROLL_UP:
gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) - ystep);
break;
case GDK_SCROLL_DOWN:
gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + ystep);
break;
case GDK_SCROLL_LEFT:
gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) - xstep);
break;
case GDK_SCROLL_RIGHT:
gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + xstep);
break;
case GDK_SCROLL_SMOOTH:
if (event->scroll.delta_x)
gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + xstep * event->scroll.delta_x);
if (event->scroll.delta_y)
gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + ystep * event->scroll.delta_y);
break;
default:
break;
}
}
return FALSE;
}
static void canvas_resize(GtkWidget *widget, GdkRectangle *allocation,
gpointer data) {
struct wd_state *state = data;
update_scroll_size(state);
}
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) {
apply_state(data);
}
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 auto_apply_selected(GSimpleAction *action, GVariant *param, gpointer data) {
struct wd_state *state = data;
state->autoapply = !state->autoapply;
g_simple_action_set_state(action, g_variant_new_boolean(state->autoapply));
}
static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data) {
struct wd_state *state = data;
if (state->capture) {
wd_capture_frame(state);
}
queue_canvas_draw(state);
return G_SOURCE_CONTINUE;
}
static void capture_selected(GSimpleAction *action, GVariant *param, gpointer data) {
struct wd_state *state = data;
state->capture = !state->capture;
g_simple_action_set_state(action, g_variant_new_boolean(state->capture));
update_tick_callback(state);
}
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 = wd_state_create();
state->zoom = DEFAULT_ZOOM;
state->canvas_tick = -1;
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);
state->grab_cursor = gdk_cursor_new_from_name(gdk_display, "grab");
state->grabbing_cursor = gdk_cursor_new_from_name(gdk_display, "grabbing");
state->move_cursor = gdk_cursor_new_from_name(gdk_display, "move");
GtkBuilder *builder = gtk_builder_new_from_resource("/wdisplays.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->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"));
state->menu_button = GTK_WIDGET(gtk_builder_get_object(builder, "menu_button"));
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_add_callback_symbol(builder, "destroy", G_CALLBACK(cleanup));
gtk_builder_connect_signals(builder, state);
gtk_box_set_homogeneous(GTK_BOX(gtk_builder_get_object(builder, "zoom_box")), FALSE);
state->canvas = wd_gl_viewport_new();
gtk_container_add(GTK_CONTAINER(state->scroller), state->canvas);
gtk_widget_add_events(state->canvas, GDK_POINTER_MOTION_MASK
| GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK
| GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
g_signal_connect(state->canvas, "realize", G_CALLBACK(canvas_realize), state);
g_signal_connect(state->canvas, "render", G_CALLBACK(canvas_render), state);
g_signal_connect(state->canvas, "unrealize", G_CALLBACK(canvas_unrealize), state);
g_signal_connect(state->canvas, "button-press-event", G_CALLBACK(canvas_click), state);
g_signal_connect(state->canvas, "button-release-event", G_CALLBACK(canvas_release), state);
g_signal_connect(state->canvas, "enter-notify-event", G_CALLBACK(canvas_enter), state);
g_signal_connect(state->canvas, "leave-notify-event", G_CALLBACK(canvas_leave), state);
g_signal_connect(state->canvas, "motion-notify-event", G_CALLBACK(canvas_motion), state);
g_signal_connect(state->canvas, "scroll-event", G_CALLBACK(canvas_scroll), state);
g_signal_connect(state->canvas, "size-allocate", G_CALLBACK(canvas_resize), state);
gtk_gl_area_set_use_es(GTK_GL_AREA(state->canvas), TRUE);
gtk_gl_area_set_has_alpha(GTK_GL_AREA(state->canvas), TRUE);
gtk_gl_area_set_auto_render(GTK_GL_AREA(state->canvas), state->capture);
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));
g_signal_connect_swapped(scroll_x_adj, "value-changed", G_CALLBACK(queue_canvas_draw), state);
g_signal_connect_swapped(scroll_y_adj, "value-changed", G_CALLBACK(queue_canvas_draw), state);
update_zoom(state);
GSimpleActionGroup *main_actions = g_simple_action_group_new();
gtk_widget_insert_action_group(state->menu_button, APP_PREFIX, G_ACTION_GROUP(main_actions));
g_object_unref(main_actions);
GSimpleAction *autoapply_action = g_simple_action_new_stateful("auto-apply", NULL,
g_variant_new_boolean(state->autoapply));
g_signal_connect(autoapply_action, "activate", G_CALLBACK(auto_apply_selected), state);
g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(autoapply_action));
GSimpleAction *capture_action = g_simple_action_new_stateful("capture-screens", NULL,
g_variant_new_boolean(state->capture));
g_signal_connect(capture_action, "activate", G_CALLBACK(capture_selected), state);
g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(capture_action));
/* 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");
}
if (state->xdg_output_manager == NULL) {
wd_fatal_error(1, "Compositor doesn't support xdg-output-unstable-v1");
}
if (state->copy_manager == NULL) {
state->capture = false;
g_simple_action_set_state(capture_action, g_variant_new_boolean(state->capture));
g_simple_action_set_enabled(capture_action, FALSE);
}
int n_monitors = gdk_display_get_n_monitors(gdk_display);
for (int i = 0; i < n_monitors; i++) {
GdkMonitor *monitor = gdk_display_get_monitor(gdk_display, i);
wd_add_output(state, gdk_wayland_monitor_get_wl_output(monitor));
}
g_signal_connect(gdk_display, "monitor-added", G_CALLBACK(monitor_added), state);
g_signal_connect(gdk_display, "monitor-removed", G_CALLBACK(monitor_removed), state);
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_show_all(window);
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;
}