/* 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 * * Copyright (C) 2004 Sven Neumann * * 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 "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "dialogs-types.h" #include "config/pikacoreconfig.h" #include "core/pika.h" #include "core/pikacontainer.h" #include "core/pikacontext.h" #include "core/pikaimage.h" #include "display/pikadisplay.h" #include "display/pikadisplay-foreach.h" #include "display/pikadisplayshell.h" #include "display/pikaimagewindow.h" #include "widgets/pikaaction.h" #include "widgets/pikacellrendererbutton.h" #include "widgets/pikacontainertreestore.h" #include "widgets/pikacontainertreeview.h" #include "widgets/pikacontainerview.h" #include "widgets/pikadnd.h" #include "widgets/pikahelp-ids.h" #include "widgets/pikamessagebox.h" #include "widgets/pikamessagedialog.h" #include "widgets/pikaviewrenderer.h" #include "widgets/pikawidgets-utils.h" #include "quit-dialog.h" #include "pika-intl.h" typedef struct _QuitDialog QuitDialog; struct _QuitDialog { Pika *pika; PikaContainer *images; PikaContext *context; gboolean do_quit; GtkWidget *dialog; PikaContainerTreeView *tree_view; GtkTreeViewColumn *save_column; GtkWidget *ok_button; PikaMessageBox *box; GtkWidget *lost_label; GtkWidget *hint_label; guint accel_key; GdkModifierType accel_mods; }; static GtkWidget * quit_close_all_dialog_new (Pika *pika, gboolean do_quit); static void quit_close_all_dialog_free (QuitDialog *private); static void quit_close_all_dialog_response (GtkWidget *dialog, gint response_id, QuitDialog *private); static void quit_close_all_dialog_accel_marshal (GClosure *closure, GValue *return_value, guint n_param_values, const GValue *param_values, gpointer invocation_hint, gpointer marshal_data); static void quit_close_all_dialog_container_changed (PikaContainer *images, PikaObject *image, QuitDialog *private); static gboolean quit_close_all_dialog_images_selected (PikaContainerView *view, GList *images, GList *paths, QuitDialog *private); static void quit_close_all_dialog_name_cell_func (GtkTreeViewColumn *tree_column, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data); static void quit_close_all_dialog_save_clicked (GtkCellRenderer *cell, const gchar *path, GdkModifierType state, QuitDialog *private); static gboolean quit_close_all_dialog_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_tip, GtkTooltip *tooltip, QuitDialog *private); static gboolean quit_close_all_idle (QuitDialog *private); /* public functions */ GtkWidget * quit_dialog_new (Pika *pika) { return quit_close_all_dialog_new (pika, TRUE); } GtkWidget * close_all_dialog_new (Pika *pika) { return quit_close_all_dialog_new (pika, FALSE); } /* private functions */ static GtkWidget * quit_close_all_dialog_new (Pika *pika, gboolean do_quit) { QuitDialog *private; GtkWidget *view; PikaContainerTreeView *tree_view; GtkTreeViewColumn *column; GtkCellRenderer *renderer; GtkWidget *dnd_widget; GtkAccelGroup *accel_group; GClosure *closure; gint rows; gint view_size; GdkRectangle geometry; GdkMonitor *monitor; gint max_rows; gint scale_factor; const gfloat rows_per_height = 32 / 1440.0f; const gint greatest_max_rows = 36; const gint least_max_rows = 6; g_return_val_if_fail (PIKA_IS_PIKA (pika), NULL); private = g_slice_new0 (QuitDialog); private->pika = pika; private->do_quit = do_quit; private->images = pika_displays_get_dirty_images (pika); private->context = pika_context_new (pika, "close-all-dialog", pika_get_user_context (pika)); g_return_val_if_fail (private->images != NULL, NULL); private->dialog = pika_message_dialog_new (do_quit ? _("Quit PIKA") : _("Close All Images"), PIKA_ICON_DIALOG_WARNING, NULL, 0, pika_standard_help_func, do_quit ? PIKA_HELP_FILE_QUIT : PIKA_HELP_FILE_CLOSE_ALL, _("_Cancel"), GTK_RESPONSE_CANCEL, NULL); private->ok_button = gtk_dialog_add_button (GTK_DIALOG (private->dialog), "", GTK_RESPONSE_OK); pika_dialog_set_alternative_button_order (GTK_DIALOG (private->dialog), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1); g_object_weak_ref (G_OBJECT (private->dialog), (GWeakNotify) quit_close_all_dialog_free, private); g_signal_connect (private->dialog, "response", G_CALLBACK (quit_close_all_dialog_response), private); /* connect D to the quit/close button */ accel_group = gtk_accel_group_new (); gtk_window_add_accel_group (GTK_WINDOW (private->dialog), accel_group); g_object_unref (accel_group); closure = g_closure_new_object (sizeof (GClosure), G_OBJECT (private->dialog)); g_closure_set_marshal (closure, quit_close_all_dialog_accel_marshal); gtk_accelerator_parse ("D", &private->accel_key, &private->accel_mods); gtk_accel_group_connect (accel_group, private->accel_key, private->accel_mods, 0, closure); private->box = PIKA_MESSAGE_DIALOG (private->dialog)->box; monitor = pika_widget_get_monitor (private->dialog); scale_factor = gdk_monitor_get_scale_factor (monitor); gdk_monitor_get_geometry (monitor, &geometry); if (scale_factor > 1) { #ifdef GDK_WINDOWING_WIN32 max_rows = (geometry.height * scale_factor * rows_per_height) / (scale_factor + 1); #else max_rows = (geometry.height * rows_per_height) / (scale_factor + 1); #endif } else { max_rows = geometry.height * rows_per_height; } max_rows = CLAMP (max_rows, least_max_rows, greatest_max_rows); view_size = pika->config->layer_preview_size; rows = CLAMP (pika_container_get_n_children (private->images), 3, max_rows); view = pika_container_tree_view_new (private->images, private->context, view_size, 1); pika_container_box_set_size_request (PIKA_CONTAINER_BOX (view), -1, rows * (view_size + 2)); private->tree_view = tree_view = PIKA_CONTAINER_TREE_VIEW (view); gtk_tree_view_column_set_expand (tree_view->main_column, TRUE); renderer = pika_container_tree_view_get_name_cell (tree_view); gtk_tree_view_column_set_cell_data_func (tree_view->main_column, renderer, quit_close_all_dialog_name_cell_func, NULL, NULL); private->save_column = column = gtk_tree_view_column_new (); renderer = pika_cell_renderer_button_new (); g_object_set (renderer, "icon-name", "document-save", NULL); gtk_tree_view_column_pack_end (column, renderer, FALSE); gtk_tree_view_column_set_attributes (column, renderer, NULL); gtk_tree_view_append_column (tree_view->view, column); pika_container_tree_view_add_toggle_cell (tree_view, renderer); g_signal_connect (renderer, "clicked", G_CALLBACK (quit_close_all_dialog_save_clicked), private); gtk_box_pack_start (GTK_BOX (private->box), view, TRUE, TRUE, 0); gtk_widget_show (view); g_signal_connect (view, "select-items", G_CALLBACK (quit_close_all_dialog_images_selected), private); dnd_widget = pika_container_view_get_dnd_widget (PIKA_CONTAINER_VIEW (view)); pika_dnd_xds_source_add (dnd_widget, (PikaDndDragViewableFunc) pika_dnd_get_drag_viewable, NULL); g_signal_connect (tree_view->view, "query-tooltip", G_CALLBACK (quit_close_all_dialog_query_tooltip), private); if (do_quit) private->lost_label = gtk_label_new (_("If you quit PIKA now, " "these changes will be lost.")); else private->lost_label = gtk_label_new (_("If you close these images now, " "changes will be lost.")); gtk_label_set_xalign (GTK_LABEL (private->lost_label), 0.0); gtk_label_set_line_wrap (GTK_LABEL (private->lost_label), TRUE); gtk_box_pack_start (GTK_BOX (private->box), private->lost_label, FALSE, FALSE, 0); gtk_widget_show (private->lost_label); private->hint_label = gtk_label_new (NULL); gtk_label_set_xalign (GTK_LABEL (private->hint_label), 0.0); gtk_label_set_line_wrap (GTK_LABEL (private->hint_label), TRUE); gtk_box_pack_start (GTK_BOX (private->box), private->hint_label, FALSE, FALSE, 0); gtk_widget_show (private->hint_label); closure = g_cclosure_new (G_CALLBACK (quit_close_all_dialog_container_changed), private, NULL); g_object_watch_closure (G_OBJECT (private->dialog), closure); g_signal_connect_closure (private->images, "add", closure, FALSE); g_signal_connect_closure (private->images, "remove", closure, FALSE); quit_close_all_dialog_container_changed (private->images, NULL, private); return private->dialog; } static void quit_close_all_dialog_free (QuitDialog *private) { g_idle_remove_by_data (private); g_object_unref (private->images); g_object_unref (private->context); g_slice_free (QuitDialog, private); } static void quit_close_all_dialog_response (GtkWidget *dialog, gint response_id, QuitDialog *private) { Pika *pika = private->pika; gboolean do_quit = private->do_quit; gtk_widget_destroy (dialog); if (response_id == GTK_RESPONSE_OK) { if (do_quit) pika_exit (pika, TRUE); else pika_displays_close (pika); } } static void quit_close_all_dialog_accel_marshal (GClosure *closure, GValue *return_value, guint n_param_values, const GValue *param_values, gpointer invocation_hint, gpointer marshal_data) { gtk_dialog_response (GTK_DIALOG (closure->data), GTK_RESPONSE_OK); /* we handled the accelerator */ g_value_set_boolean (return_value, TRUE); } static void quit_close_all_dialog_container_changed (PikaContainer *images, PikaObject *image, QuitDialog *private) { gint num_images = pika_container_get_n_children (images); gchar *accel_string; gchar *hint; gchar *markup; accel_string = gtk_accelerator_get_label (private->accel_key, private->accel_mods); pika_message_box_set_primary_text (private->box, /* TRANSLATORS: unless your language msgstr[0] applies to 1 only (as in English), replace "one" with %d. */ ngettext ("There is one image with " "unsaved changes:", "There are %d images with " "unsaved changes:", num_images), num_images); if (num_images == 0) { gtk_widget_hide (private->lost_label); if (private->do_quit) hint = g_strdup_printf (_("Press %s to quit."), accel_string); else hint = g_strdup_printf (_("Press %s to close all images."), accel_string); g_object_set (private->ok_button, "label", private->do_quit ? _("_Quit") : _("Cl_ose"), "use-stock", TRUE, "image", NULL, NULL); gtk_widget_grab_default (private->ok_button); /* When no image requires saving anymore, there is no harm in * assuming completing the original quit or close-all action is * the expected end-result. * I don't immediately exit though because of some unfinished * actions provoking warnings. Let's just close as soon as * possible with an idle source. * Also the idle source has another benefit: allowing to change * one's mind and not exit after the last save, for instance by * hitting Esc quickly while the last save is in progress. */ g_idle_add ((GSourceFunc) quit_close_all_idle, private); } else { GtkWidget *icon; if (private->do_quit) hint = g_strdup_printf (_("Press %s to discard all changes and quit."), accel_string); else hint = g_strdup_printf (_("Press %s to discard all changes and close all images."), accel_string); gtk_widget_show (private->lost_label); icon = gtk_image_new_from_icon_name (PIKA_ICON_EDIT_DELETE, GTK_ICON_SIZE_BUTTON); g_object_set (private->ok_button, "label", _("_Discard Changes"), "use-stock", FALSE, "image", icon, NULL); gtk_dialog_set_default_response (GTK_DIALOG (private->dialog), GTK_RESPONSE_CANCEL); } markup = g_strdup_printf ("%s", hint); gtk_label_set_markup (GTK_LABEL (private->hint_label), markup); g_free (markup); g_free (hint); g_free (accel_string); } static gboolean quit_close_all_dialog_images_selected (PikaContainerView *view, GList *images, GList *paths, QuitDialog *private) { /* The signal allows for multiple selection cases, but this specific * dialog only allows one image selected at a time. */ g_return_val_if_fail (g_list_length (images) <= 1, FALSE); if (images) { PikaImage *image = images->data; GList *list; for (list = pika_get_display_iter (private->pika); list; list = g_list_next (list)) { PikaDisplay *display = list->data; if (pika_display_get_image (display) == image) { pika_display_shell_present (pika_display_get_shell (display)); /* We only want to update the active shell. Give back keyboard * focus to the quit dialog after this. */ gtk_window_present (GTK_WINDOW (private->dialog)); } } } return TRUE; } static void quit_close_all_dialog_name_cell_func (GtkTreeViewColumn *tree_column, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { PikaViewRenderer *renderer; PikaImage *image; gchar *name; gtk_tree_model_get (tree_model, iter, PIKA_CONTAINER_TREE_STORE_COLUMN_RENDERER, &renderer, PIKA_CONTAINER_TREE_STORE_COLUMN_NAME, &name, -1); image = PIKA_IMAGE (renderer->viewable); if (pika_image_is_export_dirty (image)) { g_object_set (cell, "markup", NULL, "text", name, NULL); } else { GFile *file; const gchar *filename; gchar *escaped_name; gchar *escaped_filename; gchar *exported; gchar *markup; file = pika_image_get_exported_file (image); if (! file) file = pika_image_get_imported_file (image); filename = pika_file_get_utf8_name (file); escaped_name = g_markup_escape_text (name, -1); escaped_filename = g_markup_escape_text (filename, -1); exported = g_strdup_printf (_("Exported to %s"), escaped_filename); markup = g_strdup_printf ("%s\n%s", escaped_name, exported); g_free (exported); g_free (escaped_name); g_free (escaped_filename); g_object_set (cell, "text", NULL, "markup", markup, NULL); g_free (markup); } g_object_unref (renderer); g_free (name); } static void quit_close_all_dialog_save_clicked (GtkCellRenderer *cell, const gchar *path_str, GdkModifierType state, QuitDialog *private) { GtkTreePath *path = gtk_tree_path_new_from_string (path_str); GtkTreeIter iter; if (gtk_tree_model_get_iter (private->tree_view->model, &iter, path)) { PikaViewRenderer *renderer; PikaImage *image; GList *list; gtk_tree_model_get (private->tree_view->model, &iter, PIKA_CONTAINER_TREE_STORE_COLUMN_RENDERER, &renderer, -1); image = PIKA_IMAGE (renderer->viewable); g_object_unref (renderer); for (list = pika_get_display_iter (private->pika); list; list = g_list_next (list)) { PikaDisplay *display = list->data; if (pika_display_get_image (display) == image) { PikaDisplayShell *shell = pika_display_get_shell (display); PikaImageWindow *window = pika_display_shell_get_window (shell); if (window) { GAction *action; pika_display_shell_present (shell); /* Make sure the quit dialog kept keyboard focus when * the save dialog will exit. */ gtk_window_present (GTK_WINDOW (private->dialog)); if (state & GDK_SHIFT_MASK) action = g_action_map_lookup_action (G_ACTION_MAP (private->pika->app), "file-save-as"); else action = g_action_map_lookup_action (G_ACTION_MAP (private->pika->app), "file-save"); g_return_if_fail (PIKA_IS_ACTION (action)); pika_action_activate (PIKA_ACTION (action)); } break; } } } gtk_tree_path_free (path); } static gboolean quit_close_all_dialog_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_tip, GtkTooltip *tooltip, QuitDialog *private) { GtkTreePath *path; gboolean show_tip = FALSE; if (gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (widget), &x, &y, keyboard_tip, NULL, &path, NULL)) { GtkTreeViewColumn *column = NULL; gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget), x, y, NULL, &column, NULL, NULL); if (column == private->save_column) { gchar *tip = g_strconcat (_("Save this image"), "\n", pika_get_mod_string (GDK_SHIFT_MASK), " ", _("Save as"), NULL); gtk_tooltip_set_markup (tooltip, tip); gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (widget), tooltip, path); g_free (tip); show_tip = TRUE; } gtk_tree_path_free (path); } return show_tip; } static gboolean quit_close_all_idle (QuitDialog *private) { gtk_dialog_response (GTK_DIALOG (private->dialog), GTK_RESPONSE_OK); return FALSE; }