/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikawidgets-utils.c * Copyright (C) 1999-2003 Michael Natterer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include #ifdef GDK_WINDOWING_WIN32 #include #include #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif #endif #ifdef GDK_WINDOWING_X11 #include #endif #ifdef GDK_WINDOWING_WAYLAND #include #endif #ifdef PLATFORM_OSX #include #endif #include "libpikabase/pikabase.h" #include "libpikaconfig/pikaconfig.h" #include "libpikamath/pikamath.h" #include "libpikacolor/pikacolor.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "gegl/pika-babl.h" #include "config/pikaguiconfig.h" #include "core/pika.h" #include "core/pikaprogress.h" #include "core/pikatoolinfo.h" #include "pikaaccellabel.h" #include "pikaaction.h" #include "pikadialogfactory.h" #include "pikadock.h" #include "pikadockcontainer.h" #include "pikadockwindow.h" #include "pikaerrordialog.h" #include "pikasessioninfo.h" #include "pikatoolbutton.h" #include "pikauimanager.h" #include "pikawidgets-utils.h" #include "pikawindowstrategy.h" #include "pika-intl.h" #define PIKA_TOOL_OPTIONS_GUI_KEY "pika-tool-options-gui" #define PIKA_TOOL_OPTIONS_GUI_FUNC_KEY "pika-tool-options-gui-func" typedef struct { GList **blink_script; const gchar *widget_identifier; const gchar *settings_value; } BlinkSearch; typedef struct { GtkWidget *widget; gchar *settings_value; } BlinkStep; static void pika_widget_blink_after (GtkWidget *widget, gint ms_timeout); static void pika_search_widget_rec (GtkWidget *widget, BlinkSearch *data); static void pika_blink_free_script (GList *blink_scenario); static gboolean pika_window_transient_on_mapped (GtkWidget *widget, GdkEventAny *event, PikaProgress *progress); static void pika_window_set_transient_cb (GtkWidget *window, GdkEventAny *event, GBytes *handle); GtkWidget * pika_menu_item_get_image (GtkMenuItem *item) { g_return_val_if_fail (GTK_IS_MENU_ITEM (item), NULL); return g_object_get_data (G_OBJECT (item), "pika-menu-item-image"); } void pika_menu_item_set_image (GtkMenuItem *item, GtkWidget *image, PikaAction *action) { GtkWidget *hbox; GtkWidget *label; GtkWidget *accel_label; GtkWidget *old_image; g_return_if_fail (GTK_IS_MENU_ITEM (item)); g_return_if_fail (image == NULL || GTK_IS_WIDGET (image)); hbox = g_object_get_data (G_OBJECT (item), "pika-menu-item-hbox"); if (! hbox) { hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); g_object_set_data (G_OBJECT (item), "pika-menu-item-hbox", hbox); label = gtk_bin_get_child (GTK_BIN (item)); g_object_set_data (G_OBJECT (item), "pika-menu-item-label", label); gtk_label_set_xalign (GTK_LABEL (label), 0.0); g_object_ref (label); gtk_container_remove (GTK_CONTAINER (item), label); gtk_container_add (GTK_CONTAINER (hbox), label); g_object_unref (label); if (action) { accel_label = pika_accel_label_new (action); g_object_set_data (G_OBJECT (item), "pika-menu-item-accel", accel_label); gtk_container_add (GTK_CONTAINER (hbox), accel_label); gtk_widget_set_hexpand (GTK_WIDGET (accel_label), TRUE); gtk_label_set_xalign (GTK_LABEL (accel_label), 1.0); gtk_widget_show (accel_label); } gtk_container_add (GTK_CONTAINER (item), hbox); gtk_widget_show (hbox); } old_image = g_object_get_data (G_OBJECT (item), "pika-menu-item-image"); if (old_image != image) { if (old_image) { gtk_widget_destroy (old_image); g_object_set_data (G_OBJECT (item), "pika-menu-item-image", NULL); } if (image) { gtk_container_add (GTK_CONTAINER (hbox), image); gtk_box_reorder_child (GTK_BOX (hbox), image, 0); g_object_set_data (G_OBJECT (item), "pika-menu-item-image", image); gtk_widget_show (image); } } } /** * pika_menu_position: * @menu: a #GtkMenu widget * @x: pointer to horizontal position * @y: pointer to vertical position * * Positions a #GtkMenu so that it pops up on screen. This function * takes care of the preferred popup direction (taken from the widget * render direction) and it handles multiple monitors representing a * single #GdkScreen (Xinerama). * * You should call this function with @x and @y initialized to the * origin of the menu. This is typically the center of the widget the * menu is popped up from. pika_menu_position() will then decide if * and how these initial values need to be changed. **/ void pika_menu_position (GtkMenu *menu, gint *x, gint *y) { GtkWidget *widget; GtkRequisition requisition; GdkRectangle workarea; g_return_if_fail (GTK_IS_MENU (menu)); g_return_if_fail (x != NULL); g_return_if_fail (y != NULL); widget = GTK_WIDGET (menu); gdk_monitor_get_workarea (pika_widget_get_monitor (widget), &workarea); gtk_menu_set_screen (menu, gtk_widget_get_screen (widget)); gtk_widget_get_preferred_size (widget, &requisition, NULL); if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) { *x -= requisition.width; if (*x < workarea.x) *x += requisition.width; } else { if (*x + requisition.width > workarea.x + workarea.width) *x -= requisition.width; } if (*x < workarea.x) *x = workarea.x; if (*y + requisition.height > workarea.y + workarea.height) *y -= requisition.height; if (*y < workarea.y) *y = workarea.y; } void pika_grid_attach_icon (GtkGrid *grid, gint row, const gchar *icon_name, GtkWidget *widget, gint columns) { GtkWidget *image; g_return_if_fail (GTK_IS_GRID (grid)); g_return_if_fail (icon_name != NULL); g_return_if_fail (GTK_IS_WIDGET (widget)); image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_BUTTON); gtk_widget_set_halign (image, GTK_ALIGN_END); gtk_grid_attach (grid, image, 0, row, 1, 1); gtk_widget_show (image); gtk_grid_attach (grid, widget, 1, row, columns, 1); gtk_widget_show (widget); } void pika_enum_radio_box_add (GtkBox *box, GtkWidget *widget, gint enum_value, gboolean below) { GList *children; GList *list; gint pos; g_return_if_fail (GTK_IS_BOX (box)); g_return_if_fail (GTK_IS_WIDGET (widget)); children = gtk_container_get_children (GTK_CONTAINER (box)); for (list = children, pos = 1; list; list = g_list_next (list), pos++) { if (GTK_IS_RADIO_BUTTON (list->data) && GPOINTER_TO_INT (g_object_get_data (list->data, "pika-item-data")) == enum_value) { GtkWidget *radio = list->data; GtkWidget *hbox; hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0); gtk_box_reorder_child (GTK_BOX (box), hbox, pos); if (below) { GtkWidget *spacer; gint indicator_size; gint indicator_spacing; gint focus_width; gint focus_padding; gint border_width; gtk_widget_style_get (radio, "indicator-size", &indicator_size, "indicator-spacing", &indicator_spacing, "focus-line-width", &focus_width, "focus-padding", &focus_padding, NULL); border_width = gtk_container_get_border_width (GTK_CONTAINER (radio)); spacer = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_size_request (spacer, indicator_size + 3 * indicator_spacing + focus_width + focus_padding + border_width, -1); gtk_box_pack_start (GTK_BOX (hbox), spacer, FALSE, FALSE, 0); gtk_widget_show (spacer); } else { GtkSizeGroup *size_group; size_group = g_object_get_data (G_OBJECT (box), "size-group"); if (! size_group) { size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); g_object_set_data (G_OBJECT (box), "size-group", size_group); gtk_size_group_add_widget (size_group, radio); g_object_unref (size_group); } else { gtk_size_group_add_widget (size_group, radio); } gtk_box_set_spacing (GTK_BOX (hbox), 4); g_object_ref (radio); gtk_container_remove (GTK_CONTAINER (box), radio); gtk_box_pack_start (GTK_BOX (hbox), radio, FALSE, FALSE, 0); g_object_unref (radio); } gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0); gtk_widget_show (widget); g_object_bind_property (radio, "active", widget, "sensitive", G_BINDING_SYNC_CREATE); gtk_widget_show (hbox); break; } } g_list_free (children); } void pika_enum_radio_frame_add (GtkFrame *frame, GtkWidget *widget, gint enum_value, gboolean below) { GtkWidget *box; g_return_if_fail (GTK_IS_FRAME (frame)); g_return_if_fail (GTK_IS_WIDGET (widget)); box = gtk_bin_get_child (GTK_BIN (frame)); g_return_if_fail (GTK_IS_BOX (box)); pika_enum_radio_box_add (GTK_BOX (box), widget, enum_value, below); } /** * pika_widget_load_icon: * @widget: parent widget (to determine icon theme and * style) * @icon_name: icon name * @size: requested pixel size * * Loads an icon into a pixbuf with size as close as possible to @size. * If icon does not exist or fail to load, the function will fallback to * "pika-mascot-eek" instead to prevent NULL pixbuf. As a last resort, * if even the fallback failed to load, a magenta @size square will be * returned, so this function is guaranteed to always return a * #GdkPixbuf. * * Returns: a newly allocated #GdkPixbuf containing @icon_name at * size @size or a fallback icon/size. **/ GdkPixbuf * pika_widget_load_icon (GtkWidget *widget, const gchar *icon_name, gint size) { GdkPixbuf *pixbuf = NULL; GtkIconTheme *icon_theme; GtkIconInfo *icon_info; gchar *name; gint scale_factor; g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); g_return_val_if_fail (icon_name != NULL, NULL); icon_theme = gtk_icon_theme_get_for_screen (gtk_widget_get_screen (widget)); scale_factor = gtk_widget_get_scale_factor (widget); name = g_strdup_printf ("%s-symbolic", icon_name); /* This will find the symbolic icon and fallback to non-symbolic * depending on icon theme. */ icon_info = gtk_icon_theme_lookup_icon_for_scale (icon_theme, name, size, scale_factor, GTK_ICON_LOOKUP_GENERIC_FALLBACK); g_free (name); if (icon_info) { pixbuf = gtk_icon_info_load_symbolic_for_context (icon_info, gtk_widget_get_style_context (widget), NULL, NULL); g_object_unref (icon_info); if (! pixbuf) /* The icon was seemingly present in the current icon theme, yet * it failed to load. Maybe the file is broken? * As last resort, try to load "pika-mascot-eek" as fallback. * Note that we are not making more checks, so if the fallback * icon fails to load as well, the function may still return NULL. */ g_printerr ("WARNING: icon '%s' failed to load. Check the files " "in your icon theme.\n", icon_name); } else g_printerr ("WARNING: icon theme has no icon '%s'.\n", icon_name); /* First fallback: pika-mascot-eek */ if (! pixbuf) { icon_info = gtk_icon_theme_lookup_icon_for_scale (icon_theme, PIKA_ICON_MASCOT_EEK "-symbolic", size, scale_factor, GTK_ICON_LOOKUP_GENERIC_FALLBACK); if (icon_info) { pixbuf = gtk_icon_info_load_symbolic_for_context (icon_info, gtk_widget_get_style_context (widget), NULL, NULL); g_object_unref (icon_info); if (! pixbuf) g_printerr ("WARNING: icon '%s' failed to load. Check the files " "in your icon theme.\n", PIKA_ICON_MASCOT_EEK); } else { g_printerr ("WARNING: icon theme has no icon '%s'.\n", PIKA_ICON_MASCOT_EEK); } } /* Last fallback: just a magenta square. */ if (! pixbuf) { /* As last resort, just draw an ugly magenta square. */ guchar *data; gint rowstride = 3 * size * scale_factor; gint i, j; data = g_new (guchar, rowstride * size); for (i = 0; i < size; i++) { for (j = 0; j < size * scale_factor; j++) { data[i * rowstride + j * 3] = 255; data[i * rowstride + j * 3 + 1] = 0; data[i * rowstride + j * 3 + 2] = 255; } } pixbuf = gdk_pixbuf_new_from_data (data, GDK_COLORSPACE_RGB, FALSE, 8, size * scale_factor, size * scale_factor, rowstride, (GdkPixbufDestroyNotify) g_free, NULL); } /* Small assertion test to get a warning if we ever get NULL return * value, as this is never supposed to happen. */ g_return_val_if_fail (pixbuf != NULL, NULL); return pixbuf; } PikaTabStyle pika_preview_tab_style_to_icon (PikaTabStyle tab_style) { switch (tab_style) { case PIKA_TAB_STYLE_PREVIEW: tab_style = PIKA_TAB_STYLE_ICON; break; case PIKA_TAB_STYLE_PREVIEW_NAME: tab_style = PIKA_TAB_STYLE_ICON_NAME; break; case PIKA_TAB_STYLE_PREVIEW_BLURB: tab_style = PIKA_TAB_STYLE_ICON_BLURB; break; default: break; } return tab_style; } const gchar * pika_get_mod_string (GdkModifierType modifiers) { static GHashTable *mod_labels; gchar *label; if (! modifiers) return NULL; if (G_UNLIKELY (! mod_labels)) mod_labels = g_hash_table_new (g_int_hash, g_int_equal); modifiers = pika_replace_virtual_modifiers (modifiers); label = g_hash_table_lookup (mod_labels, &modifiers); if (! label) { GtkAccelLabelClass *accel_label_class; label = gtk_accelerator_get_label (0, modifiers); accel_label_class = g_type_class_ref (GTK_TYPE_ACCEL_LABEL); if (accel_label_class->mod_separator && *accel_label_class->mod_separator) { gchar *sep = g_strrstr (label, accel_label_class->mod_separator); if (sep - label == strlen (label) - strlen (accel_label_class->mod_separator)) *sep = '\0'; } g_type_class_unref (accel_label_class); g_hash_table_insert (mod_labels, g_memdup2 (&modifiers, sizeof (GdkModifierType)), label); } return label; } #define BUF_SIZE 100 /** * pika_suggest_modifiers: * @message: initial text for the message * @modifiers: bit mask of modifiers that should be suggested * @extend_selection_format: optional format string for the * "Extend selection" modifier * @toggle_behavior_format: optional format string for the * "Toggle behavior" modifier * @alt_format: optional format string for the Alt modifier * * Utility function to build a message suggesting to use some * modifiers for performing different actions (only Shift, Ctrl and * Alt are currently supported). If some of these modifiers are * already active, they will not be suggested. The optional format * strings #extend_selection_format, #toggle_behavior_format and * #alt_format may be used to describe what the modifier will do. * They must contain a single '%%s' which will be replaced by the name * of the modifier. They can also be %NULL if the modifier name * should be left alone. * * Returns: a newly allocated string containing the message. **/ gchar * pika_suggest_modifiers (const gchar *message, GdkModifierType modifiers, const gchar *extend_selection_format, const gchar *toggle_behavior_format, const gchar *alt_format) { GdkModifierType extend_mask = pika_get_extend_selection_mask (); GdkModifierType toggle_mask = pika_get_toggle_behavior_mask (); gchar msg_buf[3][BUF_SIZE]; gint num_msgs = 0; gboolean try = FALSE; if (modifiers & extend_mask) { if (extend_selection_format && *extend_selection_format) { g_snprintf (msg_buf[num_msgs], BUF_SIZE, extend_selection_format, pika_get_mod_string (extend_mask)); } else { g_strlcpy (msg_buf[num_msgs], pika_get_mod_string (extend_mask), BUF_SIZE); try = TRUE; } num_msgs++; } if (modifiers & toggle_mask) { if (toggle_behavior_format && *toggle_behavior_format) { g_snprintf (msg_buf[num_msgs], BUF_SIZE, toggle_behavior_format, pika_get_mod_string (toggle_mask)); } else { g_strlcpy (msg_buf[num_msgs], pika_get_mod_string (toggle_mask), BUF_SIZE); try = TRUE; } num_msgs++; } if (modifiers & GDK_MOD1_MASK) { if (alt_format && *alt_format) { g_snprintf (msg_buf[num_msgs], BUF_SIZE, alt_format, pika_get_mod_string (GDK_MOD1_MASK)); } else { g_strlcpy (msg_buf[num_msgs], pika_get_mod_string (GDK_MOD1_MASK), BUF_SIZE); try = TRUE; } num_msgs++; } /* This convoluted way to build the message using multiple format strings * tries to make the messages easier to translate to other languages. */ switch (num_msgs) { case 1: return g_strdup_printf (try ? _("%s (try %s)") : _("%s (%s)"), message, msg_buf[0]); case 2: return g_strdup_printf (_("%s (try %s, %s)"), message, msg_buf[0], msg_buf[1]); case 3: return g_strdup_printf (_("%s (try %s, %s, %s)"), message, msg_buf[0], msg_buf[1], msg_buf[2]); } return g_strdup (message); } #undef BUF_SIZE PikaChannelOps pika_modifiers_to_channel_op (GdkModifierType modifiers) { GdkModifierType extend_mask = pika_get_extend_selection_mask (); GdkModifierType modify_mask = pika_get_modify_selection_mask (); if (modifiers & extend_mask) { if (modifiers & modify_mask) { return PIKA_CHANNEL_OP_INTERSECT; } else { return PIKA_CHANNEL_OP_ADD; } } else if (modifiers & modify_mask) { return PIKA_CHANNEL_OP_SUBTRACT; } return PIKA_CHANNEL_OP_REPLACE; } GdkModifierType pika_replace_virtual_modifiers (GdkModifierType modifiers) { GdkDisplay *display = gdk_display_get_default (); GdkModifierType result = 0; gint i; for (i = 0; i < 8; i++) { GdkModifierType real = 1 << i; if (modifiers & real) { GdkModifierType virtual = real; gdk_keymap_add_virtual_modifiers (gdk_keymap_get_for_display (display), &virtual); if (virtual == real) result |= virtual; else result |= virtual & ~real; } } return result; } GdkModifierType pika_get_primary_accelerator_mask (void) { GdkDisplay *display = gdk_display_get_default (); return gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR); } GdkModifierType pika_get_extend_selection_mask (void) { GdkDisplay *display = gdk_display_get_default (); return gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_EXTEND_SELECTION); } GdkModifierType pika_get_modify_selection_mask (void) { GdkDisplay *display = gdk_display_get_default (); return gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_MODIFY_SELECTION); } GdkModifierType pika_get_toggle_behavior_mask (void) { GdkDisplay *display = gdk_display_get_default (); /* use the modify selection modifier */ return gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_MODIFY_SELECTION); } GdkModifierType pika_get_constrain_behavior_mask (void) { GdkDisplay *display = gdk_display_get_default (); /* use the modify selection modifier */ return gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_MODIFY_SELECTION); } GdkModifierType pika_get_all_modifiers_mask (void) { GdkDisplay *display = gdk_display_get_default (); return (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK | gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR) | gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_EXTEND_SELECTION) | gdk_keymap_get_modifier_mask (gdk_keymap_get_for_display (display), GDK_MODIFIER_INTENT_MODIFY_SELECTION)); } /** * pika_get_monitor_resolution: * @screen: a #GdkScreen * @monitor: a monitor number * @xres: returns the horizontal monitor resolution (in dpi) * @yres: returns the vertical monitor resolution (in dpi) * * Retrieves the monitor's resolution from GDK. **/ void pika_get_monitor_resolution (GdkMonitor *monitor, gdouble *xres, gdouble *yres) { GdkRectangle size_pixels; gint width_mm, height_mm; gdouble x = 0.0; gdouble y = 0.0; #ifdef PLATFORM_OSX CGSize size; #endif g_return_if_fail (GDK_IS_MONITOR (monitor)); g_return_if_fail (xres != NULL); g_return_if_fail (yres != NULL); #ifndef PLATFORM_OSX gdk_monitor_get_geometry (monitor, &size_pixels); width_mm = gdk_monitor_get_width_mm (monitor); height_mm = gdk_monitor_get_height_mm (monitor); #else width_mm = 0; height_mm = 0; size = CGDisplayScreenSize (kCGDirectMainDisplay); if (!CGSizeEqualToSize (size, CGSizeZero)) { width_mm = size.width; height_mm = size.height; } size_pixels.width = CGDisplayPixelsWide (kCGDirectMainDisplay); size_pixels.height = CGDisplayPixelsHigh (kCGDirectMainDisplay); #endif /* * From xdpyinfo.c: * * there are 2.54 centimeters to an inch; so there are 25.4 millimeters. * * dpi = N pixels / (M millimeters / (25.4 millimeters / 1 inch)) * = N pixels / (M inch / 25.4) * = N * 25.4 pixels / M inch */ if (width_mm > 0 && height_mm > 0) { x = (size_pixels.width * 25.4) / (gdouble) width_mm; y = (size_pixels.height * 25.4) / (gdouble) height_mm; } if (x < PIKA_MIN_RESOLUTION || x > PIKA_MAX_RESOLUTION || y < PIKA_MIN_RESOLUTION || y > PIKA_MAX_RESOLUTION) { g_printerr ("pika_get_monitor_resolution(): GDK returned bogus " "values for the monitor resolution, using 96 dpi instead.\n"); x = 96.0; y = 96.0; } /* round the value to full integers to give more pleasant results */ *xres = ROUND (x); *yres = ROUND (y); } gboolean pika_get_style_color (GtkWidget *widget, const gchar *property_name, GdkRGBA *color) { GdkRGBA *c = NULL; g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); g_return_val_if_fail (property_name != NULL, FALSE); g_return_val_if_fail (color != NULL, FALSE); gtk_widget_style_get (widget, property_name, &c, NULL); if (c) { *color = *c; gdk_rgba_free (c); return TRUE; } /* return ugly magenta to indicate that something is wrong */ color->red = 1.0; color->green = 1.0; color->blue = 0.0; color->alpha = 1.0; return FALSE; } void pika_window_set_hint (GtkWindow *window, PikaWindowHint hint) { g_return_if_fail (GTK_IS_WINDOW (window)); switch (hint) { case PIKA_WINDOW_HINT_NORMAL: gtk_window_set_type_hint (window, GDK_WINDOW_TYPE_HINT_NORMAL); break; case PIKA_WINDOW_HINT_UTILITY: gtk_window_set_type_hint (window, GDK_WINDOW_TYPE_HINT_UTILITY); break; case PIKA_WINDOW_HINT_KEEP_ABOVE: gtk_window_set_keep_above (window, TRUE); break; } } /* similar to what we have in libpika/pikaui.c */ static GdkWindow * pika_get_foreign_window (gpointer window) { #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) return gdk_x11_window_foreign_new_for_display (gdk_display_get_default (), (Window) window); #endif #ifdef GDK_WINDOWING_WIN32 return gdk_win32_window_foreign_new_for_display (gdk_display_get_default (), (HWND) window); #endif return NULL; } void pika_window_set_transient_for (GtkWindow *window, PikaProgress *parent) { g_signal_connect_after (window, "map-event", G_CALLBACK (pika_window_transient_on_mapped), parent); if (gtk_widget_get_mapped (GTK_WIDGET (window))) pika_window_transient_on_mapped (GTK_WIDGET (window), NULL, parent); } void pika_window_set_transient_for_handle (GtkWindow *window, GBytes *handle) { g_return_if_fail (GTK_IS_WINDOW (window)); g_return_if_fail (handle != NULL); g_signal_connect_data (window, "map-event", G_CALLBACK (pika_window_set_transient_cb), g_bytes_ref (handle), (GClosureNotify) g_bytes_unref, G_CONNECT_AFTER); if (gtk_widget_get_mapped (GTK_WIDGET (window))) pika_window_set_transient_cb (GTK_WIDGET (window), NULL, handle); } static void pika_widget_accels_changed (PikaAction *action, const gchar **accels, GtkWidget *widget) { const gchar *tooltip; const gchar *help_id; tooltip = pika_action_get_tooltip (action); help_id = pika_action_get_help_id (action); if (accels && accels[0]) { gchar *escaped = g_markup_escape_text (tooltip, -1); gchar *tmp = g_strdup_printf ("%s %s", escaped, accels[0]); g_free (escaped); pika_help_set_help_data_with_markup (widget, tmp, help_id); g_free (tmp); } else { pika_help_set_help_data (widget, tooltip, help_id); } } static void pika_accel_help_widget_weak_notify (gpointer accel_group, GObject *where_widget_was); static void pika_accel_help_accel_group_weak_notify (gpointer widget, GObject *where_action_was) { g_object_weak_unref (widget, pika_accel_help_widget_weak_notify, where_action_was); g_object_set_data (widget, "pika-accel-help-action", NULL); } static void pika_accel_help_widget_weak_notify (gpointer action, GObject *where_widget_was) { g_object_weak_unref (action, pika_accel_help_accel_group_weak_notify, where_widget_was); } void pika_widget_set_accel_help (GtkWidget *widget, PikaAction *action) { PikaAction *prev_action; prev_action = g_object_get_data (G_OBJECT (widget), "pika-accel-help-action"); if (prev_action) { g_signal_handlers_disconnect_by_func (prev_action, pika_widget_accels_changed, widget); g_object_weak_unref (G_OBJECT (action), pika_accel_help_accel_group_weak_notify, widget); g_object_weak_unref (G_OBJECT (widget), pika_accel_help_widget_weak_notify, action); g_object_set_data (G_OBJECT (widget), "pika-accel-help-action", NULL); } if (action) { gchar **accels; g_object_set_data (G_OBJECT (widget), "pika-accel-help-action", action); g_object_weak_ref (G_OBJECT (action), pika_accel_help_accel_group_weak_notify, widget); g_object_weak_ref (G_OBJECT (widget), pika_accel_help_widget_weak_notify, action); g_object_set_data (G_OBJECT (widget), "pika-accel-action", action); g_signal_connect_object (action, "accels-changed", G_CALLBACK (pika_widget_accels_changed), widget, 0); accels = pika_action_get_display_accels (action); pika_widget_accels_changed (action, (const gchar **) accels, widget); g_strfreev (accels); } else { pika_help_set_help_data (widget, pika_action_get_tooltip (action), pika_action_get_help_id (action)); } } const gchar * pika_get_message_icon_name (PikaMessageSeverity severity) { switch (severity) { case PIKA_MESSAGE_INFO: return PIKA_ICON_DIALOG_INFORMATION; case PIKA_MESSAGE_WARNING: return PIKA_ICON_DIALOG_WARNING; case PIKA_MESSAGE_ERROR: return PIKA_ICON_DIALOG_ERROR; case PIKA_MESSAGE_BUG_WARNING: case PIKA_MESSAGE_BUG_CRITICAL: return PIKA_ICON_MASCOT_EEK; } g_return_val_if_reached (PIKA_ICON_DIALOG_WARNING); } gboolean pika_get_color_tag_color (PikaColorTag color_tag, PikaRGB *color, gboolean inherited) { static const struct { guchar r; guchar g; guchar b; } colors[] = { { 0, 0, 0 }, /* none */ { 84, 102, 159 }, /* blue */ { 111, 143, 48 }, /* green */ { 210, 182, 45 }, /* yellow */ { 217, 122, 38 }, /* orange */ { 87, 53, 25 }, /* brown */ { 170, 42, 47 }, /* red */ { 99, 66, 174 }, /* violet */ { 87, 87, 87 } /* gray */ }; g_return_val_if_fail (color != NULL, FALSE); g_return_val_if_fail (color_tag < G_N_ELEMENTS (colors), FALSE); if (color_tag > PIKA_COLOR_TAG_NONE) { pika_rgba_set_uchar (color, colors[color_tag].r, colors[color_tag].g, colors[color_tag].b, 255); if (inherited) { pika_rgb_composite (color, &(PikaRGB) {1.0, 1.0, 1.0, 0.2}, PIKA_RGB_COMPOSITE_NORMAL); } return TRUE; } return FALSE; } void pika_pango_layout_set_scale (PangoLayout *layout, gdouble scale) { PangoAttrList *attrs; PangoAttribute *attr; g_return_if_fail (PANGO_IS_LAYOUT (layout)); attrs = pango_attr_list_new (); attr = pango_attr_scale_new (scale); attr->start_index = 0; attr->end_index = -1; pango_attr_list_insert (attrs, attr); pango_layout_set_attributes (layout, attrs); pango_attr_list_unref (attrs); } void pika_pango_layout_set_weight (PangoLayout *layout, PangoWeight weight) { PangoAttrList *attrs; PangoAttribute *attr; g_return_if_fail (PANGO_IS_LAYOUT (layout)); attrs = pango_attr_list_new (); attr = pango_attr_weight_new (weight); attr->start_index = 0; attr->end_index = -1; pango_attr_list_insert (attrs, attr); pango_layout_set_attributes (layout, attrs); pango_attr_list_unref (attrs); } static gboolean pika_highlight_widget_draw (GtkWidget *widget, cairo_t *cr, gpointer data) { GdkRectangle *rect = (GdkRectangle *) data; /* this code is a straight copy of draw_flash() from gtk-inspector's * inspect-button.c */ GtkAllocation alloc; if (rect) { alloc.x = rect->x; alloc.y = rect->y; alloc.width = rect->width; alloc.height = rect->height; } else if (GTK_IS_WINDOW (widget)) { GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget)); /* We don't want to draw the drag highlight around the * CSD window decorations */ if (child == NULL) return FALSE; gtk_widget_get_allocation (child, &alloc); } else { alloc.x = 0; alloc.y = 0; alloc.width = gtk_widget_get_allocated_width (widget); alloc.height = gtk_widget_get_allocated_height (widget); } cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 0.2); cairo_rectangle (cr, alloc.x + 0.5, alloc.y + 0.5, alloc.width - 1, alloc.height - 1); cairo_fill (cr); return FALSE; } /** * pika_highlight_widget: * @widget: * @highlight: * @rect: * * Turns highlighting for @widget on or off according to * @highlight, in a similar fashion to gtk_drag_highlight() * and gtk_drag_unhighlight(). * * If @rect is %NULL, highlight the full widget, otherwise highlight the * specific rectangle in widget coordinates. * When unhighlighting (i.e. @highlight is %FALSE), the value of @rect * doesn't matter, as the previously used rectangle will be reused. **/ void pika_highlight_widget (GtkWidget *widget, gboolean highlight, GdkRectangle *rect) { GdkRectangle *old_rect; gboolean old_highlight; g_return_if_fail (GTK_IS_WIDGET (widget)); highlight = highlight ? TRUE : FALSE; old_highlight = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "pika-widget-highlight")); old_rect = g_object_get_data (G_OBJECT (widget), "pika-widget-highlight-rect"); if (highlight && old_highlight && rect && old_rect && ! gdk_rectangle_equal (rect, old_rect)) { /* Highlight area changed. */ pika_highlight_widget (widget, FALSE, NULL); old_highlight = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), "pika-widget-highlight")); old_rect = g_object_get_data (G_OBJECT (widget), "pika-widget-highlight-rect"); } if (highlight != old_highlight) { if (highlight) { GdkRectangle *new_rect = NULL; if (rect) { new_rect = g_new0 (GdkRectangle, 1); *new_rect = *rect; g_object_set_data_full (G_OBJECT (widget), "pika-widget-highlight-rect", new_rect, (GDestroyNotify) g_free); } g_signal_connect_after (widget, "draw", G_CALLBACK (pika_highlight_widget_draw), new_rect); } else { if (old_rect) { g_signal_handlers_disconnect_by_func (widget, pika_highlight_widget_draw, old_rect); g_object_set_data (G_OBJECT (widget), "pika-widget-highlight-rect", NULL); } g_signal_handlers_disconnect_by_func (widget, pika_highlight_widget_draw, NULL); } g_object_set_data (G_OBJECT (widget), "pika-widget-highlight", GINT_TO_POINTER (highlight)); gtk_widget_queue_draw (widget); } } typedef struct { gint timeout_id; gint counter; GdkRectangle *rect; } WidgetBlink; static WidgetBlink * widget_blink_new (void) { WidgetBlink *blink; blink = g_slice_new (WidgetBlink); blink->timeout_id = 0; blink->counter = 0; blink->rect = NULL; return blink; } static void widget_blink_free (WidgetBlink *blink) { if (blink->timeout_id) { g_source_remove (blink->timeout_id); blink->timeout_id = 0; } if (blink->rect) g_slice_free (GdkRectangle, blink->rect); g_slice_free (WidgetBlink, blink); } static gboolean pika_widget_blink_start_timeout (GtkWidget *widget) { WidgetBlink *blink; blink = g_object_get_data (G_OBJECT (widget), "pika-widget-blink"); if (blink) { blink->timeout_id = 0; pika_widget_blink (widget); } else { /* If the data is not here anymore, our blink has been canceled * already. Also delete the script, if any. */ g_object_set_data (G_OBJECT (widget), "pika-widget-blink-script", NULL); } return G_SOURCE_REMOVE; } static gboolean pika_widget_blink_popover_remove (GtkWidget *widget) { gtk_widget_destroy (widget); return G_SOURCE_REMOVE; } static gboolean pika_widget_blink_timeout (GtkWidget *widget) { WidgetBlink *blink; GList *script; blink = g_object_get_data (G_OBJECT (widget), "pika-widget-blink"); script = g_object_get_data (G_OBJECT (widget), "pika-widget-blink-script"); pika_highlight_widget (widget, blink->counter % 2 == 1, blink->rect); blink->counter++; if (blink->counter == 1) { if (script) { BlinkStep *step = script->data; gchar *popover_text = NULL; if (step->settings_value) { const gchar *prop_name; GObject *config; prop_name = g_object_get_data (G_OBJECT (widget), "pika-widget-property-name"); config = g_object_get_data (G_OBJECT (widget), "pika-widget-property-config"); if (config && G_IS_OBJECT (config) && prop_name) { GParamSpec *param_spec; const gchar *nick; param_spec = g_object_class_find_property (G_OBJECT_GET_CLASS (config), prop_name); if (! param_spec) { g_printerr ("%s: %s has no property named '%s'.\n", G_STRFUNC, g_type_name (G_TYPE_FROM_INSTANCE (config)), prop_name); return G_SOURCE_CONTINUE; } if (! (param_spec->flags & G_PARAM_WRITABLE)) { g_printerr ("%s: property '%s' of %s is not writable.\n", G_STRFUNC, param_spec->name, g_type_name (param_spec->owner_type)); return G_SOURCE_CONTINUE; } nick = g_param_spec_get_nick (param_spec); if (g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_ENUM) || g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_INT) || g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_BOOLEAN)) { gchar *endptr; gint64 enum_value; enum_value = g_ascii_strtoll (step->settings_value, &endptr, 10); if (enum_value == 0 && endptr == step->settings_value) { g_printerr ("%s: settings value '%s' cannot properly be converted to int.\n", G_STRFUNC, step->settings_value); return G_SOURCE_CONTINUE; } g_object_set (config, prop_name, enum_value, NULL); if (nick) { if (g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_BOOLEAN)) { if ((gboolean) enum_value) /* TRANSLATORS: the %s will be replaced * by the localized label of a boolean settings * (e.g. a checkbox settings) displayed * in some dockable GUI. */ popover_text = g_strdup_printf (_("Switch \"%s\" ON"), nick); else popover_text = g_strdup_printf (_("Switch \"%s\" OFF"), nick); } else if (g_type_is_a (G_TYPE_FROM_INSTANCE (param_spec), G_TYPE_PARAM_ENUM)) { GParamSpecEnum *pspec_enum = (GParamSpecEnum *) param_spec; const GEnumValue *genum_value; genum_value = g_enum_get_value (pspec_enum->enum_class, enum_value); if (genum_value) { const gchar *enum_desc; enum_desc = pika_enum_value_get_desc (pspec_enum->enum_class, genum_value); if (enum_desc) /* TRANSLATORS: the %s will be replaced * by the localized label of a * multi-choice settings displayed * in some dockable GUI. */ popover_text = g_strdup_printf (_("Select \"%s\""), enum_desc); } } } } else { g_printerr ("%s: currently unsupported type '%s' for property %s of %s.\n", G_STRFUNC, g_type_name (G_TYPE_FROM_INSTANCE (param_spec)), param_spec->name, g_type_name (param_spec->owner_type)); } } } else if (PIKA_IS_TOOL_BUTTON (widget)) { PikaToolInfo *info; info = pika_tool_button_get_tool_info (PIKA_TOOL_BUTTON (widget)); /* TRANSLATORS: %s will be a tool name, so we'll get a * final string looking like 'Activate the "Bucket fill" tool'. */ popover_text = g_strdup_printf (_("Activate the \"%s\" tool"), info->label); } if (popover_text != NULL) { GtkWidget *popover = gtk_popover_new (widget); GtkWidget *label = gtk_label_new (popover_text); gtk_container_add (GTK_CONTAINER (popover), label); gtk_widget_show (label); gtk_widget_show (popover); g_timeout_add (1200, (GSourceFunc) pika_widget_blink_popover_remove, popover); g_free (popover_text); } } } else if (blink->counter == 3) { blink->timeout_id = 0; g_object_set_data (G_OBJECT (widget), "pika-widget-blink", NULL); if (script) { if (script->next) { BlinkStep *next_step = script->next->data; GtkWidget *next_widget = next_step->widget; g_object_set_data_full (G_OBJECT (next_widget), "pika-widget-blink-script", script->next, (GDestroyNotify) pika_blink_free_script); script->next->prev = NULL; script->next = NULL; pika_widget_blink_after (next_widget, 800); } g_object_set_data (G_OBJECT (widget), "pika-widget-blink-script", NULL); } return G_SOURCE_REMOVE; } return G_SOURCE_CONTINUE; } void pika_widget_blink (GtkWidget *widget) { WidgetBlink *blink; g_return_if_fail (GTK_IS_WIDGET (widget)); pika_widget_blink_cancel (widget); blink = widget_blink_new (); g_object_set_data_full (G_OBJECT (widget), "pika-widget-blink", blink, (GDestroyNotify) widget_blink_free); blink->timeout_id = g_timeout_add (150, (GSourceFunc) pika_widget_blink_timeout, widget); pika_highlight_widget (widget, TRUE, NULL); while ((widget = gtk_widget_get_parent (widget))) pika_widget_blink_cancel (widget); } void pika_widget_script_blink (GtkWidget *widget, const gchar *settings_value, GList **blink_scenario) { BlinkStep *step; step = g_slice_new (BlinkStep); step->widget = widget; step->settings_value = g_strdup (settings_value); *blink_scenario = g_list_append (*blink_scenario, step); while ((widget = gtk_widget_get_parent (widget))) pika_widget_blink_cancel (widget); } /* pika_blink_play_script: * @blink_scenario: * * This function will play the @blink_scenario and free the associated * data once done. */ void pika_blink_play_script (GList *blink_scenario) { BlinkStep *step; g_return_if_fail (g_list_length (blink_scenario) > 0); step = blink_scenario->data; g_object_set_data_full (G_OBJECT (step->widget), "pika-widget-blink-script", blink_scenario, (GDestroyNotify) pika_blink_free_script); pika_widget_blink (step->widget); } void pika_widget_blink_rect (GtkWidget *widget, GdkRectangle *rect) { WidgetBlink *blink; g_return_if_fail (GTK_IS_WIDGET (widget)); pika_widget_blink_cancel (widget); blink = widget_blink_new (); blink->rect = g_slice_new (GdkRectangle); *(blink->rect) = *rect; g_object_set_data_full (G_OBJECT (widget), "pika-widget-blink", blink, (GDestroyNotify) widget_blink_free); blink->timeout_id = g_timeout_add (150, (GSourceFunc) pika_widget_blink_timeout, widget); pika_highlight_widget (widget, TRUE, blink->rect); while ((widget = gtk_widget_get_parent (widget))) pika_widget_blink_cancel (widget); } void pika_widget_blink_cancel (GtkWidget *widget) { g_return_if_fail (GTK_IS_WIDGET (widget)); if (g_object_get_data (G_OBJECT (widget), "pika-widget-blink")) { pika_highlight_widget (widget, FALSE, NULL); g_object_set_data (G_OBJECT (widget), "pika-widget-blink", NULL); } } /** * pika_blink_toolbox: * @pika: * @action_name: * @blink_scenario: * * This is similar to pika_blink_dockable() for the toolbox * specifically. What it will do, additionally to blink the tool button, * is first to activate it. * * Also the identifier is easy as it is simply the action name. */ void pika_blink_toolbox (Pika *pika, const gchar *action_name, GList **blink_scenario) { PikaUIManager *ui_manager; GtkWidget *toolbox; /* As a special case, for the toolbox, we don't just raise it, * we also select the tool if one was requested. */ toolbox = pika_window_strategy_show_dockable_dialog (PIKA_WINDOW_STRATEGY (pika_get_window_strategy (pika)), pika, pika_dialog_factory_get_singleton (), pika_get_monitor_at_pointer (), "pika-toolbox"); /* Find and activate the tool. */ if (toolbox && action_name != NULL && (ui_manager = pika_dock_get_ui_manager (PIKA_DOCK (toolbox)))) { PikaAction *action; action = pika_ui_manager_find_action (ui_manager, "tools", action_name); pika_action_activate (PIKA_ACTION (action)); } pika_blink_dockable (pika, "pika-toolbox", action_name, NULL, blink_scenario); } /** * pika_blink_dockable: * @pika: * @dockable_identifier: * @widget_identifier: * @settings_value: * @blink_scenario: * * This function will raise the dockable identified by * @dockable_identifier. The list of dockable identifiers can be found * in `app/dialogs/dialogs.c`. * * Then it will find the widget identified by @widget_identifier. Note * that for propwidgets, this is usually the associated property name. * * If @settings_value is set, then it will try to change the widget * value, depending the type of widgets. Note that for now, only * property widgets of enum, int or boolean types can be set (so the * @settings_value string must represent an int value). * * Finally it will either blink this widget immediately to raise * attention, or add it to the @blink_scenario if not %NULL. The blink * scenario must be explicitly started with pika_blink_play_script() * when ready. @blink_scenario should be considered as opaque data, so * you should not free it directly and let pika_blink_play_script() do * so for you. */ void pika_blink_dockable (Pika *pika, const gchar *dockable_identifier, const gchar *widget_identifier, const gchar *settings_value, GList **blink_scenario) { GtkWidget *dockable; GdkMonitor *monitor; BlinkSearch *data; g_return_if_fail (PIKA_IS_PIKA (pika)); monitor = pika_get_monitor_at_pointer (); dockable = pika_window_strategy_show_dockable_dialog ( PIKA_WINDOW_STRATEGY (pika_get_window_strategy (pika)), pika, pika_dialog_factory_get_singleton (), monitor, dockable_identifier); if (! dockable) return; if (widget_identifier) { data = g_slice_new (BlinkSearch); data->blink_script = blink_scenario; data->widget_identifier = widget_identifier; data->settings_value = settings_value; gtk_container_foreach (GTK_CONTAINER (dockable), (GtkCallback) pika_search_widget_rec, (gpointer) data); g_slice_free (BlinkSearch, data); } } /** * pika_dock_with_window_new: * @factory: a #PikaDialogFacotry * @monitor: the #GdkMonitor the dock window should appear on * @toolbox: if %TRUE; gives a "pika-toolbox-window" with a * "pika-toolbox", "pika-dock-window"+"pika-dock" * otherwise * * Returns: the newly created #PikaDock with the #PikaDockWindow **/ GtkWidget * pika_dock_with_window_new (PikaDialogFactory *factory, GdkMonitor *monitor, gboolean toolbox) { GtkWidget *dock_window; PikaDockContainer *dock_container; GtkWidget *dock; PikaUIManager *ui_manager; g_return_val_if_fail (PIKA_IS_DIALOG_FACTORY (factory), NULL); g_return_val_if_fail (GDK_IS_MONITOR (monitor), NULL); /* Create a dock window to put the dock in. We need to create the * dock window before the dock because the dock has a dependency to * the ui manager in the dock window */ dock_window = pika_dialog_factory_dialog_new (factory, monitor, NULL /*ui_manager*/, NULL, (toolbox ? "pika-toolbox-window" : "pika-dock-window"), -1 /*view_size*/, FALSE /*present*/); dock_container = PIKA_DOCK_CONTAINER (dock_window); ui_manager = pika_dock_container_get_ui_manager (dock_container); dock = pika_dialog_factory_dialog_new (factory, monitor, ui_manager, dock_window, (toolbox ? "pika-toolbox" : "pika-dock"), -1 /*view_size*/, FALSE /*present*/); if (dock) pika_dock_window_add_dock (PIKA_DOCK_WINDOW (dock_window), PIKA_DOCK (dock), -1); return dock; } GtkWidget * pika_tools_get_tool_options_gui (PikaToolOptions *tool_options) { GtkWidget *widget; widget = g_object_get_data (G_OBJECT (tool_options), PIKA_TOOL_OPTIONS_GUI_KEY); if (! widget) { PikaToolOptionsGUIFunc func; func = g_object_get_data (G_OBJECT (tool_options), PIKA_TOOL_OPTIONS_GUI_FUNC_KEY); if (func) { widget = func (tool_options); pika_tools_set_tool_options_gui (tool_options, widget); } } return widget; } void pika_tools_set_tool_options_gui (PikaToolOptions *tool_options, GtkWidget *widget) { GtkWidget *prev_widget; prev_widget = g_object_get_data (G_OBJECT (tool_options), PIKA_TOOL_OPTIONS_GUI_KEY); if (widget == prev_widget) return; if (prev_widget) gtk_widget_destroy (prev_widget); g_object_set_data_full (G_OBJECT (tool_options), PIKA_TOOL_OPTIONS_GUI_KEY, widget ? g_object_ref_sink (widget) : NULL, widget ? (GDestroyNotify) g_object_unref : NULL); } void pika_tools_set_tool_options_gui_func (PikaToolOptions *tool_options, PikaToolOptionsGUIFunc func) { g_object_set_data (G_OBJECT (tool_options), PIKA_TOOL_OPTIONS_GUI_FUNC_KEY, func); } gboolean pika_widget_get_fully_opaque (GtkWidget *widget) { g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE); return g_object_get_data (G_OBJECT (widget), "pika-widget-fully-opaque") != NULL; } void pika_widget_set_fully_opaque (GtkWidget *widget, gboolean fully_opaque) { g_return_if_fail (GTK_IS_WIDGET (widget)); return g_object_set_data (G_OBJECT (widget), "pika-widget-fully-opaque", GINT_TO_POINTER (fully_opaque)); } static void pika_gtk_container_clear_callback (GtkWidget *widget, GtkContainer *container) { gtk_container_remove (container, widget); } void pika_gtk_container_clear (GtkContainer *container) { gtk_container_foreach (container, (GtkCallback) pika_gtk_container_clear_callback, container); } void pika_button_set_suggested (GtkWidget *button, gboolean suggested, GtkReliefStyle default_relief) { GtkStyleContext *style; g_return_if_fail (GTK_IS_BUTTON (button)); style = gtk_widget_get_style_context (button); if (suggested) { gtk_style_context_add_class (style, "suggested-action"); gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NORMAL); } else { gtk_style_context_remove_class (style, "suggested-action"); gtk_button_set_relief (GTK_BUTTON (button), default_relief); } } void pika_button_set_destructive (GtkWidget *button, gboolean destructive, GtkReliefStyle default_relief) { GtkStyleContext *style; g_return_if_fail (GTK_IS_BUTTON (button)); style = gtk_widget_get_style_context (button); if (destructive) { gtk_style_context_add_class (style, "destructive-action"); gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NORMAL); } else { gtk_style_context_remove_class (style, "destructive-action"); gtk_button_set_relief (GTK_BUTTON (button), default_relief); } } void pika_gtk_adjustment_chain (GtkAdjustment *adjustment1, GtkAdjustment *adjustment2) { g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment1)); g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment2)); g_object_bind_property (adjustment1, "value", adjustment2, "lower", G_BINDING_SYNC_CREATE); g_object_bind_property (adjustment2, "value", adjustment1, "upper", G_BINDING_SYNC_CREATE); } static gboolean pika_print_event_free (gpointer data) { g_free (data); return FALSE; } const gchar * pika_print_event (const GdkEvent *event) { gchar *str; gchar *tmp; switch (event->type) { case GDK_ENTER_NOTIFY: str = g_strdup_printf ("ENTER_NOTIFY (mode %d)", event->crossing.mode); break; case GDK_LEAVE_NOTIFY: str = g_strdup_printf ("LEAVE_NOTIFY (mode %d)", event->crossing.mode); break; case GDK_PROXIMITY_IN: str = g_strdup ("PROXIMITY_IN"); break; case GDK_PROXIMITY_OUT: str = g_strdup ("PROXIMITY_OUT"); break; case GDK_FOCUS_CHANGE: if (event->focus_change.in) str = g_strdup ("FOCUS_IN"); else str = g_strdup ("FOCUS_OUT"); break; case GDK_BUTTON_PRESS: str = g_strdup_printf ("BUTTON_PRESS (%d @ %0.0f:%0.0f)", event->button.button, event->button.x, event->button.y); break; case GDK_2BUTTON_PRESS: str = g_strdup_printf ("2BUTTON_PRESS (%d @ %0.0f:%0.0f)", event->button.button, event->button.x, event->button.y); break; case GDK_3BUTTON_PRESS: str = g_strdup_printf ("3BUTTON_PRESS (%d @ %0.0f:%0.0f)", event->button.button, event->button.x, event->button.y); break; case GDK_BUTTON_RELEASE: str = g_strdup_printf ("BUTTON_RELEASE (%d @ %0.0f:%0.0f)", event->button.button, event->button.x, event->button.y); break; case GDK_SCROLL: str = g_strdup_printf ("SCROLL (%d)", event->scroll.direction); break; case GDK_MOTION_NOTIFY: str = g_strdup_printf ("MOTION_NOTIFY (%0.0f:%0.0f %d)", event->motion.x, event->motion.y, event->motion.time); break; case GDK_KEY_PRESS: str = g_strdup_printf ("KEY_PRESS (%d, %s)", event->key.keyval, gdk_keyval_name (event->key.keyval) ? gdk_keyval_name (event->key.keyval) : ""); break; case GDK_KEY_RELEASE: str = g_strdup_printf ("KEY_RELEASE (%d, %s)", event->key.keyval, gdk_keyval_name (event->key.keyval) ? gdk_keyval_name (event->key.keyval) : ""); break; default: str = g_strdup_printf ("UNHANDLED (type %d)", event->type); break; } tmp = g_strdup_printf ("%s (device '%s', source device '%s')", str, gdk_device_get_name (gdk_event_get_device (event)), gdk_device_get_name (gdk_event_get_source_device (event))); g_free (str); str = tmp; g_idle_add (pika_print_event_free, str); return str; } gboolean pika_color_profile_store_add_defaults (PikaColorProfileStore *store, PikaColorConfig *config, PikaImageBaseType base_type, PikaPrecision precision, GError **error) { PikaColorProfile *profile; gchar *label; GError *my_error = NULL; g_return_val_if_fail (PIKA_IS_COLOR_PROFILE_STORE (store), FALSE); g_return_val_if_fail (PIKA_IS_COLOR_CONFIG (config), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); profile = pika_babl_get_builtin_color_profile (base_type, pika_babl_trc (precision)); if (base_type == PIKA_GRAY) { label = g_strdup_printf (_("Built-in grayscale (%s)"), pika_color_profile_get_label (profile)); profile = pika_color_config_get_gray_color_profile (config, &my_error); } else { label = g_strdup_printf (_("Built-in RGB (%s)"), pika_color_profile_get_label (profile)); profile = pika_color_config_get_rgb_color_profile (config, &my_error); } pika_color_profile_store_add_file (store, NULL, label); g_free (label); if (profile) { gchar *path; GFile *file; if (base_type == PIKA_GRAY) { g_object_get (config, "gray-profile", &path, NULL); file = pika_file_new_for_config_path (path, NULL); g_free (path); label = g_strdup_printf (_("Preferred grayscale (%s)"), pika_color_profile_get_label (profile)); } else { g_object_get (config, "rgb-profile", &path, NULL); file = pika_file_new_for_config_path (path, NULL); g_free (path); label = g_strdup_printf (_("Preferred RGB (%s)"), pika_color_profile_get_label (profile)); } g_object_unref (profile); pika_color_profile_store_add_file (store, file, label); g_object_unref (file); g_free (label); return TRUE; } else if (my_error) { g_propagate_error (error, my_error); return FALSE; } return TRUE; } static void connect_path_show (PikaColorProfileChooserDialog *dialog) { GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog); GFile *file = gtk_file_chooser_get_file (chooser); if (file) { /* if something is already selected in this dialog, * leave it alone */ g_object_unref (file); } else { GObject *config; const gchar *property; gchar *path = NULL; config = g_object_get_data (G_OBJECT (dialog), "profile-path-config"); property = g_object_get_data (G_OBJECT (dialog), "profile-path-property"); g_object_get (config, property, &path, NULL); if (path) { GFile *folder = pika_file_new_for_config_path (path, NULL); if (folder) { gtk_file_chooser_set_current_folder_file (chooser, folder, NULL); g_object_unref (folder); } g_free (path); } } } static void connect_path_response (PikaColorProfileChooserDialog *dialog, gint response) { if (response == GTK_RESPONSE_ACCEPT) { GtkFileChooser *chooser = GTK_FILE_CHOOSER (dialog); GFile *file = gtk_file_chooser_get_file (chooser); if (file) { GFile *folder = gtk_file_chooser_get_current_folder_file (chooser); if (folder) { GObject *config; const gchar *property; gchar *path = NULL; config = g_object_get_data (G_OBJECT (dialog), "profile-path-config"); property = g_object_get_data (G_OBJECT (dialog), "profile-path-property"); path = pika_file_get_config_path (folder, NULL); g_object_set (config, property, path, NULL); if (path) g_free (path); g_object_unref (folder); } g_object_unref (file); } } } void pika_color_profile_chooser_dialog_connect_path (GtkWidget *dialog, GObject *config, const gchar *property_name) { g_return_if_fail (PIKA_IS_COLOR_PROFILE_CHOOSER_DIALOG (dialog)); g_return_if_fail (G_IS_OBJECT (config)); g_return_if_fail (property_name != NULL); g_object_set_data_full (G_OBJECT (dialog), "profile-path-config", g_object_ref (config), (GDestroyNotify) g_object_unref); g_object_set_data_full (G_OBJECT (dialog), "profile-path-property", g_strdup (property_name), (GDestroyNotify) g_free); g_signal_connect (dialog, "show", G_CALLBACK (connect_path_show), NULL); g_signal_connect (dialog, "response", G_CALLBACK (connect_path_response), NULL); } void pika_widget_flush_expose (void) { while (g_main_context_pending (NULL)) g_main_context_iteration (NULL, FALSE); } void pika_make_valid_action_name (gchar *action_name) { /* g_action_name_is_valid() says: "action_name is valid if it consists only of * alphanumeric characters, plus ‘-‘ and ‘.’. The empty string is not a valid * action name." */ for (gint i = 0; action_name[i] != '\0'; i++) { if (action_name[i] == '-' || action_name[i] == '.' || (action_name[i] >= 'a' && action_name[i] <= 'z') || (action_name[i] >= 'A' && action_name[i] <= 'Z') || (action_name[i] >= '0' && action_name[i] <= '9')) continue; action_name[i] = '-'; } } /* private functions */ static void pika_widget_blink_after (GtkWidget *widget, gint ms_timeout) { WidgetBlink *blink; g_return_if_fail (GTK_IS_WIDGET (widget)); blink = widget_blink_new (); g_object_set_data_full (G_OBJECT (widget), "pika-widget-blink", blink, (GDestroyNotify) widget_blink_free); blink->timeout_id = g_timeout_add (ms_timeout, (GSourceFunc) pika_widget_blink_start_timeout, widget); } static void pika_search_widget_rec (GtkWidget *widget, BlinkSearch *data) { GList **blink_script = data->blink_script; const gchar *searched_id = data->widget_identifier; const gchar *settings_value = data->settings_value; const gchar *id; id = g_object_get_data (G_OBJECT (widget), "pika-widget-identifier"); if (id == NULL) /* Using propwidgets identifiers as fallback. */ id = g_object_get_data (G_OBJECT (widget), "pika-widget-property-name"); if (id && g_strcmp0 (id, searched_id) == 0) { /* Giving focus to help scrolling the dockable so that the * widget is visible. Note that it seems to work fine if the * dockable was already present, not if it was just created. * * TODO: this should be fixed so that we always make the * widget visible before blinking, otherwise it's a bit * useless when this happens. */ gtk_widget_grab_focus (widget); if (blink_script) pika_widget_script_blink (widget, settings_value, blink_script); else if (gtk_widget_is_visible (widget)) pika_widget_blink (widget); } else if (GTK_IS_CONTAINER (widget)) { gtk_container_foreach (GTK_CONTAINER (widget), (GtkCallback) pika_search_widget_rec, (gpointer) data); } } static void pika_blink_free_script (GList *blink_scenario) { GList *iter; for (iter = blink_scenario; iter; iter = iter->next) { BlinkStep *step = iter->data; g_free (step->settings_value); g_slice_free (BlinkStep, step); } g_list_free (blink_scenario); } gchar * pika_utils_make_canonical_menu_label (const gchar *path) { gchar **split_path; gchar *canon_path; /* The first underscore of each path item is a mnemonic. */ split_path = g_strsplit (path, "_", 2); canon_path = g_strjoinv ("", split_path); g_strfreev (split_path); return canon_path; } /** * pika_utils_break_menu_path: * @path: * @section_name: * * Break @path which is in the form "/_some/p_ath" into a GStrv containing "some" * and "path", in a canonical way: * - Multiple slashes are folded to one (e.g. "//some///path/" and "/some/path" * are equivalent). * - End slash or none are equivalent. * - Individual path components are canonicalized with * pika_utils_make_canonical_menu_label(), in particular with mnemonic * underscore removed. * * Moreover if @section_name is not %NULL, then a path component of the form * "[section]" at the end of the path will be removed and "section" will be * stored inside @section_name. Furthermore, "[[label]]" or "[[label]" will both * be transformed into a component path "[label]", without being removed, as a * way to create menu paths containing square brackets. */ gchar ** pika_utils_break_menu_path (const gchar *path, gchar **mnemonic_path1, gchar **section_name) { GRegex *path_regex; gchar **paths; GString *mnemonic_string = NULL; gint start = 0; g_return_val_if_fail (path != NULL, NULL); path_regex = g_regex_new ("/+", 0, 0, NULL); if (mnemonic_path1 != NULL) mnemonic_string = g_string_new (NULL); /* Get rid of leading slashes. */ while (path[start] == '/' && path[start] != '\0') start++; /* Get rid of empty last item because of trailing slashes. */ paths = g_regex_split (path_regex, path + start, 0); if (g_strv_length (paths) > 0 && strlen (paths[g_strv_length (paths) - 1]) == 0) { g_free (paths[g_strv_length (paths) - 1]); paths[g_strv_length (paths) - 1] = NULL; } for (int i = 0; paths[i]; i++) { gchar *canon_path; if (section_name && paths[i + 1] == NULL) { gint path_len; path_len = strlen (paths[i]); if (path_len > 2 && paths[i][0] == '[' && paths[i][path_len - 1] == ']') { if (paths[i][1] == '[') { if (paths[i][path_len - 2] == ']') paths[i][path_len - 1] = '\0'; canon_path = g_strdup (paths[i] + 1); g_free (paths[i]); paths[i] = canon_path; } else { paths[i][path_len - 1] = '\0'; *section_name = g_strdup (paths[i] + 1); g_free (paths[i]); paths[i] = NULL; break; } } } if (mnemonic_string != NULL) g_string_append_printf (mnemonic_string, "/%s", paths[i]); canon_path = pika_utils_make_canonical_menu_label (paths[i]); g_free (paths[i]); paths[i] = canon_path; } if (mnemonic_path1 != NULL) *mnemonic_path1 = g_string_free (mnemonic_string, FALSE); g_regex_unref (path_regex); return paths; } gboolean pika_utils_are_menu_path_identical (const gchar *path1, const gchar *path2, gchar **canonical_path1, gchar **mnemonic_path1, gchar **path1_section_name) { gchar **paths1; gchar **paths2; gboolean identical = TRUE; if (path1 == NULL) path1 = "/"; if (path2 == NULL) path2 = "/"; paths1 = pika_utils_break_menu_path (path1, mnemonic_path1, path1_section_name); paths2 = pika_utils_break_menu_path (path2, NULL, NULL); if (g_strv_length (paths1) != g_strv_length (paths2)) { identical = FALSE; } else { for (int i = 0; paths1[i]; i++) { if (g_strcmp0 (paths1[i], paths2[i]) != 0) { identical = FALSE; break; } } } if (canonical_path1) { gchar *joined = g_strjoinv ("/", paths1); *canonical_path1 = g_strdup_printf ("/%s", joined); g_free (joined); } g_strfreev (paths1); g_strfreev (paths2); return identical; } static gboolean pika_window_transient_on_mapped (GtkWidget *window, GdkEventAny *event G_GNUC_UNUSED, PikaProgress *progress) { GBytes *handle; handle = pika_progress_get_window_id (progress); if (handle == NULL) return FALSE; pika_window_set_transient_cb (window, NULL, handle); g_bytes_unref (handle); return FALSE; } static void pika_window_set_transient_cb (GtkWidget *window, GdkEventAny *event G_GNUC_UNUSED, GBytes *handle) { gboolean transient_set = FALSE; g_return_if_fail (handle != NULL); #ifdef GDK_WINDOWING_WAYLAND if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { char *wayland_handle; wayland_handle = (char *) g_bytes_get_data (handle, NULL); gdk_wayland_window_set_transient_for_exported (gtk_widget_get_window (window), wayland_handle); transient_set = TRUE; } #endif #ifdef GDK_WINDOWING_X11 if (! transient_set && GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { GdkWindow *parent; Window *handle_data; Window parent_ID; gsize handle_size; handle_data = (Window *) g_bytes_get_data (handle, &handle_size); g_return_if_fail (handle_size == sizeof (Window)); parent_ID = *handle_data; parent = pika_get_foreign_window ((gpointer) parent_ID); if (parent) gdk_window_set_transient_for (gtk_widget_get_window (window), parent); transient_set = TRUE; } #endif /* Cross-process transient-for is broken in gdk/win32. It causes hangs of the * main process and we still don't know why. * If it eventually is fixed to actually work, change this to a run-time check * of GTK+ version. Remember to change also pika_window_transient_on_mapped() * in libpika/pikaui.c * * Note: this hanging bug is still happening with GTK+3 as of mid-2023 with * steps described in comment 4 in: * https://bugzilla.gnome.org/show_bug.cgi?id=359538 */ #if 0 && defined (GDK_WINDOWING_WIN32) if (! transient_set) { GdkWindow *parent; HANDLE *handle_data; HANDLE parent_ID; gsize handle_size; handle_data = (HANDLE *) g_bytes_get_data (handle, &handle_size); g_return_if_fail (handle_size == sizeof (HANDLE)); parent_ID = *handle_data; parent = pika_get_foreign_window ((gpointer) parent_ID); if (parent) gdk_window_set_transient_for (gtk_widget_get_window (window), parent); transient_set = TRUE; } #endif } void pika_window_set_title_bar_theme (Pika *pika, GtkWidget *dialog, gboolean is_main_window) { #ifdef G_OS_WIN32 HWND hwnd; GdkWindow *window = NULL; gboolean use_dark_mode = FALSE; window = gtk_widget_get_window (GTK_WIDGET (dialog)); if (window) { if (pika) { PikaGuiConfig *config; config = PIKA_GUI_CONFIG (pika->config); use_dark_mode = config->prefer_dark_theme; } else { GtkStyleContext *style; GdkRGBA *color = NULL; /* Workaround if we don't have access to PikaGuiConfig. * If the background color is below the threshold, then we're * likely in dark mode. */ style = gtk_widget_get_style_context (dialog); gtk_style_context_get (style, gtk_style_context_get_state (style), GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &color, NULL); if (color) { if (color->red < 0.5 && color->green < 0.5 && color->blue < 0.5) use_dark_mode = TRUE; gdk_rgba_free (color); } } hwnd = (HWND) gdk_win32_window_get_handle (window); DwmSetWindowAttribute (hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &use_dark_mode, sizeof (use_dark_mode)); if (! is_main_window) { /* Toggle the window's visibility so the title bar change appears */ gdk_window_hide (window); gdk_window_show (window); } } #endif }