/* 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 * * welcome-dialog.c * Copyright (C) 2022 Jehan * * 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 #ifdef GDK_WINDOWING_WAYLAND #include #endif #include "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "dialogs-types.h" #include "core/pika.h" #include "core/pika-utils.h" #include "widgets/pikawidgets-utils.h" #include "welcome-dialog.h" #include "welcome-dialog-data.h" #include "pika-intl.h" static void welcome_dialog_release_item_activated (GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data); static void welcome_add_link (GtkGrid *grid, gint column, gint *row, const gchar *emoji, const gchar *title, const gchar *link); static void welcome_size_allocate (GtkWidget *welcome_dialog, GtkAllocation *allocation, gpointer user_data); GtkWidget * welcome_dialog_create (Pika *pika) { GtkWidget *welcome_dialog; GList *windows; GtkWidget *main_vbox; GtkWidget *stack; GtkWidget *grid; GtkWidget *switcher; GtkWidget *scrolled_window; GtkWidget *vbox; GtkWidget *hbox; GtkWidget *image; GtkWidget *listbox; GtkWidget *widget; gchar *release_link; gchar *title; gchar *markup; gchar *tmp; gint row; g_return_val_if_fail (PIKA_IS_PIKA (pika), NULL); /* Translators: the %s string will be the version, e.g. "3.0". */ title = g_strdup_printf (_("Welcome to PIKA %s"), PIKA_VERSION); windows = pika_get_image_windows (pika); welcome_dialog = pika_dialog_new (title, "pika-welcome-dialog", windows ? windows->data : NULL, 0, NULL, NULL, NULL); g_list_free (windows); gtk_window_set_position (GTK_WINDOW (welcome_dialog), GTK_WIN_POS_CENTER_ON_PARENT); g_free (title); g_signal_connect (welcome_dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12); gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (welcome_dialog))), main_vbox, TRUE, TRUE, 0); gtk_widget_show (main_vbox); stack = gtk_stack_new (); gtk_box_pack_start (GTK_BOX (main_vbox), stack, TRUE, TRUE, 0); gtk_widget_show (stack); /****************/ /* Welcome page */ /****************/ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); gtk_stack_add_titled (GTK_STACK (stack), vbox, "welcome", _("Welcome")); gtk_widget_show (vbox); image = gtk_image_new_from_icon_name ("pika-mascot", GTK_ICON_SIZE_DIALOG); gtk_widget_set_valign (image, GTK_ALIGN_CENTER); gtk_box_pack_start (GTK_BOX (vbox), image, FALSE, FALSE, 0); gtk_widget_show (image); g_signal_connect (welcome_dialog, "size-allocate", G_CALLBACK (welcome_size_allocate), image); /* Welcome title. */ /* Translators: the %s string will be the version, e.g. "3.0". */ tmp = g_strdup_printf (_("You installed PIKA %s!"), PIKA_VERSION); markup = g_strdup_printf ("%s", tmp); g_free (tmp); widget = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (widget), markup); g_free (markup); gtk_label_set_selectable (GTK_LABEL (widget), TRUE); gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_CENTER); gtk_label_set_line_wrap (GTK_LABEL (widget), FALSE); gtk_box_pack_start (GTK_BOX (vbox), widget, TRUE, TRUE, 0); gtk_widget_show (widget); grid = gtk_grid_new (); gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE); gtk_grid_set_row_spacing (GTK_GRID (grid), 0); gtk_grid_set_column_spacing (GTK_GRID (grid), 4); gtk_box_pack_start (GTK_BOX (vbox), grid, TRUE, TRUE, 0); gtk_widget_show (grid); /* Welcome message: left */ markup = _("PIKA is a Free Software for image authoring and manipulation.\n" "Want to know more?"); widget = gtk_label_new (NULL); gtk_label_set_max_width_chars (GTK_LABEL (widget), 30); /*gtk_widget_set_size_request (widget, max_width / 2, -1);*/ gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); gtk_widget_set_vexpand (widget, FALSE); gtk_widget_set_hexpand (widget, FALSE); /* Making sure the labels are well top aligned to avoid some ugly * misalignment if left and right labels have different sizes, * but also left-aligned so that the messages are slightly to the left * of the emoji/link list below. * Following design decisions by Aryeom. */ gtk_label_set_xalign (GTK_LABEL (widget), 0.0); gtk_label_set_yalign (GTK_LABEL (widget), 0.0); gtk_widget_set_margin_bottom (widget, 10); gtk_label_set_markup (GTK_LABEL (widget), markup); gtk_grid_attach (GTK_GRID (grid), widget, 0, 0, 1, 1); gtk_widget_show (widget); row = 1; welcome_add_link (GTK_GRID (grid), 0, &row, /* "globe with meridians" emoticone in UTF-8. */ "\xf0\x9f\x8c\x90", _("PIKA website"), "https://heckin.technology/AlderconeStudio/PIKApp/"); welcome_add_link (GTK_GRID (grid), 0, &row, /* "graduation cap" emoticone in UTF-8. */ "\xf0\x9f\x8e\x93", _("Tutorials"), "https://heckin.technology/AlderconeStudio/PIKApp/"); welcome_add_link (GTK_GRID (grid), 0, &row, /* "open book" emoticone in UTF-8. */ "\xf0\x9f\x93\x96", _("Documentation"), "https://heckin.technology/AlderconeStudio/PIKApp/wiki/"); /* XXX: should we add API docs for plug-in developers once it's * properly set up? */ /* Welcome message: right */ markup = _("PIKA is a Community Software under the GNU general public license v3.\n" "Want to contribute?"); widget = gtk_label_new (NULL); gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); gtk_label_set_max_width_chars (GTK_LABEL (widget), 30); /*gtk_widget_set_size_request (widget, max_width / 2, -1);*/ /* Again the alignments are important. */ gtk_label_set_xalign (GTK_LABEL (widget), 0.0); gtk_widget_set_vexpand (widget, FALSE); gtk_widget_set_hexpand (widget, FALSE); gtk_label_set_xalign (GTK_LABEL (widget), 0.0); gtk_label_set_yalign (GTK_LABEL (widget), 0.0); gtk_widget_set_margin_bottom (widget, 10); gtk_label_set_markup (GTK_LABEL (widget), markup); gtk_grid_attach (GTK_GRID (grid), widget, 1, 0, 1, 1); gtk_widget_show (widget); row = 1; welcome_add_link (GTK_GRID (grid), 1, &row, /* "keyboard" emoticone in UTF-8. */ "\xe2\x8c\xa8", _("Contributing"), "https://heckin.technology/AlderconeStudio/PIKApp/"); welcome_add_link (GTK_GRID (grid), 1, &row, /* "love letter" emoticone in UTF-8. */ "\xf0\x9f\x92\x8c", _("Donating"), "https://mastodon.art/@aldercone/"); /*****************/ /* Release Notes */ /*****************/ if (pika_welcome_dialog_n_items > 0) { gint n_demos = 0; vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_stack_add_titled (GTK_STACK (stack), vbox, "release-notes", _("Release Notes")); gtk_widget_show (vbox); /* Release note title. */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_container_set_border_width (GTK_CONTAINER (hbox), 6); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); /* Translators: the %s string will be the version, e.g. "3.0". */ tmp = g_strdup_printf (_("PIKA %s Release Notes"), PIKA_VERSION); markup = g_strdup_printf ("%s", tmp); g_free (tmp); widget = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (widget), markup); g_free (markup); gtk_label_set_selectable (GTK_LABEL (widget), FALSE); gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_CENTER); gtk_label_set_line_wrap (GTK_LABEL (widget), FALSE); gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0); gtk_widget_show (widget); image = gtk_image_new_from_icon_name ("pika-user-manual", GTK_ICON_SIZE_DIALOG); gtk_widget_set_valign (image, GTK_ALIGN_START); gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0); gtk_widget_show (image); /* Release note introduction. */ if (pika_welcome_dialog_intro_n_paragraphs) { GString *introduction = NULL; for (gint i = 0; i < pika_welcome_dialog_intro_n_paragraphs; i++) { if (i == 0) introduction = g_string_new (_(pika_welcome_dialog_intro[i])); else g_string_append_printf (introduction, "\n%s", _(pika_welcome_dialog_intro[i])); } widget = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (widget), introduction->str); gtk_label_set_max_width_chars (GTK_LABEL (widget), 70); gtk_label_set_selectable (GTK_LABEL (widget), FALSE); gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_LEFT); gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); gtk_widget_show (widget); g_string_free (introduction, TRUE); } /* Release note's change items. */ scrolled_window = gtk_scrolled_window_new (NULL, NULL); gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 0); gtk_widget_show (scrolled_window); listbox = gtk_list_box_new (); for (gint i = 0; i < pika_welcome_dialog_n_items; i++) { GtkWidget *row; gchar *markup; /* Add a bold dot for pretty listing. */ if (i < pika_welcome_dialog_n_items && pika_welcome_dialog_demos[i] != NULL) { markup = g_strdup_printf ("\xe2\x96\xb6 %s", _((gchar *) pika_welcome_dialog_items[i])); n_demos++; } else { markup = g_strdup_printf ("\xe2\x80\xa2 %s", _((gchar *) pika_welcome_dialog_items[i])); } row = gtk_list_box_row_new (); widget = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (widget), markup); gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE); gtk_label_set_line_wrap_mode (GTK_LABEL (widget), PANGO_WRAP_WORD); gtk_label_set_justify (GTK_LABEL (widget), GTK_JUSTIFY_LEFT); gtk_widget_set_halign (widget, GTK_ALIGN_START); gtk_label_set_xalign (GTK_LABEL (widget), 0.0); gtk_container_add (GTK_CONTAINER (row), widget); gtk_list_box_insert (GTK_LIST_BOX (listbox), row, -1); gtk_widget_show_all (row); g_free (markup); } gtk_container_add (GTK_CONTAINER (scrolled_window), listbox); gtk_list_box_set_selection_mode (GTK_LIST_BOX (listbox), GTK_SELECTION_NONE); g_signal_connect (listbox, "row-activated", G_CALLBACK (welcome_dialog_release_item_activated), pika); gtk_widget_show (listbox); if (n_demos > 0) { /* A small explicative string to help discoverability of the demo * ability. */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); image = gtk_image_new_from_icon_name ("dialog-information", GTK_ICON_SIZE_MENU); gtk_widget_set_valign (image, GTK_ALIGN_CENTER); gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0); gtk_widget_show (image); widget = gtk_label_new (NULL); tmp = g_strdup_printf (_("Click on release items with a %s bullet point to get a tour."), "\xe2\x96\xb6"); markup = g_strdup_printf ("%s", tmp); g_free (tmp); gtk_label_set_markup (GTK_LABEL (widget), markup); g_free (markup); gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0); gtk_widget_show (widget); /* TODO: if a demo changed settings, should we add a "reset" * button to get back to previous state? */ } /* Link to full release notes on web site at the bottom. */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); if (PIKA_MINOR_VERSION % 2 == 0) release_link = g_strdup_printf ("https://heckin.technology/AlderconeStudio/PIKApp/release-notes/pika-%d.%d.html", PIKA_MAJOR_VERSION, PIKA_MINOR_VERSION); else release_link = g_strdup ("https://heckin.technology/AlderconeStudio/PIKApp/"); widget = gtk_link_button_new_with_label (release_link, _("Learn more")); gtk_widget_show (widget); gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0); g_free (release_link); /*****************/ /* Task switcher */ /*****************/ switcher = gtk_stack_switcher_new (); gtk_stack_switcher_set_stack (GTK_STACK_SWITCHER (switcher), GTK_STACK (stack)); gtk_box_pack_start (GTK_BOX (main_vbox), switcher, FALSE, FALSE, 0); gtk_widget_set_halign (switcher, GTK_ALIGN_CENTER); gtk_widget_show (switcher); } /**************/ /* Info label */ /**************/ widget = gtk_label_new (NULL); markup = g_strdup_printf ("%s", _("This welcome dialog is only shown at first launch. " "You can show it again from the \"Help\" menu.")); gtk_label_set_markup (GTK_LABEL (widget), markup); g_free (markup); gtk_widget_show (widget); gtk_box_pack_start (GTK_BOX (main_vbox), widget, FALSE, FALSE, 0); return welcome_dialog; } static void welcome_dialog_release_item_activated (GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data) { Pika *pika = user_data; GList *blink_script = NULL; const gchar *script_string; gchar **script_steps; gint row_index; gint i; row_index = gtk_list_box_row_get_index (row); g_return_if_fail (row_index < pika_welcome_dialog_n_items); script_string = pika_welcome_dialog_demos[row_index]; if (script_string == NULL) /* Not an error. Some release items have no demos. */ return; script_steps = g_strsplit (script_string, ",", 0); for (i = 0; script_steps[i]; i++) { gchar **ids; gchar *dockable_id = NULL; gchar *widget_id = NULL; gchar **settings = NULL; gchar *settings_value = NULL; ids = g_strsplit (script_steps[i], ":", 2); /* Even if the string doesn't contain a second part, it is * NULL-terminated, hence the widget_id will simply be NULL, which * is fine when you just want to blink a dialog. */ dockable_id = ids[0]; widget_id = ids[1]; if (widget_id != NULL) { settings = g_strsplit (widget_id, "=", 2); widget_id = settings[0]; settings_value = settings[1]; } /* Allowing white spaces so that the demo in XML metadata can be * spaced-out or even on multiple lines for clarity. */ dockable_id = g_strstrip (dockable_id); if (widget_id != NULL) widget_id = g_strstrip (widget_id); /* All our dockable IDs start with "pika-". This allows to write * shorter names in the demo script. */ if (! g_str_has_prefix (dockable_id, "pika-")) { gchar *tmp = g_strdup_printf ("pika-%s", dockable_id); g_free (ids[0]); dockable_id = ids[0] = tmp; } /* Blink widget. */ if (g_strcmp0 (dockable_id, "pika-toolbox") == 0) { /* All tool button IDs start with "tools-". This allows to * write shorter tool names in the demo script. */ if (widget_id != NULL && ! g_str_has_prefix (widget_id, "tools-")) { gchar *tmp = g_strdup_printf ("tools-%s", widget_id); g_free (settings[0]); widget_id = settings[0] = tmp; } pika_blink_toolbox (pika, widget_id, &blink_script); } else { pika_blink_dockable (pika, dockable_id, widget_id, settings_value, &blink_script); } g_strfreev (ids); if (settings) g_strfreev (settings); } if (blink_script != NULL) { GList *windows = pika_get_image_windows (pika); /* Losing forcus on the welcome dialog on purpose for the main GUI * to be more readable. */ if (windows) gtk_window_present (windows->data); pika_blink_play_script (blink_script); g_list_free (windows); } g_strfreev (script_steps); } static void welcome_add_link (GtkGrid *grid, gint column, gint *row, const gchar *emoji, const gchar *title, const gchar *link) { GtkWidget *hbox; GtkWidget *button; GtkWidget *icon; /* TODO: Aryeom doesn't like the spacing here. There is too much * spacing between the link lines and between emojis and links. But we * didn't manage to find how to close a bit these 2 spacings in GTK. * :-/ */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_grid_attach (grid, hbox, column, *row, 1, 1); /* These margin are by design to emphasize a bit the link list by * moving them a tiny bit to the right instead of being exactly * aligned with the top text. */ gtk_widget_set_margin_start (hbox, 10); gtk_widget_show (hbox); ++(*row); icon = gtk_label_new (emoji); gtk_box_pack_start (GTK_BOX (hbox), icon, FALSE, FALSE, 0); gtk_widget_show (icon); button = gtk_link_button_new_with_label (link, title); gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 0); gtk_widget_show (button); } static void welcome_size_allocate (GtkWidget *welcome_dialog, GtkAllocation *allocation, gpointer user_data) { GtkWidget *image = GTK_WIDGET (user_data); GError *error = NULL; GFile *splash_file; GdkPixbuf *pixbuf; GdkMonitor *monitor; GdkRectangle workarea; gint min_width; gint min_height; gint max_width; gint max_height; gint image_width; gint image_height; if (gtk_image_get_storage_type (GTK_IMAGE (image)) == GTK_IMAGE_PIXBUF) return; monitor = pika_get_monitor_at_pointer (); gdk_monitor_get_workarea (monitor, &workarea); #ifdef GDK_WINDOWING_WAYLAND if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { /* See the long comment in app/gui/splash.c on why we do this * weird stuff for Wayland only. * See also #5322. */ min_width = workarea.width / 8; min_height = workarea.height / 8; max_width = workarea.width / 4; max_height = workarea.height / 4; } else #endif { min_width = workarea.width / 4; min_height = workarea.height / 4; max_width = workarea.width / 2; max_height = workarea.height / 2; } image_width = allocation->width + 20; image_height = allocation->height + 20; /* On big monitors, we get very huge images with a lot of empty space. * So let's go with a logic so that we want a max and min size * (relatively to desktop area), but we also want to avoid too much * empty space. This is why we compute first the dialog size without * any image in there. */ image_width = CLAMP (image_width, min_width, max_width); image_height = CLAMP (image_height, min_height, max_height); splash_file = pika_data_directory_file ("images", "pika-splash.png", NULL); pixbuf = gdk_pixbuf_new_from_file_at_scale (g_file_peek_path (splash_file), image_width, image_height, TRUE, &error); if (pixbuf) { gtk_image_set_from_pixbuf (GTK_IMAGE (image), pixbuf); g_object_unref (pixbuf); } g_object_unref (splash_file); gtk_widget_show (image); gtk_window_set_resizable (GTK_WINDOW (welcome_dialog), FALSE); }