summaryrefslogtreecommitdiffstats
path: root/src/gs-shell.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-shell.c')
-rw-r--r--src/gs-shell.c2725
1 files changed, 2725 insertions, 0 deletions
diff --git a/src/gs-shell.c b/src/gs-shell.c
new file mode 100644
index 0000000..91946ee
--- /dev/null
+++ b/src/gs-shell.c
@@ -0,0 +1,2725 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
+ * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <adwaita.h>
+#include <malloc.h>
+#include <string.h>
+#include <glib/gi18n.h>
+
+#ifdef HAVE_MOGWAI
+#include <libmogwai-schedule-client/scheduler.h>
+#endif
+
+#include "gs-common.h"
+#include "gs-shell.h"
+#include "gs-basic-auth-dialog.h"
+#include "gs-details-page.h"
+#include "gs-installed-page.h"
+#include "gs-metered-data-dialog.h"
+#include "gs-moderate-page.h"
+#include "gs-loading-page.h"
+#include "gs-search-page.h"
+#include "gs-overview-page.h"
+#include "gs-updates-page.h"
+#include "gs-category-page.h"
+#include "gs-extras-page.h"
+#include "gs-repos-dialog.h"
+#include "gs-prefs-dialog.h"
+#include "gs-update-dialog.h"
+#include "gs-update-monitor.h"
+#include "gs-utils.h"
+
+#define NARROW_WIDTH_THRESHOLD 800
+
+static const gchar *page_name[] = {
+ "unknown",
+ "overview",
+ "installed",
+ "search",
+ "updates",
+ "details",
+ "category",
+ "extras",
+ "moderate",
+ "loading",
+};
+
+typedef struct {
+ GsShellMode mode;
+ GtkWidget *focus;
+ GsCategory *category;
+ gchar *search;
+ GsApp *app;
+ gdouble vscroll_position;
+} BackEntry;
+
+struct _GsShell
+{
+ AdwApplicationWindow parent_object;
+
+ GSettings *settings;
+ GCancellable *cancellable;
+ GsPluginLoader *plugin_loader;
+ GtkWidget *header_start_widget;
+ GtkWidget *header_end_widget;
+ GQueue *back_entry_stack;
+ GPtrArray *modal_dialogs;
+ gchar *events_info_uri;
+ AdwLeaflet *main_leaflet;
+ AdwLeaflet *details_leaflet;
+ AdwViewStack *stack_loading;
+ AdwViewStack *stack_main;
+ AdwViewStack *stack_sub;
+ GsPage *page;
+
+ GBinding *sub_page_header_title_binding;
+
+#ifdef HAVE_MOGWAI
+ MwscScheduler *scheduler;
+ gboolean scheduler_held;
+ gulong scheduler_invalidated_handler;
+#endif /* HAVE_MOGWAI */
+
+ GtkWidget *main_header;
+ GtkWidget *details_header;
+ GtkWidget *metered_updates_bar;
+ GtkWidget *search_button;
+ GtkWidget *entry_search;
+ GtkWidget *search_bar;
+ GtkWidget *button_back;
+ GtkWidget *button_back2;
+ GtkWidget *notification_event;
+ GtkWidget *button_events_sources;
+ GtkWidget *button_events_no_space;
+ GtkWidget *button_events_network_settings;
+ GtkWidget *button_events_restart_required;
+ GtkWidget *button_events_more_info;
+ GtkWidget *button_events_dismiss;
+ GtkWidget *label_events;
+ GtkWidget *primary_menu;
+ GtkWidget *sub_page_header_title;
+
+ gboolean activate_after_setup;
+ gboolean is_narrow;
+ gint allocation_width;
+ guint allocation_changed_cb_id;
+
+ GsPage *pages[GS_SHELL_MODE_LAST];
+};
+
+G_DEFINE_TYPE (GsShell, gs_shell, ADW_TYPE_APPLICATION_WINDOW)
+
+typedef enum {
+ PROP_IS_NARROW = 1,
+ PROP_ALLOCATION_WIDTH,
+} GsShellProperty;
+
+enum {
+ SIGNAL_LOADED,
+ SIGNAL_LAST
+};
+
+static GParamSpec *obj_props[PROP_ALLOCATION_WIDTH + 1] = { NULL, };
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+static void
+modal_dialog_unmapped_cb (GtkWidget *dialog,
+ GsShell *shell)
+{
+ g_debug ("modal dialog %p unmapped", dialog);
+ g_ptr_array_remove (shell->modal_dialogs, dialog);
+}
+
+void
+gs_shell_modal_dialog_present (GsShell *shell, GtkWindow *window)
+{
+ GtkWindow *parent;
+
+ /* show new modal on top of old modal */
+ if (shell->modal_dialogs->len > 0) {
+ parent = g_ptr_array_index (shell->modal_dialogs,
+ shell->modal_dialogs->len - 1);
+ g_debug ("using old modal %p as parent", parent);
+ } else {
+ parent = GTK_WINDOW (shell);
+ g_debug ("using main window");
+ }
+ gtk_window_set_transient_for (window, parent);
+
+ /* add to stack, transfer ownership to here */
+ g_ptr_array_add (shell->modal_dialogs, window);
+ g_signal_connect (GTK_WIDGET (window), "unmap",
+ G_CALLBACK (modal_dialog_unmapped_cb), shell);
+
+ /* present the new one */
+ gtk_window_set_modal (window, TRUE);
+ gtk_window_present (window);
+}
+
+void
+gs_shell_activate (GsShell *shell)
+{
+ /* Waiting for plugin loader to setup first */
+ if (shell->plugin_loader == NULL) {
+ shell->activate_after_setup = TRUE;
+ return;
+ }
+
+ gtk_widget_show (GTK_WIDGET (shell));
+ gtk_window_present (GTK_WINDOW (shell));
+}
+
+static void
+gs_shell_set_header_start_widget (GsShell *shell, GtkWidget *widget)
+{
+ GtkWidget *old_widget;
+
+ old_widget = shell->header_start_widget;
+
+ if (shell->header_start_widget == widget)
+ return;
+
+ if (widget != NULL) {
+ g_object_ref (widget);
+ adw_header_bar_pack_start (ADW_HEADER_BAR (shell->main_header), widget);
+ }
+
+ shell->header_start_widget = widget;
+
+ if (old_widget != NULL) {
+ adw_header_bar_remove (ADW_HEADER_BAR (shell->main_header), old_widget);
+ g_object_unref (old_widget);
+ }
+}
+
+static void
+gs_shell_set_header_end_widget (GsShell *shell, GtkWidget *widget)
+{
+ GtkWidget *old_widget;
+
+ old_widget = shell->header_end_widget;
+
+ if (shell->header_end_widget == widget)
+ return;
+
+ if (widget != NULL) {
+ g_object_ref (widget);
+ adw_header_bar_pack_end (ADW_HEADER_BAR (shell->main_header), widget);
+ }
+
+ shell->header_end_widget = widget;
+
+ if (old_widget != NULL) {
+ adw_header_bar_remove (ADW_HEADER_BAR (shell->main_header), old_widget);
+ g_object_unref (old_widget);
+ }
+}
+
+static void
+gs_shell_refresh_auto_updates_ui (GsShell *shell)
+{
+ gboolean automatic_updates_paused;
+ gboolean automatic_updates_enabled;
+
+ automatic_updates_enabled = g_settings_get_boolean (shell->settings, "download-updates");
+
+#ifdef HAVE_MOGWAI
+ automatic_updates_paused = (shell->scheduler == NULL || !mwsc_scheduler_get_allow_downloads (shell->scheduler));
+#else
+ automatic_updates_paused = gs_plugin_loader_get_network_metered (shell->plugin_loader);
+#endif
+
+ gtk_info_bar_set_revealed (GTK_INFO_BAR (shell->metered_updates_bar),
+ gs_shell_get_mode (shell) != GS_SHELL_MODE_LOADING &&
+ automatic_updates_enabled &&
+ automatic_updates_paused);
+ gtk_info_bar_set_default_response (GTK_INFO_BAR (shell->metered_updates_bar), GTK_RESPONSE_OK);
+}
+
+static void
+gs_shell_metered_updates_bar_response_cb (GtkInfoBar *info_bar,
+ gint response_id,
+ gpointer user_data)
+{
+ GsShell *shell = GS_SHELL (user_data);
+ GtkWidget *dialog;
+
+ dialog = gs_metered_data_dialog_new (GTK_WINDOW (shell));
+ gs_shell_modal_dialog_present (shell, GTK_WINDOW (dialog));
+}
+
+static void
+gs_shell_download_updates_changed_cb (GSettings *settings,
+ const gchar *key,
+ gpointer user_data)
+{
+ GsShell *shell = user_data;
+
+ gs_shell_refresh_auto_updates_ui (shell);
+}
+
+static void
+gs_shell_network_metered_notify_cb (GsPluginLoader *plugin_loader,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+#ifndef HAVE_MOGWAI
+ GsShell *shell = user_data;
+
+ /* @automatic_updates_paused only depends on network-metered if we’re
+ * compiled without Mogwai. */
+ gs_shell_refresh_auto_updates_ui (shell);
+#endif
+}
+
+#ifdef HAVE_MOGWAI
+static void
+scheduler_invalidated_cb (GsShell *shell)
+{
+ /* The scheduler shouldn’t normally be invalidated, since we Hold() it
+ * until we’re done with it. However, if the scheduler is stopped by
+ * systemd (`systemctl stop mogwai-scheduled`) this signal will be
+ * emitted. It may also be invalidated while our main window is hidden,
+ * as we release our Hold() then. */
+ g_signal_handler_disconnect (shell->scheduler,
+ shell->scheduler_invalidated_handler);
+ shell->scheduler_invalidated_handler = 0;
+ shell->scheduler_held = FALSE;
+
+ g_clear_object (&shell->scheduler);
+}
+
+static void
+scheduler_allow_downloads_changed_cb (GsShell *shell)
+{
+ gs_shell_refresh_auto_updates_ui (shell);
+}
+
+static void
+scheduler_hold_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ g_autoptr(GError) error_local = NULL;
+ MwscScheduler *scheduler = (MwscScheduler *) source_object;
+ g_autoptr(GsShell) shell = data; /* reference added when starting the async operation */
+
+ if (mwsc_scheduler_hold_finish (scheduler, result, &error_local)) {
+ shell->scheduler_held = TRUE;
+ } else if (!g_error_matches (error_local, G_DBUS_ERROR, G_DBUS_ERROR_FAILED)) {
+ g_warning ("Couldn't hold the Mogwai Scheduler daemon: %s",
+ error_local->message);
+ }
+
+ g_clear_error (&error_local);
+
+ shell->scheduler_invalidated_handler =
+ g_signal_connect_swapped (scheduler, "invalidated",
+ (GCallback) scheduler_invalidated_cb,
+ shell);
+
+ g_signal_connect_object (scheduler, "notify::allow-downloads",
+ (GCallback) scheduler_allow_downloads_changed_cb,
+ shell,
+ G_CONNECT_SWAPPED);
+
+ g_assert (shell->scheduler == NULL);
+ shell->scheduler = scheduler;
+
+ /* Update the UI accordingly. */
+ gs_shell_refresh_auto_updates_ui (shell);
+}
+
+static void
+scheduler_release_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ MwscScheduler *scheduler = (MwscScheduler *) source_object;
+ g_autoptr(GsShell) shell = data; /* reference added when starting the async operation */
+ g_autoptr(GError) error_local = NULL;
+
+ if (!mwsc_scheduler_release_finish (scheduler, result, &error_local))
+ g_warning ("Couldn't release the Mogwai Scheduler daemon: %s",
+ error_local->message);
+
+ shell->scheduler_held = FALSE;
+ g_clear_object (&shell->scheduler);
+}
+
+static void
+scheduler_ready_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ MwscScheduler *scheduler;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsShell) shell = data; /* reference added when starting the async operation */
+
+ scheduler = mwsc_scheduler_new_finish (result, &error_local);
+
+ if (scheduler == NULL) {
+ g_warning ("%s: Error getting Mogwai Scheduler: %s", G_STRFUNC,
+ error_local->message);
+ return;
+ }
+
+ mwsc_scheduler_hold_async (scheduler,
+ "monitoring allow-downloads property",
+ NULL,
+ scheduler_hold_cb,
+ g_object_ref (shell));
+}
+#endif /* HAVE_MOGWAI */
+
+static void
+gs_shell_basic_auth_start_cb (GsPluginLoader *plugin_loader,
+ const gchar *remote,
+ const gchar *realm,
+ GsBasicAuthCallback callback,
+ gpointer callback_data,
+ GsShell *shell)
+{
+ GtkWidget *dialog;
+
+ dialog = gs_basic_auth_dialog_new (GTK_WINDOW (shell), remote, realm, callback, callback_data);
+ gs_shell_modal_dialog_present (shell, GTK_WINDOW (dialog));
+
+ /* just destroy */
+ g_signal_connect_swapped (dialog, "response",
+ G_CALLBACK (gtk_window_destroy), dialog);
+}
+
+static gboolean
+gs_shell_ask_untrusted_cb (GsPluginLoader *plugin_loader,
+ const gchar *title,
+ const gchar *msg,
+ const gchar *details,
+ const gchar *accept_label,
+ GsShell *shell)
+{
+ return gs_utils_ask_user_accepts (GTK_WINDOW (shell), title, msg, details, accept_label);
+}
+
+static void
+free_back_entry (BackEntry *entry)
+{
+ if (entry->focus != NULL)
+ g_object_remove_weak_pointer (G_OBJECT (entry->focus),
+ (gpointer *) &entry->focus);
+ g_clear_object (&entry->category);
+ g_clear_object (&entry->app);
+ g_free (entry->search);
+ g_free (entry);
+}
+
+static void
+gs_shell_clean_back_entry_stack (GsShell *shell)
+{
+ BackEntry *entry;
+
+ while ((entry = g_queue_pop_head (shell->back_entry_stack)) != NULL) {
+ free_back_entry (entry);
+ }
+}
+
+static gboolean
+gs_shell_get_mode_is_main (GsShellMode mode)
+{
+ switch (mode) {
+ case GS_SHELL_MODE_OVERVIEW:
+ case GS_SHELL_MODE_INSTALLED:
+ case GS_SHELL_MODE_SEARCH:
+ case GS_SHELL_MODE_UPDATES:
+ case GS_SHELL_MODE_LOADING:
+ return TRUE;
+ case GS_SHELL_MODE_DETAILS:
+ case GS_SHELL_MODE_CATEGORY:
+ case GS_SHELL_MODE_EXTRAS:
+ case GS_SHELL_MODE_MODERATE:
+ return FALSE;
+ default:
+ return TRUE;
+ }
+}
+
+static void search_bar_search_mode_enabled_changed_cb (GtkSearchBar *search_bar,
+ GParamSpec *pspec,
+ GsShell *shell);
+static void gs_overview_page_button_cb (GtkWidget *widget, GsShell *shell);
+
+static void
+update_header_widgets (GsShell *shell)
+{
+ GsShellMode mode = gs_shell_get_mode (shell);
+
+ /* only show the search button in overview and search pages */
+ g_signal_handlers_block_by_func (shell->search_bar, search_bar_search_mode_enabled_changed_cb, shell);
+
+ /* hide unless we're going to search */
+ gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (shell->search_bar),
+ mode == GS_SHELL_MODE_SEARCH);
+
+ g_signal_handlers_unblock_by_func (shell->search_bar, search_bar_search_mode_enabled_changed_cb, shell);
+}
+
+static void
+stack_notify_visible_child_cb (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsShell *shell = GS_SHELL (user_data);
+ GsPage *page;
+ GtkWidget *widget;
+ GsShellMode mode = gs_shell_get_mode (shell);
+ gsize i;
+
+ update_header_widgets (shell);
+
+ /* do action for mode */
+ page = shell->pages[mode];
+
+ if (mode == GS_SHELL_MODE_OVERVIEW ||
+ mode == GS_SHELL_MODE_INSTALLED ||
+ mode == GS_SHELL_MODE_UPDATES)
+ gs_shell_clean_back_entry_stack (shell);
+
+ if (shell->page != NULL)
+ gs_page_switch_from (shell->page);
+ g_set_object (&shell->page, page);
+ gs_page_switch_to (page);
+
+ /* update header bar widgets */
+ switch (mode) {
+ case GS_SHELL_MODE_OVERVIEW:
+ case GS_SHELL_MODE_INSTALLED:
+ case GS_SHELL_MODE_SEARCH:
+ gtk_widget_show (shell->search_button);
+ break;
+ case GS_SHELL_MODE_UPDATES:
+ gtk_widget_hide (shell->search_button);
+ break;
+ default:
+ /* We don't care about changing the visibility of the search
+ * button in modes appearing in sub-pages. */
+ break;
+ }
+
+ widget = gs_page_get_header_start_widget (page);
+ switch (mode) {
+ case GS_SHELL_MODE_OVERVIEW:
+ case GS_SHELL_MODE_INSTALLED:
+ case GS_SHELL_MODE_UPDATES:
+ case GS_SHELL_MODE_SEARCH:
+ gs_shell_set_header_start_widget (shell, widget);
+ break;
+ default:
+ g_assert (widget == NULL);
+ break;
+ }
+
+ widget = gs_page_get_header_end_widget (page);
+ switch (mode) {
+ case GS_SHELL_MODE_OVERVIEW:
+ case GS_SHELL_MODE_INSTALLED:
+ case GS_SHELL_MODE_UPDATES:
+ case GS_SHELL_MODE_SEARCH:
+ gs_shell_set_header_end_widget (shell, widget);
+ break;
+ default:
+ g_assert (widget == NULL);
+ break;
+ }
+
+ g_clear_object (&shell->sub_page_header_title_binding);
+ shell->sub_page_header_title_binding = g_object_bind_property (adw_view_stack_get_visible_child (shell->stack_sub), "title",
+ shell->sub_page_header_title, "label",
+ G_BINDING_SYNC_CREATE);
+
+ /* refresh the updates bar when moving out of the loading mode, but only
+ * if the Mogwai scheduler state is already known, to avoid spuriously
+ * showing the updates bar */
+#ifdef HAVE_MOGWAI
+ if (shell->scheduler != NULL)
+#else
+ if (TRUE)
+#endif
+ gs_shell_refresh_auto_updates_ui (shell);
+
+ /* destroy any existing modals */
+ if (shell->modal_dialogs != NULL) {
+ /* block signal emission of 'unmapped' since that will
+ * call g_ptr_array_remove_index. The unmapped signal may
+ * be emitted whilst running unref handlers for
+ * g_ptr_array_set_size */
+ for (i = 0; i < shell->modal_dialogs->len; ++i) {
+ GtkWidget *dialog = g_ptr_array_index (shell->modal_dialogs, i);
+ g_signal_handlers_disconnect_by_func (dialog,
+ modal_dialog_unmapped_cb,
+ shell);
+ gtk_window_destroy (GTK_WINDOW (dialog));
+ }
+ g_ptr_array_set_size (shell->modal_dialogs, 0);
+ }
+}
+
+void
+gs_shell_change_mode (GsShell *shell,
+ GsShellMode mode,
+ gpointer data,
+ gboolean scroll_up)
+{
+ GsApp *app;
+ GsPage *page;
+ gboolean mode_is_main = gs_shell_get_mode_is_main (mode);
+
+ if (gs_shell_get_mode (shell) == mode &&
+ (mode != GS_SHELL_MODE_DETAILS ||
+ data == gs_details_page_get_app (GS_DETAILS_PAGE (shell->pages[mode])))) {
+ return;
+ }
+
+ /* switch page */
+ if (mode == GS_SHELL_MODE_LOADING) {
+ adw_view_stack_set_visible_child_name (shell->stack_loading, "loading");
+ return;
+ }
+
+ adw_view_stack_set_visible_child_name (shell->stack_loading, "main");
+ if (mode == GS_SHELL_MODE_DETAILS) {
+ adw_leaflet_set_visible_child_name (shell->details_leaflet, "details");
+ } else {
+ adw_leaflet_set_visible_child_name (shell->details_leaflet, "main");
+ /* We only change the main leaflet when not reaching the details
+ * page to preserve the navigation history in the UI's state.
+ * First change the page, then the leaflet, to avoid load of
+ * the previously shown page, which will be changed shortly after. */
+ adw_view_stack_set_visible_child_name (mode_is_main ? shell->stack_main : shell->stack_sub, page_name[mode]);
+ adw_leaflet_set_visible_child_name (shell->main_leaflet, mode_is_main ? "main" : "sub");
+ }
+
+ /* do any mode-specific actions */
+ page = shell->pages[mode];
+
+ if (mode == GS_SHELL_MODE_SEARCH) {
+ gs_search_page_set_text (GS_SEARCH_PAGE (page), data);
+ gtk_editable_set_text (GTK_EDITABLE (shell->entry_search), data);
+ gtk_editable_set_position (GTK_EDITABLE (shell->entry_search), -1);
+ } else if (mode == GS_SHELL_MODE_DETAILS) {
+ app = GS_APP (data);
+ if (gs_app_get_metadata_item (app, "GnomeSoftware::show-metainfo") != NULL) {
+ gs_details_page_set_metainfo (GS_DETAILS_PAGE (page),
+ gs_app_get_local_file (app));
+ } else if (gs_app_get_local_file (app) != NULL) {
+ gs_details_page_set_local_file (GS_DETAILS_PAGE (page),
+ gs_app_get_local_file (app));
+ } else if (gs_app_get_metadata_item (app, "GnomeSoftware::from-url") != NULL) {
+ gs_details_page_set_url (GS_DETAILS_PAGE (page),
+ gs_app_get_metadata_item (app, "GnomeSoftware::from-url"));
+ } else {
+ gs_details_page_set_app (GS_DETAILS_PAGE (page), data);
+ }
+ } else if (mode == GS_SHELL_MODE_CATEGORY) {
+ gs_category_page_set_category (GS_CATEGORY_PAGE (page),
+ GS_CATEGORY (data));
+ }
+
+ if (scroll_up)
+ gs_page_scroll_up (page);
+}
+
+static gboolean
+overlay_get_child_position_cb (GtkOverlay *overlay,
+ GtkWidget *widget,
+ GdkRectangle *allocation,
+ gpointer user_data)
+{
+ GsShell *self = GS_SHELL (user_data);
+ GtkRequisition overlay_natural_size;
+
+ /* Override the default position of the in-app notification overlay
+ * to position it below the header bar. The overlay can’t easily be
+ * moved in the widget hierarchy so it doesn’t have the header bar as
+ * a child, since there are several header bars in different pages of
+ * a AdwLeaflet. */
+ g_assert (gtk_widget_is_ancestor (self->main_header, GTK_WIDGET (overlay)));
+
+ gtk_widget_get_preferred_size (widget, NULL, &overlay_natural_size);
+
+ allocation->width = overlay_natural_size.width;
+ allocation->height = overlay_natural_size.height;
+
+ allocation->x = gtk_widget_get_allocated_width (GTK_WIDGET (overlay)) / 2 - overlay_natural_size.width / 2;
+ allocation->y = gtk_widget_get_allocated_height (GTK_WIDGET (self->main_header));
+
+ return TRUE;
+}
+
+static void
+gs_overview_page_button_cb (GtkWidget *widget, GsShell *shell)
+{
+ GsShellMode mode;
+ mode = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget),
+ "gnome-software::overview-mode"));
+ gs_shell_change_mode (shell, mode, NULL, TRUE);
+}
+
+static void
+save_back_entry (GsShell *shell)
+{
+ BackEntry *entry;
+
+ entry = g_new0 (BackEntry, 1);
+ entry->mode = gs_shell_get_mode (shell);
+
+ entry->focus = gtk_window_get_focus (GTK_WINDOW (shell));
+ if (entry->focus != NULL)
+ g_object_add_weak_pointer (G_OBJECT (entry->focus),
+ (gpointer *) &entry->focus);
+
+ switch (entry->mode) {
+ case GS_SHELL_MODE_CATEGORY:
+ entry->category = gs_category_page_get_category (GS_CATEGORY_PAGE (shell->pages[GS_SHELL_MODE_CATEGORY]));
+ g_object_ref (entry->category);
+ g_debug ("pushing back entry for %s with %s",
+ page_name[entry->mode],
+ gs_category_get_id (entry->category));
+ break;
+ case GS_SHELL_MODE_SEARCH:
+ entry->search = g_strdup (gs_search_page_get_text (GS_SEARCH_PAGE (shell->pages[GS_SHELL_MODE_SEARCH])));
+ g_debug ("pushing back entry for %s with %s",
+ page_name[entry->mode], entry->search);
+ break;
+ case GS_SHELL_MODE_DETAILS:
+ entry->app = g_object_ref (gs_details_page_get_app (GS_DETAILS_PAGE (shell->pages[GS_SHELL_MODE_DETAILS])));
+ entry->vscroll_position = gs_details_page_get_vscroll_position (GS_DETAILS_PAGE (shell->pages[GS_SHELL_MODE_DETAILS]));
+ break;
+ default:
+ g_debug ("pushing back entry for %s", page_name[entry->mode]);
+ break;
+ }
+
+ g_queue_push_head (shell->back_entry_stack, entry);
+}
+
+static void
+gs_shell_plugin_events_sources_cb (GtkWidget *widget, GsShell *shell)
+{
+ gs_shell_show_sources (shell);
+}
+
+static void
+gs_shell_plugin_events_no_space_cb (GtkWidget *widget, GsShell *shell)
+{
+ g_autoptr(GError) error = NULL;
+ if (!g_spawn_command_line_async ("baobab", &error))
+ g_warning ("failed to exec baobab: %s", error->message);
+}
+
+static void
+gs_shell_plugin_events_network_settings_cb (GtkWidget *widget, GsShell *shell)
+{
+ g_autoptr(GError) error = NULL;
+ if (!g_spawn_command_line_async ("gnome-control-center network", &error))
+ g_warning ("failed to exec gnome-control-center: %s", error->message);
+}
+
+static void
+gs_shell_plugin_events_more_info_cb (GtkWidget *widget, GsShell *shell)
+{
+ g_autoptr(GError) error = NULL;
+ if (!g_app_info_launch_default_for_uri (shell->events_info_uri, NULL, &error)) {
+ g_warning ("failed to launch URI %s: %s",
+ shell->events_info_uri, error->message);
+ }
+}
+
+static void
+gs_shell_plugin_events_restart_required_cb (GtkWidget *widget, GsShell *shell)
+{
+ g_autoptr(GError) error = NULL;
+ if (!g_spawn_command_line_async (LIBEXECDIR "/gnome-software-restarter", &error))
+ g_warning ("failed to restart: %s", error->message);
+}
+
+/* this is basically a workaround for GtkSearchEntry. Due to delayed emission of the search-changed
+ * signal it can't be blocked during insertion of text into the entry. Therefore we block the
+ * precursor of that signal to be able to add text to the entry without firing the handlers
+ * connected to "search-changed"
+ */
+static void
+block_changed (GtkEditable *editable,
+ gpointer user_data)
+{
+ g_signal_stop_emission_by_name (editable, "changed");
+}
+
+static void
+block_changed_signal (GtkSearchEntry *entry)
+{
+ g_signal_connect (entry, "changed", G_CALLBACK (block_changed), NULL);
+}
+
+static void
+unblock_changed_signal (GtkSearchEntry *entry)
+{
+ g_signal_handlers_disconnect_by_func (entry, G_CALLBACK (block_changed), NULL);
+}
+
+static void
+gs_shell_go_back (GsShell *shell)
+{
+ BackEntry *entry;
+
+ /* nothing to do */
+ if (g_queue_is_empty (shell->back_entry_stack)) {
+ g_debug ("no back stack, showing overview");
+ gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, FALSE);
+ return;
+ }
+
+ entry = g_queue_pop_head (shell->back_entry_stack);
+
+ switch (entry->mode) {
+ case GS_SHELL_MODE_UNKNOWN:
+ case GS_SHELL_MODE_LOADING:
+ /* happens when using --search, --details, --install, etc. options */
+ g_debug ("popping back entry for %s", page_name[entry->mode]);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, FALSE);
+ break;
+ case GS_SHELL_MODE_CATEGORY:
+ g_debug ("popping back entry for %s with %s",
+ page_name[entry->mode],
+ gs_category_get_id (entry->category));
+ gs_shell_change_mode (shell, entry->mode, entry->category, FALSE);
+ break;
+ case GS_SHELL_MODE_SEARCH:
+ g_debug ("popping back entry for %s with %s",
+ page_name[entry->mode], entry->search);
+
+ /* set the text in the entry and move cursor to the end */
+ block_changed_signal (GTK_SEARCH_ENTRY (shell->entry_search));
+ gtk_editable_set_text (GTK_EDITABLE (shell->entry_search), entry->search);
+ gtk_editable_set_position (GTK_EDITABLE (shell->entry_search), -1);
+ unblock_changed_signal (GTK_SEARCH_ENTRY (shell->entry_search));
+
+ /* set the mode directly */
+ gs_shell_change_mode (shell, entry->mode,
+ (gpointer) entry->search, FALSE);
+ break;
+ case GS_SHELL_MODE_DETAILS:
+ g_debug ("popping back entry for %s with app %s and vscroll position %f",
+ page_name[entry->mode],
+ gs_app_get_unique_id (entry->app),
+ entry->vscroll_position);
+ gs_shell_change_mode (shell, entry->mode, entry->app, FALSE);
+ gs_details_page_set_vscroll_position (GS_DETAILS_PAGE (shell->pages[GS_SHELL_MODE_DETAILS]), entry->vscroll_position);
+ break;
+ default:
+ g_debug ("popping back entry for %s", page_name[entry->mode]);
+ gs_shell_change_mode (shell, entry->mode, NULL, FALSE);
+ break;
+ }
+
+ if (entry->focus != NULL)
+ gtk_widget_grab_focus (entry->focus);
+
+ free_back_entry (entry);
+}
+
+static void
+gs_shell_details_back_button_cb (GtkWidget *widget, GsShell *shell)
+{
+ gs_shell_go_back (shell);
+}
+
+static void
+gs_shell_back_button_cb (GtkWidget *widget, GsShell *shell)
+{
+ gs_shell_go_back (shell);
+}
+
+static void
+gs_shell_reload_cb (GsPluginLoader *plugin_loader, GsShell *shell)
+{
+ for (gsize i = 0; i < G_N_ELEMENTS (shell->pages); i++) {
+ GsPage *page = shell->pages[i];
+ if (page != NULL)
+ gs_page_reload (page);
+ }
+}
+
+static void
+gs_shell_details_page_metainfo_loaded_cb (GtkWidget *details_page,
+ GsApp *app,
+ GsShell *self)
+{
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (GS_IS_SHELL (self));
+
+ /* If the user has manually loaded some metainfo to
+ * preview, override the featured carousel with it too,
+ * so they can see how it looks in the carousel. */
+ gs_overview_page_override_featured (GS_OVERVIEW_PAGE (self->pages[GS_SHELL_MODE_OVERVIEW]), app);
+}
+
+static gboolean
+change_mode_idle (gpointer user_data)
+{
+ GsShell *shell = user_data;
+
+ gs_page_reload (GS_PAGE (shell->pages[GS_SHELL_MODE_UPDATES]));
+ gs_page_reload (GS_PAGE (shell->pages[GS_SHELL_MODE_INSTALLED]));
+
+ /* Switch only when still on the loading page, otherwise the page
+ could be changed from the command line or such, which would mean
+ hiding the chosen page. */
+ if (gs_shell_get_mode (shell) == GS_SHELL_MODE_LOADING)
+ gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, TRUE);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+overview_page_refresh_done (GsOverviewPage *overview_page, gpointer data)
+{
+ GsShell *shell = data;
+
+ g_signal_handlers_disconnect_by_func (overview_page, overview_page_refresh_done, data);
+
+ /* now that we're finished with the loading page, connect the reload signal handler */
+ g_signal_connect (shell->plugin_loader, "reload",
+ G_CALLBACK (gs_shell_reload_cb), shell);
+
+ /* schedule to change the mode in an idle callback, since it can take a
+ * while and this callback handler is typically called at the end of a
+ * long main context iteration already */
+ g_idle_add (change_mode_idle, shell);
+}
+
+static void
+initial_refresh_done (GsLoadingPage *loading_page, gpointer data)
+{
+ GsShell *shell = data;
+ gboolean been_overview;
+
+ g_signal_handlers_disconnect_by_func (loading_page, initial_refresh_done, data);
+
+ been_overview = gs_shell_get_mode (shell) == GS_SHELL_MODE_OVERVIEW;
+
+ g_signal_emit (shell, signals[SIGNAL_LOADED], 0);
+
+ /* if the "loaded" signal handler didn't change the mode, kick off async
+ * overview page refresh, and switch to the page once done */
+ if (gs_shell_get_mode (shell) == GS_SHELL_MODE_LOADING || been_overview) {
+ g_signal_connect (shell->pages[GS_SHELL_MODE_OVERVIEW], "refreshed",
+ G_CALLBACK (overview_page_refresh_done), shell);
+ gs_page_reload (GS_PAGE (shell->pages[GS_SHELL_MODE_OVERVIEW]));
+ return;
+ }
+
+ /* now that we're finished with the loading page, connect the reload signal handler */
+ g_signal_connect (shell->plugin_loader, "reload",
+ G_CALLBACK (gs_shell_reload_cb), shell);
+}
+
+static gboolean
+window_keypress_handler (GtkEventControllerKey *key_controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ GsShell *shell)
+{
+ /* handle ctrl+f shortcut */
+ if ((state & GDK_CONTROL_MASK) > 0 && keyval == GDK_KEY_f) {
+ if (!gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (shell->search_bar))) {
+ GsShellMode mode = gs_shell_get_mode (shell);
+
+ gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (shell->search_bar), TRUE);
+ gtk_widget_grab_focus (shell->entry_search);
+
+ /* If the mode doesn't have a search button,
+ * switch to the search page right away,
+ * otherwise we would show the search bar
+ * without a button to toggle it. */
+ switch (mode) {
+ case GS_SHELL_MODE_OVERVIEW:
+ case GS_SHELL_MODE_INSTALLED:
+ case GS_SHELL_MODE_SEARCH:
+ break;
+ default:
+ gs_shell_show_search (shell, "");
+ break;
+ }
+ } else {
+ gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (shell->search_bar), FALSE);
+ }
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+search_changed_handler (GObject *entry, GsShell *shell)
+{
+ g_autofree gchar *text = NULL;
+
+ text = g_strdup (gtk_editable_get_text (GTK_EDITABLE (entry)));
+ if (strlen (text) >= 2) {
+ if (gs_shell_get_mode (shell) != GS_SHELL_MODE_SEARCH) {
+ save_back_entry (shell);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_SEARCH,
+ (gpointer) text, TRUE);
+ } else {
+ gs_search_page_set_text (GS_SEARCH_PAGE (shell->pages[GS_SHELL_MODE_SEARCH]), text);
+ gs_page_switch_to (shell->pages[GS_SHELL_MODE_SEARCH]);
+ gs_page_scroll_up (shell->pages[GS_SHELL_MODE_SEARCH]);
+ }
+ }
+}
+
+static void
+search_bar_search_mode_enabled_changed_cb (GtkSearchBar *search_bar,
+ GParamSpec *pspec,
+ GsShell *shell)
+{
+ /* go back when exiting the search view */
+ if (gs_shell_get_mode (shell) == GS_SHELL_MODE_SEARCH &&
+ !gtk_search_bar_get_search_mode (search_bar))
+ gs_shell_go_back (shell);
+}
+
+static void
+go_back (GsShell *shell)
+{
+ if (adw_leaflet_get_adjacent_child (shell->details_leaflet,
+ ADW_NAVIGATION_DIRECTION_BACK)) {
+ gtk_widget_activate (shell->button_back2);
+ } else {
+ gtk_widget_activate (shell->button_back);
+ }
+}
+
+static gboolean
+window_key_pressed_cb (GtkEventControllerKey *key_controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ GsShell *shell)
+{
+ gboolean is_rtl = gtk_widget_get_direction (shell->button_back) == GTK_TEXT_DIR_RTL;
+ gboolean is_alt = (state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_ALT_MASK)) == GDK_ALT_MASK;
+
+ if ((!is_rtl && is_alt && keyval == GDK_KEY_Left) ||
+ (is_rtl && is_alt && keyval == GDK_KEY_Right) ||
+ keyval == GDK_KEY_Back) {
+ go_back (shell);
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+window_button_pressed_cb (GtkGestureClick *click_gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ GsShell *shell)
+{
+ go_back (shell);
+
+ gtk_gesture_set_state (GTK_GESTURE (click_gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static gboolean
+main_window_closed_cb (GtkWidget *dialog, gpointer user_data)
+{
+ GsShell *shell = user_data;
+
+ /* hide any notifications */
+ g_application_withdraw_notification (g_application_get_default (),
+ "installed");
+ g_application_withdraw_notification (g_application_get_default (),
+ "install-resources");
+
+ /* clear any in-app notification */
+ gtk_revealer_set_reveal_child (GTK_REVEALER (shell->notification_event), FALSE);
+
+ /* release our hold on the download scheduler */
+#ifdef HAVE_MOGWAI
+ if (shell->scheduler != NULL) {
+ if (shell->scheduler_invalidated_handler > 0)
+ g_signal_handler_disconnect (shell->scheduler,
+ shell->scheduler_invalidated_handler);
+ shell->scheduler_invalidated_handler = 0;
+
+ if (shell->scheduler_held)
+ mwsc_scheduler_release_async (shell->scheduler,
+ NULL,
+ scheduler_release_cb,
+ g_object_ref (shell));
+ else
+ g_clear_object (&shell->scheduler);
+ }
+#endif /* HAVE_MOGWAI */
+
+ gs_shell_clean_back_entry_stack (shell);
+ gtk_widget_hide (dialog);
+
+#ifdef __GLIBC__
+ /* Free unused memory with GNU extension of malloc.h */
+ malloc_trim (0);
+#endif
+
+ return TRUE;
+}
+
+static void
+gs_shell_main_window_mapped_cb (GtkWidget *widget, GsShell *shell)
+{
+ gs_plugin_loader_set_scale (shell->plugin_loader,
+ (guint) gtk_widget_get_scale_factor (widget));
+
+ /* Set up the updates bar. Do this here rather than in gs_shell_setup()
+ * since we only want to hold the scheduler open while the gnome-software
+ * main window is visible, and not while we’re running in the background. */
+#ifdef HAVE_MOGWAI
+ if (shell->scheduler == NULL)
+ mwsc_scheduler_new_async (shell->cancellable,
+ (GAsyncReadyCallback) scheduler_ready_cb,
+ g_object_ref (shell));
+#else
+ gs_shell_refresh_auto_updates_ui (shell);
+#endif /* HAVE_MOGWAI */
+}
+
+static void
+gs_shell_main_window_realized_cb (GtkWidget *widget, GsShell *shell)
+{
+ GdkRectangle geometry;
+ GdkSurface *surface;
+ GdkDisplay *display;
+ GdkMonitor *monitor;
+
+ display = gtk_widget_get_display (GTK_WIDGET (shell));
+ surface = gtk_native_get_surface (GTK_NATIVE (shell));
+ monitor = gdk_display_get_monitor_at_surface (display, surface);
+
+ /* adapt the window for low and medium resolution screens */
+ if (monitor != NULL) {
+ gdk_monitor_get_geometry (monitor, &geometry);
+ if (geometry.width < 800 || geometry.height < 600) {
+ } else if (geometry.width < 1366 || geometry.height < 768) {
+ gtk_window_set_default_size (GTK_WINDOW (shell), 1050, 600);
+ }
+ }
+}
+
+typedef enum {
+ GS_SHELL_EVENT_BUTTON_NONE = 0,
+ GS_SHELL_EVENT_BUTTON_SOURCES = 1 << 0,
+ GS_SHELL_EVENT_BUTTON_NO_SPACE = 1 << 1,
+ GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS = 1 << 2,
+ GS_SHELL_EVENT_BUTTON_MORE_INFO = 1 << 3,
+ GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED = 1 << 4,
+ GS_SHELL_EVENT_BUTTON_LAST
+} GsShellEventButtons;
+
+static gboolean
+gs_shell_has_disk_examination_app (void)
+{
+ g_autofree gchar *baobab = g_find_program_in_path ("baobab");
+ return (baobab != NULL);
+}
+
+static void
+gs_shell_show_event_app_notify (GsShell *shell,
+ const gchar *title,
+ GsShellEventButtons buttons)
+{
+ /* set visible */
+ gtk_revealer_set_reveal_child (GTK_REVEALER (shell->notification_event), TRUE);
+
+ /* sources button */
+ gtk_widget_set_visible (shell->button_events_sources,
+ (buttons & GS_SHELL_EVENT_BUTTON_SOURCES) > 0);
+
+ /* no-space button */
+ gtk_widget_set_visible (shell->button_events_no_space,
+ (buttons & GS_SHELL_EVENT_BUTTON_NO_SPACE) > 0 &&
+ gs_shell_has_disk_examination_app());
+
+ /* network settings button */
+ gtk_widget_set_visible (shell->button_events_network_settings,
+ (buttons & GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS) > 0);
+
+ /* restart button */
+ gtk_widget_set_visible (shell->button_events_restart_required,
+ (buttons & GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED) > 0);
+
+ /* more-info button */
+ gtk_widget_set_visible (shell->button_events_more_info,
+ (buttons & GS_SHELL_EVENT_BUTTON_MORE_INFO) > 0);
+
+ /* dismiss button */
+ gtk_widget_set_visible (shell->button_events_dismiss,
+ (buttons & GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED) == 0);
+
+ /* set title */
+ gtk_label_set_markup (GTK_LABEL (shell->label_events), title);
+ gtk_widget_set_visible (shell->label_events, title != NULL);
+}
+
+void
+gs_shell_show_notification (GsShell *shell, const gchar *title)
+{
+ gs_shell_show_event_app_notify (shell, title, GS_SHELL_EVENT_BUTTON_NONE);
+}
+
+static gchar *
+gs_shell_get_title_from_origin (GsApp *app)
+{
+ /* get a title, falling back */
+ if (gs_app_get_origin_hostname (app) != NULL) {
+ /* TRANSLATORS: this is part of the in-app notification,
+ * where the %s is the truncated hostname, e.g.
+ * 'alt.fedoraproject.org' */
+ return g_strdup_printf (_("“%s”"), gs_app_get_origin_hostname (app));
+ }
+ if (gs_app_get_origin (app) != NULL) {
+ /* TRANSLATORS: this is part of the in-app notification,
+ * where the %s is the origin id, e.g. 'fedora' */
+ return g_strdup_printf (_("“%s”"), gs_app_get_origin (app));
+ }
+ return g_strdup_printf ("“%s”", gs_app_get_id (app));
+}
+
+/* return a name for the app, using quotes if the name is more than one word */
+static gchar *
+gs_shell_get_title_from_app (GsApp *app)
+{
+ const gchar *tmp = gs_app_get_name (app);
+ if (tmp != NULL) {
+ if (g_strstr_len (tmp, -1, " ") != NULL) {
+ /* TRANSLATORS: this is part of the in-app notification,
+ * where the %s is a multi-word localised app name
+ * e.g. 'Getting things GNOME!" */
+ return g_strdup_printf (_("“%s”"), tmp);
+ }
+ return g_strdup (tmp);
+ }
+ return g_strdup_printf (_("“%s”"), gs_app_get_id (app));
+}
+
+static gchar *
+get_first_lines (const gchar *str)
+{
+ const gchar *end = str;
+ /* Some errors can have an "introduction", thus pick few initial lines, not only the first. */
+ for (guint lines = 0; end != NULL && lines < 7; lines++) {
+ end = strchr (end, '\n');
+ if (end != NULL)
+ end++;
+ }
+ if (end != NULL) {
+ g_autofree gchar *tmp = g_strndup (str, end - str);
+ /* Translators: The '%s' is replaced with an error message, which had been shortened.
+ The dots at the end are there to highlight that to the user. */
+ return g_strdup_printf (_("%s…"), tmp);
+ }
+ return g_strdup (str);
+}
+
+static void
+gs_shell_append_detailed_error (GsShell *shell, GString *str, const GError *error)
+{
+ g_autofree gchar *text = get_first_lines (error->message);
+ if (text != NULL) {
+ g_autofree gchar *escaped = g_markup_escape_text (text, -1);
+ g_string_append_printf (str, ":\n%s", escaped);
+ }
+}
+
+static gboolean
+gs_shell_show_event_refresh (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ GsPluginAction action = gs_plugin_event_get_action (event);
+ g_autofree gchar *str_origin = NULL;
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ /* ignore any errors from background downloads */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_DOWNLOAD_FAILED)) {
+ if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ if (gs_app_get_bundle_kind (origin) == AS_BUNDLE_KIND_CABINET) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the source (e.g. "alt.fedoraproject.org") */
+ g_string_append_printf (str, _("Unable to download "
+ "firmware updates from %s"),
+ str_origin);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the source (e.g. "alt.fedoraproject.org") */
+ g_string_append_printf (str, _("Unable to download updates from %s"),
+ str_origin);
+ if (!gs_app_has_management_plugin (origin, NULL))
+ buttons |= GS_SHELL_EVENT_BUTTON_SOURCES;
+ }
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates"));
+ }
+ gs_shell_append_detailed_error (shell, str, error);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates: "
+ "internet access was required but wasn’t available"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the source (e.g. "alt.fedoraproject.org") */
+ g_string_append_printf (str, _("Unable to download updates "
+ "from %s: not enough disk space"),
+ str_origin);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates: "
+ "not enough disk space"));
+ }
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates: "
+ "authentication was required"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates: "
+ "authentication was invalid"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates: you do not have"
+ " permission to install software"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ if (action == GS_PLUGIN_ACTION_DOWNLOAD) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to download updates"));
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to get list of updates"));
+ }
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_install (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *app = gs_plugin_event_get_app (event);
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autofree gchar *str_app = NULL;
+ g_autofree gchar *str_origin = NULL;
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ str_app = gs_shell_get_title_from_app (app);
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_DOWNLOAD_FAILED)) {
+ if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the first %s is the application name (e.g. "GIMP") and
+ * the second %s is the origin, e.g. "Fedora Project [fedoraproject.org]" */
+ g_string_append_printf (str, _("Unable to install %s as "
+ "download failed from %s"),
+ str_app, str_origin);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to install %s "
+ "as download failed"),
+ str_app);
+ }
+ gs_shell_append_detailed_error (shell, str, error);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) {
+ if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the first %s is the application name (e.g. "GIMP")
+ * and the second %s is the name of the runtime, e.g.
+ * "GNOME SDK [flatpak.gnome.org]" */
+ g_string_append_printf (str, _("Unable to install %s as "
+ "runtime %s not available"),
+ str_app, str_origin);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to install %s "
+ "as not supported"),
+ str_app);
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to install: internet access was "
+ "required but wasn’t available"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_INVALID_FORMAT)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to install: the application has an invalid format"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to install %s: "
+ "not enough disk space"),
+ str_app);
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install %s: "
+ "authentication was required"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to install %s: "
+ "authentication was invalid"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to install %s: "
+ "you do not have permission to "
+ "install software"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AC_POWER_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "Dell XPS 13") */
+ g_string_append_printf (str, _("Unable to install %s: "
+ "AC power is required"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "Dell XPS 13") */
+ g_string_append_printf (str, _("Unable to install %s: "
+ "The battery level is too low"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to install %s"), str_app);
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_update (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *app = gs_plugin_event_get_app (event);
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autofree gchar *str_app = NULL;
+ g_autofree gchar *str_origin = NULL;
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ /* ignore any errors from background downloads */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_DOWNLOAD_FAILED)) {
+ if (app != NULL && origin != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the first %s is the app name (e.g. "GIMP") and
+ * the second %s is the origin, e.g. "Fedora" or
+ * "Fedora Project [fedoraproject.org]" */
+ g_string_append_printf (str, _("Unable to update %s from %s as download failed"),
+ str_app, str_origin);
+ buttons = TRUE;
+ } else if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to update %s as download failed"),
+ str_app);
+ } else if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the origin, e.g. "Fedora" or
+ * "Fedora Project [fedoraproject.org]" */
+ g_string_append_printf (str, _("Unable to install updates from %s as download failed"),
+ str_origin);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install updates as download failed"));
+ }
+ gs_shell_append_detailed_error (shell, str, error);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Unable to update: "
+ "internet access was required but "
+ "wasn’t available"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to update %s: "
+ "not enough disk space"),
+ str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install updates: "
+ "not enough disk space"));
+ }
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED)) {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to update %s: "
+ "authentication was required"),
+ str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install updates: "
+ "authentication was required"));
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID)) {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to update %s: "
+ "authentication was invalid"),
+ str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install updates: "
+ "authentication was invalid"));
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to update %s: "
+ "you do not have permission to "
+ "update software"),
+ str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install updates: "
+ "you do not have permission to "
+ "update software"));
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AC_POWER_REQUIRED)) {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "Dell XPS 13") */
+ g_string_append_printf (str, _("Unable to update %s: "
+ "AC power is required"),
+ str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "Dell XPS 13") */
+ g_string_append_printf (str, _("Unable to install updates: "
+ "AC power is required"));
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)) {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "Dell XPS 13") */
+ g_string_append_printf (str, _("Unable to update %s: "
+ "The battery level is too low"),
+ str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "Dell XPS 13") */
+ g_string_append_printf (str, _("Unable to install updates: "
+ "The battery level is too low"));
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ if (app != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to update %s"), str_app);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append_printf (str, _("Unable to install updates"));
+ }
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_upgrade (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *app = gs_plugin_event_get_app (event);
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_autofree gchar *str_app = NULL;
+ g_autofree gchar *str_origin = NULL;
+
+ str_app = g_strdup_printf ("%s %s", gs_app_get_name (app), gs_app_get_version (app));
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_DOWNLOAD_FAILED)) {
+ if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the first %s is the distro name (e.g. "Fedora 25") and
+ * the second %s is the origin, e.g. "Fedora Project [fedoraproject.org]" */
+ g_string_append_printf (str, _("Unable to upgrade to %s from %s"),
+ str_app, str_origin);
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the app name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to upgrade to %s "
+ "as download failed"),
+ str_app);
+ }
+ gs_shell_append_detailed_error (shell, str, error);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "internet access was required but "
+ "wasn’t available"),
+ str_app);
+ buttons |= GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "not enough disk space"),
+ str_app);
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "authentication was required"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "authentication was invalid"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "you do not have permission to upgrade"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AC_POWER_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "AC power is required"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s: "
+ "The battery level is too low"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the distro name (e.g. "Fedora 25") */
+ g_string_append_printf (str, _("Unable to upgrade to %s"), str_app);
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_remove (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *app = gs_plugin_event_get_app (event);
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_autofree gchar *str_app = NULL;
+
+ str_app = gs_shell_get_title_from_app (app);
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to remove %s: authentication was required"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to remove %s: authentication was invalid"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to remove %s: you do not have"
+ " permission to remove software"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AC_POWER_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to remove %s: "
+ "AC power is required"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)) {
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to remove %s: "
+ "The battery level is too low"),
+ str_app);
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* non-interactive generic */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the %s is the application name (e.g. "GIMP") */
+ g_string_append_printf (str, _("Unable to remove %s"), str_app);
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_launch (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *app = gs_plugin_event_get_app (event);
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_autofree gchar *str_app = NULL;
+ g_autofree gchar *str_origin = NULL;
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) {
+ if (app != NULL && origin != NULL) {
+ str_app = gs_shell_get_title_from_app (app);
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * where the first %s is the application name (e.g. "GIMP")
+ * and the second %s is the name of the runtime, e.g.
+ * "GNOME SDK [flatpak.gnome.org]" */
+ g_string_append_printf (str, _("Unable to launch %s: %s is not installed"),
+ str_app,
+ str_origin);
+ } else {
+ /* non-interactive generic */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+ /* TRANSLATORS: we failed to get a proper error code */
+ g_string_append (str, _("Sorry, something went wrong"));
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Not enough disk space — free up some space "
+ "and try again"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* non-interactive generic */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+ /* TRANSLATORS: we failed to get a proper error code */
+ g_string_append (str, _("Sorry, something went wrong"));
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_file_to_app (GsShell *shell, GsPluginEvent *event)
+{
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Failed to install file: not supported"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Failed to install file: authentication failed"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Not enough disk space — free up some space "
+ "and try again"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* non-interactive generic */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+ /* TRANSLATORS: we failed to get a proper error code */
+ g_string_append (str, _("Sorry, something went wrong"));
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_url_to_app (GsShell *shell, GsPluginEvent *event)
+{
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Failed to install: not supported"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Failed to install: authentication failed"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Not enough disk space — free up some space "
+ "and try again"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* non-interactive generic */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+ /* TRANSLATORS: we failed to get a proper error code */
+ g_string_append (str, _("Sorry, something went wrong"));
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event_fallback (GsShell *shell, GsPluginEvent *event)
+{
+ GsApp *origin = gs_plugin_event_get_origin (event);
+ GsShellEventButtons buttons = GS_SHELL_EVENT_BUTTON_NONE;
+ const GError *error = gs_plugin_event_get_error (event);
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_autofree gchar *str_origin = NULL;
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_DOWNLOAD_FAILED)) {
+ if (origin != NULL) {
+ str_origin = gs_shell_get_title_from_origin (origin);
+ /* TRANSLATORS: failure text for the in-app notification,
+ * the %s is the origin, e.g. "Fedora" or
+ * "Fedora Project [fedoraproject.org]" */
+ g_string_append_printf (str, _("Unable to contact %s"),
+ str_origin);
+ }
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: failure text for the in-app notification */
+ g_string_append (str, _("Not enough disk space — free up some space "
+ "and try again"));
+ buttons |= GS_SHELL_EVENT_BUTTON_NO_SPACE;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_RESTART_REQUIRED)) {
+ /* TRANSLATORS: failure text for the in-app notification, where the 'Software' means this application, aka 'GNOME Software'. */
+ g_string_append (str, _("Software needs to be restarted to use new plugins."));
+ buttons |= GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED;
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AC_POWER_REQUIRED)) {
+ /* TRANSLATORS: need to be connected to the AC power */
+ g_string_append (str, _("AC power is required"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)) {
+ /* TRANSLATORS: not enough juice to do this safely */
+ g_string_append (str, _("The battery level is too low"));
+ } else if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* Do nothing. */
+ } else {
+ /* non-interactive generic */
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE))
+ return FALSE;
+ /* TRANSLATORS: we failed to get a proper error code */
+ g_string_append (str, _("Sorry, something went wrong"));
+ gs_shell_append_detailed_error (shell, str, error);
+ }
+
+ if (str->len == 0)
+ return FALSE;
+
+ /* add more-info button */
+ if (origin != NULL) {
+ const gchar *uri = gs_app_get_url (origin, AS_URL_KIND_HELP);
+ if (uri != NULL) {
+ g_free (shell->events_info_uri);
+ shell->events_info_uri = g_strdup (uri);
+ buttons |= GS_SHELL_EVENT_BUTTON_MORE_INFO;
+ }
+ }
+
+ /* show in-app notification */
+ gs_shell_show_event_app_notify (shell, str->str, buttons);
+ return TRUE;
+}
+
+static gboolean
+gs_shell_show_event (GsShell *shell, GsPluginEvent *event)
+{
+ const GError *error;
+ GsPluginAction action;
+ GsPluginJob *job;
+
+ /* get error */
+ error = gs_plugin_event_get_error (event);
+ if (error == NULL)
+ return FALSE;
+
+ /* name and shame the plugin */
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT)) {
+ gs_shell_show_event_app_notify (shell, error->message,
+ GS_SHELL_EVENT_BUTTON_NONE);
+ return TRUE;
+ }
+
+ job = gs_plugin_event_get_job (event);
+ if (GS_IS_PLUGIN_JOB_REFRESH_METADATA (job))
+ return gs_shell_show_event_refresh (shell, event);
+
+ /* split up the events by action */
+ action = gs_plugin_event_get_action (event);
+ switch (action) {
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ return gs_shell_show_event_refresh (shell, event);
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_INSTALL_REPO:
+ case GS_PLUGIN_ACTION_ENABLE_REPO:
+ return gs_shell_show_event_install (shell, event);
+ case GS_PLUGIN_ACTION_UPDATE:
+ return gs_shell_show_event_update (shell, event);
+ case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD:
+ return gs_shell_show_event_upgrade (shell, event);
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_REMOVE_REPO:
+ case GS_PLUGIN_ACTION_DISABLE_REPO:
+ return gs_shell_show_event_remove (shell, event);
+ case GS_PLUGIN_ACTION_LAUNCH:
+ return gs_shell_show_event_launch (shell, event);
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ return gs_shell_show_event_file_to_app (shell, event);
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ return gs_shell_show_event_url_to_app (shell, event);
+ default:
+ break;
+ }
+
+ /* capture some warnings every time */
+ return gs_shell_show_event_fallback (shell, event);
+}
+
+static void
+gs_shell_rescan_events (GsShell *shell)
+{
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ /* find the first active event and show it */
+ event = gs_plugin_loader_get_event_default (shell->plugin_loader);
+ if (event != NULL) {
+ if (!gs_shell_show_event (shell, event)) {
+ GsPluginAction action = gs_plugin_event_get_action (event);
+ const GError *error = gs_plugin_event_get_error (event);
+ if (error != NULL &&
+ !g_error_matches (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error,
+ G_IO_ERROR,
+ G_IO_ERROR_CANCELLED)) {
+ g_warning ("not handling error %s for action %s: %s",
+ gs_plugin_error_to_string (error->code),
+ gs_plugin_action_to_string (action),
+ error->message);
+ }
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID);
+ return;
+ }
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE);
+ return;
+ }
+
+ /* nothing to show */
+ gtk_revealer_set_reveal_child (GTK_REVEALER (shell->notification_event), FALSE);
+}
+
+static void
+gs_shell_events_notify_cb (GsPluginLoader *plugin_loader,
+ GParamSpec *pspec,
+ GsShell *shell)
+{
+ gs_shell_rescan_events (shell);
+}
+
+static void
+gs_shell_plugin_event_dismissed_cb (GtkButton *button, GsShell *shell)
+{
+ guint i;
+ g_autoptr(GPtrArray) events = NULL;
+
+ /* mark any events currently showing as invalid */
+ events = gs_plugin_loader_get_events (shell->plugin_loader);
+ for (i = 0; i < events->len; i++) {
+ GsPluginEvent *event = g_ptr_array_index (events, i);
+ if (gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE)) {
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID);
+ gs_plugin_event_remove_flag (event, GS_PLUGIN_EVENT_FLAG_VISIBLE);
+ }
+ }
+
+ /* show the next event */
+ gs_shell_rescan_events (shell);
+}
+
+static void
+gs_shell_setup_pages (GsShell *shell)
+{
+ for (gsize i = 0; i < G_N_ELEMENTS (shell->pages); i++) {
+ g_autoptr(GError) error = NULL;
+ GsPage *page = shell->pages[i];
+ if (page != NULL &&
+ !gs_page_setup (page, shell,
+ shell->plugin_loader,
+ shell->cancellable,
+ &error)) {
+ g_warning ("Failed to setup panel: %s", error->message);
+ }
+ }
+}
+
+static void
+gs_shell_add_about_menu_item (GsShell *shell)
+{
+ g_autoptr(GMenuItem) menu_item = NULL;
+
+ /* TRANSLATORS: this is the menu item that opens the about window */
+ menu_item = g_menu_item_new (_("About Software"), "app.about");
+ g_menu_append_item (G_MENU (shell->primary_menu), menu_item);
+}
+
+static void
+updates_page_notify_counter_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsPage *page = GS_PAGE (obj);
+ GsShell *shell = GS_SHELL (user_data);
+ AdwViewStackPage *stack_page;
+ gboolean needs_attention;
+
+ /* Update the needs-attention child property of the page in the
+ * AdwViewStack. There’s no need to account for whether it’s the currently
+ * visible page, as the CSS rules do that for us. This can’t be a simple
+ * property binding, though, as it’s a binding between an object
+ * property and a child property. */
+ needs_attention = (gs_page_get_counter (page) > 0);
+
+ stack_page = adw_view_stack_get_page (shell->stack_main, GTK_WIDGET (page));
+ adw_view_stack_page_set_needs_attention (stack_page, needs_attention);
+}
+
+static void
+category_page_app_clicked_cb (GsCategoryPage *page,
+ GsApp *app,
+ gpointer user_data)
+{
+ GsShell *shell = GS_SHELL (user_data);
+
+ gs_shell_show_app (shell, app);
+}
+
+static void
+details_page_app_clicked_cb (GsDetailsPage *page,
+ GsApp *app,
+ gpointer user_data)
+{
+ GsShell *shell = GS_SHELL (user_data);
+
+ gs_shell_show_app (shell, app);
+}
+
+void
+gs_shell_setup (GsShell *shell, GsPluginLoader *plugin_loader, GCancellable *cancellable)
+{
+ GsOdrsProvider *odrs_provider;
+
+ g_return_if_fail (GS_IS_SHELL (shell));
+
+ shell->plugin_loader = g_object_ref (plugin_loader);
+ g_signal_connect_object (shell->plugin_loader, "notify::events",
+ G_CALLBACK (gs_shell_events_notify_cb),
+ shell, 0);
+ g_signal_connect_object (shell->plugin_loader, "notify::network-metered",
+ G_CALLBACK (gs_shell_network_metered_notify_cb),
+ shell, 0);
+ g_signal_connect_object (shell->plugin_loader, "basic-auth-start",
+ G_CALLBACK (gs_shell_basic_auth_start_cb),
+ shell, 0);
+ g_signal_connect_object (shell->plugin_loader, "ask-untrusted",
+ G_CALLBACK (gs_shell_ask_untrusted_cb),
+ shell, 0);
+
+ g_object_bind_property (shell->plugin_loader, "allow-updates",
+ shell->pages[GS_SHELL_MODE_UPDATES], "visible",
+ G_BINDING_SYNC_CREATE);
+
+ shell->cancellable = g_object_ref (cancellable);
+
+ shell->settings = g_settings_new ("org.gnome.software");
+
+ /* set up pages */
+ gs_shell_setup_pages (shell);
+
+ /* set up the metered data info bar and mogwai */
+ g_signal_connect (shell->settings, "changed::download-updates",
+ (GCallback) gs_shell_download_updates_changed_cb, shell);
+
+ odrs_provider = gs_plugin_loader_get_odrs_provider (shell->plugin_loader);
+ gs_details_page_set_odrs_provider (GS_DETAILS_PAGE (shell->pages[GS_SHELL_MODE_DETAILS]), odrs_provider);
+ gs_moderate_page_set_odrs_provider (GS_MODERATE_PAGE (shell->pages[GS_SHELL_MODE_MODERATE]), odrs_provider);
+
+ /* coldplug */
+ gs_shell_rescan_events (shell);
+
+ /* primary menu */
+ gs_shell_add_about_menu_item (shell);
+
+ if (g_settings_get_boolean (shell->settings, "download-updates")) {
+ /* show loading page, which triggers the initial refresh */
+ gs_shell_change_mode (shell, GS_SHELL_MODE_LOADING, NULL, TRUE);
+ } else {
+ g_debug ("Skipped refresh of the repositories due to 'download-updates' disabled");
+ initial_refresh_done (GS_LOADING_PAGE (shell->pages[GS_SHELL_MODE_LOADING]), shell);
+
+ if (g_settings_get_boolean (shell->settings, "first-run"))
+ g_settings_set_boolean (shell->settings, "first-run", FALSE);
+ }
+
+ if (shell->activate_after_setup) {
+ shell->activate_after_setup = FALSE;
+ gs_shell_activate (shell);
+ }
+}
+
+void
+gs_shell_reset_state (GsShell *shell)
+{
+ /* reset to overview, unless we're in the loading state which advances
+ * to overview on its own */
+ if (gs_shell_get_mode (shell) != GS_SHELL_MODE_LOADING)
+ gs_shell_change_mode (shell, GS_SHELL_MODE_OVERVIEW, NULL, TRUE);
+
+ gs_shell_clean_back_entry_stack (shell);
+}
+
+void
+gs_shell_set_mode (GsShell *shell, GsShellMode mode)
+{
+ gs_shell_change_mode (shell, mode, NULL, TRUE);
+}
+
+GsShellMode
+gs_shell_get_mode (GsShell *shell)
+{
+ const gchar *name;
+
+ if (g_strcmp0 (adw_view_stack_get_visible_child_name (shell->stack_loading), "loading") == 0)
+ return GS_SHELL_MODE_LOADING;
+
+ if (g_strcmp0 (adw_leaflet_get_visible_child_name (shell->details_leaflet), "details") == 0)
+ return GS_SHELL_MODE_DETAILS;
+
+ if (g_strcmp0 (adw_leaflet_get_visible_child_name (shell->main_leaflet), "main") == 0)
+ name = adw_view_stack_get_visible_child_name (shell->stack_main);
+ else
+ name = adw_view_stack_get_visible_child_name (shell->stack_sub);
+
+ for (gsize i = 0; i < G_N_ELEMENTS (page_name); i++)
+ if (g_strcmp0 (page_name[i], name) == 0)
+ return i;
+
+ g_assert_not_reached ();
+}
+
+const gchar *
+gs_shell_get_mode_string (GsShell *shell)
+{
+ GsShellMode mode = gs_shell_get_mode (shell);
+ return page_name[mode];
+}
+
+void
+gs_shell_install (GsShell *shell, GsApp *app, GsShellInteraction interaction)
+{
+ save_back_entry (shell);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS,
+ (gpointer) app, TRUE);
+ gs_page_install_app (shell->pages[GS_SHELL_MODE_DETAILS], app, interaction, shell->cancellable);
+}
+
+void
+gs_shell_uninstall (GsShell *shell, GsApp *app)
+{
+ save_back_entry (shell);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS, (gpointer) app, TRUE);
+ gs_page_remove_app (shell->pages[GS_SHELL_MODE_DETAILS], app, shell->cancellable);
+}
+
+void
+gs_shell_show_installed_updates (GsShell *shell)
+{
+ GtkWidget *dialog;
+
+ dialog = gs_update_dialog_new (shell->plugin_loader);
+
+ gs_shell_modal_dialog_present (shell, GTK_WINDOW (dialog));
+}
+
+void
+gs_shell_show_sources (GsShell *shell)
+{
+ GtkWidget *dialog;
+
+ /* use if available */
+ if (g_spawn_command_line_async ("software-properties-gtk", NULL))
+ return;
+
+ dialog = gs_repos_dialog_new (GTK_WINDOW (shell), shell->plugin_loader);
+ gs_shell_modal_dialog_present (shell, GTK_WINDOW (dialog));
+}
+
+void
+gs_shell_show_prefs (GsShell *shell)
+{
+ GtkWidget *dialog;
+
+ dialog = gs_prefs_dialog_new (GTK_WINDOW (shell), shell->plugin_loader);
+ gs_shell_modal_dialog_present (shell, GTK_WINDOW (dialog));
+}
+
+void
+gs_shell_show_app (GsShell *shell, GsApp *app)
+{
+ save_back_entry (shell);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS, app, TRUE);
+ gs_shell_activate (shell);
+}
+
+void
+gs_shell_show_category (GsShell *shell, GsCategory *category)
+{
+ save_back_entry (shell);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_CATEGORY, category, TRUE);
+}
+
+void gs_shell_show_extras_search (GsShell *shell, const gchar *mode, gchar **resources, const gchar *desktop_id, const gchar *ident)
+{
+ save_back_entry (shell);
+ gs_extras_page_search (GS_EXTRAS_PAGE (shell->pages[GS_SHELL_MODE_EXTRAS]), mode, resources, desktop_id, ident);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_EXTRAS, NULL, TRUE);
+ gs_shell_activate (shell);
+}
+
+void
+gs_shell_show_search (GsShell *shell, const gchar *search)
+{
+ save_back_entry (shell);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_SEARCH,
+ (gpointer) search, TRUE);
+}
+
+void
+gs_shell_show_local_file (GsShell *shell, GFile *file)
+{
+ g_autoptr(GsApp) app = gs_app_new (NULL);
+ save_back_entry (shell);
+ gs_app_set_local_file (app, file);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS,
+ (gpointer) app, TRUE);
+ gs_shell_activate (shell);
+}
+
+/**
+ * gs_shell_show_metainfo:
+ * @shell: a #GsShell
+ * @file: path to a metainfo file to display
+ *
+ * Open a metainfo file and display it on the details page as if it were
+ * published in a repository configured on the system.
+ *
+ * This is intended for app developers to be able to test their metainfo files
+ * locally.
+ *
+ * Since: 42
+ */
+void
+gs_shell_show_metainfo (GsShell *shell, GFile *file)
+{
+ g_autoptr(GsApp) app = gs_app_new (NULL);
+
+ g_return_if_fail (GS_IS_SHELL (shell));
+ g_return_if_fail (G_IS_FILE (file));
+ save_back_entry (shell);
+ gs_app_set_metadata (app, "GnomeSoftware::show-metainfo", "1");
+ gs_app_set_local_file (app, file);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_DETAILS,
+ (gpointer) app, TRUE);
+ gs_shell_activate (shell);
+}
+
+void
+gs_shell_show_search_result (GsShell *shell, const gchar *id, const gchar *search)
+{
+ save_back_entry (shell);
+ gs_search_page_set_appid_to_show (GS_SEARCH_PAGE (shell->pages[GS_SHELL_MODE_SEARCH]), id);
+ gs_shell_change_mode (shell, GS_SHELL_MODE_SEARCH,
+ (gpointer) search, TRUE);
+}
+
+void
+gs_shell_show_uri (GsShell *shell, const gchar *url)
+{
+ gtk_show_uri (GTK_WINDOW (shell), url, GDK_CURRENT_TIME);
+}
+
+/**
+ * gs_shell_get_is_narrow:
+ * @shell: a #GsShell
+ *
+ * Get the value of #GsShell:is-narrow.
+ *
+ * Returns: %TRUE if the window is in narrow mode, %FALSE otherwise
+ *
+ * Since: 41
+ */
+gboolean
+gs_shell_get_is_narrow (GsShell *shell)
+{
+ g_return_val_if_fail (GS_IS_SHELL (shell), FALSE);
+
+ return shell->is_narrow;
+}
+
+static gint
+gs_shell_get_allocation_width (GsShell *self)
+{
+ return self->allocation_width;
+}
+
+static void
+gs_shell_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsShell *shell = GS_SHELL (object);
+
+ switch ((GsShellProperty) prop_id) {
+ case PROP_IS_NARROW:
+ g_value_set_boolean (value, gs_shell_get_is_narrow (shell));
+ break;
+ case PROP_ALLOCATION_WIDTH:
+ g_value_set_int (value, gs_shell_get_allocation_width (shell));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_shell_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ switch ((GsShellProperty) prop_id) {
+ case PROP_IS_NARROW:
+ case PROP_ALLOCATION_WIDTH:
+ /* Read only. */
+ g_assert_not_reached ();
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_shell_dispose (GObject *object)
+{
+ GsShell *shell = GS_SHELL (object);
+
+ g_clear_object (&shell->sub_page_header_title_binding);
+
+ if (shell->back_entry_stack != NULL) {
+ g_queue_free_full (shell->back_entry_stack, (GDestroyNotify) free_back_entry);
+ shell->back_entry_stack = NULL;
+ }
+ g_clear_object (&shell->cancellable);
+ g_clear_object (&shell->plugin_loader);
+ g_clear_object (&shell->header_start_widget);
+ g_clear_object (&shell->header_end_widget);
+ g_clear_object (&shell->page);
+ g_clear_pointer (&shell->events_info_uri, g_free);
+ g_clear_pointer (&shell->modal_dialogs, g_ptr_array_unref);
+ g_clear_object (&shell->settings);
+
+#ifdef HAVE_MOGWAI
+ if (shell->scheduler != NULL) {
+ if (shell->scheduler_invalidated_handler > 0)
+ g_signal_handler_disconnect (shell->scheduler,
+ shell->scheduler_invalidated_handler);
+
+ if (shell->scheduler_held)
+ mwsc_scheduler_release_async (shell->scheduler,
+ NULL,
+ scheduler_release_cb,
+ g_object_ref (shell));
+ else
+ g_clear_object (&shell->scheduler);
+ }
+#endif /* HAVE_MOGWAI */
+
+ G_OBJECT_CLASS (gs_shell_parent_class)->dispose (object);
+}
+
+static gboolean
+allocation_changed_cb (gpointer user_data)
+{
+ GsShell *shell = GS_SHELL (user_data);
+ GtkAllocation allocation;
+ gboolean is_narrow;
+ GtkStyleContext *context;
+
+ gtk_widget_get_allocation (GTK_WIDGET (shell), &allocation);
+
+ is_narrow = allocation.width <= NARROW_WIDTH_THRESHOLD;
+
+ if (shell->is_narrow != is_narrow) {
+ shell->is_narrow = is_narrow;
+ g_object_notify_by_pspec (G_OBJECT (shell), obj_props[PROP_IS_NARROW]);
+ }
+
+ if (shell->allocation_width != allocation.width) {
+ shell->allocation_width = allocation.width;
+ g_object_notify_by_pspec (G_OBJECT (shell), obj_props[PROP_ALLOCATION_WIDTH]);
+ }
+
+ shell->allocation_changed_cb_id = 0;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (shell));
+
+ if (is_narrow)
+ gtk_style_context_add_class (context, "narrow");
+ else
+ gtk_style_context_remove_class (context, "narrow");
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_shell_size_allocate (GtkWidget *widget,
+ gint width,
+ gint height,
+ gint baseline)
+{
+ GsShell *shell = GS_SHELL (widget);
+
+ GTK_WIDGET_CLASS (gs_shell_parent_class)->size_allocate (widget,
+ width,
+ height,
+ baseline);
+
+ /* Delay updating is-narrow so children can adapt to it, which isn't
+ * possible during the widget's allocation phase as it would break their
+ * size request.
+ */
+ if (shell->allocation_changed_cb_id == 0)
+ shell->allocation_changed_cb_id = g_idle_add (allocation_changed_cb, shell);
+}
+
+static void
+gs_shell_class_init (GsShellClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gs_shell_get_property;
+ object_class->set_property = gs_shell_set_property;
+ object_class->dispose = gs_shell_dispose;
+
+ widget_class->size_allocate = gs_shell_size_allocate;
+
+ /**
+ * GsShell:is-narrow:
+ *
+ * Whether the window is in narrow mode.
+ *
+ * Pages can track this property to adapt to the available width.
+ *
+ * Since: 41
+ */
+ obj_props[PROP_IS_NARROW] =
+ g_param_spec_boolean ("is-narrow", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsShell:allocation-width:
+ *
+ * The last allocation width for the window.
+ *
+ * The pages can track this property, possibly in combination with the #GsShell:is-narrow,
+ * to adapt its content to the available width.
+ *
+ * Since: 43
+ */
+ obj_props[PROP_ALLOCATION_WIDTH] =
+ g_param_spec_int ("allocation-width", NULL, NULL,
+ G_MININT, G_MAXINT, 0,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+
+ signals [SIGNAL_LOADED] =
+ g_signal_new ("loaded",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-shell.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsShell, main_header);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, main_leaflet);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, details_header);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, details_leaflet);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, stack_loading);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, stack_main);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, stack_sub);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, metered_updates_bar);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, search_button);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, entry_search);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, search_bar);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_back);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_back2);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, notification_event);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_events_sources);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_events_no_space);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_events_network_settings);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_events_restart_required);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_events_more_info);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, button_events_dismiss);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, label_events);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, primary_menu);
+ gtk_widget_class_bind_template_child (widget_class, GsShell, sub_page_header_title);
+
+ gtk_widget_class_bind_template_child_full (widget_class, "overview_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_OVERVIEW]));
+ gtk_widget_class_bind_template_child_full (widget_class, "updates_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_UPDATES]));
+ gtk_widget_class_bind_template_child_full (widget_class, "installed_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_INSTALLED]));
+ gtk_widget_class_bind_template_child_full (widget_class, "moderate_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_MODERATE]));
+ gtk_widget_class_bind_template_child_full (widget_class, "loading_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_LOADING]));
+ gtk_widget_class_bind_template_child_full (widget_class, "search_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_SEARCH]));
+ gtk_widget_class_bind_template_child_full (widget_class, "details_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_DETAILS]));
+ gtk_widget_class_bind_template_child_full (widget_class, "category_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_CATEGORY]));
+ gtk_widget_class_bind_template_child_full (widget_class, "extras_page", FALSE, G_STRUCT_OFFSET (GsShell, pages[GS_SHELL_MODE_EXTRAS]));
+
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_main_window_mapped_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_main_window_realized_cb);
+ gtk_widget_class_bind_template_callback (widget_class, main_window_closed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, window_key_pressed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, window_keypress_handler);
+ gtk_widget_class_bind_template_callback (widget_class, window_button_pressed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_details_back_button_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_back_button_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_overview_page_button_cb);
+ gtk_widget_class_bind_template_callback (widget_class, updates_page_notify_counter_cb);
+ gtk_widget_class_bind_template_callback (widget_class, category_page_app_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_bar_search_mode_enabled_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_changed_handler);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_plugin_events_sources_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_plugin_events_no_space_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_plugin_events_network_settings_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_plugin_events_restart_required_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_plugin_events_more_info_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_plugin_event_dismissed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_metered_updates_bar_response_cb);
+ gtk_widget_class_bind_template_callback (widget_class, stack_notify_visible_child_cb);
+ gtk_widget_class_bind_template_callback (widget_class, initial_refresh_done);
+ gtk_widget_class_bind_template_callback (widget_class, overlay_get_child_position_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gs_shell_details_page_metainfo_loaded_cb);
+ gtk_widget_class_bind_template_callback (widget_class, details_page_app_clicked_cb);
+
+ gtk_widget_class_add_binding_action (widget_class, GDK_KEY_q, GDK_CONTROL_MASK, "window.close", NULL);
+}
+
+static void
+gs_shell_init (GsShell *shell)
+{
+ gtk_widget_init_template (GTK_WIDGET (shell));
+
+ gtk_search_bar_connect_entry (GTK_SEARCH_BAR (shell->search_bar), GTK_EDITABLE (shell->entry_search));
+
+ shell->back_entry_stack = g_queue_new ();
+ shell->modal_dialogs = g_ptr_array_new ();
+}
+
+GsShell *
+gs_shell_new (void)
+{
+ return GS_SHELL (g_object_new (GS_TYPE_SHELL, NULL));
+}