/* 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 "libpikamath/pikamath.h" #include "libpikawidgets/pikawidgets.h" #include "dialogs-types.h" #include "config/pikacoreconfig.h" #include "widgets/pikawidgets-utils.h" #include "about.h" #include "git-version.h" #include "about-dialog.h" #include "pika-update.h" #include "pika-version.h" #include "pika-intl.h" typedef struct { GtkWidget *dialog; Pika *pika; GtkWidget *update_frame; PikaCoreConfig *config; GtkWidget *anim_area; PangoLayout *layout; guint timer; gint index; gint animstep; gint state; gboolean visible; } PikaAboutDialog; static void about_dialog_map (GtkWidget *widget, PikaAboutDialog *dialog); static void about_dialog_unmap (GtkWidget *widget, PikaAboutDialog *dialog); static GdkPixbuf * about_dialog_load_logo (void); static void about_dialog_add_animation (GtkWidget *vbox, PikaAboutDialog *dialog); static void about_dialog_add_update (PikaAboutDialog *dialog, PikaCoreConfig *config); static gboolean about_dialog_anim_draw (GtkWidget *widget, cairo_t *cr, PikaAboutDialog *dialog); static void about_dialog_reshuffle (PikaAboutDialog *dialog); static gboolean about_dialog_timer (gpointer data); #ifdef PIKA_UNSTABLE static void about_dialog_add_unstable_message (GtkWidget *vbox); #endif /* PIKA_UNSTABLE */ static void about_dialog_last_release_changed (PikaCoreConfig *config, const GParamSpec *pspec, PikaAboutDialog *dialog); static void about_dialog_download_clicked (GtkButton *button, const gchar *link); GtkWidget * about_dialog_create (Pika *pika, PikaCoreConfig *config) { static PikaAboutDialog dialog; if (! dialog.dialog) { GtkWidget *widget; GtkWidget *container; GdkPixbuf *pixbuf; GList *children; gchar *copyright; gchar *version; dialog.pika = pika; dialog.config = config; pixbuf = about_dialog_load_logo (); copyright = g_strdup_printf (PIKA_COPYRIGHT, PIKA_GIT_LAST_COMMIT_YEAR); if (pika_version_get_revision () > 0) /* Translators: the %s is PIKA version, the %d is the * installer/package revision. * For instance: "2.10.18 (revision 2)" */ version = g_strdup_printf (_("%s (revision %d)"), PIKA_VERSION, pika_version_get_revision ()); else version = g_strdup (PIKA_VERSION); widget = g_object_new (GTK_TYPE_ABOUT_DIALOG, "role", "pika-about", "window-position", GTK_WIN_POS_CENTER, "title", _("About PIKA"), "program-name", PIKA_ACRONYM, "version", version, "copyright", copyright, "comments", PIKA_NAME, "license", PIKA_LICENSE, "wrap-license", TRUE, "logo", pixbuf, "website", "https://heckin.technology/AlderconeStudio/PIKApp/", "website-label", _("Visit the PIKA website"), NULL); if (pixbuf) g_object_unref (pixbuf); g_free (copyright); g_free (version); g_set_weak_pointer (&dialog.dialog, widget); g_signal_connect (widget, "response", G_CALLBACK (gtk_widget_destroy), NULL); g_signal_connect (widget, "map", G_CALLBACK (about_dialog_map), &dialog); g_signal_connect (widget, "unmap", G_CALLBACK (about_dialog_unmap), &dialog); /* kids, don't try this at home! */ container = gtk_dialog_get_content_area (GTK_DIALOG (widget)); children = gtk_container_get_children (GTK_CONTAINER (container)); if (GTK_IS_BOX (children->data)) { about_dialog_add_animation (children->data, &dialog); #ifdef PIKA_UNSTABLE about_dialog_add_unstable_message (children->data); #endif /* PIKA_UNSTABLE */ about_dialog_add_update (&dialog, config); } else g_warning ("%s: ooops, no box in this container?", G_STRLOC); g_list_free (children); } return dialog.dialog; } static void about_dialog_map (GtkWidget *widget, PikaAboutDialog *dialog) { pika_update_refresh (dialog->config); if (dialog->layout && dialog->timer == 0) { dialog->state = 0; dialog->index = 0; dialog->animstep = 0; dialog->visible = FALSE; about_dialog_reshuffle (dialog); dialog->timer = g_timeout_add (800, about_dialog_timer, dialog); } #ifdef G_OS_WIN32 pika_window_set_title_bar_theme (dialog->pika, widget, FALSE); #endif } static void about_dialog_unmap (GtkWidget *widget, PikaAboutDialog *dialog) { if (dialog->timer) { g_source_remove (dialog->timer); dialog->timer = 0; } } static GdkPixbuf * about_dialog_load_logo (void) { GdkPixbuf *pixbuf = NULL; GFile *file; GInputStream *input; file = pika_data_directory_file ("images", #ifdef PIKA_UNSTABLE "pika-devel-logo.png", #else "pika-logo.png", #endif NULL); input = G_INPUT_STREAM (g_file_read (file, NULL, NULL)); g_object_unref (file); if (input) { pixbuf = gdk_pixbuf_new_from_stream (input, NULL, NULL); g_object_unref (input); } return pixbuf; } static void about_dialog_add_animation (GtkWidget *vbox, PikaAboutDialog *dialog) { gint height; dialog->anim_area = gtk_drawing_area_new (); gtk_box_pack_start (GTK_BOX (vbox), dialog->anim_area, FALSE, FALSE, 0); gtk_box_reorder_child (GTK_BOX (vbox), dialog->anim_area, 5); gtk_widget_show (dialog->anim_area); dialog->layout = gtk_widget_create_pango_layout (dialog->anim_area, NULL); g_object_weak_ref (G_OBJECT (dialog->anim_area), (GWeakNotify) g_object_unref, dialog->layout); pango_layout_get_pixel_size (dialog->layout, NULL, &height); gtk_widget_set_size_request (dialog->anim_area, -1, 2 * height); g_signal_connect (dialog->anim_area, "draw", G_CALLBACK (about_dialog_anim_draw), dialog); } static void about_dialog_add_update (PikaAboutDialog *dialog, PikaCoreConfig *config) { GtkWidget *container; GList *children; GtkWidget *vbox; GtkWidget *frame; GtkWidget *box; GtkWidget *box2; GtkWidget *label; GtkWidget *button; GtkWidget *button_image; GtkWidget *button_label; GDateTime *datetime; gchar *date; gchar *text; if (dialog->update_frame) { gtk_widget_destroy (dialog->update_frame); dialog->update_frame = NULL; } /* Get the dialog vbox. */ container = gtk_dialog_get_content_area (GTK_DIALOG (dialog->dialog)); children = gtk_container_get_children (GTK_CONTAINER (container)); g_return_if_fail (GTK_IS_BOX (children->data)); vbox = children->data; g_list_free (children); /* The update frame. */ frame = gtk_frame_new (NULL); gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 2); box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_container_add (GTK_CONTAINER (frame), box); /* Button in the frame. */ button = gtk_button_new (); gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0); gtk_widget_show (button); box2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_container_add (GTK_CONTAINER (button), box2); gtk_widget_show (box2); button_image = gtk_image_new_from_icon_name (NULL, GTK_ICON_SIZE_DIALOG); gtk_box_pack_start (GTK_BOX (box2), button_image, FALSE, FALSE, 0); gtk_widget_show (button_image); button_label = gtk_label_new (NULL); gtk_box_pack_start (GTK_BOX (box2), button_label, FALSE, FALSE, 0); gtk_container_child_set (GTK_CONTAINER (box2), button_label, "expand", TRUE, NULL); gtk_widget_show (button_label); if (config->last_known_release != NULL) { /* There is a newer version. */ const gchar *download_url = NULL; gchar *comment = NULL; /* We want the frame to stand out. */ label = gtk_label_new (NULL); text = g_strdup_printf ("%s", _("Update available!")); gtk_label_set_markup (GTK_LABEL (label), text); g_free (text); gtk_widget_show (label); gtk_frame_set_label_widget (GTK_FRAME (frame), label); gtk_frame_set_label_align (GTK_FRAME (frame), 0.5, 0.5); gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_OUT); gtk_box_reorder_child (GTK_BOX (vbox), frame, 3); /* Button is an update link. */ gtk_image_set_from_icon_name (GTK_IMAGE (button_image), "software-update-available", GTK_ICON_SIZE_DIALOG); #ifdef PIKA_UNSTABLE download_url = "https://heckin.technology/AlderconeStudio/PIKApp/"; #else download_url = "https://heckin.technology/AlderconeStudio/PIKApp/"; #endif g_signal_connect (button, "clicked", (GCallback) about_dialog_download_clicked, (gpointer) download_url); /* The preferred localized date representation without the time. */ datetime = g_date_time_new_from_unix_local (config->last_release_timestamp); date = g_date_time_format (datetime, "%x"); g_date_time_unref (datetime); if (config->last_revision > 0) { /* This is actually a new revision of current version. */ text = g_strdup_printf (_("Download PIKA %s revision %d (released on %s)\n"), config->last_known_release, config->last_revision, date); /* Finally an optional release comment. */ if (config->last_release_comment) { /* Translators: <> tags are Pango markup. Please keep these * markups in your translation. */ comment = g_strdup_printf (_("Release comment: %s"), config->last_release_comment); } } else { text = g_strdup_printf (_("Download PIKA %s (released on %s)\n"), config->last_known_release, date); } gtk_label_set_text (GTK_LABEL (button_label), text); g_free (text); g_free (date); if (comment) { label = gtk_label_new (NULL); gtk_label_set_max_width_chars (GTK_LABEL (label), 80); gtk_label_set_markup (GTK_LABEL (label), comment); gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); g_free (comment); gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); gtk_widget_show (label); } } else { /* Button is a "Check for updates" action. */ gtk_image_set_from_icon_name (GTK_IMAGE (button_image), "view-refresh", GTK_ICON_SIZE_MENU); gtk_label_set_text (GTK_LABEL (button_label), _("Check for updates")); g_signal_connect_swapped (button, "clicked", (GCallback) pika_update_check, config); } gtk_box_reorder_child (GTK_BOX (vbox), frame, 4); /* Last check date box. */ box2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); if (config->last_known_release != NULL) gtk_widget_set_margin_top (box2, 20); gtk_container_add (GTK_CONTAINER (box), box2); gtk_widget_show (box2); /* Show a small "Check for updates" button only if the big one has * been replaced by a download button. */ if (config->last_known_release != NULL) { button = gtk_button_new_from_icon_name ("view-refresh", GTK_ICON_SIZE_MENU); gtk_widget_set_tooltip_text (button, _("Check for updates")); gtk_box_pack_start (GTK_BOX (box2), button, FALSE, FALSE, 0); g_signal_connect_swapped (button, "clicked", (GCallback) pika_update_check, config); gtk_widget_show (button); } if (config->check_update_timestamp > 0) { gchar *subtext; gchar *time; datetime = g_date_time_new_from_unix_local (config->check_update_timestamp); date = g_date_time_format (datetime, "%x"); time = g_date_time_format (datetime, "%X"); /* Translators: first string is the date in the locale's date * representation (e.g., 12/31/99), second is the time in the * locale's time representation (e.g., 23:13:48). */ subtext = g_strdup_printf (_("Last checked on %s at %s"), date, time); g_date_time_unref (datetime); g_free (date); g_free (time); text = g_strdup_printf ("%s", subtext); label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (label), text); gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER); gtk_box_pack_start (GTK_BOX (box2), label, FALSE, FALSE, 0); gtk_container_child_set (GTK_CONTAINER (box2), label, "expand", TRUE, NULL); gtk_widget_show (label); g_free (text); g_free (subtext); } gtk_widget_show (box); gtk_widget_show (frame); g_set_weak_pointer (&dialog->update_frame, frame); /* Reconstruct the dialog when release info changes. */ g_signal_connect (config, "notify::last-known-release", (GCallback) about_dialog_last_release_changed, dialog); } static void about_dialog_reshuffle (PikaAboutDialog *dialog) { } static gboolean about_dialog_anim_draw (GtkWidget *widget, cairo_t *cr, PikaAboutDialog *dialog) { GtkStyleContext *style = gtk_widget_get_style_context (widget); GtkAllocation allocation; GdkRGBA color; gdouble alpha = 0.0; gint x, y; gint width, height; if (! dialog->visible) return FALSE; if (dialog->animstep < 16) { alpha = (gfloat) dialog->animstep / 15.0; } else if (dialog->animstep < 18) { alpha = 1.0; } else if (dialog->animstep < 33) { alpha = 1.0 - ((gfloat) (dialog->animstep - 17)) / 15.0; } gtk_style_context_get_color (style, gtk_style_context_get_state (style), &color); gdk_cairo_set_source_rgba (cr, &color); gtk_widget_get_allocation (widget, &allocation); pango_layout_get_pixel_size (dialog->layout, &width, &height); x = (allocation.width - width) / 2; y = (allocation.height - height) / 2; cairo_move_to (cr, x, y); cairo_push_group (cr); pango_cairo_show_layout (cr, dialog->layout); cairo_pop_group_to_source (cr); cairo_paint_with_alpha (cr, alpha); return FALSE; } static gchar * insert_spacers (const gchar *string) { GString *str = g_string_new (NULL); gchar *normalized; gchar *ptr; gunichar unichr; normalized = g_utf8_normalize (string, -1, G_NORMALIZE_DEFAULT_COMPOSE); ptr = normalized; while ((unichr = g_utf8_get_char (ptr))) { g_string_append_unichar (str, unichr); g_string_append_unichar (str, 0x200b); /* ZERO WIDTH SPACE */ ptr = g_utf8_next_char (ptr); } g_free (normalized); return g_string_free (str, FALSE); } static void decorate_text (PikaAboutDialog *dialog, gint anim_type, gdouble time) { const gchar *text; const gchar *ptr; gint letter_count = 0; gint cluster_start, cluster_end; gunichar unichr; PangoAttrList *attrlist = NULL; PangoAttribute *attr; PangoRectangle irect = {0, 0, 0, 0}; PangoRectangle lrect = {0, 0, 0, 0}; text = pango_layout_get_text (dialog->layout); g_return_if_fail (text != NULL); attrlist = pango_attr_list_new (); switch (anim_type) { case 0: /* Fade in */ break; case 1: /* Fade in, spread */ ptr = text; cluster_start = 0; while ((unichr = g_utf8_get_char (ptr))) { ptr = g_utf8_next_char (ptr); cluster_end = (ptr - text); if (unichr == 0x200b) { lrect.width = (1.0 - time) * 15.0 * PANGO_SCALE + 0.5; attr = pango_attr_shape_new (&irect, &lrect); attr->start_index = cluster_start; attr->end_index = cluster_end; pango_attr_list_change (attrlist, attr); } cluster_start = cluster_end; } break; case 2: /* Fade in, sinewave */ ptr = text; cluster_start = 0; while ((unichr = g_utf8_get_char (ptr))) { if (unichr == 0x200b) { cluster_end = ptr - text; attr = pango_attr_rise_new ((1.0 -time) * 18000 * sin (4.0 * time + (float) letter_count * 0.7)); attr->start_index = cluster_start; attr->end_index = cluster_end; pango_attr_list_change (attrlist, attr); letter_count++; cluster_start = cluster_end; } ptr = g_utf8_next_char (ptr); } break; default: g_printerr ("Unknown animation type %d\n", anim_type); } pango_layout_set_attributes (dialog->layout, attrlist); pango_attr_list_unref (attrlist); } static gboolean about_dialog_timer (gpointer data) { PikaAboutDialog *dialog = data; gint timeout = 0; if (dialog->animstep == 0) { gchar *text = NULL; dialog->visible = TRUE; switch (dialog->state) { case 0: dialog->timer = g_timeout_add (30, about_dialog_timer, dialog); dialog->state += 1; return FALSE; case 1: text = insert_spacers (_("PIKA is brought to you by")); dialog->state += 1; break; default: g_return_val_if_reached (TRUE); break; } g_return_val_if_fail (text != NULL, TRUE); pango_layout_set_text (dialog->layout, text, -1); pango_layout_set_attributes (dialog->layout, NULL); g_free (text); } if (dialog->animstep < 16) { decorate_text (dialog, 2, ((gfloat) dialog->animstep) / 15.0); } else if (dialog->animstep == 16) { timeout = 800; } else if (dialog->animstep == 17) { timeout = 30; } else if (dialog->animstep < 33) { decorate_text (dialog, 1, 1.0 - ((gfloat) (dialog->animstep - 17)) / 15.0); } else if (dialog->animstep == 33) { dialog->visible = FALSE; timeout = 300; } else { dialog->visible = FALSE; dialog->animstep = -1; timeout = 30; } dialog->animstep++; gtk_widget_queue_draw (dialog->anim_area); if (timeout > 0) { dialog->timer = g_timeout_add (timeout, about_dialog_timer, dialog); return FALSE; } /* else keep the current timeout */ return TRUE; } #ifdef PIKA_UNSTABLE static void about_dialog_add_unstable_message (GtkWidget *vbox) { GtkWidget *label; gchar *text; text = g_strdup_printf (_("This is an unstable development release\n" "commit %s"), PIKA_GIT_VERSION_ABBREV); label = gtk_label_new (text); g_free (text); gtk_label_set_selectable (GTK_LABEL (label), TRUE); gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER); pika_label_set_attributes (GTK_LABEL (label), PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC, -1); gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0); gtk_box_reorder_child (GTK_BOX (vbox), label, 2); gtk_widget_show (label); } #endif /* PIKA_UNSTABLE */ static void about_dialog_last_release_changed (PikaCoreConfig *config, const GParamSpec *pspec, PikaAboutDialog *dialog) { g_signal_handlers_disconnect_by_func (config, (GCallback) about_dialog_last_release_changed, dialog); if (! dialog->dialog) return; about_dialog_add_update (dialog, config); } static void about_dialog_download_clicked (GtkButton *button, const gchar *link) { GtkWidget *window; window = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_WINDOW); if (window) gtk_show_uri_on_window (GTK_WINDOW (window), link, GDK_CURRENT_TIME, NULL); }