/* 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): * Spencer Kimball and Peter Mattis * * 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 #include "libpikabase/pikabase.h" #include "libpikacolor/pikacolor.h" #include "libpikaconfig/pikaconfig.h" #include "libpikamath/pikamath.h" #include "libpikawidgets/pikawidgets.h" #include "display-types.h" #include "config/pikadisplayconfig.h" #include "core/pika.h" #include "core/pikacontext.h" #include "core/pikaimage.h" #include "core/pikaimage-color-profile.h" #include "core/pikaprogress.h" #include "widgets/pikaaction.h" #include "widgets/pikawidgets-utils.h" #include "pikadisplay.h" #include "pikadisplayshell.h" #include "pikadisplayshell-scale.h" #include "pikaimagewindow.h" #include "pikascalecombobox.h" #include "pikastatusbar.h" #include "pika-intl.h" /* maximal width of the string holding the cursor-coordinates */ #define CURSOR_LEN 256 /* the spacing of the hbox */ #define HBOX_SPACING 1 /* spacing between the icon and the statusbar label */ #define ICON_SPACING 2 /* timeout (in milliseconds) for temporary statusbar messages */ #define MESSAGE_TIMEOUT 8000 /* minimal interval (in microseconds) between progress updates */ #define MIN_PROGRESS_UPDATE_INTERVAL 50000 typedef struct _PikaStatusbarMsg PikaStatusbarMsg; struct _PikaStatusbarMsg { guint context_id; gchar *icon_name; gchar *text; }; static void pika_statusbar_progress_iface_init (PikaProgressInterface *iface); static void pika_statusbar_dispose (GObject *object); static void pika_statusbar_finalize (GObject *object); static void pika_statusbar_screen_changed (GtkWidget *widget, GdkScreen *previous); static void pika_statusbar_style_updated (GtkWidget *widget); static PikaProgress * pika_statusbar_progress_start (PikaProgress *progress, gboolean cancellable, const gchar *message); static void pika_statusbar_progress_end (PikaProgress *progress); static gboolean pika_statusbar_progress_is_active (PikaProgress *progress); static void pika_statusbar_progress_set_text (PikaProgress *progress, const gchar *message); static void pika_statusbar_progress_set_value (PikaProgress *progress, gdouble percentage); static gdouble pika_statusbar_progress_get_value (PikaProgress *progress); static void pika_statusbar_progress_pulse (PikaProgress *progress); static gboolean pika_statusbar_progress_message (PikaProgress *progress, Pika *pika, PikaMessageSeverity severity, const gchar *domain, const gchar *message); static void pika_statusbar_progress_canceled (GtkWidget *button, PikaStatusbar *statusbar); static void pika_statusbar_soft_proof_button_toggled (GtkWidget *button, PikaStatusbar *statusbar); static void pika_statusbar_soft_proof_profile_changed (GtkComboBox *combo, PikaStatusbar *statusbar); static void pika_statusbar_soft_proof_rendering_intent_changed (GtkComboBox *combo, PikaStatusbar *statusbar); static void pika_statusbar_soft_proof_bpc_toggled (GtkWidget *button, PikaStatusbar *statusbar); static void pika_statusbar_soft_proof_optimize_changed (GtkWidget *button, PikaStatusbar *statusbar); static void pika_statusbar_soft_proof_gamut_toggled (GtkWidget *button, PikaStatusbar *statusbar); static gboolean pika_statusbar_soft_proof_popover_shown (GtkWidget *button, GdkEventButton *bevent, PikaStatusbar *statusbar); static gboolean pika_statusbar_label_draw (GtkWidget *widget, cairo_t *cr, PikaStatusbar *statusbar); static void pika_statusbar_update (PikaStatusbar *statusbar); static void pika_statusbar_unit_changed (PikaUnitComboBox *combo, PikaStatusbar *statusbar); static void pika_statusbar_scale_changed (PikaScaleComboBox *combo, PikaStatusbar *statusbar); static void pika_statusbar_scale_activated (PikaScaleComboBox *combo, PikaStatusbar *statusbar); static void pika_statusbar_shell_image_changed(PikaStatusbar *statusbar, PikaImage *image, PikaContext *context); static void pika_statusbar_shell_image_simulation_changed (PikaImage *image, PikaStatusbar *statusbar); static gboolean pika_statusbar_rotate_pressed (GtkWidget *event_box, GdkEvent *event, PikaStatusbar *statusbar); static gboolean pika_statusbar_horiz_flip_pressed (GtkWidget *event_box, GdkEvent *event, PikaStatusbar *statusbar); static gboolean pika_statusbar_vert_flip_pressed (GtkWidget *event_box, GdkEvent *event, PikaStatusbar *statusbar); static void pika_statusbar_shell_scaled (PikaDisplayShell *shell, PikaStatusbar *statusbar); static void pika_statusbar_shell_rotated (PikaDisplayShell *shell, PikaStatusbar *statusbar); static void pika_statusbar_shell_status_notify(PikaDisplayShell *shell, const GParamSpec *pspec, PikaStatusbar *statusbar); static void pika_statusbar_shell_color_config_notify (GObject *config, const GParamSpec *pspec, PikaStatusbar *statusbar); static void pika_statusbar_shell_set_image (PikaStatusbar *statusbar, PikaImage *image); static guint pika_statusbar_get_context_id (PikaStatusbar *statusbar, const gchar *context); static gboolean pika_statusbar_temp_timeout (PikaStatusbar *statusbar); static void pika_statusbar_add_size_widget (PikaStatusbar *statusbar, GtkWidget *widget); static void pika_statusbar_update_size (PikaStatusbar *statusbar); static void pika_statusbar_add_message (PikaStatusbar *statusbar, guint context_id, const gchar *icon_name, const gchar *format, va_list args, gboolean move_to_front) G_GNUC_PRINTF (4, 0); static void pika_statusbar_remove_message (PikaStatusbar *statusbar, guint context_id); static void pika_statusbar_msg_free (PikaStatusbarMsg *msg); static gchar * pika_statusbar_vprintf (const gchar *format, va_list args) G_GNUC_PRINTF (1, 0); static GdkPixbuf * pika_statusbar_load_icon (PikaStatusbar *statusbar, const gchar *icon_name); static gboolean pika_statusbar_queue_pos_redraw (gpointer data); G_DEFINE_TYPE_WITH_CODE (PikaStatusbar, pika_statusbar, GTK_TYPE_FRAME, G_IMPLEMENT_INTERFACE (PIKA_TYPE_PROGRESS, pika_statusbar_progress_iface_init)) #define parent_class pika_statusbar_parent_class static void pika_statusbar_class_init (PikaStatusbarClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = pika_statusbar_dispose; object_class->finalize = pika_statusbar_finalize; widget_class->screen_changed = pika_statusbar_screen_changed; widget_class->style_updated = pika_statusbar_style_updated; gtk_widget_class_set_css_name (widget_class, "statusbar"); } static void pika_statusbar_progress_iface_init (PikaProgressInterface *iface) { iface->start = pika_statusbar_progress_start; iface->end = pika_statusbar_progress_end; iface->is_active = pika_statusbar_progress_is_active; iface->set_text = pika_statusbar_progress_set_text; iface->set_value = pika_statusbar_progress_set_value; iface->get_value = pika_statusbar_progress_get_value; iface->pulse = pika_statusbar_progress_pulse; iface->message = pika_statusbar_progress_message; } static void pika_statusbar_init (PikaStatusbar *statusbar) { GtkWidget *hbox; GtkWidget *hbox2; GtkWidget *image; GtkWidget *label; GtkWidget *grid; GtkWidget *separator; GtkWidget *profile_chooser; PikaUnitStore *store; gchar *text; GFile *file; GtkListStore *combo_store; gint row; gtk_frame_set_shadow_type (GTK_FRAME (statusbar), GTK_SHADOW_IN); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, HBOX_SPACING); gtk_container_add (GTK_CONTAINER (statusbar), hbox); gtk_widget_show (hbox); /* When changing the text of the cursor_label, it requests a resize * bubbling up to containers, up to the display shell. If the resizing * actually happens (even when the size is the same), a full "draw" * signal is triggered on the whole canvas, which is not a good thing * as a general rule (and very bad on some platforms such as macOS). * It's too late to do anything when processing the "draw" signal * because then we can't know if some part of the invalidated * rectangle really needs to be redrawn. What we do is not propagate * the size request back to container parents. Instead we queue the * resize directly on the widget. * * Note that the "resize-mode" property seems to be unrecommended now * (though only the public functions are deprecated, we get no * deprecation setting as a property) but it's still here in GTK3 and * still seems like the proper way to avoid propagating useless * no-actual-size-change resizes to container widgets. * XXX On GTK4, we will likely have to test again and if it's still a * problem, make a different fix. * * See discussion in MR !572. */ g_object_set (statusbar, "resize-mode", GTK_RESIZE_QUEUE, NULL); statusbar->shell = NULL; statusbar->messages = NULL; statusbar->context_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); statusbar->seq_context_id = 1; statusbar->temp_context_id = pika_statusbar_get_context_id (statusbar, "pika-statusbar-temp"); statusbar->cursor_format_str[0] = '\0'; statusbar->cursor_format_str_f[0] = '\0'; statusbar->length_format_str[0] = '\0'; statusbar->progress_active = FALSE; statusbar->progress_shown = FALSE; statusbar->cursor_label = gtk_label_new ("8888, 8888"); pika_statusbar_add_size_widget (statusbar, statusbar->cursor_label); gtk_box_pack_start (GTK_BOX (hbox), statusbar->cursor_label, FALSE, FALSE, 0); gtk_widget_show (statusbar->cursor_label); store = pika_unit_store_new (2); statusbar->unit_combo = pika_unit_combo_box_new_with_model (store); g_object_unref (store); /* see issue #2642 */ gtk_combo_box_set_wrap_width (GTK_COMBO_BOX (statusbar->unit_combo), 1); gtk_widget_set_can_focus (statusbar->unit_combo, FALSE); g_object_set (statusbar->unit_combo, "focus-on-click", FALSE, NULL); pika_statusbar_add_size_widget (statusbar, statusbar->unit_combo); gtk_box_pack_start (GTK_BOX (hbox), statusbar->unit_combo, FALSE, FALSE, 0); gtk_widget_show (statusbar->unit_combo); g_signal_connect (statusbar->unit_combo, "changed", G_CALLBACK (pika_statusbar_unit_changed), statusbar); statusbar->scale_combo = pika_scale_combo_box_new (); gtk_widget_set_can_focus (statusbar->scale_combo, FALSE); g_object_set (statusbar->scale_combo, "focus-on-click", FALSE, NULL); pika_statusbar_add_size_widget (statusbar, statusbar->scale_combo); gtk_box_pack_start (GTK_BOX (hbox), statusbar->scale_combo, FALSE, FALSE, 0); gtk_widget_show (statusbar->scale_combo); g_signal_connect (statusbar->scale_combo, "changed", G_CALLBACK (pika_statusbar_scale_changed), statusbar); g_signal_connect (statusbar->scale_combo, "entry-activated", G_CALLBACK (pika_statusbar_scale_activated), statusbar); /* Shell transform status */ statusbar->rotate_widget = gtk_event_box_new (); pika_statusbar_add_size_widget (statusbar, statusbar->rotate_widget); gtk_box_pack_start (GTK_BOX (hbox), statusbar->rotate_widget, FALSE, FALSE, 1); gtk_widget_show (statusbar->rotate_widget); statusbar->rotate_label = gtk_label_new (NULL); gtk_container_add (GTK_CONTAINER (statusbar->rotate_widget), statusbar->rotate_label); gtk_widget_show (statusbar->rotate_label); g_signal_connect (statusbar->rotate_widget, "button-press-event", G_CALLBACK (pika_statusbar_rotate_pressed), statusbar); statusbar->horizontal_flip_icon = gtk_event_box_new (); pika_statusbar_add_size_widget (statusbar, statusbar->horizontal_flip_icon); gtk_box_pack_start (GTK_BOX (hbox), statusbar->horizontal_flip_icon, FALSE, FALSE, 1); gtk_widget_show (statusbar->horizontal_flip_icon); image = gtk_image_new_from_icon_name ("object-flip-horizontal", GTK_ICON_SIZE_MENU); gtk_container_add (GTK_CONTAINER (statusbar->horizontal_flip_icon), image); gtk_widget_show (image); g_signal_connect (statusbar->horizontal_flip_icon, "button-press-event", G_CALLBACK (pika_statusbar_horiz_flip_pressed), statusbar); statusbar->vertical_flip_icon = gtk_event_box_new (); pika_statusbar_add_size_widget (statusbar, statusbar->vertical_flip_icon); gtk_box_pack_start (GTK_BOX (hbox), statusbar->vertical_flip_icon, FALSE, FALSE, 1); gtk_widget_show (statusbar->vertical_flip_icon); image = gtk_image_new_from_icon_name ("object-flip-vertical", GTK_ICON_SIZE_MENU); gtk_container_add (GTK_CONTAINER (statusbar->vertical_flip_icon), image); gtk_widget_show (image); g_signal_connect (statusbar->vertical_flip_icon, "button-press-event", G_CALLBACK (pika_statusbar_vert_flip_pressed), statusbar); statusbar->label = gtk_label_new (""); gtk_label_set_ellipsize (GTK_LABEL (statusbar->label), PANGO_ELLIPSIZE_END); gtk_label_set_justify (GTK_LABEL (statusbar->label), GTK_JUSTIFY_LEFT); gtk_widget_set_halign (statusbar->label, GTK_ALIGN_START); pika_statusbar_add_size_widget (statusbar, statusbar->label); gtk_box_pack_start (GTK_BOX (hbox), statusbar->label, TRUE, TRUE, 1); gtk_widget_show (statusbar->label); g_signal_connect_after (statusbar->label, "draw", G_CALLBACK (pika_statusbar_label_draw), statusbar); statusbar->progressbar = g_object_new (GTK_TYPE_PROGRESS_BAR, "show-text", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL); pika_statusbar_add_size_widget (statusbar, statusbar->progressbar); gtk_box_pack_start (GTK_BOX (hbox), statusbar->progressbar, TRUE, TRUE, 0); /* don't show the progress bar */ /* construct the cancel button's contents manually because we * always want image and label regardless of settings, and we want * a menu size image. */ statusbar->cancel_button = gtk_button_new (); gtk_widget_set_can_focus (statusbar->cancel_button, FALSE); gtk_button_set_relief (GTK_BUTTON (statusbar->cancel_button), GTK_RELIEF_NONE); gtk_widget_set_sensitive (statusbar->cancel_button, FALSE); pika_statusbar_add_size_widget (statusbar, statusbar->cancel_button); gtk_box_pack_end (GTK_BOX (hbox), statusbar->cancel_button, FALSE, FALSE, 0); /* don't show the cancel button */ hbox2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_container_add (GTK_CONTAINER (statusbar->cancel_button), hbox2); gtk_widget_show (hbox2); image = gtk_image_new_from_icon_name ("gtk-cancel", GTK_ICON_SIZE_MENU); gtk_box_pack_start (GTK_BOX (hbox2), image, FALSE, FALSE, 2); gtk_widget_show (image); label = gtk_label_new (_("Cancel")); gtk_box_pack_start (GTK_BOX (hbox2), label, FALSE, FALSE, 2); gtk_widget_show (label); g_signal_connect (statusbar->cancel_button, "clicked", G_CALLBACK (pika_statusbar_progress_canceled), statusbar); /* soft proofing button */ statusbar->soft_proof_button = gtk_toggle_button_new(); gtk_widget_set_can_focus (statusbar->soft_proof_button, FALSE); gtk_button_set_relief (GTK_BUTTON (statusbar->soft_proof_button), GTK_RELIEF_NONE); image = gtk_image_new_from_icon_name (PIKA_ICON_DISPLAY_FILTER_PROOF, GTK_ICON_SIZE_MENU); gtk_container_add (GTK_CONTAINER (statusbar->soft_proof_button), image); gtk_widget_show (image); gtk_widget_show (statusbar->soft_proof_button); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->soft_proof_button), FALSE); /* The soft-proof toggle button is placed in a GtkEventBox * so it can be disabled while still allowing users to right-click * and access the soft-proofing menu */ statusbar->soft_proof_container = gtk_event_box_new (); gtk_container_add (GTK_CONTAINER (statusbar->soft_proof_container), statusbar->soft_proof_button); gtk_box_pack_end (GTK_BOX (hbox), statusbar->soft_proof_container, FALSE, FALSE, 0); pika_statusbar_add_size_widget (statusbar, statusbar->soft_proof_container); gtk_widget_show (statusbar->soft_proof_container); pika_help_set_help_data (statusbar->soft_proof_container, _("Toggle soft-proofing view when " "a soft-proofing profile is set\n" "Right-click to show the soft-proofing " "options"), NULL); gtk_widget_set_events (statusbar->soft_proof_container, GDK_BUTTON_PRESS_MASK); g_signal_connect (statusbar->soft_proof_container, "button-press-event", G_CALLBACK (pika_statusbar_soft_proof_popover_shown), statusbar); gtk_event_box_set_above_child (GTK_EVENT_BOX (statusbar->soft_proof_container), FALSE); /* soft proofing popover */ row = 0; statusbar->soft_proof_popover = gtk_popover_new (statusbar->soft_proof_container); gtk_popover_set_modal (GTK_POPOVER (statusbar->soft_proof_popover), TRUE); grid = gtk_grid_new (); gtk_grid_set_row_spacing (GTK_GRID (grid), 6); label = gtk_label_new (NULL); text = g_strdup_printf ("%s", _("Soft-Proofing")); gtk_label_set_markup (GTK_LABEL (label), text); g_free (text); gtk_grid_attach (GTK_GRID (grid), label, 0, row++, 2, 1); gtk_widget_show (label); statusbar->proof_colors_toggle = gtk_check_button_new_with_mnemonic (_("_Proof Colors")); gtk_grid_attach (GTK_GRID (grid), statusbar->proof_colors_toggle, 0, row++, 1, 1); g_signal_connect (statusbar->proof_colors_toggle, "clicked", G_CALLBACK (pika_statusbar_soft_proof_button_toggled), statusbar); gtk_widget_show (statusbar->proof_colors_toggle); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->proof_colors_toggle), FALSE); statusbar->profile_label = gtk_label_new (NULL); text = g_strdup_printf ("%s: %s", _("Current Soft-Proofing Profile"), _("None")); gtk_label_set_markup (GTK_LABEL (statusbar->profile_label), text); g_free (text); gtk_grid_attach (GTK_GRID (grid), statusbar->profile_label, 0, row++, 2, 1); gtk_widget_show (statusbar->profile_label); file = pika_directory_file ("profilerc", NULL); combo_store = pika_color_profile_store_new (file); g_object_unref (file); pika_color_profile_store_add_file (PIKA_COLOR_PROFILE_STORE (combo_store), NULL, NULL); profile_chooser = pika_color_profile_chooser_dialog_new (_("Soft-Proofing Profile"), NULL, GTK_FILE_CHOOSER_ACTION_OPEN); statusbar->profile_combo = pika_color_profile_combo_box_new_with_model (profile_chooser, GTK_TREE_MODEL (combo_store)); pika_color_profile_combo_box_set_active_file (PIKA_COLOR_PROFILE_COMBO_BOX (statusbar->profile_combo), NULL, NULL); pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_Soft-proofing Profile: "), 0.0, 0.5, statusbar->profile_combo, 1); gtk_widget_show (statusbar->profile_combo); g_signal_connect (statusbar->profile_combo, "changed", G_CALLBACK (pika_statusbar_soft_proof_profile_changed), statusbar); combo_store = pika_int_store_new ("Perceptual", PIKA_COLOR_RENDERING_INTENT_PERCEPTUAL, "Relative Colorimetric", PIKA_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC, "Saturation", PIKA_COLOR_RENDERING_INTENT_SATURATION, "Absolute Colorimetric", PIKA_COLOR_RENDERING_INTENT_ABSOLUTE_COLORIMETRIC, NULL); statusbar->rendering_intent_combo = g_object_new (PIKA_TYPE_INT_COMBO_BOX, "model", combo_store, "visible", TRUE, NULL); pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("_Rendering Intent: "), 0.0, 0.5, statusbar->rendering_intent_combo, 1); gtk_widget_show (statusbar->rendering_intent_combo); g_signal_connect (statusbar->rendering_intent_combo, "changed", G_CALLBACK (pika_statusbar_soft_proof_rendering_intent_changed), statusbar); statusbar->bpc_toggle = gtk_check_button_new_with_mnemonic (_("Use _Black Point Compensation")); gtk_grid_attach (GTK_GRID (grid), statusbar->bpc_toggle, 0, row++, 1, 1); gtk_widget_show (statusbar->bpc_toggle); g_signal_connect (statusbar->bpc_toggle, "clicked", G_CALLBACK (pika_statusbar_soft_proof_bpc_toggled), statusbar); separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); gtk_grid_attach (GTK_GRID (grid),separator, 0, row++, 1, 1); gtk_widget_show (separator); statusbar->optimize_combo = pika_int_combo_box_new (_("Speed"), TRUE, _("Precision / Color Fidelity"), FALSE, NULL); pika_grid_attach_aligned (GTK_GRID (grid), 0, row++, _("O_ptimize soft-proofing for: "), 0.0, 0.5, statusbar->optimize_combo, 1); gtk_widget_show (statusbar->optimize_combo); g_signal_connect (statusbar->optimize_combo, "changed", G_CALLBACK (pika_statusbar_soft_proof_optimize_changed), statusbar); statusbar->out_of_gamut_toggle = gtk_check_button_new_with_mnemonic (_("_Mark Out of Gamut Colors")); gtk_grid_attach (GTK_GRID (grid), statusbar->out_of_gamut_toggle, 0, row++, 1, 1); gtk_widget_show (statusbar->out_of_gamut_toggle); g_signal_connect (statusbar->out_of_gamut_toggle, "clicked", G_CALLBACK (pika_statusbar_soft_proof_gamut_toggled), statusbar); gtk_container_add (GTK_CONTAINER (statusbar->soft_proof_popover), grid); gtk_widget_show (grid); pika_statusbar_update_size (statusbar); } static void pika_statusbar_dispose (GObject *object) { PikaStatusbar *statusbar = PIKA_STATUSBAR (object); if (statusbar->pika) { g_signal_handlers_disconnect_by_func (pika_get_user_context (statusbar->pika), pika_statusbar_shell_image_changed, statusbar); statusbar->pika = NULL; } pika_statusbar_shell_set_image (statusbar, NULL); if (statusbar->temp_timeout_id) { g_source_remove (statusbar->temp_timeout_id); statusbar->temp_timeout_id = 0; } if (statusbar->statusbar_pos_redraw_idle_id) { g_source_remove (statusbar->statusbar_pos_redraw_idle_id); statusbar->statusbar_pos_redraw_idle_id = 0; } g_clear_pointer (&statusbar->size_widgets, g_slist_free); G_OBJECT_CLASS (parent_class)->dispose (object); } static void pika_statusbar_finalize (GObject *object) { PikaStatusbar *statusbar = PIKA_STATUSBAR (object); g_clear_object (&statusbar->icon); g_clear_pointer (&statusbar->icon_hash, g_hash_table_unref); g_clear_pointer (&statusbar->cursor_string_last, g_free); g_clear_pointer (&statusbar->cursor_string_todraw, g_free); g_slist_free_full (statusbar->messages, (GDestroyNotify) pika_statusbar_msg_free); statusbar->messages = NULL; g_clear_pointer (&statusbar->context_ids, g_hash_table_destroy); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_statusbar_screen_changed (GtkWidget *widget, GdkScreen *previous) { PikaStatusbar *statusbar = PIKA_STATUSBAR (widget); if (GTK_WIDGET_CLASS (parent_class)->screen_changed) GTK_WIDGET_CLASS (parent_class)->screen_changed (widget, previous); g_clear_pointer (&statusbar->icon_hash, g_hash_table_unref); } static void pika_statusbar_style_updated (GtkWidget *widget) { PikaStatusbar *statusbar = PIKA_STATUSBAR (widget); PangoLayout *layout; GTK_WIDGET_CLASS (parent_class)->style_updated (widget); g_clear_pointer (&statusbar->icon_hash, g_hash_table_unref); layout = gtk_widget_create_pango_layout (widget, " "); pango_layout_get_pixel_size (layout, &statusbar->icon_space_width, NULL); g_object_unref (layout); pika_statusbar_update_size (statusbar); } static PikaProgress * pika_statusbar_progress_start (PikaProgress *progress, gboolean cancellable, const gchar *message) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); if (! statusbar->progress_active) { GtkWidget *bar = statusbar->progressbar; statusbar->progress_active = TRUE; statusbar->progress_value = 0.0; statusbar->progress_last_update_time = g_get_monotonic_time (); pika_statusbar_push (statusbar, "progress", NULL, "%s", message); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (bar), 0.0); gtk_widget_set_sensitive (statusbar->cancel_button, cancellable); if (cancellable) { if (message) { gchar *tooltip = g_strdup_printf (_("Cancel %s"), message); pika_help_set_help_data_with_markup (statusbar->cancel_button, tooltip, NULL); g_free (tooltip); } gtk_widget_show (statusbar->cancel_button); } gtk_widget_show (statusbar->progressbar); gtk_widget_hide (statusbar->label); if (! gtk_widget_get_visible (GTK_WIDGET (statusbar))) { gtk_widget_show (GTK_WIDGET (statusbar)); statusbar->progress_shown = TRUE; } pika_widget_flush_expose (); pika_statusbar_override_window_title (statusbar); return progress; } return NULL; } static void pika_statusbar_progress_end (PikaProgress *progress) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); if (statusbar->progress_active) { GtkWidget *bar = statusbar->progressbar; if (statusbar->progress_shown) { gtk_widget_hide (GTK_WIDGET (statusbar)); statusbar->progress_shown = FALSE; } statusbar->progress_active = FALSE; statusbar->progress_value = 0.0; gtk_widget_hide (bar); gtk_widget_show (statusbar->label); pika_statusbar_pop (statusbar, "progress"); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (bar), 0.0); gtk_widget_set_sensitive (statusbar->cancel_button, FALSE); gtk_widget_hide (statusbar->cancel_button); pika_statusbar_restore_window_title (statusbar); } } static gboolean pika_statusbar_progress_is_active (PikaProgress *progress) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); return statusbar->progress_active; } static void pika_statusbar_progress_set_text (PikaProgress *progress, const gchar *message) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); if (statusbar->progress_active) { pika_statusbar_replace (statusbar, "progress", NULL, "%s", message); pika_widget_flush_expose (); pika_statusbar_override_window_title (statusbar); } } static void pika_statusbar_progress_set_value (PikaProgress *progress, gdouble percentage) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); if (statusbar->progress_active) { guint64 time = g_get_monotonic_time (); if (time - statusbar->progress_last_update_time >= MIN_PROGRESS_UPDATE_INTERVAL) { GtkWidget *bar = statusbar->progressbar; GtkAllocation allocation; gdouble diff; gtk_widget_get_allocation (bar, &allocation); statusbar->progress_value = percentage; diff = fabs (percentage - gtk_progress_bar_get_fraction (GTK_PROGRESS_BAR (bar))); /* only update the progress bar if this causes a visible change */ if (allocation.width * diff >= 1.0) { statusbar->progress_last_update_time = time; gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (bar), percentage); pika_widget_flush_expose (); } } } } static gdouble pika_statusbar_progress_get_value (PikaProgress *progress) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); if (statusbar->progress_active) return statusbar->progress_value; return 0.0; } static void pika_statusbar_progress_pulse (PikaProgress *progress) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); if (statusbar->progress_active) { guint64 time = g_get_monotonic_time (); if (time - statusbar->progress_last_update_time >= MIN_PROGRESS_UPDATE_INTERVAL) { GtkWidget *bar = statusbar->progressbar; statusbar->progress_last_update_time = time; gtk_progress_bar_pulse (GTK_PROGRESS_BAR (bar)); pika_widget_flush_expose (); } } } static gboolean pika_statusbar_progress_message (PikaProgress *progress, Pika *pika, PikaMessageSeverity severity, const gchar *domain, const gchar *message) { PikaStatusbar *statusbar = PIKA_STATUSBAR (progress); PangoLayout *layout; const gchar *icon_name; gboolean handle_msg = FALSE; /* don't accept a message if we are already displaying a more severe one */ if (statusbar->temp_timeout_id && statusbar->temp_severity > severity) return FALSE; /* we can only handle short one-liners */ layout = gtk_widget_create_pango_layout (statusbar->label, message); icon_name = pika_get_message_icon_name (severity); if (pango_layout_get_line_count (layout) == 1) { GtkWidget *label_box = gtk_widget_get_parent (statusbar->label); GtkAllocation label_allocation; gint text_width, max_label_width, x; gtk_widget_get_allocation (label_box, &label_allocation); if (gtk_widget_translate_coordinates (statusbar->label, label_box, 0, 0, &x, NULL)) { max_label_width = label_allocation.width - x; pango_layout_get_pixel_size (layout, &text_width, NULL); if (text_width < max_label_width) { if (icon_name) { GdkPixbuf *pixbuf; gint scale_factor; pixbuf = pika_statusbar_load_icon (statusbar, icon_name); scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (statusbar)); text_width += ICON_SPACING + gdk_pixbuf_get_width (pixbuf) / scale_factor; g_object_unref (pixbuf); handle_msg = (text_width < max_label_width); } else { handle_msg = TRUE; } } } } g_object_unref (layout); if (handle_msg) pika_statusbar_push_temp (statusbar, severity, icon_name, "%s", message); return handle_msg; } static void pika_statusbar_progress_canceled (GtkWidget *button, PikaStatusbar *statusbar) { if (statusbar->progress_active) pika_progress_cancel (PIKA_PROGRESS (statusbar)); } static void pika_statusbar_soft_proof_button_toggled (GtkWidget *button, PikaStatusbar *statusbar) { PikaColorConfig *color_config; PikaColorManagementMode mode; gboolean active; color_config = pika_display_shell_get_color_config (statusbar->shell); mode = pika_color_config_get_mode (color_config); active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); if (active) { mode = PIKA_COLOR_MANAGEMENT_SOFTPROOF; } else { if (mode != PIKA_COLOR_MANAGEMENT_OFF) mode = PIKA_COLOR_MANAGEMENT_DISPLAY; } if (mode != pika_color_config_get_mode (color_config)) { g_object_set (color_config, "mode", mode, NULL); statusbar->shell->color_config_set = TRUE; } /* Updates soft-proofing buttons */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->soft_proof_button), active); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->proof_colors_toggle), active); pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static void pika_statusbar_soft_proof_profile_changed (GtkComboBox *combo, PikaStatusbar *statusbar) { PikaImage *image; PikaColorConfig *color_config; GFile *file; PikaColorProfile *simulation_profile = NULL; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); image = statusbar->image; color_config = pika_display_shell_get_color_config (statusbar->shell); file = pika_color_profile_combo_box_get_active_file (PIKA_COLOR_PROFILE_COMBO_BOX (combo)); if (file) { simulation_profile = pika_color_profile_new_from_file (file, NULL); g_object_unref (file); } if (image) pika_image_set_simulation_profile (image, simulation_profile); pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static void pika_statusbar_soft_proof_rendering_intent_changed (GtkComboBox *combo, PikaStatusbar *statusbar) { PikaImage *image; PikaColorConfig *color_config; PikaColorRenderingIntent intent; PikaColorRenderingIntent active; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); image = statusbar->image; color_config = pika_display_shell_get_color_config (statusbar->shell); if (image) { intent = pika_image_get_simulation_intent (image); active = (PikaColorRenderingIntent) gtk_combo_box_get_active (GTK_COMBO_BOX (combo)); if (active != intent) { pika_image_set_simulation_intent (image, active); pika_image_flush (image); } } pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static void pika_statusbar_soft_proof_bpc_toggled (GtkWidget *button, PikaStatusbar *statusbar) { PikaImage *image; PikaColorConfig *color_config; gboolean bpc_enabled; gboolean active; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); image = statusbar->image; color_config = pika_display_shell_get_color_config (statusbar->shell); if (image) { bpc_enabled = pika_image_get_simulation_bpc (image); active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); if (active != bpc_enabled) { pika_image_set_simulation_bpc (image, active); pika_image_flush (image); } } pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static void pika_statusbar_soft_proof_optimize_changed (GtkWidget *combo, PikaStatusbar *statusbar) { PikaColorConfig *color_config; gint optimize; gint active; color_config = pika_display_shell_get_color_config (statusbar->shell); optimize = pika_color_config_get_simulation_optimize (color_config); pika_int_combo_box_get_active (PIKA_INT_COMBO_BOX (combo), &active); if (active != optimize) { g_object_set (color_config, "simulation-optimize", active, NULL); statusbar->shell->color_config_set = TRUE; } pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static void pika_statusbar_soft_proof_gamut_toggled (GtkWidget *button, PikaStatusbar *statusbar) { PikaColorConfig *color_config; gboolean out_of_gamut; gboolean active; color_config = pika_display_shell_get_color_config (statusbar->shell); out_of_gamut = pika_color_config_get_simulation_gamut_check (color_config); active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); if (active != out_of_gamut) { g_object_set (color_config, "simulation-gamut-check", active, NULL); statusbar->shell->color_config_set = TRUE; } pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static gboolean pika_statusbar_soft_proof_popover_shown (GtkWidget *button, GdkEventButton *bevent, PikaStatusbar *statusbar) { if (bevent->type == GDK_BUTTON_PRESS) { if (bevent->button == 3) gtk_widget_show (statusbar->soft_proof_popover); if (bevent->button == 1 && gtk_widget_get_sensitive (statusbar->soft_proof_button)) { /* Since a GtkEventBox now covers the toggle so we can't click it, * directly, we have to flip the toggle ourselves before we call * the soft-proof button so it produces the right result */ gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (statusbar->soft_proof_button)); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->soft_proof_button), ! active); pika_statusbar_soft_proof_button_toggled (statusbar->soft_proof_button, statusbar); } } return TRUE; } static void pika_statusbar_set_text (PikaStatusbar *statusbar, const gchar *icon_name, const gchar *text) { if (statusbar->progress_active) { gtk_progress_bar_set_text (GTK_PROGRESS_BAR (statusbar->progressbar), text); } else { g_clear_object (&statusbar->icon); if (icon_name) statusbar->icon = pika_statusbar_load_icon (statusbar, icon_name); if (statusbar->icon) { gchar *tmp; gint scale_factor; gint n_spaces; gchar spaces[] = " "; scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (statusbar)); /* Make sure icon_space_width has been initialized to avoid a * division by zero. */ if (statusbar->icon_space_width == 0) pika_statusbar_style_updated (GTK_WIDGET (statusbar)); g_return_if_fail (statusbar->icon_space_width != 0); /* prepend enough spaces for the icon plus one space */ n_spaces = (gdk_pixbuf_get_width (statusbar->icon) / scale_factor + ICON_SPACING) / statusbar->icon_space_width; n_spaces++; tmp = g_strconcat (spaces + strlen (spaces) - n_spaces, text, NULL); gtk_label_set_text (GTK_LABEL (statusbar->label), tmp); g_free (tmp); } else { gtk_label_set_text (GTK_LABEL (statusbar->label), text); } } } static void pika_statusbar_update (PikaStatusbar *statusbar) { PikaStatusbarMsg *msg = NULL; if (statusbar->messages) msg = statusbar->messages->data; if (msg && msg->text) { pika_statusbar_set_text (statusbar, msg->icon_name, msg->text); } else { pika_statusbar_set_text (statusbar, NULL, ""); } } /* public functions */ GtkWidget * pika_statusbar_new (void) { return g_object_new (PIKA_TYPE_STATUSBAR, NULL); } void pika_statusbar_set_shell (PikaStatusbar *statusbar, PikaDisplayShell *shell) { g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell)); if (shell == statusbar->shell) return; if (statusbar->shell) { g_signal_handlers_disconnect_by_func (statusbar->shell, pika_statusbar_shell_scaled, statusbar); g_signal_handlers_disconnect_by_func (statusbar->shell, pika_statusbar_shell_rotated, statusbar); g_signal_handlers_disconnect_by_func (statusbar->shell, pika_statusbar_shell_status_notify, statusbar); if (statusbar->shell->color_config) g_signal_handlers_disconnect_by_func (statusbar->shell->color_config, pika_statusbar_shell_color_config_notify, statusbar); } if (statusbar->pika) { PikaContext *context; context = pika_get_user_context (statusbar->pika); g_signal_handlers_disconnect_by_func (context, pika_statusbar_shell_image_changed, statusbar); pika_statusbar_shell_set_image (statusbar, NULL); } statusbar->shell = shell; g_signal_connect_object (statusbar->shell, "scaled", G_CALLBACK (pika_statusbar_shell_scaled), statusbar, 0); g_signal_connect_object (statusbar->shell, "rotated", G_CALLBACK (pika_statusbar_shell_rotated), statusbar, 0); g_signal_connect_object (statusbar->shell, "notify::status", G_CALLBACK (pika_statusbar_shell_status_notify), statusbar, 0); if (statusbar->shell->color_config) g_signal_connect (statusbar->shell->color_config, "notify", G_CALLBACK (pika_statusbar_shell_color_config_notify), statusbar); statusbar->pika = pika_display_get_pika (statusbar->shell->display); if (statusbar->pika) { PikaContext *context; PikaImage *image; context = pika_get_user_context (statusbar->pika); image = pika_context_get_image (context); g_signal_connect_swapped (context, "image-changed", G_CALLBACK (pika_statusbar_shell_image_changed), statusbar); pika_statusbar_shell_image_changed (statusbar, image, context); } pika_statusbar_shell_rotated (shell, statusbar); } gboolean pika_statusbar_get_visible (PikaStatusbar *statusbar) { g_return_val_if_fail (PIKA_IS_STATUSBAR (statusbar), FALSE); if (statusbar->progress_shown) return FALSE; return gtk_widget_get_visible (GTK_WIDGET (statusbar)); } void pika_statusbar_set_visible (PikaStatusbar *statusbar, gboolean visible) { g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); if (statusbar->progress_shown) { if (visible) { statusbar->progress_shown = FALSE; return; } } gtk_widget_set_visible (GTK_WIDGET (statusbar), visible); } void pika_statusbar_empty (PikaStatusbar *statusbar) { g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); gtk_widget_hide (statusbar->cursor_label); gtk_widget_hide (statusbar->unit_combo); gtk_widget_hide (statusbar->scale_combo); gtk_widget_hide (statusbar->rotate_widget); gtk_widget_hide (statusbar->horizontal_flip_icon); gtk_widget_hide (statusbar->vertical_flip_icon); gtk_widget_hide (statusbar->soft_proof_button); } void pika_statusbar_fill (PikaStatusbar *statusbar) { g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); gtk_widget_show (statusbar->cursor_label); gtk_widget_show (statusbar->unit_combo); gtk_widget_show (statusbar->scale_combo); gtk_widget_show (statusbar->rotate_widget); gtk_widget_show (statusbar->soft_proof_button); pika_statusbar_shell_rotated (statusbar->shell, statusbar); } void pika_statusbar_override_window_title (PikaStatusbar *statusbar) { GtkWidget *toplevel; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); toplevel = gtk_widget_get_toplevel (GTK_WIDGET (statusbar)); if (pika_image_window_is_iconified (PIKA_IMAGE_WINDOW (toplevel))) { const gchar *message = pika_statusbar_peek (statusbar, "progress"); if (message) gtk_window_set_title (GTK_WINDOW (toplevel), message); } } void pika_statusbar_restore_window_title (PikaStatusbar *statusbar) { GtkWidget *toplevel; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); toplevel = gtk_widget_get_toplevel (GTK_WIDGET (statusbar)); if (pika_image_window_is_iconified (PIKA_IMAGE_WINDOW (toplevel))) { g_object_notify (G_OBJECT (statusbar->shell), "title"); } } void pika_statusbar_push (PikaStatusbar *statusbar, const gchar *context, const gchar *icon_name, const gchar *format, ...) { va_list args; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (context != NULL); g_return_if_fail (format != NULL); va_start (args, format); pika_statusbar_push_valist (statusbar, context, icon_name, format, args); va_end (args); } void pika_statusbar_push_valist (PikaStatusbar *statusbar, const gchar *context, const gchar *icon_name, const gchar *format, va_list args) { guint context_id; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (context != NULL); g_return_if_fail (format != NULL); context_id = pika_statusbar_get_context_id (statusbar, context); pika_statusbar_add_message (statusbar, context_id, icon_name, format, args, /* move_to_front = */ TRUE); } void pika_statusbar_push_coords (PikaStatusbar *statusbar, const gchar *context, const gchar *icon_name, PikaCursorPrecision precision, const gchar *title, gdouble x, const gchar *separator, gdouble y, const gchar *help) { PikaDisplayShell *shell; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (title != NULL); g_return_if_fail (separator != NULL); if (help == NULL) help = ""; shell = statusbar->shell; switch (precision) { case PIKA_CURSOR_PRECISION_PIXEL_CENTER: x = (gint) x; y = (gint) y; break; case PIKA_CURSOR_PRECISION_PIXEL_BORDER: x = RINT (x); y = RINT (y); break; case PIKA_CURSOR_PRECISION_SUBPIXEL: break; } if (shell->unit == PIKA_UNIT_PIXEL) { if (precision == PIKA_CURSOR_PRECISION_SUBPIXEL) { pika_statusbar_push (statusbar, context, icon_name, statusbar->cursor_format_str_f, title, x, separator, y, help); } else { pika_statusbar_push (statusbar, context, icon_name, statusbar->cursor_format_str, title, (gint) RINT (x), separator, (gint) RINT (y), help); } } else /* show real world units */ { gdouble xres; gdouble yres; pika_image_get_resolution (pika_display_get_image (shell->display), &xres, &yres); pika_statusbar_push (statusbar, context, icon_name, statusbar->cursor_format_str, title, pika_pixels_to_units (x, shell->unit, xres), separator, pika_pixels_to_units (y, shell->unit, yres), help); } } void pika_statusbar_push_length (PikaStatusbar *statusbar, const gchar *context, const gchar *icon_name, const gchar *title, PikaOrientationType axis, gdouble value, const gchar *help) { PikaDisplayShell *shell; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (title != NULL); if (help == NULL) help = ""; shell = statusbar->shell; if (shell->unit == PIKA_UNIT_PIXEL) { pika_statusbar_push (statusbar, context, icon_name, statusbar->length_format_str, title, (gint) RINT (value), help); } else /* show real world units */ { gdouble xres; gdouble yres; gdouble resolution; pika_image_get_resolution (pika_display_get_image (shell->display), &xres, &yres); switch (axis) { case PIKA_ORIENTATION_HORIZONTAL: resolution = xres; break; case PIKA_ORIENTATION_VERTICAL: resolution = yres; break; default: g_return_if_reached (); break; } pika_statusbar_push (statusbar, context, icon_name, statusbar->length_format_str, title, pika_pixels_to_units (value, shell->unit, resolution), help); } } void pika_statusbar_replace (PikaStatusbar *statusbar, const gchar *context, const gchar *icon_name, const gchar *format, ...) { va_list args; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (context != NULL); g_return_if_fail (format != NULL); va_start (args, format); pika_statusbar_replace_valist (statusbar, context, icon_name, format, args); va_end (args); } void pika_statusbar_replace_valist (PikaStatusbar *statusbar, const gchar *context, const gchar *icon_name, const gchar *format, va_list args) { guint context_id; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (context != NULL); g_return_if_fail (format != NULL); context_id = pika_statusbar_get_context_id (statusbar, context); pika_statusbar_add_message (statusbar, context_id, icon_name, format, args, /* move_to_front = */ FALSE); } const gchar * pika_statusbar_peek (PikaStatusbar *statusbar, const gchar *context) { GSList *list; guint context_id; g_return_val_if_fail (PIKA_IS_STATUSBAR (statusbar), NULL); g_return_val_if_fail (context != NULL, NULL); context_id = pika_statusbar_get_context_id (statusbar, context); for (list = statusbar->messages; list; list = list->next) { PikaStatusbarMsg *msg = list->data; if (msg->context_id == context_id) { return msg->text; } } return NULL; } void pika_statusbar_pop (PikaStatusbar *statusbar, const gchar *context) { guint context_id; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (context != NULL); context_id = pika_statusbar_get_context_id (statusbar, context); pika_statusbar_remove_message (statusbar, context_id); } void pika_statusbar_push_temp (PikaStatusbar *statusbar, PikaMessageSeverity severity, const gchar *icon_name, const gchar *format, ...) { va_list args; va_start (args, format); pika_statusbar_push_temp_valist (statusbar, severity, icon_name, format, args); va_end (args); } void pika_statusbar_push_temp_valist (PikaStatusbar *statusbar, PikaMessageSeverity severity, const gchar *icon_name, const gchar *format, va_list args) { g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); g_return_if_fail (severity <= PIKA_MESSAGE_WARNING); g_return_if_fail (format != NULL); /* don't accept a message if we are already displaying a more severe one */ if (statusbar->temp_timeout_id && statusbar->temp_severity > severity) return; if (statusbar->temp_timeout_id) g_source_remove (statusbar->temp_timeout_id); statusbar->temp_timeout_id = g_timeout_add (MESSAGE_TIMEOUT, (GSourceFunc) pika_statusbar_temp_timeout, statusbar); statusbar->temp_severity = severity; pika_statusbar_add_message (statusbar, statusbar->temp_context_id, icon_name, format, args, /* move_to_front = */ TRUE); if (severity >= PIKA_MESSAGE_WARNING) pika_widget_blink (statusbar->label); } void pika_statusbar_pop_temp (PikaStatusbar *statusbar) { g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); if (statusbar->temp_timeout_id) { g_source_remove (statusbar->temp_timeout_id); statusbar->temp_timeout_id = 0; pika_statusbar_remove_message (statusbar, statusbar->temp_context_id); } } void pika_statusbar_update_cursor (PikaStatusbar *statusbar, PikaCursorPrecision precision, gdouble x, gdouble y) { PikaDisplayShell *shell; PikaImage *image; gchar buffer[CURSOR_LEN]; g_return_if_fail (PIKA_IS_STATUSBAR (statusbar)); shell = statusbar->shell; image = pika_display_get_image (shell->display); if (! image || x < 0 || y < 0 || x >= pika_image_get_width (image) || y >= pika_image_get_height (image)) { gtk_widget_set_sensitive (statusbar->cursor_label, FALSE); } else { gtk_widget_set_sensitive (statusbar->cursor_label, TRUE); } switch (precision) { case PIKA_CURSOR_PRECISION_PIXEL_CENTER: x = (gint) x; y = (gint) y; break; case PIKA_CURSOR_PRECISION_PIXEL_BORDER: x = RINT (x); y = RINT (y); break; case PIKA_CURSOR_PRECISION_SUBPIXEL: break; } statusbar->cursor_precision = precision; if (shell->unit == PIKA_UNIT_PIXEL) { if (precision == PIKA_CURSOR_PRECISION_SUBPIXEL) { g_snprintf (buffer, sizeof (buffer), statusbar->cursor_format_str_f, "", x, ", ", y, ""); } else { g_snprintf (buffer, sizeof (buffer), statusbar->cursor_format_str, "", (gint) RINT (x), ", ", (gint) RINT (y), ""); } } else /* show real world units */ { GtkTreeModel *model; PikaUnitStore *store; model = gtk_combo_box_get_model (GTK_COMBO_BOX (statusbar->unit_combo)); store = PIKA_UNIT_STORE (model); pika_unit_store_set_pixel_values (store, x, y); pika_unit_store_get_values (store, shell->unit, &x, &y); g_snprintf (buffer, sizeof (buffer), statusbar->cursor_format_str, "", x, ", ", y, ""); } if (g_strcmp0 (buffer, statusbar->cursor_string_last) == 0) return; g_free (statusbar->cursor_string_todraw); statusbar->cursor_string_todraw = g_strdup (buffer); if (statusbar->statusbar_pos_redraw_idle_id == 0) { statusbar->statusbar_pos_redraw_idle_id = g_idle_add_full (G_PRIORITY_LOW, pika_statusbar_queue_pos_redraw, statusbar, NULL); } } void pika_statusbar_clear_cursor (PikaStatusbar *statusbar) { gtk_label_set_text (GTK_LABEL (statusbar->cursor_label), ""); gtk_widget_set_sensitive (statusbar->cursor_label, TRUE); } /* private functions */ static gboolean pika_statusbar_label_draw (GtkWidget *widget, cairo_t *cr, PikaStatusbar *statusbar) { if (statusbar->icon) { cairo_surface_t *surface; PangoRectangle rect; GtkAllocation allocation; gint scale_factor; gint x, y; gtk_label_get_layout_offsets (GTK_LABEL (widget), &x, &y); gtk_widget_get_allocation (widget, &allocation); x -= allocation.x; y -= allocation.y; pango_layout_index_to_pos (gtk_label_get_layout (GTK_LABEL (widget)), 0, &rect); /* the rectangle width is negative when rendering right-to-left */ x += PANGO_PIXELS (rect.x) + (rect.width < 0 ? PANGO_PIXELS (rect.width) : 0); y += PANGO_PIXELS (rect.y); scale_factor = gtk_widget_get_scale_factor (widget); surface = gdk_cairo_surface_create_from_pixbuf (statusbar->icon, scale_factor, NULL); cairo_set_source_surface (cr, surface, x, y); cairo_surface_destroy (surface); cairo_paint (cr); } return FALSE; } static void pika_statusbar_shell_scaled (PikaDisplayShell *shell, PikaStatusbar *statusbar) { static PangoLayout *layout = NULL; PikaImage *image = pika_display_get_image (shell->display); GtkTreeModel *model; const gchar *text; gint image_width; gint image_height; gdouble image_xres; gdouble image_yres; gint width; if (image) { image_width = pika_image_get_width (image); image_height = pika_image_get_height (image); pika_image_get_resolution (image, &image_xres, &image_yres); } else { image_width = shell->disp_width; image_height = shell->disp_height; image_xres = shell->display->config->monitor_xres; image_yres = shell->display->config->monitor_yres; } g_signal_handlers_block_by_func (statusbar->scale_combo, pika_statusbar_scale_changed, statusbar); pika_scale_combo_box_set_scale (PIKA_SCALE_COMBO_BOX (statusbar->scale_combo), pika_zoom_model_get_factor (shell->zoom)); g_signal_handlers_unblock_by_func (statusbar->scale_combo, pika_statusbar_scale_changed, statusbar); model = gtk_combo_box_get_model (GTK_COMBO_BOX (statusbar->unit_combo)); pika_unit_store_set_resolutions (PIKA_UNIT_STORE (model), image_xres, image_yres); g_signal_handlers_block_by_func (statusbar->unit_combo, pika_statusbar_unit_changed, statusbar); pika_unit_combo_box_set_active (PIKA_UNIT_COMBO_BOX (statusbar->unit_combo), shell->unit); g_signal_handlers_unblock_by_func (statusbar->unit_combo, pika_statusbar_unit_changed, statusbar); if (shell->unit == PIKA_UNIT_PIXEL) { g_snprintf (statusbar->cursor_format_str, sizeof (statusbar->cursor_format_str), "%%s%%d%%s%%d%%s"); g_snprintf (statusbar->cursor_format_str_f, sizeof (statusbar->cursor_format_str_f), "%%s%%.1f%%s%%.1f%%s"); g_snprintf (statusbar->length_format_str, sizeof (statusbar->length_format_str), "%%s%%d%%s"); statusbar->cursor_w_digits = 0; statusbar->cursor_h_digits = 0; } else /* show real world units */ { gint w_digits; gint h_digits; w_digits = pika_unit_get_scaled_digits (shell->unit, image_xres); h_digits = pika_unit_get_scaled_digits (shell->unit, image_yres); g_snprintf (statusbar->cursor_format_str, sizeof (statusbar->cursor_format_str), "%%s%%.%df%%s%%.%df%%s", w_digits, h_digits); strcpy (statusbar->cursor_format_str_f, statusbar->cursor_format_str); g_snprintf (statusbar->length_format_str, sizeof (statusbar->length_format_str), "%%s%%.%df%%s", MAX (w_digits, h_digits)); statusbar->cursor_w_digits = w_digits; statusbar->cursor_h_digits = h_digits; } pika_statusbar_update_cursor (statusbar, PIKA_CURSOR_PRECISION_SUBPIXEL, -image_width, -image_height); text = gtk_label_get_text (GTK_LABEL (statusbar->cursor_label)); /* one static layout for all displays should be fine */ if (! layout) layout = gtk_widget_create_pango_layout (statusbar->cursor_label, NULL); pango_layout_set_text (layout, text, -1); pango_layout_get_pixel_size (layout, &width, NULL); gtk_widget_set_size_request (statusbar->cursor_label, width, -1); pika_statusbar_clear_cursor (statusbar); } static void pika_statusbar_shell_rotated (PikaDisplayShell *shell, PikaStatusbar *statusbar) { if (shell->rotate_angle != 0.0) { /* Degree symbol U+00B0. There are no spaces between the value and the * unit for angular rotation. */ gchar *text = g_strdup_printf (" %.2f\xC2\xB0", shell->rotate_angle); gtk_label_set_text (GTK_LABEL (statusbar->rotate_label), text); g_free (text); gtk_widget_show (statusbar->rotate_widget); } else { gtk_widget_hide (statusbar->rotate_widget); } if (shell->flip_horizontally) gtk_widget_show (statusbar->horizontal_flip_icon); else gtk_widget_hide (statusbar->horizontal_flip_icon); if (shell->flip_vertically) gtk_widget_show (statusbar->vertical_flip_icon); else gtk_widget_hide (statusbar->vertical_flip_icon); } static void pika_statusbar_shell_status_notify (PikaDisplayShell *shell, const GParamSpec *pspec, PikaStatusbar *statusbar) { pika_statusbar_replace (statusbar, "title", NULL, "%s", shell->status); } static void pika_statusbar_shell_color_config_notify (GObject *config, const GParamSpec *pspec, PikaStatusbar *statusbar) { gboolean active = FALSE; gint optimize = 0; PikaColorConfig *color_config = PIKA_COLOR_CONFIG (config); PikaColorManagementMode mode = pika_color_config_get_mode (color_config); if (mode == PIKA_COLOR_MANAGEMENT_SOFTPROOF) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->soft_proof_button), TRUE); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->proof_colors_toggle), TRUE); gtk_button_set_relief (GTK_BUTTON (statusbar->soft_proof_button), GTK_RELIEF_NORMAL); } else { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->soft_proof_button), FALSE); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->proof_colors_toggle), FALSE); gtk_button_set_relief (GTK_BUTTON (statusbar->soft_proof_button), GTK_RELIEF_NONE); } optimize = pika_color_config_get_simulation_optimize (color_config); pika_int_combo_box_set_active (PIKA_INT_COMBO_BOX (statusbar->optimize_combo), optimize); active = pika_color_config_get_simulation_gamut_check (color_config); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->out_of_gamut_toggle), active); } static void pika_statusbar_shell_set_image (PikaStatusbar *statusbar, PikaImage *image) { g_return_if_fail (image == NULL || PIKA_IS_IMAGE (image)); if (image != statusbar->image) { if (statusbar->image) { g_signal_handlers_disconnect_by_func (statusbar->image, pika_statusbar_shell_image_simulation_changed, statusbar); g_object_unref (statusbar->image); } } statusbar->image = image; if (statusbar->image) { g_object_ref (statusbar->image); g_signal_connect (statusbar->image, "simulation-profile-changed", G_CALLBACK (pika_statusbar_shell_image_simulation_changed), statusbar); g_signal_connect (statusbar->image, "simulation-intent-changed", G_CALLBACK (pika_statusbar_shell_image_simulation_changed), statusbar); g_signal_connect (statusbar->image, "simulation-bpc-changed", G_CALLBACK (pika_statusbar_shell_image_simulation_changed), statusbar); pika_statusbar_shell_image_simulation_changed (statusbar->image, statusbar); } } static void pika_statusbar_unit_changed (PikaUnitComboBox *combo, PikaStatusbar *statusbar) { pika_display_shell_set_unit (statusbar->shell, pika_unit_combo_box_get_active (combo)); } static void pika_statusbar_scale_changed (PikaScaleComboBox *combo, PikaStatusbar *statusbar) { pika_display_shell_scale (statusbar->shell, PIKA_ZOOM_TO, pika_scale_combo_box_get_scale (combo), PIKA_ZOOM_FOCUS_BEST_GUESS); } static void pika_statusbar_scale_activated (PikaScaleComboBox *combo, PikaStatusbar *statusbar) { gtk_widget_grab_focus (statusbar->shell->canvas); } static void pika_statusbar_shell_image_changed (PikaStatusbar *statusbar, PikaImage *image, PikaContext *context) { PikaColorConfig *color_config = NULL; if (image == statusbar->image) return; if (statusbar->shell && statusbar->shell->display) color_config = pika_display_shell_get_color_config (statusbar->shell); pika_statusbar_shell_set_image (statusbar, image); pika_statusbar_shell_color_config_notify (G_OBJECT (color_config), NULL, statusbar); } static void pika_statusbar_shell_image_simulation_changed (PikaImage *image, PikaStatusbar *statusbar) { PikaColorProfile *simulation_profile = NULL; gchar *text; const gchar *profile_label; PikaColorRenderingIntent intent = PIKA_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC; gboolean bpc = FALSE; if (image) { simulation_profile = pika_image_get_simulation_profile (image); intent = pika_image_get_simulation_intent (image); bpc = pika_image_get_simulation_bpc (image); } if (simulation_profile && PIKA_IS_COLOR_PROFILE (simulation_profile)) { profile_label = pika_color_profile_get_label (simulation_profile); gtk_widget_set_sensitive (statusbar->soft_proof_button, TRUE); gtk_widget_set_sensitive (statusbar->proof_colors_toggle, TRUE); } else { profile_label = _("None"); gtk_widget_set_sensitive (statusbar->soft_proof_button, FALSE); gtk_widget_set_sensitive (statusbar->proof_colors_toggle, FALSE); } gtk_event_box_set_above_child (GTK_EVENT_BOX (statusbar->soft_proof_container), TRUE); text = g_strdup_printf ("%s: %s", _("Current Soft-Proofing Profile"), profile_label); gtk_label_set_markup (GTK_LABEL (statusbar->profile_label), text); g_free (text); pika_color_profile_combo_box_set_active_profile (PIKA_COLOR_PROFILE_COMBO_BOX (statusbar->profile_combo), simulation_profile); gtk_combo_box_set_active (GTK_COMBO_BOX (statusbar->rendering_intent_combo), intent); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (statusbar->bpc_toggle), bpc); } static gboolean pika_statusbar_rotate_pressed (GtkWidget *event_box, GdkEvent *event, PikaStatusbar *statusbar) { GAction *action; action = g_action_map_lookup_action (G_ACTION_MAP (statusbar->pika->app), "view-rotate-other"); pika_action_activate (PIKA_ACTION (action)); return FALSE; } static gboolean pika_statusbar_horiz_flip_pressed (GtkWidget *event_box, GdkEvent *event, PikaStatusbar *statusbar) { GAction *action; action = g_action_map_lookup_action (G_ACTION_MAP (statusbar->pika->app), "view-flip-horizontally"); pika_action_activate (PIKA_ACTION (action)); return FALSE; } static gboolean pika_statusbar_vert_flip_pressed (GtkWidget *event_box, GdkEvent *event, PikaStatusbar *statusbar) { GAction *action; action = g_action_map_lookup_action (G_ACTION_MAP (statusbar->pika->app), "view-flip-vertically"); pika_action_activate (PIKA_ACTION (action)); return FALSE; } static guint pika_statusbar_get_context_id (PikaStatusbar *statusbar, const gchar *context) { guint id = GPOINTER_TO_UINT (g_hash_table_lookup (statusbar->context_ids, context)); if (! id) { id = statusbar->seq_context_id++; g_hash_table_insert (statusbar->context_ids, g_strdup (context), GUINT_TO_POINTER (id)); } return id; } static gboolean pika_statusbar_temp_timeout (PikaStatusbar *statusbar) { pika_statusbar_pop_temp (statusbar); return FALSE; } static void pika_statusbar_add_size_widget (PikaStatusbar *statusbar, GtkWidget *widget) { statusbar->size_widgets = g_slist_prepend (statusbar->size_widgets, widget); } static void pika_statusbar_update_size (PikaStatusbar *statusbar) { GSList *iter; gint height = -1; for (iter = statusbar->size_widgets; iter; iter = g_slist_next (iter)) { GtkWidget *widget = iter->data; gint natural_height; gboolean visible; visible = gtk_widget_get_visible (widget); gtk_widget_set_visible (widget, TRUE); gtk_widget_get_preferred_height (widget, NULL, &natural_height); gtk_widget_set_visible (widget, visible); height = MAX (height, natural_height); } gtk_widget_set_size_request (GTK_WIDGET (statusbar), -1, height); } static void pika_statusbar_add_message (PikaStatusbar *statusbar, guint context_id, const gchar *icon_name, const gchar *format, va_list args, gboolean move_to_front) { gchar *message; GSList *list; PikaStatusbarMsg *msg; gint position; message = pika_statusbar_vprintf (format, args); for (list = statusbar->messages; list; list = g_slist_next (list)) { msg = list->data; if (msg->context_id == context_id) { gboolean is_front_message = (list == statusbar->messages); if ((is_front_message || ! move_to_front) && strcmp (msg->text, message) == 0 && g_strcmp0 (msg->icon_name, icon_name) == 0) { g_free (message); return; } if (move_to_front) { statusbar->messages = g_slist_remove (statusbar->messages, msg); pika_statusbar_msg_free (msg); break; } else { g_free (msg->icon_name); msg->icon_name = g_strdup (icon_name); g_free (msg->text); msg->text = message; if (is_front_message) pika_statusbar_update (statusbar); return; } } } msg = g_slice_new (PikaStatusbarMsg); msg->context_id = context_id; msg->icon_name = g_strdup (icon_name); msg->text = message; /* find the position at which to insert the new message */ position = 0; /* progress messages are always at the front of the list */ if (! (statusbar->progress_active && context_id == pika_statusbar_get_context_id (statusbar, "progress"))) { if (statusbar->progress_active) position++; /* temporary messages are in front of all other non-progress messages */ if (statusbar->temp_timeout_id && context_id != statusbar->temp_context_id) position++; } statusbar->messages = g_slist_insert (statusbar->messages, msg, position); if (position == 0) pika_statusbar_update (statusbar); } static void pika_statusbar_remove_message (PikaStatusbar *statusbar, guint context_id) { GSList *list; gboolean needs_update = FALSE; for (list = statusbar->messages; list; list = g_slist_next (list)) { PikaStatusbarMsg *msg = list->data; if (msg->context_id == context_id) { needs_update = (list == statusbar->messages); statusbar->messages = g_slist_remove (statusbar->messages, msg); pika_statusbar_msg_free (msg); break; } } if (needs_update) pika_statusbar_update (statusbar); } static void pika_statusbar_msg_free (PikaStatusbarMsg *msg) { g_free (msg->icon_name); g_free (msg->text); g_slice_free (PikaStatusbarMsg, msg); } static gchar * pika_statusbar_vprintf (const gchar *format, va_list args) { gchar *message; gchar *newline; message = g_strdup_vprintf (format, args); /* guard us from multi-line strings */ newline = strchr (message, '\r'); if (newline) *newline = '\0'; newline = strchr (message, '\n'); if (newline) *newline = '\0'; return message; } static GdkPixbuf * pika_statusbar_load_icon (PikaStatusbar *statusbar, const gchar *icon_name) { GdkPixbuf *icon; if (G_UNLIKELY (! statusbar->icon_hash)) { statusbar->icon_hash = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) g_object_unref); } icon = g_hash_table_lookup (statusbar->icon_hash, icon_name); if (icon) return g_object_ref (icon); icon = pika_widget_load_icon (statusbar->label, icon_name, 16); /* this is not optimal but so what */ if (g_hash_table_size (statusbar->icon_hash) > 16) g_hash_table_remove_all (statusbar->icon_hash); g_hash_table_insert (statusbar->icon_hash, g_strdup (icon_name), g_object_ref (icon)); return icon; } static gboolean pika_statusbar_queue_pos_redraw (gpointer data) { PikaStatusbar *statusbar = PIKA_STATUSBAR (data); PikaDisplayShell *shell; PikaImage *image; gint image_width = 0; gint image_height = 0; gint label_width_chars = 2; shell = statusbar->shell; image = pika_display_get_image (shell->display); if (image) { image_width = pika_image_get_width (image); image_height = pika_image_get_height (image); /* The number of chars within up to 2 times the image bounds is: * - max width chars: floor (log10 (2 * max_width)) + 1 * - max height chars: floor (log10 (2 * max_height)) + 1 * - the comma and a space: + 2 * - optional 2 minus characters when going in negative * dimensions: + 2 * - optional decimal separators and digits when showing * fractional values. * * The goal of this is to avoid the label size jumping up and * down. Actually it was not a problem on Linux, but this was * reported on macOS. * See: https://gitlab.gnome.org/GNOME/gimp/-/merge_requests/572#note_1389445 * So we just compute what looks like a reasonable "biggest size" * in worst cases. * Of course, it could still happen for people going way * off-canvas but that's acceptable edge-case. */ if (shell->unit == PIKA_UNIT_PIXEL) { label_width_chars = floor (log10 (2 * image_width)) + floor (log10 (2 * image_height)) + 6; if (statusbar->cursor_precision == PIKA_CURSOR_PRECISION_SUBPIXEL) /* In subpixel precision, we have a 1 digit precision per * dimension, so 4 additional characters, decimal * separators-included. */ label_width_chars += 4; } else /* showing real world units */ { GtkTreeModel *model; PikaUnitStore *store; gdouble max_x = (gdouble) image_width; gdouble max_y = (gdouble) image_height; model = gtk_combo_box_get_model (GTK_COMBO_BOX (statusbar->unit_combo)); store = PIKA_UNIT_STORE (model); pika_unit_store_set_pixel_values (store, max_x, max_y); pika_unit_store_get_values (store, shell->unit, &max_x, &max_y); label_width_chars = /* max width (int) up to 2 times image bounds - 1 */ floor (log10 (2 * max_x)) + /* max height (int) up to 2 times image bounds - 1 */ floor (log10 (2 * max_y)) + /* + 2 + comma + space + 2 minuses */ 6 + /* 2 (optional) decimal separators + digits. */ (statusbar->cursor_w_digits > 0 ? 1 + statusbar->cursor_w_digits : 0) + (statusbar->cursor_h_digits > 0 ? 1 + statusbar->cursor_h_digits : 0); } } g_free (statusbar->cursor_string_last); gtk_label_set_width_chars (GTK_LABEL (statusbar->cursor_label), label_width_chars); gtk_label_set_text (GTK_LABEL (statusbar->cursor_label), statusbar->cursor_string_todraw); statusbar->cursor_string_last = statusbar->cursor_string_todraw; statusbar->cursor_string_todraw = NULL; statusbar->statusbar_pos_redraw_idle_id = 0; return G_SOURCE_REMOVE; }