/* 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 * * 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 "libpikathumb/pikathumb.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "config/pikacoreconfig.h" #include "core/pika.h" #include "core/pikacontext.h" #include "core/pikaimagefile.h" #include "core/pikaprogress.h" #include "core/pikasubprogress.h" #include "plug-in/pikapluginmanager-file.h" #include "pikafiledialog.h" /* eek */ #include "pikathumbbox.h" #include "pikaview.h" #include "pikaviewrenderer-frame.h" #include "pikawidgets-utils.h" #include "pika-intl.h" /* local function prototypes */ static void pika_thumb_box_progress_iface_init (PikaProgressInterface *iface); static void pika_thumb_box_dispose (GObject *object); static void pika_thumb_box_finalize (GObject *object); static PikaProgress * pika_thumb_box_progress_start (PikaProgress *progress, gboolean cancellable, const gchar *message); static void pika_thumb_box_progress_end (PikaProgress *progress); static gboolean pika_thumb_box_progress_is_active (PikaProgress *progress); static void pika_thumb_box_progress_set_value (PikaProgress *progress, gdouble percentage); static gdouble pika_thumb_box_progress_get_value (PikaProgress *progress); static void pika_thumb_box_progress_pulse (PikaProgress *progress); static gboolean pika_thumb_box_progress_message (PikaProgress *progress, Pika *pika, PikaMessageSeverity severity, const gchar *domain, const gchar *message); static gboolean pika_thumb_box_ebox_button_press (GtkWidget *widget, GdkEventButton *bevent, PikaThumbBox *box); static void pika_thumb_box_thumbnail_clicked (GtkWidget *widget, GdkModifierType state, PikaThumbBox *box); static void pika_thumb_box_imagefile_info_changed (PikaImagefile *imagefile, PikaThumbBox *box); static void pika_thumb_box_thumb_state_notify (PikaThumbnail *thumb, GParamSpec *pspec, PikaThumbBox *box); static void pika_thumb_box_create_thumbnails (PikaThumbBox *box, gboolean force); static void pika_thumb_box_create_thumbnail (PikaThumbBox *box, GFile *file, PikaThumbnailSize size, gboolean force, PikaProgress *progress); static gboolean pika_thumb_box_auto_thumbnail (PikaThumbBox *box); G_DEFINE_TYPE_WITH_CODE (PikaThumbBox, pika_thumb_box, GTK_TYPE_FRAME, G_IMPLEMENT_INTERFACE (PIKA_TYPE_PROGRESS, pika_thumb_box_progress_iface_init)) #define parent_class pika_thumb_box_parent_class static void pika_thumb_box_class_init (PikaThumbBoxClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = pika_thumb_box_dispose; object_class->finalize = pika_thumb_box_finalize; gtk_widget_class_set_css_name (widget_class, "treeview"); } static void pika_thumb_box_init (PikaThumbBox *box) { gtk_frame_set_shadow_type (GTK_FRAME (box), GTK_SHADOW_IN); box->idle_id = 0; } static void pika_thumb_box_progress_iface_init (PikaProgressInterface *iface) { iface->start = pika_thumb_box_progress_start; iface->end = pika_thumb_box_progress_end; iface->is_active = pika_thumb_box_progress_is_active; iface->set_value = pika_thumb_box_progress_set_value; iface->get_value = pika_thumb_box_progress_get_value; iface->pulse = pika_thumb_box_progress_pulse; iface->message = pika_thumb_box_progress_message; } static void pika_thumb_box_dispose (GObject *object) { PikaThumbBox *box = PIKA_THUMB_BOX (object); if (box->idle_id) { g_source_remove (box->idle_id); box->idle_id = 0; } G_OBJECT_CLASS (parent_class)->dispose (object); box->progress = NULL; } static void pika_thumb_box_finalize (GObject *object) { PikaThumbBox *box = PIKA_THUMB_BOX (object); pika_thumb_box_take_files (box, NULL); g_clear_object (&box->imagefile); G_OBJECT_CLASS (parent_class)->finalize (object); } static PikaProgress * pika_thumb_box_progress_start (PikaProgress *progress, gboolean cancellable, const gchar *message) { PikaThumbBox *box = PIKA_THUMB_BOX (progress); if (! box->progress) return NULL; if (! box->progress_active) { GtkProgressBar *bar = GTK_PROGRESS_BAR (box->progress); GtkWidget *toplevel; gtk_progress_bar_set_fraction (bar, 0.0); box->progress_active = TRUE; toplevel = gtk_widget_get_toplevel (GTK_WIDGET (box)); if (PIKA_IS_FILE_DIALOG (toplevel)) gtk_dialog_set_response_sensitive (GTK_DIALOG (toplevel), GTK_RESPONSE_CANCEL, cancellable); return progress; } return NULL; } static void pika_thumb_box_progress_end (PikaProgress *progress) { if (pika_thumb_box_progress_is_active (progress)) { PikaThumbBox *box = PIKA_THUMB_BOX (progress); GtkProgressBar *bar = GTK_PROGRESS_BAR (box->progress); gtk_progress_bar_set_fraction (bar, 0.0); box->progress_active = FALSE; } } static gboolean pika_thumb_box_progress_is_active (PikaProgress *progress) { PikaThumbBox *box = PIKA_THUMB_BOX (progress); return (box->progress && box->progress_active); } static void pika_thumb_box_progress_set_value (PikaProgress *progress, gdouble percentage) { if (pika_thumb_box_progress_is_active (progress)) { PikaThumbBox *box = PIKA_THUMB_BOX (progress); GtkProgressBar *bar = GTK_PROGRESS_BAR (box->progress); gtk_progress_bar_set_fraction (bar, percentage); } } static gdouble pika_thumb_box_progress_get_value (PikaProgress *progress) { if (pika_thumb_box_progress_is_active (progress)) { PikaThumbBox *box = PIKA_THUMB_BOX (progress); GtkProgressBar *bar = GTK_PROGRESS_BAR (box->progress); return gtk_progress_bar_get_fraction (bar); } return 0.0; } static void pika_thumb_box_progress_pulse (PikaProgress *progress) { if (pika_thumb_box_progress_is_active (progress)) { PikaThumbBox *box = PIKA_THUMB_BOX (progress); GtkProgressBar *bar = GTK_PROGRESS_BAR (box->progress); gtk_progress_bar_pulse (bar); } } static gboolean pika_thumb_box_progress_message (PikaProgress *progress, Pika *pika, PikaMessageSeverity severity, const gchar *domain, const gchar *message) { /* PikaThumbBox never shows any messages */ return TRUE; } /* stupid PikaHeader class just so we get a "header" CSS node */ #define PIKA_TYPE_HEADER (pika_header_get_type ()) typedef struct _GtkBox PikaHeader; typedef struct _GtkBoxClass PikaHeaderClass; static GType pika_header_get_type (void) G_GNUC_CONST; G_DEFINE_TYPE (PikaHeader, pika_header, GTK_TYPE_BOX) static void pika_header_class_init (PikaHeaderClass *klass) { gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "header"); } static void pika_header_init (PikaHeader *header) { gtk_orientable_set_orientation (GTK_ORIENTABLE (header), GTK_ORIENTATION_VERTICAL); } /* public functions */ GtkWidget * pika_thumb_box_new (PikaContext *context) { PikaThumbBox *box; GtkWidget *vbox; GtkWidget *vbox2; GtkWidget *ebox; GtkWidget *button; GtkWidget *label; gchar *str; gint h, v; g_return_val_if_fail (PIKA_IS_CONTEXT (context), NULL); box = g_object_new (PIKA_TYPE_THUMB_BOX, NULL); gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (box)), GTK_STYLE_CLASS_VIEW); box->context = context; ebox = gtk_event_box_new (); gtk_container_add (GTK_CONTAINER (box), ebox); gtk_widget_show (ebox); g_signal_connect (ebox, "button-press-event", G_CALLBACK (pika_thumb_box_ebox_button_press), box); str = g_strdup_printf (_("Click to update preview\n" "%s-Click to force update even " "if preview is up-to-date"), pika_get_mod_string (pika_get_toggle_behavior_mask ())); pika_help_set_help_data (ebox, str, NULL); g_free (str); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_container_add (GTK_CONTAINER (ebox), vbox); gtk_widget_show (vbox); vbox2 = g_object_new (PIKA_TYPE_HEADER, NULL); gtk_box_pack_start (GTK_BOX (vbox), vbox2, FALSE, FALSE, 0); gtk_widget_show (vbox2); button = gtk_button_new (); gtk_box_pack_start (GTK_BOX (vbox2), button, FALSE, FALSE, 0); gtk_widget_show (button); label = gtk_label_new_with_mnemonic (_("Pr_eview")); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_container_add (GTK_CONTAINER (button), label); gtk_widget_show (label); g_signal_connect (button, "button-press-event", G_CALLBACK (gtk_true), NULL); g_signal_connect (button, "button-release-event", G_CALLBACK (gtk_true), NULL); g_signal_connect (button, "enter-notify-event", G_CALLBACK (gtk_true), NULL); g_signal_connect (button, "leave-notify-event", G_CALLBACK (gtk_true), NULL); vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_container_set_border_width (GTK_CONTAINER (vbox2), 4); gtk_box_pack_start (GTK_BOX (vbox), vbox2, TRUE, TRUE, 0); gtk_widget_show (vbox2); box->imagefile = pika_imagefile_new (context->pika, NULL); g_signal_connect (box->imagefile, "info-changed", G_CALLBACK (pika_thumb_box_imagefile_info_changed), box); g_signal_connect (pika_imagefile_get_thumbnail (box->imagefile), "notify::thumb-state", G_CALLBACK (pika_thumb_box_thumb_state_notify), box); pika_view_renderer_get_frame_size (&h, &v); box->preview = pika_view_new (context, PIKA_VIEWABLE (box->imagefile), /* add padding for the shadow frame */ context->pika->config->thumbnail_size + MAX (h, v), 0, FALSE); gtk_style_context_add_class (gtk_widget_get_style_context (box->preview), GTK_STYLE_CLASS_VIEW); gtk_box_pack_start (GTK_BOX (vbox2), box->preview, FALSE, FALSE, 0); gtk_widget_show (box->preview); gtk_label_set_mnemonic_widget (GTK_LABEL (label), box->preview); g_signal_connect (box->preview, "clicked", G_CALLBACK (pika_thumb_box_thumbnail_clicked), box); box->filename = gtk_label_new (_("No selection")); gtk_label_set_max_width_chars (GTK_LABEL (box->filename), 1); gtk_label_set_ellipsize (GTK_LABEL (box->filename), PANGO_ELLIPSIZE_MIDDLE); gtk_label_set_justify (GTK_LABEL (box->filename), GTK_JUSTIFY_CENTER); pika_label_set_attributes (GTK_LABEL (box->filename), PANGO_ATTR_STYLE, PANGO_STYLE_OBLIQUE, -1); gtk_box_pack_start (GTK_BOX (vbox2), box->filename, FALSE, FALSE, 0); gtk_widget_show (box->filename); box->info = gtk_label_new (" \n \n \n "); gtk_label_set_justify (GTK_LABEL (box->info), GTK_JUSTIFY_CENTER); gtk_label_set_line_wrap (GTK_LABEL (box->info), TRUE); pika_label_set_attributes (GTK_LABEL (box->info), PANGO_ATTR_SCALE, PANGO_SCALE_SMALL, -1); gtk_box_pack_start (GTK_BOX (vbox2), box->info, FALSE, FALSE, 0); gtk_widget_show (box->info); box->progress = gtk_progress_bar_new (); gtk_widget_set_halign (box->progress, GTK_ALIGN_FILL); gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR (box->progress), TRUE); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (box->progress), "Fog"); gtk_box_pack_end (GTK_BOX (vbox2), box->progress, FALSE, FALSE, 0); /* don't gtk_widget_show (box->progress); */ gtk_widget_set_size_request (GTK_WIDGET (box), MAX ((gint) PIKA_THUMB_SIZE_NORMAL, (gint) context->pika->config->thumbnail_size) + 2 * MAX (h, v), -1); return GTK_WIDGET (box); } void pika_thumb_box_take_file (PikaThumbBox *box, GFile *file) { g_return_if_fail (PIKA_IS_THUMB_BOX (box)); g_return_if_fail (file == NULL || G_IS_FILE (file)); if (box->idle_id) { g_source_remove (box->idle_id); box->idle_id = 0; } pika_imagefile_set_file (box->imagefile, file); if (file) { gchar *basename = g_path_get_basename (pika_file_get_utf8_name (file)); gtk_label_set_text (GTK_LABEL (box->filename), basename); g_free (basename); } else { gtk_label_set_text (GTK_LABEL (box->filename), _("No selection")); } gtk_widget_set_sensitive (GTK_WIDGET (box), file != NULL); pika_imagefile_update (box->imagefile); } void pika_thumb_box_take_files (PikaThumbBox *box, GSList *files) { g_return_if_fail (PIKA_IS_THUMB_BOX (box)); if (box->files) { g_slist_free_full (box->files, (GDestroyNotify) g_object_unref); box->files = NULL; } box->files = files; } /* private functions */ static gboolean pika_thumb_box_ebox_button_press (GtkWidget *widget, GdkEventButton *bevent, PikaThumbBox *box) { pika_thumb_box_thumbnail_clicked (widget, bevent->state, box); return TRUE; } static void pika_thumb_box_thumbnail_clicked (GtkWidget *widget, GdkModifierType state, PikaThumbBox *box) { pika_thumb_box_create_thumbnails (box, (state & pika_get_toggle_behavior_mask ()) ? TRUE : FALSE); } static void pika_thumb_box_imagefile_info_changed (PikaImagefile *imagefile, PikaThumbBox *box) { gtk_label_set_text (GTK_LABEL (box->info), pika_imagefile_get_desc_string (imagefile)); } static void pika_thumb_box_thumb_state_notify (PikaThumbnail *thumb, GParamSpec *pspec, PikaThumbBox *box) { if (box->idle_id) return; if (thumb->image_state == PIKA_THUMB_STATE_REMOTE) return; switch (thumb->thumb_state) { case PIKA_THUMB_STATE_NOT_FOUND: case PIKA_THUMB_STATE_OLD: box->idle_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) pika_thumb_box_auto_thumbnail, box, NULL); break; default: break; } } static void pika_thumb_box_create_thumbnails (PikaThumbBox *box, gboolean force) { Pika *pika = box->context->pika; PikaProgress *progress = PIKA_PROGRESS (box); PikaFileDialog *dialog = NULL; GtkWidget *toplevel; GSList *list; gint n_files; gint i; if (pika->config->thumbnail_size == PIKA_THUMBNAIL_SIZE_NONE) return; toplevel = gtk_widget_get_toplevel (GTK_WIDGET (box)); if (PIKA_IS_FILE_DIALOG (toplevel)) dialog = PIKA_FILE_DIALOG (toplevel); pika_set_busy (pika); if (dialog) pika_file_dialog_set_sensitive (dialog, FALSE); else gtk_widget_set_sensitive (toplevel, FALSE); if (box->files) { gtk_widget_hide (box->info); gtk_widget_show (box->progress); } n_files = g_slist_length (box->files); if (n_files > 1) { gchar *str; pika_progress_start (PIKA_PROGRESS (box), TRUE, "%s", ""); progress = pika_sub_progress_new (PIKA_PROGRESS (box)); pika_sub_progress_set_step (PIKA_SUB_PROGRESS (progress), 0, n_files); for (list = box->files->next, i = 1; list; list = g_slist_next (list), i++) { str = g_strdup_printf (_("Thumbnail %d of %d"), i, n_files); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (box->progress), str); g_free (str); pika_progress_set_value (progress, 0.0); while (g_main_context_pending (NULL)) g_main_context_iteration (NULL, FALSE); pika_thumb_box_create_thumbnail (box, list->data, pika->config->thumbnail_size, force, progress); if (dialog && dialog->canceled) goto canceled; pika_sub_progress_set_step (PIKA_SUB_PROGRESS (progress), i, n_files); } str = g_strdup_printf (_("Thumbnail %d of %d"), n_files, n_files); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (box->progress), str); g_free (str); pika_progress_set_value (progress, 0.0); while (g_main_context_pending (NULL)) g_main_context_iteration (NULL, FALSE); } if (box->files) { pika_thumb_box_create_thumbnail (box, box->files->data, pika->config->thumbnail_size, force, progress); pika_progress_set_value (progress, 1.0); } canceled: if (n_files > 1) { g_object_unref (progress); pika_progress_end (PIKA_PROGRESS (box)); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (box->progress), ""); } if (box->files) { gtk_widget_hide (box->progress); gtk_widget_show (box->info); } if (dialog) pika_file_dialog_set_sensitive (dialog, TRUE); else gtk_widget_set_sensitive (toplevel, TRUE); pika_unset_busy (pika); } static void pika_thumb_box_create_thumbnail (PikaThumbBox *box, GFile *file, PikaThumbnailSize size, gboolean force, PikaProgress *progress) { PikaThumbnail *thumb = pika_imagefile_get_thumbnail (box->imagefile); gchar *basename; basename = g_path_get_basename (pika_file_get_utf8_name (file)); gtk_label_set_text (GTK_LABEL (box->filename), basename); g_free (basename); pika_imagefile_set_file (box->imagefile, file); if (force || (pika_thumbnail_peek_thumb (thumb, (PikaThumbSize) size) < PIKA_THUMB_STATE_FAILED && ! pika_thumbnail_has_failed (thumb))) { GError *error = NULL; if (! pika_imagefile_create_thumbnail (box->imagefile, box->context, progress, size, ! force, &error)) { pika_message_literal (box->context->pika, G_OBJECT (progress), PIKA_MESSAGE_ERROR, error->message); g_clear_error (&error); } } } static gboolean pika_thumb_box_auto_thumbnail (PikaThumbBox *box) { Pika *pika = box->context->pika; PikaThumbnail *thumb = pika_imagefile_get_thumbnail (box->imagefile); GFile *file = pika_imagefile_get_file (box->imagefile); box->idle_id = 0; if (thumb->image_state == PIKA_THUMB_STATE_NOT_FOUND) return FALSE; switch (thumb->thumb_state) { case PIKA_THUMB_STATE_NOT_FOUND: case PIKA_THUMB_STATE_OLD: if (thumb->image_filesize < pika->config->thumbnail_filesize_limit && ! pika_thumbnail_has_failed (thumb) && pika_plug_in_manager_file_procedure_find_by_extension (pika->plug_in_manager, PIKA_FILE_PROCEDURE_GROUP_OPEN, file)) { if (thumb->image_filesize > 0) { gchar *size; gchar *text; size = g_format_size (thumb->image_filesize); text = g_strdup_printf ("%s\n%s", size, _("Creating preview...")); gtk_label_set_text (GTK_LABEL (box->info), text); g_free (text); g_free (size); } else { gtk_label_set_text (GTK_LABEL (box->info), _("Creating preview...")); } pika_imagefile_create_thumbnail_weak (box->imagefile, box->context, PIKA_PROGRESS (box), pika->config->thumbnail_size, TRUE); } break; default: break; } return FALSE; }