Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions src/gui/gtk.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/gui/gtk.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
144 changes: 144 additions & 0 deletions src/libs/tools/viewswitcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading