diff --git a/src/gui/gtk.c b/src/gui/gtk.c index 59cafd390218..d010ea142c1d 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -1704,6 +1704,45 @@ static gboolean _ui_toast_button_press_event(GtkWidget *widget, return TRUE; } +#ifdef GDK_WINDOWING_WAYLAND +static gboolean _top_panel_drag_button_press(GtkWidget *w, GdkEventButton *e, gpointer data) +{ + if(e->button != GDK_BUTTON_PRIMARY) + return FALSE; + + GtkWidget *target = gtk_get_event_widget((GdkEvent*)e); + + // Don't process on interactive widgets + if(GTK_IS_BUTTON(target) || GTK_IS_ENTRY(target) || GTK_IS_COMBO_BOX(target) || + (GTK_IS_EVENT_BOX(target) && g_object_get_data(G_OBJECT(target), "view-label"))) + return FALSE; + + // Double-click to maximize/restore window + if(e->type == GDK_2BUTTON_PRESS) + { + GtkWindow *win = GTK_WINDOW(darktable.gui->ui->main_window); + GdkWindow *gdk_win = gtk_widget_get_window(GTK_WIDGET(win)); + + if(gdk_win && (gdk_window_get_state(gdk_win) & GDK_WINDOW_STATE_MAXIMIZED)) + gtk_window_unmaximize(win); + else + gtk_window_maximize(win); + + return TRUE; + } + + // Single click to drag window + if(e->type == GDK_BUTTON_PRESS) + { + gtk_window_begin_move_drag(GTK_WINDOW(darktable.gui->ui->main_window), + e->button, e->x_root, e->y_root, e->time); + return TRUE; + } + + return FALSE; +} +#endif + static GtkWidget *_init_outer_border(const gint width, const gint height, const gint which) @@ -1736,14 +1775,23 @@ static void _init_widgets(dt_gui_gtk_t *gui) gtk_widget_set_name(widget, "main_window"); gui->ui->main_window = widget; + // Initialize headerbar pointer + gui->widgets.header_bar = NULL; + #ifdef GDK_WINDOWING_WAYLAND if(dt_gui_get_session_type() == DT_GUI_SESSION_WAYLAND) { + // Create an empty headerbar for CSD (Client-Side Decorations) + // Window control buttons are integrated into viewswitcher module GtkWidget *header_bar = gtk_header_bar_new(); - gtk_header_bar_set_title(GTK_HEADER_BAR(header_bar), "darktable"); - gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header_bar), TRUE); + gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header_bar), FALSE); gtk_window_set_titlebar(GTK_WINDOW(widget), header_bar); - gtk_widget_show(header_bar); + + // Hide the headerbar completely - we use darktable's own top panel + gtk_widget_set_visible(header_bar, FALSE); + gtk_widget_set_no_show_all(header_bar, TRUE); + + gui->widgets.header_bar = header_bar; } #endif @@ -2712,9 +2760,26 @@ static void _ui_init_panel_top(dt_ui_t *ui, /* create the panel box */ ui->panels[DT_UI_PANEL_TOP] = widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_set_hexpand(GTK_WIDGET(widget), TRUE); - gtk_grid_attach(GTK_GRID(container), widget, 1, 0, 3, 1); gtk_widget_set_name(widget, "top-hinter"); +#ifdef GDK_WINDOWING_WAYLAND + // On Wayland, wrap TOP panel in event box to enable window dragging + if(dt_gui_get_session_type() == DT_GUI_SESSION_WAYLAND) + { + GtkWidget *event_box = gtk_event_box_new(); + gtk_widget_set_name(event_box, "top-panel-eventbox"); + gtk_container_add(GTK_CONTAINER(event_box), widget); + gtk_widget_add_events(event_box, GDK_BUTTON_PRESS_MASK); + g_signal_connect(G_OBJECT(event_box), "button-press-event", + G_CALLBACK(_top_panel_drag_button_press), NULL); + gtk_grid_attach(GTK_GRID(container), event_box, 1, 0, 3, 1); + } + else +#endif + { + gtk_grid_attach(GTK_GRID(container), widget, 1, 0, 3, 1); + } + /* add container for top left */ ui->containers[DT_UI_CONTAINER_PANEL_TOP_LEFT] = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); diff --git a/src/gui/gtk.h b/src/gui/gtk.h index 8ee46777e981..5b6d2a2c9d96 100644 --- a/src/gui/gtk.h +++ b/src/gui/gtk.h @@ -46,6 +46,9 @@ typedef struct dt_gui_widgets_t /* resize of left/right panels */ gboolean panel_handle_dragging; int panel_handle_x, panel_handle_y; + + // Wayland CSD headerbar + GtkWidget *header_bar; } dt_gui_widgets_t; typedef struct dt_gui_scrollbars_t diff --git a/src/libs/tools/viewswitcher.c b/src/libs/tools/viewswitcher.c index ea294a7902d9..5b2e22ce06ac 100644 --- a/src/libs/tools/viewswitcher.c +++ b/src/libs/tools/viewswitcher.c @@ -42,6 +42,12 @@ typedef struct dt_lib_viewswitcher_t { GList *labels; GtkWidget *dropdown; +#ifdef GDK_WINDOWING_WAYLAND + GtkWidget *window_controls_box; + GtkWidget *minimize_btn; + GtkWidget *maximize_btn; + GtkWidget *close_btn; +#endif } dt_lib_viewswitcher_t; /* callback when a view label is pressed */ @@ -55,6 +61,14 @@ static void _lib_viewswitcher_view_cannot_change_callback(gpointer instance, dt_ dt_view_t *new_view, dt_lib_module_t *self); static void _switch_view(const dt_view_t *view); +#ifdef GDK_WINDOWING_WAYLAND +/* window control button callbacks */ +static void _minimize_window(GtkWidget *widget, dt_lib_module_t *self); +static void _maximize_window(GtkWidget *widget, dt_lib_module_t *self); +static void _close_window(GtkWidget *widget, dt_lib_module_t *self); +static gboolean _window_state_changed(GtkWidget *widget, GdkEventWindowState *event, dt_lib_module_t *self); +#endif + const char *name(dt_lib_module_t *self) { return _("viewswitcher"); @@ -156,6 +170,98 @@ void gui_init(dt_lib_module_t *self) if(model) g_object_unref(model); +#ifdef GDK_WINDOWING_WAYLAND + if(dt_gui_get_session_type() == DT_GUI_SESSION_WAYLAND) + { + // Separator + gtk_box_pack_start(GTK_BOX(self->widget), + gtk_separator_new(GTK_ORIENTATION_VERTICAL), + FALSE, FALSE, DT_PIXEL_APPLY_DPI(5)); + + // Create button container + d->window_controls_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + + // Read gtk-decoration-layout to respect user's button preferences + GtkSettings *settings = gtk_settings_get_default(); + gchar *layout = NULL; + g_object_get(settings, "gtk-decoration-layout", &layout, NULL); + + // Parse layout (format: "close,minimize,maximize:menu" or ":close") + // We only care about the right side (after colon) + gchar *right_layout = layout ? g_strrstr(layout, ":") : NULL; + if(right_layout) right_layout++; // Skip the colon + + // Default to "minimize,maximize,close" if no layout found + if(!right_layout || !*right_layout) + right_layout = "minimize,maximize,close"; + + // Apply GTK theme classes with hover effects + const gchar *css = "button.titlebutton { " + "background-image: none; background-color: transparent; " + "border: none; box-shadow: none; text-shadow: none; " + "} " + "button.titlebutton:hover { " + "background-color: alpha(currentColor, 0.1); " + "} " + "button.titlebutton:active { " + "background-color: alpha(currentColor, 0.2); " + "}"; + GtkCssProvider *provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(provider, css, -1, NULL); + + // Parse and create buttons in order specified by layout + gchar **button_names = g_strsplit(right_layout, ",", -1); + for(int i = 0; button_names[i] != NULL; i++) + { + gchar *btn_name = g_strstrip(button_names[i]); + GtkWidget *button = NULL; + GCallback callback = NULL; + + if(g_strcmp0(btn_name, "minimize") == 0) + { + button = d->minimize_btn = gtk_button_new_from_icon_name("window-minimize-symbolic", GTK_ICON_SIZE_MENU); + callback = G_CALLBACK(_minimize_window); + } + else if(g_strcmp0(btn_name, "maximize") == 0) + { + button = d->maximize_btn = gtk_button_new_from_icon_name("window-maximize-symbolic", GTK_ICON_SIZE_MENU); + callback = G_CALLBACK(_maximize_window); + } + else if(g_strcmp0(btn_name, "close") == 0) + { + button = d->close_btn = gtk_button_new_from_icon_name("window-close-symbolic", GTK_ICON_SIZE_MENU); + callback = G_CALLBACK(_close_window); + } + + if(button) + { + GtkStyleContext *ctx = gtk_widget_get_style_context(button); + gtk_style_context_add_class(ctx, "titlebutton"); + gtk_style_context_add_class(ctx, btn_name); + gtk_style_context_add_provider(ctx, GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_USER + 2); + gtk_widget_set_name(button, ""); + gtk_widget_set_can_focus(button, FALSE); + gtk_box_pack_start(GTK_BOX(d->window_controls_box), button, FALSE, FALSE, 0); + + if(callback) + g_signal_connect(button, "clicked", callback, self); + } + } + + g_strfreev(button_names); + g_free(layout); + g_object_unref(provider); + + // Connect window state change handler + if(d->maximize_btn) + g_signal_connect(dt_ui_main_window(darktable.gui->ui), "window-state-event", + G_CALLBACK(_window_state_changed), self); + + gtk_box_pack_start(GTK_BOX(self->widget), d->window_controls_box, FALSE, FALSE, 0); + } +#endif + /* connect callback to view change signal */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_VIEWMANAGER_VIEW_CHANGED, _lib_viewswitcher_view_changed_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_VIEWMANAGER_VIEW_CANNOT_CHANGE, _lib_viewswitcher_view_cannot_change_callback); @@ -280,6 +386,44 @@ static gboolean _lib_viewswitcher_button_press_callback(GtkWidget *w, GdkEventBu return FALSE; } +#ifdef GDK_WINDOWING_WAYLAND +static void _minimize_window(GtkWidget *w, dt_lib_module_t *self) +{ + gtk_window_iconify(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui))); +} + +static void _maximize_window(GtkWidget *w, dt_lib_module_t *self) +{ + GtkWindow *win = GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)); + GdkWindow *gdk_win = gtk_widget_get_window(GTK_WIDGET(win)); + + if(gdk_win && (gdk_window_get_state(gdk_win) & GDK_WINDOW_STATE_MAXIMIZED)) + gtk_window_unmaximize(win); + else + gtk_window_maximize(win); +} + +static void _close_window(GtkWidget *w, dt_lib_module_t *self) +{ + dt_control_quit(); +} + +static gboolean _window_state_changed(GtkWidget *w, GdkEventWindowState *event, dt_lib_module_t *self) +{ + dt_lib_viewswitcher_t *d = self->data; + + if(d->maximize_btn && (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED)) + { + const gboolean maximized = event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED; + gtk_button_set_image(GTK_BUTTON(d->maximize_btn), + gtk_image_new_from_icon_name(maximized ? "window-restore-symbolic" + : "window-maximize-symbolic", + GTK_ICON_SIZE_MENU)); + } + return FALSE; +} +#endif + // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py