From 6e161184d1e4a7ebaaeb4cdda0fd5fb52b0410f6 Mon Sep 17 00:00:00 2001 From: Adrien Plazas Date: Mon, 20 Sep 2021 15:21:51 +0200 Subject: [PATCH] Add AdwAboutWindow --- demo/adw-demo-debug-info.c | 170 + demo/adw-demo-debug-info.h | 9 + demo/adw-demo-window.c | 2 + demo/adw-demo-window.ui | 8 + demo/adwaita-demo.c | 66 +- demo/adwaita-demo.gresources.xml | 3 + demo/icons/org.example.Typeset.Source.svg | 3635 +++++++++++++++++ .../actions/widget-about-symbolic.svg | 4 + .../scalable/apps/org.example.Typeset.svg | 1 + demo/meson.build | 14 + demo/pages/about/adw-demo-page-about.c | 99 + demo/pages/about/adw-demo-page-about.h | 11 + demo/pages/about/adw-demo-page-about.ui | 24 + doc/images/about-window-credits-dark.png | Bin 0 -> 5416 bytes doc/images/about-window-credits.png | Bin 0 -> 6542 bytes doc/images/about-window-dark.png | Bin 0 -> 22206 bytes doc/images/about-window.png | Bin 0 -> 23900 bytes doc/libadwaita.toml.in | 4 + doc/tools/data/about-window-credits.ui | 44 + doc/tools/data/about-window.ui | 14 + doc/visual-index.md | 7 + po/POTFILES.in | 2 + src/adw-about-window.c | 3222 +++++++++++++++ src/adw-about-window.h | 175 + src/adw-about-window.ui | 811 ++++ src/adwaita.gresources.xml | 3 + src/adwaita.h | 1 + .../actions/adw-external-link-symbolic.svg | 1 + .../actions/adw-mail-send-symbolic.svg | 1 + src/meson.build | 2 + src/stylesheet/widgets/_misc.scss | 46 + tests/meson.build | 1 + tests/test-about-window.c | 109 + 33 files changed, 8461 insertions(+), 28 deletions(-) create mode 100644 demo/adw-demo-debug-info.c create mode 100644 demo/adw-demo-debug-info.h create mode 100644 demo/icons/org.example.Typeset.Source.svg create mode 100644 demo/icons/scalable/actions/widget-about-symbolic.svg create mode 100644 demo/icons/scalable/apps/org.example.Typeset.svg create mode 100644 demo/pages/about/adw-demo-page-about.c create mode 100644 demo/pages/about/adw-demo-page-about.h create mode 100644 demo/pages/about/adw-demo-page-about.ui create mode 100644 doc/images/about-window-credits-dark.png create mode 100644 doc/images/about-window-credits.png create mode 100644 doc/images/about-window-dark.png create mode 100644 doc/images/about-window.png create mode 100644 doc/tools/data/about-window-credits.ui create mode 100644 doc/tools/data/about-window.ui create mode 100644 src/adw-about-window.c create mode 100644 src/adw-about-window.h create mode 100644 src/adw-about-window.ui create mode 100644 src/icons/scalable/actions/adw-external-link-symbolic.svg create mode 100644 src/icons/scalable/actions/adw-mail-send-symbolic.svg create mode 100644 tests/test-about-window.c diff --git a/demo/adw-demo-debug-info.c b/demo/adw-demo-debug-info.c new file mode 100644 index 00000000..d67db893 --- /dev/null +++ b/demo/adw-demo-debug-info.c @@ -0,0 +1,170 @@ +#include "config.h" + +#include "adw-demo-debug-info.h" + +/* Copied and adapted from gtk/inspector/general.c */ +static void +get_gtk_info (const char **backend, + const char **renderer) +{ + GdkDisplay *display = gdk_display_get_default (); + GdkSurface *surface; + GskRenderer *gsk_renderer; + + if (!g_strcmp0 (G_OBJECT_TYPE_NAME (display), "GdkX11Display")) + *backend = "X11"; + else if (!g_strcmp0 (G_OBJECT_TYPE_NAME (display), "GdkWaylandDisplay")) + *backend = "Wayland"; + else if (!g_strcmp0 (G_OBJECT_TYPE_NAME (display), "GdkBroadwayDisplay")) + *backend = "Broadway"; + else if (!g_strcmp0 (G_OBJECT_TYPE_NAME (display), "GdkWin32Display")) + *backend = "Windows"; + else if (!g_strcmp0 (G_OBJECT_TYPE_NAME (display), "GdkMacosDisplay")) + *backend = "macOS"; + else + *backend = G_OBJECT_TYPE_NAME (display); + + surface = gdk_surface_new_toplevel (display); + gsk_renderer = gsk_renderer_new_for_surface (surface); + if (!g_strcmp0 (G_OBJECT_TYPE_NAME (gsk_renderer), "GskVulkanRenderer")) + *renderer = "Vulkan"; + else if (!g_strcmp0 (G_OBJECT_TYPE_NAME (gsk_renderer), "GskGLRenderer")) + *renderer = "GL"; + else if (!g_strcmp0 (G_OBJECT_TYPE_NAME (gsk_renderer), "GskCairoRenderer")) + *renderer = "Cairo"; + else + *renderer = G_OBJECT_TYPE_NAME (gsk_renderer); + + gsk_renderer_unrealize (gsk_renderer); + g_object_unref (gsk_renderer); + gdk_surface_destroy (surface); +} + +#ifndef G_OS_WIN32 +static char * +get_flatpak_info (const char *group, + const char *key) +{ + GKeyFile *keyfile = g_key_file_new (); + char *ret = NULL; + + if (g_key_file_load_from_file (keyfile, "/.flatpak-info", 0, NULL)) + ret = g_key_file_get_string (keyfile, group, key, NULL); + + g_key_file_unref (keyfile); + + return ret; +} +#endif + +char * +adw_demo_generate_debug_info (void) +{ + GString *string = g_string_new (NULL); +#ifndef G_OS_WIN32 + gboolean flatpak = g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS); +#endif + + g_string_append_printf (string, "Libadwaita demo: %s (%s)\n", ADW_VERSION_S, + ADW_DEMO_VCS_TAG); + g_string_append (string, "\n"); + + g_string_append (string, "Compiled against:\n"); + g_string_append_printf (string, "- GLib: %d.%d.%d\n", GLIB_MAJOR_VERSION, + GLIB_MINOR_VERSION, + GLIB_MICRO_VERSION); + g_string_append_printf (string, "- GTK: %d.%d.%d\n", GTK_MAJOR_VERSION, + GTK_MINOR_VERSION, + GTK_MICRO_VERSION); + g_string_append (string, "\n"); + + g_string_append (string, "Running against:\n"); + g_string_append_printf (string, "- GLib: %d.%d.%d\n", glib_major_version, + glib_minor_version, + glib_micro_version); + g_string_append_printf (string, "- GTK: %d.%d.%d\n", gtk_get_major_version (), + gtk_get_minor_version (), + gtk_get_micro_version ()); + g_string_append (string, "\n"); + + { + char *os_name = g_get_os_info (G_OS_INFO_KEY_NAME); + char *os_version = g_get_os_info (G_OS_INFO_KEY_VERSION); + + g_string_append (string, "System:\n"); + g_string_append_printf (string, "- Name: %s\n", os_name); + g_string_append_printf (string, "- Version: %s\n", os_version); + g_string_append (string, "\n"); + + g_free (os_name); + g_free (os_version); + } + + { + const char *backend, *renderer; + + get_gtk_info (&backend, &renderer); + + g_string_append (string, "GTK:\n"); + g_string_append_printf (string, "- GDK backend: %s\n", backend); + g_string_append_printf (string, "- GSK renderer: %s\n", renderer); + g_string_append (string, "\n"); + } + +#ifndef G_OS_WIN32 + if (flatpak) { + char *runtime = get_flatpak_info ("Application", "runtime"); + char *runtime_commit = get_flatpak_info ("Instance", "runtime-commit"); + char *arch = get_flatpak_info ("Instance", "arch"); + char *flatpak_version = get_flatpak_info ("Instance", "flatpak-version"); + char *devel = get_flatpak_info ("Instance", "devel"); + + g_string_append (string, "Flatpak:\n"); + g_string_append_printf (string, "- Runtime: %s\n", runtime); + g_string_append_printf (string, "- Runtime commit: %s\n", runtime_commit); + g_string_append_printf (string, "- Arch: %s\n", arch); + g_string_append_printf (string, "- Flatpak version: %s\n", flatpak_version); + g_string_append_printf (string, "- Devel: %s\n", devel ? "yes" : "no"); + g_string_append (string, "\n"); + + g_free (runtime); + g_free (runtime_commit); + g_free (arch); + g_free (flatpak_version); + g_free (devel); + } +#endif + + { + const char *desktop = g_getenv ("XDG_CURRENT_DESKTOP"); + const char *session_desktop = g_getenv ("XDG_SESSION_DESKTOP"); + const char *session_type = g_getenv ("XDG_SESSION_TYPE"); + const char *lang = g_getenv ("LANG"); + const char *builder = g_getenv ("INSIDE_GNOME_BUILDER"); + const char *gtk_debug = g_getenv ("GTK_DEBUG"); + const char *gtk_theme = g_getenv ("GTK_THEME"); + const char *adw_debug_color_scheme = g_getenv ("ADW_DEBUG_COLOR_SCHEME"); + const char *adw_debug_high_contrast = g_getenv ("ADW_DEBUG_HIGH_CONTRAST"); + const char *adw_disable_portal = g_getenv ("ADW_DISABLE_PORTAL"); + + g_string_append (string, "Environment:\n"); + g_string_append_printf (string, "- Desktop: %s\n", desktop); + g_string_append_printf (string, "- Session: %s (%s)\n", session_desktop, + session_type); + g_string_append_printf (string, "- Language: %s\n", lang); + g_string_append_printf (string, "- Running inside Builder: %s\n", builder ? "yes" : "no"); + + if (gtk_debug) + g_string_append_printf (string, "- GTK_DEBUG: %s\n", gtk_debug); + if (gtk_theme) + g_string_append_printf (string, "- GTK_THEME: %s\n", gtk_theme); + if (adw_debug_color_scheme) + g_string_append_printf (string, "- ADW_DEBUG_COLOR_SCHEME: %s\n", adw_debug_color_scheme); + if (adw_debug_high_contrast) + g_string_append_printf (string, "- ADW_DEBUG_HIGH_CONTRAST: %s\n", adw_debug_high_contrast); + if (adw_disable_portal) + g_string_append_printf (string, "- ADW_DISABLE_PORTAL: %s\n", adw_disable_portal); + } + + return g_string_free (string, FALSE); +} diff --git a/demo/adw-demo-debug-info.h b/demo/adw-demo-debug-info.h new file mode 100644 index 00000000..341eae9f --- /dev/null +++ b/demo/adw-demo-debug-info.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +char *adw_demo_generate_debug_info (void); + +G_END_DECLS diff --git a/demo/adw-demo-window.c b/demo/adw-demo-window.c index b15faf9c..74edef49 100644 --- a/demo/adw-demo-window.c +++ b/demo/adw-demo-window.c @@ -2,6 +2,7 @@ #include +#include "pages/about/adw-demo-page-about.h" #include "pages/animations/adw-demo-page-animations.h" #include "pages/avatar/adw-demo-page-avatar.h" #include "pages/buttons/adw-demo-page-buttons.h" @@ -116,6 +117,7 @@ adw_demo_window_init (AdwDemoWindow *self) { AdwStyleManager *manager = adw_style_manager_get_default (); + g_type_ensure (ADW_TYPE_DEMO_PAGE_ABOUT); g_type_ensure (ADW_TYPE_DEMO_PAGE_ANIMATIONS); g_type_ensure (ADW_TYPE_DEMO_PAGE_AVATAR); g_type_ensure (ADW_TYPE_DEMO_PAGE_BUTTONS); diff --git a/demo/adw-demo-window.ui b/demo/adw-demo-window.ui index 7f3799bf..76f90f9f 100644 --- a/demo/adw-demo-window.ui +++ b/demo/adw-demo-window.ui @@ -228,6 +228,14 @@ + + + About Window + + + + + diff --git a/demo/adwaita-demo.c b/demo/adwaita-demo.c index 54bbb1de..ff968883 100644 --- a/demo/adwaita-demo.c +++ b/demo/adwaita-demo.c @@ -2,6 +2,7 @@ #include #include +#include "adw-demo-debug-info.h" #include "adw-demo-preferences-window.h" #include "adw-demo-window.h" @@ -31,7 +32,7 @@ show_about (GSimpleAction *action, GVariant *state, gpointer user_data) { - const char *authors[] = { + const char *developers[] = { "Adrien Plazas", "Alexander Mikhaylenko", "Andrei Lișiță", @@ -42,38 +43,47 @@ show_about (GSimpleAction *action, NULL }; - const char *artists[] = { + const char *designers[] = { "GNOME Design Team", NULL - }; + }; GtkApplication *app = GTK_APPLICATION (user_data); GtkWindow *window = gtk_application_get_active_window (app); - char *version; - - version = g_strdup_printf ("%s\nRunning against libadwaita %d.%d.%d, GTK %d.%d.%d", - ADW_VERSION_S, - adw_get_major_version (), - adw_get_minor_version (), - adw_get_micro_version (), - gtk_get_major_version (), - gtk_get_minor_version (), - gtk_get_micro_version ()); - - gtk_show_about_dialog (window, - "program-name", _("Adwaita Demo"), - "title", _("About Adwaita Demo"), - "logo-icon-name", "org.gnome.Adwaita1.Demo", - "version", version, - "copyright", "Copyright © 2017–2021 Purism SPC", - "comments", _("Tour of the features in Libadwaita"), - "website", "https://gitlab.gnome.org/GNOME/libadwaita", - "license-type", GTK_LICENSE_LGPL_2_1, - "authors", authors, - "artists", artists, - "translator-credits", _("translator-credits"), - NULL); - g_free (version); + char *debug_info; + GtkWidget *about; + + debug_info = adw_demo_generate_debug_info (); + + about = + g_object_new (ADW_TYPE_ABOUT_WINDOW, + "transient-for", window, + "application-icon", "org.gnome.Adwaita1.Demo", + "application-name", _("Adwaita Demo"), + "developer-name", _("The GNOME Project"), + "version", ADW_VERSION_S, + "website", "https://gitlab.gnome.org/GNOME/libadwaita", + "issue-url", "https://gitlab.gnome.org/GNOME/libadwaita/-/issues/new", + "debug-info", debug_info, + "debug-info-filename", "adwaita-1-demo-debug-info.txt", + "copyright", "© 2017–2022 Purism SPC", + "license-type", GTK_LICENSE_LGPL_2_1, + "developers", developers, + "designers", designers, + "artists", designers, + "translator-credits", _("translator-credits"), + NULL); + + adw_about_window_add_link (ADW_ABOUT_WINDOW (about), + _("_Documentation"), + "https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/"); + adw_about_window_add_link (ADW_ABOUT_WINDOW (about), + _("_Chat"), + "https://matrix.to/#/#libadwaita:gnome.org"); + + gtk_window_present (GTK_WINDOW (about)); + + g_free (debug_info); } static void diff --git a/demo/adwaita-demo.gresources.xml b/demo/adwaita-demo.gresources.xml index b27a6b98..b307ecea 100644 --- a/demo/adwaita-demo.gresources.xml +++ b/demo/adwaita-demo.gresources.xml @@ -25,6 +25,7 @@ icons/scalable/actions/view-sidebar-start-symbolic-rtl.svg icons/scalable/actions/view-sidebar-end-symbolic.svg icons/scalable/actions/view-sidebar-end-symbolic-rtl.svg + icons/scalable/actions/widget-about-symbolic.svg icons/scalable/actions/widget-carousel-symbolic.svg icons/scalable/actions/widget-clamp-symbolic.svg icons/scalable/actions/widget-dialog-symbolic.svg @@ -35,6 +36,7 @@ icons/scalable/actions/widget-toast-symbolic.svg icons/scalable/actions/widget-view-switcher-symbolic.svg icons/scalable/apps/org.gnome.Boxes.svg + icons/scalable/apps/org.example.Typeset.svg icons/scalable/status/dark-mode-symbolic.svg icons/scalable/status/light-mode-symbolic.svg icons/scalable/status/tab-audio-playing-symbolic.svg @@ -43,6 +45,7 @@ style-dark.css + pages/about/adw-demo-page-about.ui pages/animations/adw-demo-page-animations.ui pages/avatar/adw-demo-page-avatar.ui pages/buttons/adw-demo-page-buttons.ui diff --git a/demo/icons/org.example.Typeset.Source.svg b/demo/icons/org.example.Typeset.Source.svg new file mode 100644 index 00000000..392d686a --- /dev/null +++ b/demo/icons/org.example.Typeset.Source.svg @@ -0,0 +1,3635 @@ + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + GNOME Design Team + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hicolor + Symbolic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + battery is full and there is no a/c connected. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/icons/scalable/actions/widget-about-symbolic.svg b/demo/icons/scalable/actions/widget-about-symbolic.svg new file mode 100644 index 00000000..12329841 --- /dev/null +++ b/demo/icons/scalable/actions/widget-about-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/demo/icons/scalable/apps/org.example.Typeset.svg b/demo/icons/scalable/apps/org.example.Typeset.svg new file mode 100644 index 00000000..b93b5b88 --- /dev/null +++ b/demo/icons/scalable/apps/org.example.Typeset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/meson.build b/demo/meson.build index 13280a64..d41d19e9 100644 --- a/demo/meson.build +++ b/demo/meson.build @@ -2,6 +2,17 @@ if get_option('examples') subdir('data') +demo_config_data = configuration_data() +demo_config_data.set_quoted('ADW_DEMO_VCS_TAG', '@VCS_TAG@') + +demo_config_h = vcs_tag( + input: configure_file( + output: 'config.h.in', + configuration: demo_config_data + ), + output: 'config.h' +) + adwaita_demo_resources = gnome.compile_resources( 'adwaita-demo-resources', 'adwaita-demo.gresources.xml', @@ -12,6 +23,7 @@ adwaita_demo_resources = gnome.compile_resources( adwaita_demo_sources = [ adwaita_demo_resources, + 'pages/about/adw-demo-page-about.c', 'pages/animations/adw-demo-page-animations.c', 'pages/avatar/adw-demo-page-avatar.c', 'pages/buttons/adw-demo-page-buttons.c', @@ -32,8 +44,10 @@ adwaita_demo_sources = [ 'pages/welcome/adw-demo-page-welcome.c', 'adwaita-demo.c', + 'adw-demo-debug-info.c', 'adw-demo-preferences-window.c', 'adw-demo-window.c', + demo_config_h, libadwaita_generated_headers, ] diff --git a/demo/pages/about/adw-demo-page-about.c b/demo/pages/about/adw-demo-page-about.c new file mode 100644 index 00000000..a7f67e05 --- /dev/null +++ b/demo/pages/about/adw-demo-page-about.c @@ -0,0 +1,99 @@ +#include "adw-demo-page-about.h" + +#include + +struct _AdwDemoPageAbout +{ + AdwBin parent_instance; +}; + +G_DEFINE_TYPE (AdwDemoPageAbout, adw_demo_page_about, ADW_TYPE_BIN) + +static void +demo_run_cb (AdwDemoPageAbout *self) +{ + GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self)); + GtkWidget *about; + + const char *developers[] = { + "Angela Avery ", + NULL + }; + + const char *artists[] = { + "GNOME Design Team", + NULL + }; + + const char *special_thanks[] = { + "My cat", + NULL + }; + + const char *release_notes = "\ +

\ + This release adds the following features:\ +

\ +
    \ +
  • Added a way to export fonts.
  • \ +
  • Better support for monospace fonts.
  • \ +
  • Added a way to preview italic text.
  • \ +
  • Bug fixes and performance improvements.
  • \ +
  • Translation updates.
  • \ +
\ + "; + + about = + g_object_new (ADW_TYPE_ABOUT_WINDOW, + "transient-for", root, + "application-icon", "org.example.Typeset", + "application-name", _("Typeset"), + "developer-name", _("Angela Avery"), + "version", "1.2.3", + "release-notes-version", "1.2.0", + "release-notes", release_notes, + "comments", _("Typeset is an app that doesn’t exist and is used as an example content for this about window."), + "website", "https://example.org", + "issue-url", "https://example.org", + "support-url", "https://example.org", + "copyright", "© 2022 Angela Avery", + "license-type", GTK_LICENSE_LGPL_2_1, + "developers", developers, + "artists", artists, + "translator-credits", _("translator-credits"), + NULL); + + adw_about_window_add_link (ADW_ABOUT_WINDOW (about), + _("_Documentation"), + "https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AboutWindow.html"); + + adw_about_window_add_legal_section (ADW_ABOUT_WINDOW (about), + _("Fonts"), + NULL, + GTK_LICENSE_CUSTOM, + "This application uses font data from somewhere."); + + adw_about_window_add_acknowledgement_section (ADW_ABOUT_WINDOW (about), + _("Special thanks to"), + special_thanks); + + gtk_window_present (GTK_WINDOW (about)); + + gtk_window_present (GTK_WINDOW (about)); +} + +static void +adw_demo_page_about_class_init (AdwDemoPageAboutClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Adwaita1/Demo/ui/pages/about/adw-demo-page-about.ui"); + + gtk_widget_class_install_action (widget_class, "demo.run", NULL, (GtkWidgetActionActivateFunc) demo_run_cb); +} + +static void +adw_demo_page_about_init (AdwDemoPageAbout *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} diff --git a/demo/pages/about/adw-demo-page-about.h b/demo/pages/about/adw-demo-page-about.h new file mode 100644 index 00000000..35c733c9 --- /dev/null +++ b/demo/pages/about/adw-demo-page-about.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define ADW_TYPE_DEMO_PAGE_ABOUT (adw_demo_page_about_get_type()) + +G_DECLARE_FINAL_TYPE (AdwDemoPageAbout, adw_demo_page_about, ADW, DEMO_PAGE_ABOUT, AdwBin) + +G_END_DECLS diff --git a/demo/pages/about/adw-demo-page-about.ui b/demo/pages/about/adw-demo-page-about.ui new file mode 100644 index 00000000..0430d6a2 --- /dev/null +++ b/demo/pages/about/adw-demo-page-about.ui @@ -0,0 +1,24 @@ + + + + + + diff --git a/doc/images/about-window-credits-dark.png b/doc/images/about-window-credits-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..51af7c310eb70660361ae13d2f3b9d8d662cc28c GIT binary patch literal 5416 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 literal 0 HcmV?d00001 diff --git a/doc/images/about-window-credits.png b/doc/images/about-window-credits.png new file mode 100644 index 0000000000000000000000000000000000000000..2491c0908ead0b38df44a420a2f7a7b7535422ab GIT binary patch literal 6542 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 Q$Np^Me9rxBokI}eUlk0~*Z=?k literal 0 HcmV?d00001 diff --git a/doc/images/about-window.png b/doc/images/about-window.png new file mode 100644 index 0000000000000000000000000000000000000000..da4b5d7aac2936d2e0e2a2f4765e0a799706458b GIT binary patch literal 23900 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 KRE_?H=>G#em`ynV literal 0 HcmV?d00001 diff --git a/doc/libadwaita.toml.in b/doc/libadwaita.toml.in index 5be4004a..27de1b24 100644 --- a/doc/libadwaita.toml.in +++ b/doc/libadwaita.toml.in @@ -54,6 +54,10 @@ content_files = [ "visual-index.md", ] content_images = [ + "images/about-window.png", + "images/about-window-dark.png", + "images/about-window-credits.png", + "images/about-window-credits-dark.png", "images/action-row.png", "images/action-row-dark.png", "images/adaptive-boxed-lists-narrow.png", diff --git a/doc/tools/data/about-window-credits.ui b/doc/tools/data/about-window-credits.ui new file mode 100644 index 00000000..5f8de338 --- /dev/null +++ b/doc/tools/data/about-window-credits.ui @@ -0,0 +1,44 @@ + + + + + + + 400 + + + 12 + 12 + 12 + 12 + + + Edgar Allan Poe + mailto:edgar@poe.com + True + + + adw-mail-send-symbolic + + + + + + + The GNOME Project + https://www.gnome.org + True + + + adw-external-link-symbolic + + + + + + + + + diff --git a/doc/tools/data/about-window.ui b/doc/tools/data/about-window.ui new file mode 100644 index 00000000..b43699f6 --- /dev/null +++ b/doc/tools/data/about-window.ui @@ -0,0 +1,14 @@ + + + + + + application-x-executable + Application + Developer Name + 1.2.3 + https://example.org + Author + © 2022 Example + + diff --git a/doc/visual-index.md b/doc/visual-index.md index 0a2f42d2..97079e66 100644 --- a/doc/visual-index.md +++ b/doc/visual-index.md @@ -95,6 +95,13 @@ Slug: visual-index preferences-window ](class.PreferencesWindow.html) +## About Window + +[ + + about-window +](class.AboutWindow.html) + ## Navigation ### Carousel diff --git a/po/POTFILES.in b/po/POTFILES.in index 31db95b2..1794c171 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,5 +1,7 @@ # List of source files containing translatable strings. # Please keep this file sorted alphabetically. +src/adw-about-window.c +src/adw-about-window.ui src/adw-entry-row.ui src/adw-inspector-page.c src/adw-inspector-page.ui diff --git a/src/adw-about-window.c b/src/adw-about-window.c new file mode 100644 index 00000000..d6c43584 --- /dev/null +++ b/src/adw-about-window.c @@ -0,0 +1,3222 @@ +/* + * Copyright (C) 2021-2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" +#include + +#include "adw-about-window.h" + +#include "adw-action-row.h" +#include "adw-leaflet.h" +#include "adw-macros-private.h" +#include "adw-message-dialog.h" +#include "adw-preferences-group.h" +#include "adw-toast-overlay.h" + +#define HEADERBAR_STYLE_THRESHOLD 190 + +/** + * AdwAboutWindow: + * + * A window showing information about the application. + * + * + * + * about-window + * + * + * An about window is typically opened when the user activates the `About …` + * item in the application's primary menu. All parts of the window are optional. + * + * ## Main page + * + * `AdwAboutWindow` prominently displays the application's icon, name, developer + * name and version. They can be set with the [property@AboutWindow:application-icon], + * [property@AboutWindow:application-name], + * [property@AboutWindow:developer-name] and [property@AboutWindow:version] + * respectively. + * + * ## What's New + * + * `AdwAboutWindow` provides a way for applications to display their release + * notes, set with the [property@AboutWindow:release-notes] property. + * + * Release notes are formatted the same way as + * [AppStream descriptions](https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-description). + * + * The supported formatting options are: + * + * * Paragraph (`

`) + * * Ordered list (`

    `), with list items (`
  1. `) + * * Unordered list (`
      `), with list items (`
    • `) + * + * Within paragraphs and list items, emphasis (``) and inline code + * (``) text styles are supported. The emphasis is rendered in italic, + * while inline code is shown in a monospaced font. + * + * Any text outside paragraphs or list items is ignored. + * + * Nested lists are not supported. + * + * Only one version can be shown at a time. By default, the displayed version + * number matches [property@AboutWindow:version]. Use + * [property@AboutWindow:release-notes-version] to override it. + * + * ## Details + * + * The Details page displays the application comments and links. + * + * The comments can be set with the [property@AboutWindow:comments] property. + * Unlike [property@Gtk.AboutDialog:comments], this string can be long and + * detailed. It can also contain links and Pango markup. + * + * To set the application website, use [property@AboutWindow:website]. + * To add extra links below the website, use [method@AboutWindow.add_link]. + * + * If the Details page doesn't have any other content besides website, the + * website will be displayed on the main page instead. + * + * ## Troubleshooting + * + * `AdwAboutWindow` displays the following two links on the main page: + * + * * Support Questions, set with the [property@AboutWindow:support-url] property, + * * Report an Issue, set with the [property@AboutWindow:issue-url] property. + * + * Additionally, applications can provide debugging information. It will be + * shown separately on the Troubleshooting page. Use the + * [property@AboutWindow:debug-info] property to specify it. + * + * It's intended to be attached to issue reports when reporting issues against + * the application. As such, it cannot contain markup or links. + * + * `AdwAboutWindow` provides a quick way to save debug information to a file. + * When saving, [property@AboutWindow:debug-info-filename] would be used as + * the suggested filename. + * + * ## Credits and Acknowledgements + * + * The Credits page has the following default sections: + * + * * Developers, set with the [property@AboutWindow:developers] property, + * * Designers, set with the [property@AboutWindow:designers] property, + * * Artists, set with the [property@AboutWindow:artists] property, + * * Documenters, set with the [property@AboutWindow:documenters] property, + * * Translators, set with the [property@AboutWindow:translator-credits] property. + * + * When setting translator credits, use the strings `"translator-credits"` or + * `"translator_credits"` and mark them as translatable. + * + * The default sections that don't contain any names won't be displayed. + * + * The Credits page can also contain an arbitrary number of extra sections below + * the default ones. Use [method@AboutWindow.add_credit_section] to add them. + * + * The Acknowledgements page can be used to acknowledge additional people and + * organizations for their non-development contributions. Use + * [method@AboutWindow.add_acknowledgement_section] to add sections to it. For + * example, it can be used to list backers in a crowdfunded project or to give + * special thanks. + * + * Each of the people or organizations can have an email address or a website + * specified. To add a email address, use a string like + * `Edgar Allan Poe `. To specify a website with a title, use a + * string like `The GNOME Project https://www.gnome.org`: + * + * + * + * about-window-credits + * + * + * ## Legal + * + * The Legal page displays the copyright and licensing information for the + * application and other modules. + * + * The copyright string is set with the [property@AboutWindow:copyright] + * property and should be a short string of one or two lines, for example: + * `© 2022 Example`. + * + * Licensing information can be quickly set from a list of known licenses with + * the [property@AboutWindow:license-type] property. If the application's + * license is not in the list, [property@AboutWindow:license] can be used + * instead. + * + * To add information about other modules, such as application dependencies or + * data, use [method@AboutWindow.add_legal_section]. + * + * ## Constructing + * + * To make constructing an `AdwAboutWindow` as convenient as possible, you can + * use the function [func@show_about_window] which constructs and shows a + * window. + * + * ```c + * static void + * show_about (GtkApplication *app) + * { + * const char *developers[] = { + * "Angela Avery", + * NULL + * }; + * + * const char *designers[] = { + * "GNOME Design Team", + * NULL + * }; + * + * adw_show_about_window (gtk_application_get_active_window (app), + * "application-name", _("Example"), + * "application-icon", "org.example.App", + * "version", "1.2.3", + * "copyright", "© 2022 Angela Avery", + * "issue-url", "https://gitlab.gnome.org/example/example/-/issues/new", + * "license-type", GTK_LICENSE_GPL_3_0, + * "developers", developers, + * "designers", designers, + * "translator-credits", _("translator-credits"), + * NULL); + * } + * ``` + * + * ## CSS nodes + * + * `AdwAboutWindow` has a main CSS node with the name `window` and the + * style class `.about`. + * + * Since: 1.2 + */ + +/* Copied from GTK 4 for consistency with GtkAboutDialog. */ +typedef struct { + const char *name; + const char *url; +} LicenseInfo; + +/* Copied from GTK 4 for consistency with GtkAboutDialog. */ +/* LicenseInfo for each GtkLicense type; keep in the same order as the + * enumeration. */ +static const LicenseInfo gtk_license_info [] = { + { NULL, NULL }, + { NULL, NULL }, + { N_("GNU General Public License, version 2 or later"), "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { N_("GNU General Public License, version 3 or later"), "https://www.gnu.org/licenses/gpl-3.0.html" }, + { N_("GNU Lesser General Public License, version 2.1 or later"), "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, + { N_("GNU Lesser General Public License, version 3 or later"), "https://www.gnu.org/licenses/lgpl-3.0.html" }, + { N_("BSD 2-Clause License"), "https://opensource.org/licenses/bsd-license.php" }, + { N_("The MIT License (MIT)"), "https://opensource.org/licenses/mit-license.php" }, + { N_("Artistic License 2.0"), "https://opensource.org/licenses/artistic-license-2.0.php" }, + { N_("GNU General Public License, version 2 only"), "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { N_("GNU General Public License, version 3 only"), "https://www.gnu.org/licenses/gpl-3.0.html" }, + { N_("GNU Lesser General Public License, version 2.1 only"), "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, + { N_("GNU Lesser General Public License, version 3 only"), "https://www.gnu.org/licenses/lgpl-3.0.html" }, + { N_("GNU Affero General Public License, version 3 or later"), "https://www.gnu.org/licenses/agpl-3.0.html" }, + { N_("GNU Affero General Public License, version 3 only"), "https://www.gnu.org/licenses/agpl-3.0.html" }, + { N_("BSD 3-Clause License"), "https://opensource.org/licenses/BSD-3-Clause" }, + { N_("Apache License, Version 2.0"), "https://opensource.org/licenses/Apache-2.0" }, + { N_("Mozilla Public License 2.0"), "https://opensource.org/licenses/MPL-2.0" } +}; +/* Copied from GTK 4 for consistency with GtkAboutDialog. */ +/* Keep this static assertion updated with the last element of the enumeration, + * and make sure it matches the last element of the array. */ +G_STATIC_ASSERT (G_N_ELEMENTS (gtk_license_info) - 1 == GTK_LICENSE_MPL_2_0); + +typedef struct { + char *name; + char **people; +} CreditsSection; + +typedef struct { + char *title; + char *copyright; + char *license; + GtkLicense license_type; +} LegalSection; + +struct _AdwAboutWindow { + AdwWindow parent_instance; + + GtkWidget *leaflet; + GtkWidget *subpage_stack; + GtkWidget *toast_overlay; + GtkWidget *website_row; + GtkWidget *links_group; + GtkWidget *details_website_row; + GtkWidget *credits_box; + GtkWidget *legal_box; + GtkWidget *acknowledgements_box; + GtkWidget *debug_info_page; + GtkTextBuffer *release_notes_buffer; + + char *application_icon; + char *application_name; + char *developer_name; + char *version; + char *release_notes_version; + char *release_notes; + char *comments; + char *website; + char *support_url; + char *issue_url; + char *debug_info; + char *debug_info_filename; + char **developers; + char **designers; + char **artists; + char **documenters; + char *translator_credits; + GSList *credit_sections; + char *copyright; + char *license; + GtkLicense license_type; + GSList *legal_sections; + gboolean has_custom_links; +}; + +G_DEFINE_TYPE (AdwAboutWindow, adw_about_window, ADW_TYPE_WINDOW) + +enum { + PROP_0, + PROP_APPLICATION_ICON, + PROP_APPLICATION_NAME, + PROP_DEVELOPER_NAME, + PROP_VERSION, + PROP_RELEASE_NOTES_VERSION, + PROP_RELEASE_NOTES, + PROP_COMMENTS, + PROP_WEBSITE, + PROP_SUPPORT_URL, + PROP_ISSUE_URL, + PROP_DEBUG_INFO, + PROP_DEBUG_INFO_FILENAME, + PROP_DEVELOPERS, + PROP_DESIGNERS, + PROP_ARTISTS, + PROP_DOCUMENTERS, + PROP_TRANSLATOR_CREDITS, + PROP_COPYRIGHT, + PROP_LICENSE_TYPE, + PROP_LICENSE, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_ACTIVATE_LINK, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +static void +free_credit_section (CreditsSection *section) +{ + g_free (section->name); + g_strfreev (section->people); + g_free (section); +} + +static void +free_legal_section (LegalSection *section) +{ + g_free (section->title); + g_free (section->license); + g_free (section); +} + +static gboolean +boolean_or (AdwAboutWindow *self, + int number, + ...) +{ + va_list args; + + va_start (args, number); + + for (gsize i = 0; i < number; i++) + if (va_arg (args, gboolean)) { + va_end (args); + + return TRUE; + } + + va_end (args); + + return FALSE; +} + +static gboolean +string_is_not_empty (AdwAboutWindow *self, + const char *string) +{ + return string && string[0]; +} + +static char * +get_headerbar_name (AdwAboutWindow *self, + double value) +{ + double threshold = 0; + + if (self->application_icon && *self->application_icon && + self->application_name && *self->application_name) + threshold = HEADERBAR_STYLE_THRESHOLD; + + return g_strdup (value > threshold ? "regular" : "top"); +} + +static inline void +activate_link (AdwAboutWindow *self, + const char *uri) +{ + gboolean ret = FALSE; + g_signal_emit (self, signals[SIGNAL_ACTIVATE_LINK], 0, uri, &ret); +} + +static gboolean +activate_link_cb (AdwAboutWindow *self, + const char *uri) +{ + activate_link (self, uri); + + return GDK_EVENT_STOP; +} + +static gboolean +activate_link_default_cb (AdwAboutWindow *self, + const char *uri) +{ + /* FIXME: We need our own alternative that uses AdwMessageDialog for errors */ + gtk_show_uri (GTK_WINDOW (self), uri, GDK_CURRENT_TIME); + + return GDK_EVENT_STOP; +} + +/* Adapted from text_buffer_new() in gtkaboutdialog.c */ +static void +parse_person (char *person, + char **name, + char **link, + gboolean *is_email) +{ + + const char *q1, *q2, *r1, *r2; + + q1 = strchr (person, '<'); + q2 = q1 ? strchr (q1, '>') : NULL; + r1 = strstr (person, "http://"); + r2 = strstr (person, "https://"); + + if (!r1 || (r1 && r2 && r2 < r1)) + r1 = r2; + if (r1) { + r2 = strpbrk (r1, " \n\t>"); + if (!r2) + r2 = strchr (r1, '\0'); + } else { + r2 = NULL; + } + + if (r1 && r2 && (!q1 || !q2 || (r1 <= q1 + 1))) { + q1 = r1; + q2 = r2; + } + + if (q1 && q2) { + *name = g_strndup (person, q1 - person); + *is_email = (*q1 == '<'); + + if (*is_email) + *link = g_strndup (q1 + 1, q2 - q1 - 1); + else + *link = g_strndup (q1, q2 - q1); + } else { + *name = g_strdup (person); + *link = NULL; + *is_email = FALSE; + } + + g_strstrip (*name); +} + +static void +add_credits_section (GtkWidget *box, + const char *title, + char **people) +{ + GtkWidget *group; + char **person; + + if (!people || !*people) + return; + + group = adw_preferences_group_new (); + adw_preferences_group_set_title (ADW_PREFERENCES_GROUP (group), title); + + for (person = people; *person; person++) { + GtkWidget *row; + char *name = NULL; + char *link = NULL; + gboolean is_email = FALSE; + + if (!*person) + continue; + + parse_person (*person, &name, &link, &is_email); + + row = adw_action_row_new (); + adw_preferences_row_set_use_markup (ADW_PREFERENCES_ROW (row), FALSE); + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), name); + adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), row); + + if (link) { + GtkWidget *image = gtk_image_new (); + + if (is_email) + gtk_image_set_from_icon_name (GTK_IMAGE (image), "adw-mail-send-symbolic"); + else + gtk_image_set_from_icon_name (GTK_IMAGE (image), "adw-external-link-symbolic"); + + adw_action_row_add_suffix (ADW_ACTION_ROW (row), image); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE); + gtk_actionable_set_action_name (GTK_ACTIONABLE (row), "about.show-url"); + + if (is_email) { + char *escaped = g_uri_escape_string (link, NULL, FALSE); + char *mailto = g_strconcat ("mailto:", escaped, NULL); + + gtk_actionable_set_action_target (GTK_ACTIONABLE (row), "s", mailto); + + g_free (mailto); + g_free (escaped); + } else { + gtk_actionable_set_action_target (GTK_ACTIONABLE (row), "s", link); + } + + gtk_widget_set_tooltip_text (row, link); + } + + g_free (name); + g_free (link); + } + + gtk_box_append (GTK_BOX (box), group); +} + +static void +update_credits (AdwAboutWindow *self) +{ + GtkWidget *widget; + char **translators; + GSList *l; + + while ((widget = gtk_widget_get_first_child (self->credits_box))) + gtk_box_remove (GTK_BOX (self->credits_box), widget); + + if (self->translator_credits && + g_strcmp0 (self->translator_credits, "translator_credits") && + g_strcmp0 (self->translator_credits, "translator-credits")) + translators = g_strsplit (self->translator_credits, "\n", 0); + else + translators = NULL; + + add_credits_section (self->credits_box, _("Code by"), self->developers); + add_credits_section (self->credits_box, _("Design by"), self->designers); + add_credits_section (self->credits_box, _("Artwork by"), self->artists); + add_credits_section (self->credits_box, _("Documentation by"), self->documenters); + add_credits_section (self->credits_box, _("Translated by"), translators); + + for (l = self->credit_sections; l; l = l->next) { + CreditsSection *section = l->data; + + add_credits_section (self->credits_box, section->name, section->people); + } + + g_strfreev (translators); + + gtk_widget_set_visible (self->credits_box, + !!gtk_widget_get_first_child (self->credits_box)); +} + +static char * +get_license_text (GtkLicense license_type, + const char *license) +{ + if (license_type == GTK_LICENSE_UNKNOWN) + return NULL; + + if (license_type == GTK_LICENSE_CUSTOM) + return g_strdup (license); + + /* Translators: this is the license preamble; the string at the end + * contains the name of the license as link text. + */ + return g_strdup_printf (_("This application comes with absolutely no warranty. See the %s for details."), + gtk_license_info[license_type].url, + _(gtk_license_info[license_type].name)); +} + +static void +append_legal_section (AdwAboutWindow *self, + LegalSection *section, + gboolean force_title) +{ + GtkWidget *label; + char *license; + + if (force_title) + g_assert (section->title); + + license = get_license_text (section->license_type, section->license); + + if ((!section->copyright || !*section->copyright) && + (!license || !*license) && !force_title) { + g_free (license); + + return; + } + + if (section->title && *section->title) { + label = gtk_label_new (section->title); + gtk_label_set_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_wrap_mode (GTK_LABEL (label), PANGO_WRAP_WORD_CHAR); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_widget_add_css_class (label, "heading"); + gtk_box_append (GTK_BOX (self->legal_box), label); + } + + if ((!section->copyright || !*section->copyright) && + (!license || !*license)) { + g_free (license); + + return; + } + + label = gtk_label_new (NULL); + gtk_label_set_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_wrap_mode (GTK_LABEL (label), PANGO_WRAP_WORD_CHAR); + gtk_label_set_xalign (GTK_LABEL (label), 0); + gtk_label_set_selectable (GTK_LABEL (label), TRUE); + gtk_widget_add_css_class (label, "body"); + g_signal_connect_swapped (label, "activate-link", G_CALLBACK (activate_link_cb), self); + + if (section->copyright && *section->copyright && license && *license) { + char *text = g_strconcat (section->copyright, "\n\n", license, NULL); + + gtk_label_set_markup (GTK_LABEL (label), text); + + g_free (text); + } else if (section->copyright && *section->copyright) { + gtk_label_set_markup (GTK_LABEL (label), section->copyright); + } else { + gtk_label_set_markup (GTK_LABEL (label), license); + } + + gtk_box_append (GTK_BOX (self->legal_box), label); + + g_free (license); +} + +static void +update_legal (AdwAboutWindow *self) +{ + LegalSection default_section; + GtkWidget *widget; + GSList *l; + + while ((widget = gtk_widget_get_first_child (self->legal_box))) + gtk_box_remove (GTK_BOX (self->legal_box), widget); + + /* We only want to show the default title if there's more than one section */ + if (self->legal_sections) + default_section.title = _("This Application"); + else + default_section.title = NULL; + + default_section.copyright = self->copyright; + default_section.license_type = self->license_type; + default_section.license = self->license; + append_legal_section (self, &default_section, FALSE); + + for (l = self->legal_sections; l; l = l->next) + append_legal_section (self, l->data, TRUE); + + gtk_widget_set_visible (self->legal_box, + !!gtk_widget_get_first_child (self->legal_box)); +} + +typedef enum { + STATE_NONE, + STATE_PARAGRAPH, + STATE_UNORDERED_LIST, + STATE_UNORDERED_ITEM, + STATE_ORDERED_LIST, + STATE_ORDERED_ITEM, +} ReleaseNotesState; + +typedef struct { + GtkTextBuffer *buffer; + GtkTextIter iter; + + ReleaseNotesState state; + int n_item; + int section_start; + int paragraph_start; + gboolean last_trailing_space; +} ReleaseNotesParserData; + +static void +start_element_handler (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + gpointer user_data, + GError **error) +{ + ReleaseNotesParserData *pdata = user_data; + + switch (pdata->state) { + case STATE_NONE: + if (!g_strcmp0 (element_name, "p")) { + pdata->state = STATE_PARAGRAPH; + pdata->paragraph_start = gtk_text_iter_get_offset (&pdata->iter); + } + + if (!g_strcmp0 (element_name, "ul")) + pdata->state = STATE_UNORDERED_LIST; + + if (!g_strcmp0 (element_name, "ol")) + pdata->state = STATE_ORDERED_LIST; + + if (pdata->state != STATE_NONE) { + pdata->section_start = gtk_text_iter_get_offset (&pdata->iter); + break; + } + + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unexpected element '%s'", + element_name); + break; + + case STATE_PARAGRAPH: + case STATE_UNORDERED_ITEM: + case STATE_ORDERED_ITEM: + if (!g_strcmp0 (element_name, "em") || + !g_strcmp0 (element_name, "code")) { + break; + } + + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unexpected element '%s'", + element_name); + break; + + case STATE_UNORDERED_LIST: + case STATE_ORDERED_LIST: + if (!g_strcmp0 (element_name, "li")) { + char *bullet; + + if (pdata->n_item > 1) + gtk_text_buffer_insert (pdata->buffer, &pdata->iter, "\n", -1); + + if (pdata->state == STATE_ORDERED_LIST) { + pdata->state = STATE_ORDERED_ITEM; + bullet = g_strdup_printf ("%d. ", pdata->n_item); + } else { + pdata->state = STATE_UNORDERED_ITEM; + bullet = g_strdup ("• "); + } + + gtk_text_buffer_insert_with_tags_by_name (pdata->buffer, &pdata->iter, bullet, -1, "bullet", NULL); + pdata->paragraph_start = gtk_text_iter_get_offset (&pdata->iter); + + g_free (bullet); + + break; + } + + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unexpected element '%s'", + element_name); + break; + + default: + g_assert_not_reached (); + } + + if (!g_strcmp0 (element_name, "em") || + !g_strcmp0 (element_name, "code") || + !g_strcmp0 (element_name, "ul") || + !g_strcmp0 (element_name, "ol") || + !g_strcmp0 (element_name, "li")) { + g_markup_collect_attributes (element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_INVALID, + NULL); + return; + } + + /* We don't support attributes anywhere, so error out if we encounter any. */ + g_markup_collect_attributes (element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_INVALID, + NULL); +} + +static void +end_element_handler (GMarkupParseContext *context, + const char *element_name, + gpointer user_data, + GError **error) +{ + ReleaseNotesParserData *pdata = user_data; + + if (!g_strcmp0 (element_name, "p") || + !g_strcmp0 (element_name, "ul") || + !g_strcmp0 (element_name, "ol")) { + + if (pdata->section_start != gtk_text_iter_get_offset (&pdata->iter)) { + gtk_text_buffer_insert (pdata->buffer, &pdata->iter, "\n", -1); + + if (pdata->section_start > 0 && !g_strcmp0 (element_name, "p")) { + GtkTextIter start_iter; + gtk_text_buffer_get_iter_at_offset (pdata->buffer, &start_iter, pdata->section_start); + gtk_text_buffer_apply_tag_by_name (pdata->buffer, "section", &start_iter, &pdata->iter); + } + } + + pdata->state = STATE_NONE; + pdata->section_start = -1; + pdata->paragraph_start = -1; + pdata->n_item = 1; + return; + } + + if (!g_strcmp0 (element_name, "li")) { + if (pdata->state == STATE_UNORDERED_ITEM) + pdata->state = STATE_UNORDERED_LIST; + else if (pdata->state == STATE_ORDERED_ITEM) + pdata->state = STATE_ORDERED_LIST; + else + g_assert_not_reached (); + + if (pdata->section_start > 0 && pdata->n_item == 1) { + GtkTextIter start_iter; + gtk_text_buffer_get_iter_at_offset (pdata->buffer, &start_iter, + pdata->section_start); + gtk_text_buffer_apply_tag_by_name (pdata->buffer, "section", &start_iter, &pdata->iter); + } + + pdata->n_item++; + pdata->paragraph_start = -1; + return; + } +} + +static void +text_handler (GMarkupParseContext *context, + const char *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + ReleaseNotesParserData *pdata = user_data; + const char *element; + char *escaped; + static GRegex *regex = NULL; + gboolean leading_space; + gboolean trailing_space; + + if (pdata->state != STATE_PARAGRAPH && + pdata->state != STATE_UNORDERED_ITEM && + pdata->state != STATE_ORDERED_ITEM) + return; + + /* Replace random amounts of spaces and newlines with single spaces */ + if (!regex) { + GError *regex_error = NULL; + + regex = g_regex_new ("\\s+", G_REGEX_OPTIMIZE, 0, ®ex_error); + + if (regex_error) { + g_error ("Couldn't compile regex: %s", regex_error->message); + g_error_free (regex_error); + } + } + + element = g_markup_parse_context_get_element (context); + + escaped = g_regex_replace_literal (regex, text, text_len, 0, " ", 0, error); + + if (*error) + return; + + if (!*escaped) { + g_free (escaped); + return; + } + + leading_space = *escaped == ' '; + trailing_space = escaped[strlen (escaped) - 1] == ' '; + + g_strstrip (escaped); + + /* This might have made the string empty, skip it in that case */ + if (!*escaped) { + g_free (escaped); + pdata->last_trailing_space = trailing_space; + return; + } + + /* We've got rid of inner spaces before and . Bring them back */ + if ((leading_space || pdata->last_trailing_space) && + pdata->paragraph_start != gtk_text_iter_get_offset (&pdata->iter)) + gtk_text_buffer_insert (pdata->buffer, &pdata->iter, " ", -1); + + if (!g_strcmp0 (element, "em") || !g_strcmp0 (element, "code")) + gtk_text_buffer_insert_with_tags_by_name (pdata->buffer, &pdata->iter, + escaped, -1, element, NULL); + else + gtk_text_buffer_insert (pdata->buffer, &pdata->iter, escaped, -1); + + pdata->last_trailing_space = trailing_space; + + g_free (escaped); +} + +static const GMarkupParser markup_parser = +{ + start_element_handler, + end_element_handler, + text_handler, + NULL, + NULL +}; + +static void +update_release_notes (AdwAboutWindow *self) +{ + GMarkupParseContext *context; + ReleaseNotesParserData pdata; + GError *error = NULL; + GtkTextIter end_iter; + const char *version = NULL; + + gtk_text_buffer_set_text (self->release_notes_buffer, "", -1); + + if (!self->release_notes || !*self->release_notes) + return; + + pdata.buffer = self->release_notes_buffer; + gtk_text_buffer_get_start_iter (self->release_notes_buffer, &pdata.iter); + + if (self->release_notes_version && *self->release_notes_version) + version = self->release_notes_version; + else if (self->version && *self->version) + version = self->version; + + if (version) { + char *heading = g_strdup_printf (_("Version %s"), version); + + gtk_text_buffer_insert_with_tags_by_name (self->release_notes_buffer, + &pdata.iter, heading, -1, + "heading", NULL); + gtk_text_buffer_insert (self->release_notes_buffer, &pdata.iter, "\n", -1); + + g_free (heading); + } + + pdata.state = STATE_NONE; + pdata.n_item = 0; + pdata.last_trailing_space = FALSE; + context = g_markup_parse_context_new (&markup_parser, 0, &pdata, NULL); + + if (!g_markup_parse_context_parse (context, self->release_notes, -1, &error) || + !g_markup_parse_context_end_parse (context, &error)) { + int line_number, char_number; + char *position; + g_markup_parse_context_get_position (context, &line_number, &char_number); + + g_critical ("Unable to parse release notes: %s at line %d, char %d", error->message, line_number, char_number); + + gtk_text_buffer_set_text (self->release_notes_buffer, "", -1); + gtk_text_buffer_get_start_iter (self->release_notes_buffer, &pdata.iter); + + gtk_text_buffer_insert (self->release_notes_buffer, &pdata.iter, _("Unable to parse release notes:"), -1); + gtk_text_buffer_insert (self->release_notes_buffer, &pdata.iter, "\n", -1); + + gtk_text_buffer_insert (self->release_notes_buffer, &pdata.iter, error->message, -1); + gtk_text_buffer_insert (self->release_notes_buffer, &pdata.iter, "\n", -1); + + position = g_strdup_printf (_("Line: %d, character: %d"), line_number, char_number); + gtk_text_buffer_insert (self->release_notes_buffer, &pdata.iter, position, -1); + + g_markup_parse_context_free (context); + g_error_free (error); + g_free (position); + + return; + } + + gtk_text_iter_backward_chars (&pdata.iter, 2); + gtk_text_buffer_get_end_iter (self->release_notes_buffer, &end_iter); + gtk_text_buffer_delete (self->release_notes_buffer, &pdata.iter, &end_iter); + + g_markup_parse_context_free (context); +} + +static void +update_links (AdwAboutWindow *self) +{ + gboolean has_website = self->website && *self->website; + gboolean has_comments = self->comments && *self->comments; + gboolean show_details = has_comments || self->has_custom_links; + + gtk_widget_set_visible (self->website_row, has_website && !show_details); + + gtk_widget_set_visible (self->details_website_row, has_website && show_details); + gtk_widget_set_visible (self->links_group, + (has_website && has_comments) || self->has_custom_links); +} + +static void +adw_about_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + AdwAboutWindow *self = ADW_ABOUT_WINDOW (object); + + switch (prop_id) { + case PROP_APPLICATION_ICON: + g_value_set_string (value, adw_about_window_get_application_icon (self)); + break; + case PROP_APPLICATION_NAME: + g_value_set_string (value, adw_about_window_get_application_name (self)); + break; + case PROP_DEVELOPER_NAME: + g_value_set_string (value, adw_about_window_get_developer_name (self)); + break; + case PROP_VERSION: + g_value_set_string (value, adw_about_window_get_version (self)); + break; + case PROP_RELEASE_NOTES_VERSION: + g_value_set_string (value, adw_about_window_get_release_notes_version (self)); + break; + case PROP_RELEASE_NOTES: + g_value_set_string (value, adw_about_window_get_release_notes (self)); + break; + case PROP_COMMENTS: + g_value_set_string (value, adw_about_window_get_comments (self)); + break; + case PROP_WEBSITE: + g_value_set_string (value, adw_about_window_get_website (self)); + break; + case PROP_SUPPORT_URL: + g_value_set_string (value, adw_about_window_get_support_url (self)); + break; + case PROP_ISSUE_URL: + g_value_set_string (value, adw_about_window_get_issue_url (self)); + break; + case PROP_DEBUG_INFO: + g_value_set_string (value, adw_about_window_get_debug_info (self)); + break; + case PROP_DEBUG_INFO_FILENAME: + g_value_set_string (value, adw_about_window_get_debug_info_filename (self)); + break; + case PROP_DEVELOPERS: + g_value_set_boxed (value, adw_about_window_get_developers (self)); + break; + case PROP_DESIGNERS: + g_value_set_boxed (value, adw_about_window_get_designers (self)); + break; + case PROP_ARTISTS: + g_value_set_boxed (value, adw_about_window_get_artists (self)); + break; + case PROP_DOCUMENTERS: + g_value_set_boxed (value, adw_about_window_get_documenters (self)); + break; + case PROP_TRANSLATOR_CREDITS: + g_value_set_string (value, adw_about_window_get_translator_credits (self)); + break; + case PROP_COPYRIGHT: + g_value_set_string (value, adw_about_window_get_copyright (self)); + break; + case PROP_LICENSE_TYPE: + g_value_set_enum (value, adw_about_window_get_license_type (self)); + break; + case PROP_LICENSE: + g_value_set_string (value, adw_about_window_get_license (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_about_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + AdwAboutWindow *self = ADW_ABOUT_WINDOW (object); + + switch (prop_id) { + case PROP_APPLICATION_ICON: + adw_about_window_set_application_icon (self, g_value_get_string (value)); + break; + case PROP_APPLICATION_NAME: + adw_about_window_set_application_name (self, g_value_get_string (value)); + break; + case PROP_DEVELOPER_NAME: + adw_about_window_set_developer_name (self, g_value_get_string (value)); + break; + case PROP_VERSION: + adw_about_window_set_version (self, g_value_get_string (value)); + break; + case PROP_RELEASE_NOTES_VERSION: + adw_about_window_set_release_notes_version (self, g_value_get_string (value)); + break; + case PROP_RELEASE_NOTES: + adw_about_window_set_release_notes (self, g_value_get_string (value)); + break; + case PROP_COMMENTS: + adw_about_window_set_comments (self, g_value_get_string (value)); + break; + case PROP_WEBSITE: + adw_about_window_set_website (self, g_value_get_string (value)); + break; + case PROP_SUPPORT_URL: + adw_about_window_set_support_url (self, g_value_get_string (value)); + break; + case PROP_ISSUE_URL: + adw_about_window_set_issue_url (self, g_value_get_string (value)); + break; + case PROP_DEBUG_INFO: + adw_about_window_set_debug_info (self, g_value_get_string (value)); + break; + case PROP_DEBUG_INFO_FILENAME: + adw_about_window_set_debug_info_filename (self, g_value_get_string (value)); + break; + case PROP_DEVELOPERS: + adw_about_window_set_developers (self, g_value_get_boxed (value)); + break; + case PROP_DESIGNERS: + adw_about_window_set_designers (self, g_value_get_boxed (value)); + break; + case PROP_DOCUMENTERS: + adw_about_window_set_documenters (self, g_value_get_boxed (value)); + break; + case PROP_ARTISTS: + adw_about_window_set_artists (self, g_value_get_boxed (value)); + break; + case PROP_TRANSLATOR_CREDITS: + adw_about_window_set_translator_credits (self, g_value_get_string (value)); + break; + case PROP_COPYRIGHT: + adw_about_window_set_copyright (self, g_value_get_string (value)); + break; + case PROP_LICENSE_TYPE: + adw_about_window_set_license_type (self, g_value_get_enum (value)); + break; + case PROP_LICENSE: + adw_about_window_set_license (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_about_window_finalize (GObject *object) +{ + AdwAboutWindow *self = ADW_ABOUT_WINDOW (object); + + g_free (self->application_icon); + g_free (self->application_name); + g_free (self->developer_name); + g_free (self->version); + g_free (self->release_notes_version); + g_free (self->release_notes); + g_free (self->comments); + g_free (self->website); + g_free (self->support_url); + g_free (self->issue_url); + g_free (self->debug_info); + g_free (self->debug_info_filename); + + g_strfreev (self->developers); + g_strfreev (self->designers); + g_strfreev (self->artists); + g_strfreev (self->documenters); + g_free (self->translator_credits); + g_slist_free_full (self->credit_sections, (GDestroyNotify) free_credit_section); + + g_free (self->copyright); + g_free (self->license); + g_slist_free_full (self->legal_sections, (GDestroyNotify) free_legal_section); + + G_OBJECT_CLASS (adw_about_window_parent_class)->finalize (object); +} + +static void +back_cb (AdwAboutWindow *self) +{ + adw_leaflet_navigate (ADW_LEAFLET (self->leaflet), ADW_NAVIGATION_DIRECTION_BACK); +} + +static void +subpage_cb (AdwAboutWindow *self, + const char *action_name, + GVariant *params) +{ + const char *name = g_variant_get_string (params, NULL); + + gtk_stack_set_visible_child_name (GTK_STACK (self->subpage_stack), name); + adw_leaflet_navigate (ADW_LEAFLET (self->leaflet), ADW_NAVIGATION_DIRECTION_FORWARD); +} + +static void +show_url_cb (AdwAboutWindow *self, + const char *action_name, + GVariant *params) +{ + const char *url = g_variant_get_string (params, NULL); + + activate_link (self, url); +} + +static void +show_url_property_cb (AdwAboutWindow *self, + const char *action_name, + GVariant *params) +{ + const char *property = g_variant_get_string (params, NULL); + char *url; + + g_object_get (self, property, &url, NULL); + + activate_link (self, url); + + g_free (url); +} + +static void +copy_property_cb (AdwAboutWindow *self, + const char *action_name, + GVariant *params) +{ + const char *property = g_variant_get_string (params, NULL); + char *value; + + g_object_get (self, property, &value, NULL); + + if (value && *value) { + GdkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (self)); + + gdk_clipboard_set_text (clipboard, value); + + adw_toast_overlay_add_toast (ADW_TOAST_OVERLAY (self->toast_overlay), + adw_toast_new (_("Copied to clipboard"))); + } + + g_free (value); +} + +static void +debug_cb (AdwAboutWindow *self) +{ + adw_leaflet_navigate (ADW_LEAFLET (self->leaflet), ADW_NAVIGATION_DIRECTION_FORWARD); +} + +static void +save_debug_info_response_cb (GtkFileChooser *chooser, + GtkResponseType response, + AdwAboutWindow *self) +{ + gtk_native_dialog_hide (GTK_NATIVE_DIALOG (chooser)); + + if (response == GTK_RESPONSE_ACCEPT) { + GFile *file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (chooser)); + GError *error = NULL; + + g_file_replace_contents (file, + self->debug_info, + strlen (self->debug_info), + NULL, + FALSE, + G_FILE_CREATE_NONE, + NULL, + NULL, + &error); + + if (error) { + GtkWidget *dialog = adw_message_dialog_new (GTK_WINDOW (self), + _("Unable to save debugging information"), + NULL); + adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog), + "%s", error->message); + adw_message_dialog_add_response (ADW_MESSAGE_DIALOG (dialog), + "close", _("Close")); + + gtk_window_present (GTK_WINDOW (dialog)); + + g_error_free (error); + } + + g_object_unref (file); + } + + gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser)); +} + +static void +save_debug_info_cb (AdwAboutWindow *self) +{ + GtkFileChooserNative *chooser; + + chooser = gtk_file_chooser_native_new (_("Save debugging information"), + GTK_WINDOW (self), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Save"), + _("_Cancel")); + + if (self->debug_info_filename && *self->debug_info_filename) + gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (chooser), self->debug_info_filename); + + gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), TRUE); + + g_signal_connect (chooser, "response", G_CALLBACK (save_debug_info_response_cb), self); + + gtk_native_dialog_show (GTK_NATIVE_DIALOG (chooser)); +} + +static gboolean +close_cb (GtkWidget *widget, + GVariant *args, + gpointer user_data) +{ + AdwAboutWindow *self = ADW_ABOUT_WINDOW (widget); + + if (adw_leaflet_navigate (ADW_LEAFLET (self->leaflet), ADW_NAVIGATION_DIRECTION_BACK)) + return GDK_EVENT_STOP; + + gtk_window_close (GTK_WINDOW (self)); + + return GDK_EVENT_STOP; +} + +static gboolean +save_debug_info_shortcut_cb (GtkWidget *widget, + GVariant *args, + gpointer user_data) +{ + AdwAboutWindow *self = ADW_ABOUT_WINDOW (widget); + + if (adw_leaflet_get_visible_child (ADW_LEAFLET (self->leaflet)) != self->debug_info_page) + return GDK_EVENT_PROPAGATE; + + save_debug_info_cb (self); + + return GDK_EVENT_STOP; +} + +static void +adw_about_window_class_init (AdwAboutWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = adw_about_window_finalize; + object_class->get_property = adw_about_window_get_property; + object_class->set_property = adw_about_window_set_property; + + /** + * AdwAboutWindow:application-icon: (attributes org.gtk.Property.get=adw_about_window_get_application_icon org.gtk.Property.set=adw_about_window_set_application_icon) + * + * The name of the application icon. + * + * The icon is displayed at the top of the main page. + * + * Since: 1.2 + */ + props[PROP_APPLICATION_ICON] = + g_param_spec_string ("application-icon", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:application-name: (attributes org.gtk.Property.get=adw_about_window_get_application_name org.gtk.Property.set=adw_about_window_set_application_name) + * + * The name of the application. + * + * The name is displayed at the top of the main page. + * + * Since: 1.2 + */ + props[PROP_APPLICATION_NAME] = + g_param_spec_string ("application-name", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:developer-name: (attributes org.gtk.Property.get=adw_about_window_get_developer_name org.gtk.Property.set=adw_about_window_set_developer_name) + * + * The developer name. + * + * The developer name is displayed on the main page, under the application + * name. + * + * If the application is developed by multiple people, the developer name can + * be set to values like "AppName team", "AppName developers" or + * "The AppName project", and the individual contributors can be listed on the + * Credits page, with [property@AboutWindow:developers] and related + * properties. + * + * Since: 1.2 + */ + props[PROP_DEVELOPER_NAME] = + g_param_spec_string ("developer-name", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:version: (attributes org.gtk.Property.get=adw_about_window_get_version org.gtk.Property.set=adw_about_window_set_version) + * + * The version of the application. + * + * The version is displayed on the main page. + * + * If [property@AboutWindow:release-notes-version] is not set, the version + * will also be displayed above the release notes on the What's New page. + * + * Since: 1.2 + */ + props[PROP_VERSION] = + g_param_spec_string ("version", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:release-notes-version: (attributes org.gtk.Property.get=adw_about_window_get_release_notes_version org.gtk.Property.set=adw_about_window_set_release_notes_version) + * + * The version described by the application's release notes. + * + * The release notes version is displayed on the What's New page, above the + * release notes. + * + * If not set, [property@AboutWindow:version] will be used instead. + * + * For example, an application with the current version 2.0.2 might want to + * keep the release notes from 2.0.0, and set the release notes version + * accordingly. + * + * See [property@AboutWindow:release-notes]. + * + * Since: 1.2 + */ + props[PROP_RELEASE_NOTES_VERSION] = + g_param_spec_string ("release-notes-version", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:release-notes: (attributes org.gtk.Property.get=adw_about_window_get_release_notes org.gtk.Property.set=adw_about_window_set_release_notes) + * + * The release notes of the application. + * + * Release notes are displayed on the the What's New page. + * + * Release notes are formatted the same way as + * [AppStream descriptions](https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-description). + * + * The supported formatting options are: + * + * * Paragraph (`

      `) + * * Ordered list (`

        `), with list items (`
      1. `) + * * Unordered list (`
          `), with list items (`
        • `) + * + * Within paragraphs and list items, emphasis (``) and inline code + * (``) text styles are supported. The emphasis is rendered in italic, + * while inline code is shown in a monospaced font. + * + * Any text outside paragraphs or list items is ignored. + * + * Nested lists are not supported. + * + * `AdwAboutWindow` displays the version above the release notes. If set, the + * [property@AboutWindow:release-notes-version] of the property will be used + * as the version; otherwise, [property@AboutWindow:version] is used. + * + * Since: 1.2 + */ + props[PROP_RELEASE_NOTES] = + g_param_spec_string ("release-notes", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:comments: (attributes org.gtk.Property.get=adw_about_window_get_comments org.gtk.Property.set=adw_about_window_set_comments) + * + * The comments about the application. + * + * Comments will be shown on the Details page, above links. + * + * Unlike [property@Gtk.AboutDialog:comments], this string can be long and + * detailed. It can also contain links and Pango markup. + * + * Since: 1.2 + */ + props[PROP_COMMENTS] = + g_param_spec_string ("comments", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:website: (attributes org.gtk.Property.get=adw_about_window_get_website org.gtk.Property.set=adw_about_window_set_website) + * + * The URL of the application's website. + * + * Website is displayed on the Details page, below comments, or on the main + * page if the Details page doesn't have any other content. + * + * Applications can add other links below, see [method@AboutWindow.add_link]. + * + * Since: 1.2 + */ + props[PROP_WEBSITE] = + g_param_spec_string ("website", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:support-url: (attributes org.gtk.Property.get=adw_about_window_get_support_url org.gtk.Property.set=adw_about_window_set_support_url) + * + * The URL of the application's support page. + * + * The support page link is displayed on the main page. + * + * Since: 1.2 + */ + props[PROP_SUPPORT_URL] = + g_param_spec_string ("support-url", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:issue-url: (attributes org.gtk.Property.get=adw_about_window_get_issue_url org.gtk.Property.set=adw_about_window_set_issue_url) + * + * The URL for the application's issue tracker. + * + * The issue tracker link is displayed on the main page. + * + * Since: 1.2 + */ + props[PROP_ISSUE_URL] = + g_param_spec_string ("issue-url", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:debug-info: (attributes org.gtk.Property.get=adw_about_window_get_debug_info org.gtk.Property.set=adw_about_window_set_debug_info) + * + * The debug information. + * + * Debug information will be shown on the Troubleshooting page. It's intended + * to be attached to issue reports when reporting issues against the + * application. + * + * `AdwAboutWindow` provides a quick way to save debug information to a file. + * When saving, [property@AboutWindow:debug-info-filename] would be used as + * the suggested filename. + * + * Debug information cannot contain markup or links. + * + * Since: 1.2 + */ + props[PROP_DEBUG_INFO] = + g_param_spec_string ("debug-info", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:debug-info-filename: (attributes org.gtk.Property.get=adw_about_window_get_debug_info_filename org.gtk.Property.set=adw_about_window_set_debug_info_filename) + * + * The debug information filename. + * + * It will be used as the suggested filename when saving debug information to + * a file. + * + * See [property@AboutWindow:debug-info]. + * + * Since: 1.2 + */ + props[PROP_DEBUG_INFO_FILENAME] = + g_param_spec_string ("debug-info-filename", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:developers: (attributes org.gtk.Property.get=adw_about_window_get_developers org.gtk.Property.set=adw_about_window_set_developers) + * + * The list of developers of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for + * more details. + * + * See also: + * + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ + props[PROP_DEVELOPERS] = + g_param_spec_boxed ("developers", NULL, NULL, + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:designers: (attributes org.gtk.Property.get=adw_about_window_get_designers org.gtk.Property.set=adw_about_window_set_designers) + * + * The list of designers of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for + * more details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ + props[PROP_DESIGNERS] = + g_param_spec_boxed ("designers", NULL, NULL, + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:artists: (attributes org.gtk.Property.get=adw_about_window_get_artists org.gtk.Property.set=adw_about_window_set_artists) + * + * The list of artists of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for + * more details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ + props[PROP_ARTISTS] = + g_param_spec_boxed ("artists", NULL, NULL, + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:documenters: (attributes org.gtk.Property.get=adw_about_window_get_documenters org.gtk.Property.set=adw_about_window_set_documenters) + * + * The list of documenters of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for + * more details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ + props[PROP_DOCUMENTERS] = + g_param_spec_boxed ("documenters", NULL, NULL, + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:translator-credits: (attributes org.gtk.Property.get=adw_about_window_get_translator_credits org.gtk.Property.set=adw_about_window_set_translator_credits) + * + * The translator credits string. + * + * It will be displayed on the Credits page. + * + * This string should be `"translator-credits"` or `"translator_credits"` and + * should be marked as translatable. + * + * The string may contain email addresses and URLs, see the introduction for + * more details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ + props[PROP_TRANSLATOR_CREDITS] = + g_param_spec_string ("translator-credits", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:copyright: (attributes org.gtk.Property.get=adw_about_window_get_copyright org.gtk.Property.set=adw_about_window_set_copyright) + * + * The copyright information. + * + * This should be a short string of one or two lines, for example: + * `© 2022 Example`. + * + * The copyright information will be displayed on the Legal page, above the + * application license. + * + * [method@AboutWindow.add_legal_section] can be used to add copyright + * information for the application dependencies or other components. + * + * Since: 1.2 + */ + props[PROP_COPYRIGHT] = + g_param_spec_string ("copyright", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:license-type: (attributes org.gtk.Property.get=adw_about_window_get_license_type org.gtk.Property.set=adw_about_window_set_license_type) + * + * The license type. + * + * Allows to set the application's license froma list of known licenses. + * + * If the application's license is not in the list, + * [property@AboutWindow:license] can be used instead. The license type will + * be automatically set to `GTK_LICENSE_CUSTOM` in that case. + * + * If set to `GTK_LICENSE_UNKNOWN`, no information will be displayed. + * + * If the license type is different from `GTK_LICENSE_CUSTOM`. + * [property@AboutWindow:license] will be cleared out. + * + * The license description will be displayed on the Legal page, below the + * copyright information. + * + * [method@AboutWindow.add_legal_section] can be used to add license + * information for the application dependencies or other components. + * + * Since: 1.2 + */ + props[PROP_LICENSE_TYPE] = + g_param_spec_enum ("license-type", NULL, NULL, + GTK_TYPE_LICENSE, + GTK_LICENSE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwAboutWindow:license: (attributes org.gtk.Property.get=adw_about_window_get_license org.gtk.Property.set=adw_about_window_set_license) + * + * The license text. + * + * This can be used to set a custom text for the license if it can't be set + * via [property@AboutWindow:license-type]. + * + * When set, [property@AboutWindow:license-type] will be set to + * `GTK_LICENSE_CUSTOM`. + * + * The license text will be displayed on the Legal page, below the copyright + * information. + * + * License text can contain Pango markup and links. + * + * [method@AboutWindow.add_legal_section] can be used to add license + * information for the application dependencies or other components. + * + * Since: 1.2 + */ + props[PROP_LICENSE] = + g_param_spec_string ("license", NULL, NULL, + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * AdwAboutWindow::activate-link: + * @self: an about window + * @uri: the URI to activate + * + * Emitted when a URL is activated. + * + * Applications may connect to it to override the default behavior, which is + * to call [func@Gtk.show_uri]. + * + * Returns: `TRUE` if the link has been activated + * + * Since: 1.0 + */ + signals[SIGNAL_ACTIVATE_LINK] = + g_signal_new ("activate-link", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + g_signal_accumulator_true_handled, + NULL, NULL, + G_TYPE_BOOLEAN, + 1, + G_TYPE_STRING); + + g_signal_override_class_handler ("activate-link", + G_TYPE_FROM_CLASS (klass), + G_CALLBACK (activate_link_default_cb)); + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/Adwaita/ui/adw-about-window.ui"); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, leaflet); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, subpage_stack); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, toast_overlay); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, website_row); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, links_group); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, details_website_row); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, credits_box); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, legal_box); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, acknowledgements_box); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, debug_info_page); + gtk_widget_class_bind_template_child (widget_class, AdwAboutWindow, release_notes_buffer); + + gtk_widget_class_bind_template_callback (widget_class, boolean_or); + gtk_widget_class_bind_template_callback (widget_class, string_is_not_empty); + gtk_widget_class_bind_template_callback (widget_class, get_headerbar_name); + gtk_widget_class_bind_template_callback (widget_class, activate_link_cb); + + gtk_widget_class_install_action (widget_class, "about.back", NULL, + (GtkWidgetActionActivateFunc) back_cb); + gtk_widget_class_install_action (widget_class, "about.subpage", "s", + (GtkWidgetActionActivateFunc) subpage_cb); + gtk_widget_class_install_action (widget_class, "about.show-url", "s", + (GtkWidgetActionActivateFunc) show_url_cb); + gtk_widget_class_install_action (widget_class, "about.show-url-property", "s", + (GtkWidgetActionActivateFunc) show_url_property_cb); + gtk_widget_class_install_action (widget_class, "about.copy-property", "s", + (GtkWidgetActionActivateFunc) copy_property_cb); + gtk_widget_class_install_action (widget_class, "about.debug", NULL, + (GtkWidgetActionActivateFunc) debug_cb); + gtk_widget_class_install_action (widget_class, "about.save-debug-info", NULL, + (GtkWidgetActionActivateFunc) save_debug_info_cb); + + gtk_widget_class_add_binding (widget_class, GDK_KEY_Escape, 0, close_cb, NULL); + gtk_widget_class_add_binding (widget_class, GDK_KEY_S, GDK_CONTROL_MASK, + save_debug_info_shortcut_cb, NULL); +} + +static void +adw_about_window_init (AdwAboutWindow *self) +{ + self->application_icon = g_strdup (""); + self->application_name = g_strdup (""); + self->developer_name = g_strdup (""); + self->version = g_strdup (""); + self->release_notes_version = g_strdup (""); + self->release_notes = g_strdup (""); + self->comments = g_strdup (""); + self->website = g_strdup (""); + self->support_url = g_strdup (""); + self->issue_url = g_strdup (""); + self->debug_info = g_strdup (""); + self->debug_info_filename = g_strdup (""); + self->copyright = g_strdup (""); + self->license = g_strdup (""); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_text_buffer_create_tag (self->release_notes_buffer, "em", + "style", PANGO_STYLE_ITALIC, + NULL); + gtk_text_buffer_create_tag (self->release_notes_buffer, "code", + "family", "monospace", + NULL); + gtk_text_buffer_create_tag (self->release_notes_buffer, "bullet", + "font-features", "tnum=1", + "left-margin", 24, + "pixels-above-lines", 6, + NULL); + gtk_text_buffer_create_tag (self->release_notes_buffer, "section", + "pixels-above-lines", 12, + NULL); + gtk_text_buffer_create_tag (self->release_notes_buffer, "heading", + "weight", PANGO_WEIGHT_BOLD, + NULL); +} + +/** + * adw_about_window_new: + * + * Creates a new `AdwAboutWindow`. + * + * Returns: the newly created `AdwAboutWindow` + * + * Since: 1.2 + */ +GtkWidget * +adw_about_window_new (void) +{ + return g_object_new (ADW_TYPE_ABOUT_WINDOW, NULL); +} + +/** + * adw_about_window_get_application_icon: (attributes org.gtk.Method.get_property=application-icon) + * @self: an about window + * + * Gets the name of the application icon for @self. + * + * Returns: the application icon name + * + * Since: 1.2 + */ +const char * +adw_about_window_get_application_icon (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->application_icon; +} + +/** + * adw_about_window_set_application_icon: (attributes org.gtk.Method.set_property=application-icon) + * @self: an about window + * @application_icon: the application icon name + * + * Sets the name of the application icon for @self. + * + * The icon is displayed at the top of the main page. + * + * Since: 1.2 + */ +void +adw_about_window_set_application_icon (AdwAboutWindow *self, + const char *application_icon) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (application_icon != NULL); + + if (g_strcmp0 (self->application_icon, application_icon) == 0) + return; + + g_free (self->application_icon); + self->application_icon = g_strdup (application_icon); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APPLICATION_ICON]); +} + +/** + * adw_about_window_get_application_name: (attributes org.gtk.Method.get_property=application-name) + * @self: an about window + * + * Gets the application name for @self. + * + * Returns: the application name + * + * Since: 1.2 + */ +const char * +adw_about_window_get_application_name (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->application_name; +} + +/** + * adw_about_window_set_application_name: (attributes org.gtk.Method.set_property=application-name) + * @self: an about window + * @application_name: the application name + * + * Sets the application name for @self. + * + * The name is displayed at the top of the main page. + * + * Since: 1.2 + */ +void +adw_about_window_set_application_name (AdwAboutWindow *self, + const char *application_name) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (application_name != NULL); + + if (g_strcmp0 (self->application_name, application_name) == 0) + return; + + g_free (self->application_name); + self->application_name = g_strdup (application_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APPLICATION_NAME]); +} + +/** + * adw_about_window_get_developer_name: (attributes org.gtk.Method.get_property=developer-name) + * @self: an about window + * + * Gets the developer name for @self. + * + * Returns: the developer_name + * + * Since: 1.2 + */ +const char * +adw_about_window_get_developer_name (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->developer_name; +} + +/** + * adw_about_window_set_developer_name: (attributes org.gtk.Method.set_property=developer-name) + * @self: an about window + * @developer_name: the developer name + * + * Sets the developer name for @self. + * + * The developer name is displayed on the main page, under the application name. + * + * If the application is developed by multiple people, the developer name can be + * set to values like "AppName team", "AppName developers" or + * "The AppName project", and the individual contributors can be listed on the + * Credits page, with [property@AboutWindow:developers] and related properties. + * + * Since: 1.2 + */ +void +adw_about_window_set_developer_name (AdwAboutWindow *self, + const char *developer_name) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (developer_name != NULL); + + if (g_strcmp0 (self->developer_name, developer_name) == 0) + return; + + g_free (self->developer_name); + self->developer_name = g_strdup (developer_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVELOPER_NAME]); +} + +/** + * adw_about_window_get_version: (attributes org.gtk.Method.get_property=version) + * @self: an about window + * + * Gets the version for @self. + * + * Returns: the version + * + * Since: 1.2 + */ +const char * +adw_about_window_get_version (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->version; +} + +/** + * adw_about_window_set_version: (attributes org.gtk.Method.set_property=version) + * @self: an about window + * @version: the version + * + * Sets the version for @self. + * + * The version is displayed on the main page. + * + * If [property@AboutWindow:release-notes-version] is not set, the version will + * also be displayed above the release notes on the What's New page. + * + * Since: 1.2 + */ +void +adw_about_window_set_version (AdwAboutWindow *self, + const char *version) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (version != NULL); + + if (g_strcmp0 (self->version, version) == 0) + return; + + g_free (self->version); + self->version = g_strdup (version); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VERSION]); +} + +/** + * adw_about_window_get_release_notes_version: (attributes org.gtk.Method.get_property=release-notes-version) + * @self: an about window + * + * Gets the version described by the application's release notes. + * + * Returns: the release notes version + * + * Since: 1.2 + */ +const char * +adw_about_window_get_release_notes_version (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->release_notes_version; +} + +/** + * adw_about_window_set_release_notes_version: (attributes org.gtk.Method.set_property=release-notes-version) + * @self: an about window + * @version: the release notes version + * + * Sets the version described by the application's release notes. + * + * The release notes version is displayed on the What's New page, above the + * release notes. + * + * If not set, [property@AboutWindow:version] will be used instead. + * + * For example, an application with the current version 2.0.2 might want to + * keep the release notes from 2.0.0, and set the release notes version + * accordingly. + * + * See [property@AboutWindow:release-notes]. + * + * Since: 1.2 + */ +void +adw_about_window_set_release_notes_version (AdwAboutWindow *self, + const char *version) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (version != NULL); + + if (g_strcmp0 (self->release_notes_version, version) == 0) + return; + + g_free (self->release_notes_version); + self->release_notes_version = g_strdup (version); + + update_release_notes (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RELEASE_NOTES_VERSION]); +} + +/** + * adw_about_window_get_release_notes: (attributes org.gtk.Method.get_property=release-notes) + * @self: an about window + * + * Gets the release notes for @self. + * + * Returns: the release notes + * + * Since: 1.2 + */ +const char * +adw_about_window_get_release_notes (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->release_notes; +} + +/** + * adw_about_window_set_release_notes: (attributes org.gtk.Method.set_property=release-notes) + * @self: an about window + * @release_notes: the release notes + * + * Sets the release notes for @self. + * + * Release notes are displayed on the the What's New page. + * + * Release notes are formatted the same way as + * [AppStream descriptions](https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-description). + * + * The supported formatting options are: + * + * * Paragraph (`

          `) + * * Ordered list (`

            `), with list items (`
          1. `) + * * Unordered list (`
              `), with list items (`
            • `) + * + * Within paragraphs and list items, emphasis (``) and inline code + * (``) text styles are supported. The emphasis is rendered in italic, + * while inline code is shown in a monospaced font. + * + * Any text outside paragraphs or list items is ignored. + * + * Nested lists are not supported. + * + * `AdwAboutWindow` displays the version above the release notes. If set, the + * [property@AboutWindow:release-notes-version] of the property will be used + * as the version; otherwise, [property@AboutWindow:version] is used. + * + * Since: 1.2 + */ +void +adw_about_window_set_release_notes (AdwAboutWindow *self, + const char *release_notes) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (release_notes != NULL); + + if (g_strcmp0 (self->release_notes, release_notes) == 0) + return; + + g_free (self->release_notes); + self->release_notes = g_strdup (release_notes); + + update_release_notes (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RELEASE_NOTES]); +} + +/** + * adw_about_window_get_comments: (attributes org.gtk.Method.get_property=comments) + * @self: an about window + * + * Gets the comments about the application. + * + * Returns: the comments + * + * Since: 1.2 + */ +const char * +adw_about_window_get_comments (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->comments; +} + +/** + * adw_about_window_set_comments: (attributes org.gtk.Method.set_property=comments) + * @self: an about window + * @comments: the comments + * + * Sets the comments about the application. + * + * Comments will be shown on the Details page, above links. + * + * Unlike [property@Gtk.AboutDialog:comments], this string can be long and + * detailed. It can also contain links and Pango markup. + * + * Since: 1.2 + */ +void +adw_about_window_set_comments (AdwAboutWindow *self, + const char *comments) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (comments != NULL); + + if (g_strcmp0 (self->comments, comments) == 0) + return; + + g_free (self->comments); + self->comments = g_strdup (comments); + + update_links (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COMMENTS]); +} + +/** + * adw_about_window_get_website: (attributes org.gtk.Method.get_property=website) + * @self: an about window + * + * Gets the application website URL for @self. + * + * Returns: the website URL + * + * Since: 1.2 + */ +const char * +adw_about_window_get_website (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->website; +} + +/** + * adw_about_window_set_website: (attributes org.gtk.Method.set_property=website) + * @self: an about window + * @website: the website URL + * + * Sets the application website URL for @self. + * + * Website is displayed on the Details page, below comments, or on the main page + * if the Details page doesn't have any other content. + * + * Applications can add other links below, see [method@AboutWindow.add_link]. + * + * Since: 1.2 + */ +void +adw_about_window_set_website (AdwAboutWindow *self, + const char *website) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (website != NULL); + + if (g_strcmp0 (self->website, website) == 0) + return; + + g_free (self->website); + self->website = g_strdup (website); + + update_links (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_WEBSITE]); +} + +/** + * adw_about_window_get_support_url: (attributes org.gtk.Method.get_property=support-url) + * @self: an about window + * + * Gets the URL of the support page for @self. + * + * Returns: the support page URL + * + * Since: 1.2 + */ +const char * +adw_about_window_get_support_url (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->support_url; +} + +/** + * adw_about_window_set_support_url: (attributes org.gtk.Method.set_property=support-url) + * @self: an about window + * @support_url: the support page URL + * + * Sets the URL of the support page for @self. + * + * The support page link is displayed on the main page. + * + * Since: 1.2 + */ +void +adw_about_window_set_support_url (AdwAboutWindow *self, + const char *support_url) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (support_url != NULL); + + if (g_strcmp0 (self->support_url, support_url) == 0) + return; + + g_free (self->support_url); + self->support_url = g_strdup (support_url); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUPPORT_URL]); +} + +/** + * adw_about_window_get_issue_url: (attributes org.gtk.Method.get_property=issue-url) + * @self: an about window + * + * Gets the issue tracker URL for @self. + * + * Returns: the issue tracker URL + * + * Since: 1.2 + */ +const char * +adw_about_window_get_issue_url (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->issue_url; +} + +/** + * adw_about_window_set_issue_url: (attributes org.gtk.Method.set_property=issue-url) + * @self: an about window + * @issue_url: the issue tracker URL + * + * Sets the issue tracker URL for @self. + * + * The issue tracker link is displayed on the main page. + * + * Since: 1.2 + */ +void +adw_about_window_set_issue_url (AdwAboutWindow *self, + const char *issue_url) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (issue_url != NULL); + + if (g_strcmp0 (self->issue_url, issue_url) == 0) + return; + + g_free (self->issue_url); + self->issue_url = g_strdup (issue_url); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ISSUE_URL]); +} + +/** + * adw_about_window_add_link: + * @self: an about window + * @title: the link title + * @url: the link URL + * + * Adds an extra link to the Details page. + * + * Extra links are displayed under the comment and website. + * + * Underlines in @title will be interpreted as indicating a mnemonic. + * + * See [property@AboutWindow:website]. + * + * Since: 1.2 + */ +void +adw_about_window_add_link (AdwAboutWindow *self, + const char *title, + const char *url) +{ + GtkWidget *row; + + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (title != NULL); + g_return_if_fail (url != NULL); + + row = adw_action_row_new (); + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), title); + adw_preferences_row_set_use_underline (ADW_PREFERENCES_ROW (row), TRUE); + + adw_action_row_add_suffix (ADW_ACTION_ROW (row), + gtk_image_new_from_icon_name ("adw-external-link-symbolic")); + + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE); + gtk_actionable_set_action_name (GTK_ACTIONABLE (row), "about.show-url"); + gtk_actionable_set_action_target (GTK_ACTIONABLE (row), "s", url); + + gtk_widget_set_tooltip_text (row, url); + + adw_preferences_group_add (ADW_PREFERENCES_GROUP (self->links_group), row); + + self->has_custom_links = TRUE; + + update_links (self); + +} + +/** + * adw_about_window_get_debug_info: (attributes org.gtk.Method.get_property=debug-info) + * @self: an about window + * + * Gets the debug information for @self. + * + * Returns: the debug information + * + * Since: 1.2 + */ +const char * +adw_about_window_get_debug_info (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->debug_info; +} + +/** + * adw_about_window_set_debug_info: (attributes org.gtk.Method.set_property=debug-info) + * @self: an about window + * @debug_info: the debug information + * + * Sets the debug information for @self. + * + * Debug information will be shown on the Troubleshooting page. It's intended + * to be attached to issue reports when reporting issues against the + * application. + * + * `AdwAboutWindow` provides a quick way to save debug information to a file. + * When saving, [property@AboutWindow:debug-info-filename] would be used as + * the suggested filename. + * + * Debug information cannot contain markup or links. + * + * Since: 1.2 + */ +void +adw_about_window_set_debug_info (AdwAboutWindow *self, + const char *debug_info) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (debug_info != NULL); + + if (g_strcmp0 (self->debug_info, debug_info) == 0) + return; + + g_free (self->debug_info); + self->debug_info = g_strdup (debug_info); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEBUG_INFO]); +} + +/** + * adw_about_window_get_debug_info_filename: (attributes org.gtk.Method.get_property=debug-info-filename) + * @self: an about window + * + * Gets the debug information filename for @self. + * + * Returns: the debug information filename + * + * Since: 1.2 + */ +const char * +adw_about_window_get_debug_info_filename (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->debug_info_filename; +} + +/** + * adw_about_window_set_debug_info_filename: (attributes org.gtk.Method.set_property=debug-info) + * @self: an about window + * @filename: the debug info filename + * + * Sets the debug information filename for @self. + * + * It will be used as the suggested filename when saving debug information to a + * file. + * + * See [property@AboutWindow:debug-info]. + * + * Since: 1.2 + */ +void +adw_about_window_set_debug_info_filename (AdwAboutWindow *self, + const char *filename) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (filename != NULL); + + if (g_strcmp0 (self->debug_info_filename, filename) == 0) + return; + + g_free (self->debug_info_filename); + self->debug_info_filename = g_strdup (filename); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEBUG_INFO_FILENAME]); +} + +/** + * adw_about_window_get_developers: (attributes org.gtk.Method.get_property=developers) + * @self: an about window + * + * Gets the list of developers of the application. + * + * Returns: (nullable) (transfer none) (array zero-terminated=1): The list of developers + * + * Since: 1.2 + */ +const char * const * +adw_about_window_get_developers (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return (const char * const *) self->developers; +} + +/** + * adw_about_window_set_developers: (attributes org.gtk.Method.set_property=developers) + * @self: an about window + * @developers: (nullable) (transfer none) (array zero-terminated=1): the list of developers + * + * Sets the list of developers of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for more + * details. + * + * See also: + * + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ +void +adw_about_window_set_developers (AdwAboutWindow *self, + const char **developers) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + + if ((const char **) self->developers == developers) + return; + + g_strfreev (self->developers); + self->developers = g_strdupv ((char **) developers); + + update_credits (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVELOPERS]); +} + +/** + * adw_about_window_get_designers: (attributes org.gtk.Method.get_property=designers) + * @self: an about window + * + * Gets the list of designers of the application. + * + * Returns: (nullable) (transfer none) (array zero-terminated=1): The list of designers + * + * Since: 1.2 + */ +const char * const * +adw_about_window_get_designers (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return (const char * const *) self->designers; +} + +/** + * adw_about_window_set_designers: (attributes org.gtk.Method.set_property=designers) + * @self: an about window + * @designers: (nullable) (transfer none) (array zero-terminated=1): the list of designers + * + * Sets the list of designers of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for more + * details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ +void +adw_about_window_set_designers (AdwAboutWindow *self, + const char **designers) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + + if ((const char **) self->designers == designers) + return; + + g_strfreev (self->designers); + self->designers = g_strdupv ((char **) designers); + + update_credits (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESIGNERS]); +} + +/** + * adw_about_window_get_artists: (attributes org.gtk.Method.get_property=artists) + * @self: an about window + * + * Gets the list of artists of the application. + * + * Returns: (nullable) (transfer none) (array zero-terminated=1): The list of artists + * + * Since: 1.2 + */ +const char * const * +adw_about_window_get_artists (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return (const char * const *) self->artists; +} + +/** + * adw_about_window_set_artists: (attributes org.gtk.Method.set_property=artists) + * @self: an about window + * @artists: (nullable) (transfer none) (array zero-terminated=1): the list of artists + * + * Sets the list of artists of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for more + * details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ +void +adw_about_window_set_artists (AdwAboutWindow *self, + const char **artists) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + + if ((const char **) self->artists == artists) + return; + + g_strfreev (self->artists); + self->artists = g_strdupv ((char **) artists); + + update_credits (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ARTISTS]); +} + +/** + * adw_about_window_get_documenters: (attributes org.gtk.Method.get_property=documenters) + * @self: an about window + * + * Gets the list of documenters of the application. + * + * Returns: (nullable) (transfer none) (array zero-terminated=1): The list of documenters + * + * Since: 1.2 + */ +const char * const * +adw_about_window_get_documenters (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return (const char * const *) self->documenters; +} + +/** + * adw_about_window_set_documenters: (attributes org.gtk.Method.set_property=documenters) + * @self: an about window + * @documenters: (nullable) (transfer none) (array zero-terminated=1): the list of documenters + * + * Sets the list of documenters of the application. + * + * It will be displayed on the Credits page. + * + * Each name may contain email addresses and URLs, see the introduction for more + * details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ +void +adw_about_window_set_documenters (AdwAboutWindow *self, + const char **documenters) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + + if ((const char **) self->documenters == documenters) + return; + + g_strfreev (self->documenters); + self->documenters = g_strdupv ((char **) documenters); + + update_credits (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DOCUMENTERS]); +} + +/** + * adw_about_window_get_translator_credits: (attributes org.gtk.Method.get_property=translator-credits) + * @self: an about window + * + * Gets the translator credits string. + * + * Returns: The translator credits string + * + * Since: 1.2 + */ +const char * +adw_about_window_get_translator_credits (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->translator_credits; +} + +/** + * adw_about_window_set_translator_credits: (attributes org.gtk.Method.set_property=translator-credits) + * @self: an about window + * @translator_credits: the translator credits + * + * Sets the translator credits string. + * + * It will be displayed on the Credits page. + * + * This string should be `"translator-credits"` or `"translator_credits"` and + * should be marked as translatable. + * + * The string may contain email addresses and URLs, see the introduction for + * more details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [method@AboutWindow.add_credit_section] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ +void +adw_about_window_set_translator_credits (AdwAboutWindow *self, + const char *translator_credits) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (translator_credits != NULL); + + if (self->translator_credits == translator_credits) + return; + + g_free (self->translator_credits); + self->translator_credits = g_strdup (translator_credits); + + update_credits (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSLATOR_CREDITS]); +} + +/** + * adw_about_window_add_credit_section: + * @self: an about window + * @name: (nullable): the section name + * @people: (array zero-terminated=1): the list of names + * + * Adds an extra section to the Credits page. + * + * Extra sections are displayed below the standard categories. + * + * Each name may contain email addresses and URLs, see the introduction for more + * details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_acknowledgement_section] + * + * Since: 1.2 + */ +void +adw_about_window_add_credit_section (AdwAboutWindow *self, + const char *name, + const char **people) +{ + CreditsSection *section; + + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (people != NULL); + + section = g_new0 (CreditsSection, 1); + section->name = g_strdup (name); + section->people = g_strdupv ((char **) people); + + self->credit_sections = g_slist_append (self->credit_sections, section); + + update_credits (self); +} + +/** + * adw_about_window_add_acknowledgement_section: + * @self: an about window + * @name: (nullable): the section name + * @people: (array zero-terminated=1): the list of names + * + * Adds a section to the Acknowledgements page. + * + * This can be used to acknowledge additional people and organizations for their + * non-development contributions - for example, backers in a crowdfunded + * project. + * + * Each name may contain email addresses and URLs, see the introduction for more + * details. + * + * See also: + * + * * [property@AboutWindow:developers] + * * [property@AboutWindow:designers] + * * [property@AboutWindow:artists] + * * [property@AboutWindow:documenters] + * * [property@AboutWindow:translator-credits] + * * [method@AboutWindow.add_credit_section] + * + * Since: 1.2 + */ +void +adw_about_window_add_acknowledgement_section (AdwAboutWindow *self, + const char *name, + const char **people) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (people != NULL); + + add_credits_section (self->acknowledgements_box, name, (char **) people); + + gtk_widget_show (self->acknowledgements_box); +} + +/** + * adw_about_window_get_copyright: (attributes org.gtk.Method.get_property=copyright) + * @self: an about window + * + * Gets the copyright information for @self. + * + * Returns: the copyright information + * + * Since: 1.2 + */ +const char * +adw_about_window_get_copyright (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->copyright; +} + +/** + * adw_about_window_set_copyright: (attributes org.gtk.Method.set_property=copyright) + * @self: an about window + * @copyright: the copyright information + * + * Sets the copyright information for @self. + * + * This should be a short string of one or two lines, for example: + * `© 2022 Example`. + * + * The copyright information will be displayed on the Legal page, before the + * application license. + * + * [method@AboutWindow.add_legal_section] can be used to add copyright + * information for the application dependencies or other components. + * + * Since: 1.2 + */ +void +adw_about_window_set_copyright (AdwAboutWindow *self, + const char *copyright) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (copyright != NULL); + + if (g_strcmp0 (self->copyright, copyright) == 0) + return; + + g_free (self->copyright); + self->copyright = g_strdup (copyright); + + update_legal (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COPYRIGHT]); +} + +/** + * adw_about_window_get_license_type: (attributes org.gtk.Method.get_property=license-type) + * @self: an about window + * + * Gets the license type for @self. + * + * Returns: the license type + * + * Since: 1.2 + */ +GtkLicense +adw_about_window_get_license_type (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), GTK_LICENSE_UNKNOWN); + + return self->license_type; +} + +/** + * adw_about_window_set_license_type: (attributes org.gtk.Method.set_property=license-type) + * @self: an about window + * @license_type: the license type + * + * Sets the license for @self from a list of known licenses. + * + * If the application's license is not in the list, + * [property@AboutWindow:license] can be used instead. The license type will be + * automatically set to `GTK_LICENSE_CUSTOM` in that case. + * + * If @license_type is `GTK_LICENSE_UNKNOWN`, no information will be displayed. + * + * If @license_type is different from `GTK_LICENSE_CUSTOM`. + * [property@AboutWindow:license] will be cleared out. + * + * The license description will be displayed on the Legal page, below the + * copyright information. + * + * [method@AboutWindow.add_legal_section] can be used to add license information + * for the application dependencies or other components. + * + * Since: 1.2 + */ +void +adw_about_window_set_license_type (AdwAboutWindow *self, + GtkLicense license_type) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (license_type >= GTK_LICENSE_UNKNOWN && + license_type < G_N_ELEMENTS (gtk_license_info)); + + if (self->license_type == license_type) + return; + + if (license_type != GTK_LICENSE_CUSTOM) { + g_free (self->license); + self->license = g_strdup (""); + } + + self->license_type = license_type; + + update_legal (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LICENSE]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LICENSE_TYPE]); +} + +/** + * adw_about_window_get_license: (attributes org.gtk.Method.get_property=license) + * @self: an about window + * + * Gets the license for @self. + * + * Returns: the license + * + * Since: 1.2 + */ +const char * +adw_about_window_get_license (AdwAboutWindow *self) +{ + g_return_val_if_fail (ADW_IS_ABOUT_WINDOW (self), NULL); + + return self->license; +} + +/** + * adw_about_window_set_license: (attributes org.gtk.Method.set_property=license) + * @self: an about window + * @license: the license + * + * Sets the license for @self. + * + * This can be used to set a custom text for the license if it can't be set via + * [property@AboutWindow:license-type]. + * + * When set, [property@AboutWindow:license-type] will be set to + * `GTK_LICENSE_CUSTOM`. + * + * The license text will be displayed on the Legal page, below the copyright + * information. + * + * License text can contain Pango markup and links. + * + * [method@AboutWindow.add_legal_section] can be used to add license information + * for the application dependencies or other components. + * + * Since: 1.2 + */ +void +adw_about_window_set_license (AdwAboutWindow *self, + const char *license) +{ + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (license != NULL); + + if (g_strcmp0 (self->license, license) == 0) + return; + + g_object_freeze_notify (G_OBJECT (self)); + + g_free (self->license); + self->license = g_strdup (license); + self->license_type = GTK_LICENSE_CUSTOM; + + update_legal (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LICENSE]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LICENSE_TYPE]); + + g_object_thaw_notify (G_OBJECT (self)); +} + +/** + * adw_about_window_add_legal_section: + * @self: an about window + * @title: the name of the section + * @copyright: (nullable): a copyright string + * @license_type: the type of license + * @license: (nullable): custom license information + * + * Adds an extra section to the Legal page. + * + * Extra sections will be displayed below the application's own information. + * + * The parameters @copyright, @license_type and @license will be used to present + * the it the same way as [property@AboutWindow:copyright], + * [property@AboutWindow:license-type] and [property@AboutWindow:license] are + * for the application's own information. + * + * See those properties for more details. + * + * This can be useful to attribute the application dependencies or data. + * + * Examples: + * + * ```c + * adw_about_window_add_legal_section (ADW_ABOUT_WINDOW (about), + * _("Copyright and a known license"), + * "© 2022 Example", + * GTK_LICENSE_LGPL_2_1, + * NULL); + * + * adw_about_window_add_legal_section (ADW_ABOUT_WINDOW (about), + * _("Copyright and custom license"), + * "© 2022 Example", + * GTK_LICENSE_CUSTOM, + * "Custom license text"); + * + * adw_about_window_add_legal_section (ADW_ABOUT_WINDOW (about), + * _("Copyright only"), + * "© 2022 Example", + * GTK_LICENSE_UNKNOWN, + * NULL); + * + * adw_about_window_add_legal_section (ADW_ABOUT_WINDOW (about), + * _("Custom license only"), + * NULL, + * GTK_LICENSE_CUSTOM, + * "Something completely custom here."); + * ``` + * + * Since: 1.2 + */ +void +adw_about_window_add_legal_section (AdwAboutWindow *self, + const char *title, + const char *copyright, + GtkLicense license_type, + const char *license) +{ + LegalSection *section; + + g_return_if_fail (ADW_IS_ABOUT_WINDOW (self)); + g_return_if_fail (title != NULL); + g_return_if_fail (license_type >= GTK_LICENSE_UNKNOWN && + license_type < G_N_ELEMENTS (gtk_license_info)); + + section = g_new0 (LegalSection, 1); + section->title = g_strdup (title); + section->copyright = g_strdup (copyright); + section->license_type = license_type; + section->license = g_strdup (license); + + self->legal_sections = g_slist_append (self->legal_sections, section); + + update_legal (self); +} + +/** + * adw_show_about_window: + * @parent: (nullable): the parent top-level window + * @first_property_name: the name of the first property + * @...: value of first property, followed by more pairs of property name and + * value, `NULL`-terminated + * + * A convenience function for showing an application’s about window. + */ +void +adw_show_about_window (GtkWindow *parent, + const char *first_property_name, + ...) +{ + GtkWidget *window; + va_list var_args; + + window = adw_about_window_new (); + + va_start (var_args, first_property_name); + g_object_set_valist (G_OBJECT (window), first_property_name, var_args); + va_end (var_args); + + if (parent) + gtk_window_set_transient_for (GTK_WINDOW (window), parent); + + gtk_window_present (GTK_WINDOW (window)); +} diff --git a/src/adw-about-window.h b/src/adw-about-window.h new file mode 100644 index 00000000..e6a041d4 --- /dev/null +++ b/src/adw-about-window.h @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2021-2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION) +#error "Only can be included directly." +#endif + +#include "adw-version.h" + +#include +#include "adw-window.h" + +G_BEGIN_DECLS + +#define ADW_TYPE_ABOUT_WINDOW (adw_about_window_get_type()) + +ADW_AVAILABLE_IN_1_2 +G_DECLARE_FINAL_TYPE (AdwAboutWindow, adw_about_window, ADW, ABOUT_WINDOW, AdwWindow) + +ADW_AVAILABLE_IN_1_2 +GtkWidget *adw_about_window_new (void) G_GNUC_WARN_UNUSED_RESULT; + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_application_name (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_application_name (AdwAboutWindow *self, + const char *application_name); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_application_icon (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_application_icon (AdwAboutWindow *self, + const char *application_icon); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_developer_name (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_developer_name (AdwAboutWindow *self, + const char *developer_name); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_version (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_version (AdwAboutWindow *self, + const char *version); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_release_notes_version (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_release_notes_version (AdwAboutWindow *self, + const char *version); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_release_notes (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_release_notes (AdwAboutWindow *self, + const char *release_notes); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_comments (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_comments (AdwAboutWindow *self, + const char *comments); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_website (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_website (AdwAboutWindow *self, + const char *website); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_support_url (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_support_url (AdwAboutWindow *self, + const char *support_url); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_issue_url (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_issue_url (AdwAboutWindow *self, + const char *issue_url); + +ADW_AVAILABLE_IN_1_2 +void adw_about_window_add_link (AdwAboutWindow *self, + const char *title, + const char *url); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_debug_info (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_debug_info (AdwAboutWindow *self, + const char *debug_info); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_debug_info_filename (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_debug_info_filename (AdwAboutWindow *self, + const char *filename); + +ADW_AVAILABLE_IN_1_2 +const char * const *adw_about_window_get_developers (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_developers (AdwAboutWindow *self, + const char **developers); + +ADW_AVAILABLE_IN_1_2 +const char * const *adw_about_window_get_designers (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_designers (AdwAboutWindow *self, + const char **designers); + +ADW_AVAILABLE_IN_1_2 +const char * const *adw_about_window_get_artists (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_artists (AdwAboutWindow *self, + const char **artists); + +ADW_AVAILABLE_IN_1_2 +const char * const *adw_about_window_get_documenters (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_documenters (AdwAboutWindow *self, + const char **documenters); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_translator_credits (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_translator_credits (AdwAboutWindow *self, + const char *translator_credits); + +ADW_AVAILABLE_IN_1_2 +void adw_about_window_add_credit_section (AdwAboutWindow *self, + const char *name, + const char **people); + +ADW_AVAILABLE_IN_1_2 +void adw_about_window_add_acknowledgement_section (AdwAboutWindow *self, + const char *name, + const char **people); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_copyright (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_copyright (AdwAboutWindow *self, + const char *copyright); + +ADW_AVAILABLE_IN_1_2 +GtkLicense adw_about_window_get_license_type (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_license_type (AdwAboutWindow *self, + GtkLicense license_type); + +ADW_AVAILABLE_IN_1_2 +const char *adw_about_window_get_license (AdwAboutWindow *self); +ADW_AVAILABLE_IN_1_2 +void adw_about_window_set_license (AdwAboutWindow *self, + const char *license); + +ADW_AVAILABLE_IN_1_2 +void adw_about_window_add_legal_section (AdwAboutWindow *self, + const char *title, + const char *copyright, + GtkLicense license_type, + const char *license); + +ADW_AVAILABLE_IN_1_2 +void adw_show_about_window (GtkWindow *parent, + const char *first_property_name, + ...); + +G_END_DECLS diff --git a/src/adw-about-window.ui b/src/adw-about-window.ui new file mode 100644 index 00000000..d3c1ff91 --- /dev/null +++ b/src/adw-about-window.ui @@ -0,0 +1,811 @@ + + + + + diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml index da724d86..58a0d7c4 100644 --- a/src/adwaita.gresources.xml +++ b/src/adwaita.gresources.xml @@ -4,11 +4,14 @@ glsl/fade.glsl glsl/mask.glsl icons/scalable/actions/adw-entry-apply-symbolic.svg + icons/scalable/actions/adw-external-link-symbolic.svg icons/scalable/actions/adw-expander-arrow-symbolic.svg + icons/scalable/actions/adw-mail-send-symbolic.svg icons/scalable/status/avatar-default-symbolic.svg icons/scalable/status/adw-tab-icon-missing-symbolic.svg + adw-about-window.ui adw-action-row.ui adw-combo-row.ui adw-entry-row.ui diff --git a/src/adwaita.h b/src/adwaita.h index a2fda744..509a9740 100644 --- a/src/adwaita.h +++ b/src/adwaita.h @@ -21,6 +21,7 @@ G_BEGIN_DECLS #define _ADWAITA_INSIDE #include "adw-version.h" +#include "adw-about-window.h" #include "adw-action-row.h" #include "adw-animation.h" #include "adw-animation-target.h" diff --git a/src/icons/scalable/actions/adw-external-link-symbolic.svg b/src/icons/scalable/actions/adw-external-link-symbolic.svg new file mode 100644 index 00000000..549d0f12 --- /dev/null +++ b/src/icons/scalable/actions/adw-external-link-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/scalable/actions/adw-mail-send-symbolic.svg b/src/icons/scalable/actions/adw-mail-send-symbolic.svg new file mode 100644 index 00000000..932bd11c --- /dev/null +++ b/src/icons/scalable/actions/adw-mail-send-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index eb2d9041..b2c47498 100644 --- a/src/meson.build +++ b/src/meson.build @@ -81,6 +81,7 @@ libadwaita_private_sources += adw_private_enums libadwaita_generated_headers += [adw_public_enums[1]] src_headers = [ + 'adw-about-window.h', 'adw-action-row.h', 'adw-animation.h', 'adw-animation-target.h', @@ -145,6 +146,7 @@ libadwaita_init_public_types = custom_target('adw-public-types.c', ) src_sources = [ + 'adw-about-window.c', 'adw-action-row.c', 'adw-animation.c', 'adw-animation-target.c', diff --git a/src/stylesheet/widgets/_misc.scss b/src/stylesheet/widgets/_misc.scss index 6c4ff1f7..3d35a672 100644 --- a/src/stylesheet/widgets/_misc.scss +++ b/src/stylesheet/widgets/_misc.scss @@ -96,6 +96,52 @@ statusbar { padding: 6px 10px 6px 10px; } +/****************** + * AdwAboutWindow * + ******************/ + +window.about { + .main-page { + scrollbar.vertical { + // Keep in sync with header bar height + margin-top: 47px; + background: none; + box-shadow: none; + } + + > viewport > clamp > box { + margin: 12px; + margin-top: 59px; // 12px + 47px + border-spacing: 6px; + + > box { + margin-top: 18px; + border-spacing: 18px; + margin-bottom: 6px; + } + } + + .app-version { + padding: 3px 18px; + color: $accent_color; + border-radius: 999px; + margin-top: 3px; + } + } + + .subpage { + > viewport > clamp > box { + margin: 18px 12px; + border-spacing: 18px; + } + + > clamp > textview { + background: none; + color: inherit; + } + } +} + /***************** * AdwStatusPage * *****************/ diff --git a/tests/meson.build b/tests/meson.build index b2a660e5..2f17a06c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -24,6 +24,7 @@ if cc.get_argument_syntax() != 'msvc' endif test_names = [ + 'test-about-window', 'test-action-row', 'test-animation', 'test-animation-target', diff --git a/tests/test-about-window.c b/tests/test-about-window.c new file mode 100644 index 00000000..f9c8f6b7 --- /dev/null +++ b/tests/test-about-window.c @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Author: Alexander Mikhaylenko + */ + +#include + +static void +test_adw_about_window_create (void) +{ + AdwAboutWindow *window = ADW_ABOUT_WINDOW (adw_about_window_new ()); + + const char *developers[] = { + "Angela Avery", + NULL + }; + + const char *designers[] = { + "GNOME Design Team", + NULL + }; + + const char *artists[] = { + "GNOME Design Team", + NULL + }; + + const char *documenters[] = { + "Angela Avery", + NULL + }; + + const char *credits[] = { + "Angela Avery", + NULL + }; + + const char *acknowledgements[] = { + "Angela Avery", + NULL + }; + + g_assert_nonnull (window); + + g_object_set (G_OBJECT (window), + "application-name", "Example", + "application-icon", "org.gnome.Example", + "developer-name", "Angela Avery", + "version", "1.2.3", + "release-notes-version", "1.2.0", + "release-notes", "

              Example

              ", + "comments", "Comments", + "website", "https://example.org", + "issue-url", "https://example.org", + "support-url", "https://example.org", + "debug-info", "Debug", + "debug-info-filename", "debug.txt", + "developers", developers, + "designers", designers, + "artists", artists, + "documenters", documenters, + "translator-credits", "translator-credits", + "copyright", "© 2022 Angela Avery", + "license-type", GTK_LICENSE_GPL_3_0, + NULL); + + g_assert_cmpstr (adw_about_window_get_application_name (window), ==, "Example"); + g_assert_cmpstr (adw_about_window_get_application_icon (window), ==, "org.gnome.Example"); + g_assert_cmpstr (adw_about_window_get_developer_name (window), ==, "Angela Avery"); + g_assert_cmpstr (adw_about_window_get_version (window), ==, "1.2.3"); + g_assert_cmpstr (adw_about_window_get_release_notes_version (window), ==, "1.2.0"); + g_assert_cmpstr (adw_about_window_get_release_notes (window), ==, "

              Example

              "); + g_assert_cmpstr (adw_about_window_get_comments (window), ==, "Comments"); + g_assert_cmpstr (adw_about_window_get_website (window), ==, "https://example.org"); + g_assert_cmpstr (adw_about_window_get_issue_url (window), ==, "https://example.org"); + g_assert_cmpstr (adw_about_window_get_support_url (window), ==, "https://example.org"); + g_assert_cmpstr (adw_about_window_get_debug_info (window), ==, "Debug"); + g_assert_cmpstr (adw_about_window_get_debug_info_filename (window), ==, "debug.txt"); + g_assert_cmpstrv (adw_about_window_get_developers (window), developers); + g_assert_cmpstrv (adw_about_window_get_designers (window), designers); + g_assert_cmpstrv (adw_about_window_get_artists (window), artists); + g_assert_cmpstrv (adw_about_window_get_documenters (window), documenters); + g_assert_cmpstr (adw_about_window_get_translator_credits (window), ==, "translator-credits"); + g_assert_cmpstr (adw_about_window_get_copyright (window), ==, "© 2022 Angela Avery"); + g_assert_cmpuint (adw_about_window_get_license_type (window), ==, GTK_LICENSE_GPL_3_0); + + adw_about_window_add_link (window, "Example", "https://example.org"); + adw_about_window_add_credit_section (window, "Example", credits); + adw_about_window_add_acknowledgement_section (window, "Example", acknowledgements); + adw_about_window_add_legal_section (window, "Example", "© 2022 Example", GTK_LICENSE_GPL_3_0, NULL); + adw_about_window_add_legal_section (window, "Example", "© 2022 Example", GTK_LICENSE_CUSTOM, "License"); + + g_assert_finalize_object (window); +} + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + adw_init (); + + g_test_add_func ("/Adwaita/AboutWindow/create", test_adw_about_window_create); + + return g_test_run (); +} -- GitLab