diff options
Diffstat (limited to '')
-rw-r--r-- | src/gs-page.c | 857 |
1 files changed, 857 insertions, 0 deletions
diff --git a/src/gs-page.c b/src/gs-page.c new file mode 100644 index 0000000..7a465be --- /dev/null +++ b/src/gs-page.c @@ -0,0 +1,857 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <string.h> +#include <glib/gi18n.h> + +#include "gs-application.h" +#include "gs-download-utils.h" +#include "gs-page.h" +#include "gs-common.h" +#include "gs-screenshot-image.h" + +typedef struct +{ + GsPluginLoader *plugin_loader; + GsShell *shell; + GtkWidget *header_start_widget; + GtkWidget *header_end_widget; + gboolean is_active; +} GsPagePrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GsPage, gs_page, GTK_TYPE_WIDGET) + +typedef enum { + PROP_TITLE = 1, + PROP_COUNTER, + PROP_VADJUSTMENT, +} GsPageProperty; + +static GParamSpec *obj_props[PROP_VADJUSTMENT + 1] = { NULL, }; + +GsShell * +gs_page_get_shell (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + return priv->shell; +} + +typedef struct { + GsApp *app; + GsPage *page; + GCancellable *cancellable; + gulong notify_quirk_id; + GtkWidget *button_install; + GsPluginAction action; + GsShellInteraction interaction; + gboolean propagate_error; +} GsPageHelper; + +static void +gs_page_helper_free (GsPageHelper *helper) +{ + if (helper->notify_quirk_id > 0) + g_signal_handler_disconnect (helper->app, helper->notify_quirk_id); + if (helper->app != NULL) + g_object_unref (helper->app); + if (helper->page != NULL) + g_object_unref (helper->page); + if (helper->cancellable != NULL) + g_object_unref (helper->cancellable); + g_slice_free (GsPageHelper, helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPageHelper, gs_page_helper_free); + +static void +gs_page_update_app_response_close_cb (GtkDialog *dialog, gint response, gpointer user_data) +{ + gtk_window_destroy (GTK_WINDOW (dialog)); +} + +static void +gs_page_show_update_message (GsPageHelper *helper, AsScreenshot *ss) +{ + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + GPtrArray *images; + GtkWidget *dialog; + g_autofree gchar *escaped = NULL; + + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (helper->page), GTK_TYPE_WINDOW)), + GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", gs_app_get_name (helper->app)); + escaped = g_markup_escape_text (as_screenshot_get_caption (ss), -1); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", escaped); + + /* image is optional */ + images = as_screenshot_get_images (ss); + if (images->len) { + GtkWidget *content_area; + GtkWidget *ssimg; + g_autoptr(SoupSession) soup_session = NULL; + + /* load screenshot */ + soup_session = gs_build_soup_session (); + ssimg = gs_screenshot_image_new (soup_session); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg), 400, 225); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), + helper->cancellable); + gtk_widget_set_margin_start (ssimg, 24); + gtk_widget_set_margin_end (ssimg, 24); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_box_append (GTK_BOX (content_area), ssimg); + } + + /* handle this async */ + g_signal_connect (dialog, "response", + G_CALLBACK (gs_page_update_app_response_close_cb), helper); + gs_shell_modal_dialog_present (priv->shell, GTK_WINDOW (dialog)); +} + +static void +gs_page_app_installed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsPage *page = helper->page; + gboolean ret; + g_autoptr(GError) error = NULL; + + ret = gs_plugin_loader_job_action_finish (plugin_loader, + res, + &error); + + gs_application_emit_install_resources_done (GS_APPLICATION (g_application_get_default ()), NULL, error); + + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_debug ("App install cancelled with error: %s", error->message); + return; + } + if (!ret) { + if (helper->propagate_error) { + gs_plugin_loader_claim_error (plugin_loader, + NULL, + helper->action, + helper->app, + helper->interaction == GS_SHELL_INTERACTION_FULL, + error); + } else { + g_warning ("failed to install %s: %s", gs_app_get_id (helper->app), error->message); + } + return; + } + + /* the single update needs system reboot, e.g. for firmware */ + if (gs_app_has_quirk (helper->app, GS_APP_QUIRK_NEEDS_REBOOT)) { + g_autoptr(GsAppList) list = gs_app_list_new (); + gs_app_list_add (list, helper->app); + gs_utils_reboot_notify (list, TRUE); + } + + /* tell the user what they have to do */ + if (gs_app_get_kind (helper->app) == AS_COMPONENT_KIND_FIRMWARE && + gs_app_has_quirk (helper->app, GS_APP_QUIRK_NEEDS_USER_ACTION)) { + AsScreenshot *ss = gs_app_get_action_screenshot (helper->app); + if (ss != NULL && as_screenshot_get_caption (ss) != NULL) + gs_page_show_update_message (helper, ss); + } + + /* only show this if the window is not active */ + if (gs_app_is_installed (helper->app) && + helper->action == GS_PLUGIN_ACTION_INSTALL && + !gtk_window_is_active (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (helper->page), GTK_TYPE_WINDOW))) && + ((helper->interaction) & GS_SHELL_INTERACTION_NOTIFY) != 0) + gs_app_notify_installed (helper->app); + + if (gs_app_is_installed (helper->app) && + GS_PAGE_GET_CLASS (page)->app_installed != NULL) { + GS_PAGE_GET_CLASS (page)->app_installed (page, helper->app); + } +} + +static void +gs_page_app_removed_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + GsPage *page = helper->page; + gboolean ret; + g_autoptr(GError) error = NULL; + + ret = gs_plugin_loader_job_action_finish (plugin_loader, + res, + &error); + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) || + g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_debug ("%s", error->message); + return; + } + if (!ret) { + g_warning ("failed to uninstall: %s", error->message); + return; + } + + /* the app removal needs system reboot, e.g. for rpm-ostree */ + if (gs_app_has_quirk (helper->app, GS_APP_QUIRK_NEEDS_REBOOT)) { + g_autoptr(GsAppList) list = gs_app_list_new (); + gs_app_list_add (list, helper->app); + gs_utils_reboot_notify (list, FALSE); + } + + if (!gs_app_is_installed (helper->app) && + GS_PAGE_GET_CLASS (page)->app_removed != NULL) { + GS_PAGE_GET_CLASS (page)->app_removed (page, helper->app); + } +} + +GtkWidget * +gs_page_get_header_start_widget (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + return priv->header_start_widget; +} + +void +gs_page_set_header_start_widget (GsPage *page, GtkWidget *widget) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_set_object (&priv->header_start_widget, widget); +} + +GtkWidget * +gs_page_get_header_end_widget (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + return priv->header_end_widget; +} + +void +gs_page_set_header_end_widget (GsPage *page, GtkWidget *widget) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_set_object (&priv->header_end_widget, widget); +} + +void +gs_page_install_app (GsPage *page, + GsApp *app, + GsShellInteraction interaction, + GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + GsPageHelper *helper; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* probably non-free */ + if (gs_app_get_state (app) == GS_APP_STATE_UNAVAILABLE) { + GtkResponseType response; + + response = gs_app_notify_unavailable (app, GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (page), GTK_TYPE_WINDOW))); + if (response != GTK_RESPONSE_OK) { + g_autoptr(GError) error_local = NULL; + g_set_error_literal (&error_local, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("User declined installation")); + gs_application_emit_install_resources_done (GS_APPLICATION (g_application_get_default ()), NULL, error_local); + return; + } + } + + helper = g_slice_new0 (GsPageHelper); + helper->app = g_object_ref (app); + helper->page = g_object_ref (page); + helper->cancellable = g_object_ref (cancellable); + helper->interaction = interaction; + helper->propagate_error = TRUE; + + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) { + helper->action = GS_PLUGIN_ACTION_INSTALL_REPO; + plugin_job = gs_plugin_job_manage_repository_new (helper->app, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL | + ((interaction == GS_SHELL_INTERACTION_FULL) ? + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE : 0)); + gs_plugin_job_set_propagate_error (plugin_job, helper->propagate_error); + } else { + helper->action = GS_PLUGIN_ACTION_INSTALL; + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", (interaction == GS_SHELL_INTERACTION_FULL), + "propagate-error", helper->propagate_error, + "app", helper->app, + NULL); + } + + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + helper->cancellable, + gs_page_app_installed_cb, + helper); +} + +static void +gs_page_update_app_response_cb (GtkDialog *dialog, + gint response, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* unmap the dialog */ + gtk_window_destroy (GTK_WINDOW (dialog)); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) + return; + + g_debug ("update %s", gs_app_get_id (helper->app)); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE, + "interactive", TRUE, + "propagate-error", helper->propagate_error, + "app", helper->app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, + plugin_job, + helper->cancellable, + gs_page_app_installed_cb, + helper); + g_steal_pointer (&helper); +} + +static void +gs_page_notify_quirk_cb (GsApp *app, GParamSpec *pspec, GsPageHelper *helper) +{ + gtk_widget_set_sensitive (helper->button_install, + !gs_app_has_quirk (helper->app, + GS_APP_QUIRK_NEEDS_USER_ACTION)); +} + +static void +gs_page_needs_user_action (GsPageHelper *helper, AsScreenshot *ss) +{ + GtkWidget *content_area; + GtkWidget *dialog; + g_autoptr(SoupSession) soup_session = NULL; + GtkWidget *ssimg; + g_autofree gchar *escaped = NULL; + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (helper->page), GTK_TYPE_WINDOW)), + GTK_DIALOG_MODAL | + GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CANCEL, + /* TRANSLATORS: this is a prompt message, and + * '%s' is an application summary, e.g. 'GNOME Clocks' */ + _("Prepare %s"), + gs_app_get_name (helper->app)); + escaped = g_markup_escape_text (as_screenshot_get_caption (ss), -1); + gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog), + "%s", escaped); + + /* this will be enabled when the device is in the right mode */ + helper->button_install = gtk_dialog_add_button (GTK_DIALOG (dialog), + /* TRANSLATORS: update the fw */ + _("Install"), + GTK_RESPONSE_OK); + helper->notify_quirk_id = + g_signal_connect (helper->app, "notify::quirk", + G_CALLBACK (gs_page_notify_quirk_cb), + helper); + gtk_widget_set_sensitive (helper->button_install, FALSE); + + /* load screenshot */ + soup_session = gs_build_soup_session (); + ssimg = gs_screenshot_image_new (soup_session); + gs_screenshot_image_set_screenshot (GS_SCREENSHOT_IMAGE (ssimg), ss); + gs_screenshot_image_set_size (GS_SCREENSHOT_IMAGE (ssimg), 400, 225); + gs_screenshot_image_load_async (GS_SCREENSHOT_IMAGE (ssimg), + helper->cancellable); + gtk_widget_set_margin_start (ssimg, 24); + gtk_widget_set_margin_end (ssimg, 24); + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_box_append (GTK_BOX (content_area), ssimg); + + /* handle this async */ + g_signal_connect (dialog, "response", + G_CALLBACK (gs_page_update_app_response_cb), helper); + gs_shell_modal_dialog_present (priv->shell, GTK_WINDOW (dialog)); +} + +void +gs_page_update_app (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + GsPageHelper *helper; + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* non-firmware applications do not have to be prepared */ + helper = g_slice_new0 (GsPageHelper); + helper->action = GS_PLUGIN_ACTION_UPDATE; + helper->app = g_object_ref (app); + helper->page = g_object_ref (page); + helper->cancellable = g_object_ref (cancellable); + helper->propagate_error = TRUE; + + /* tell the user what they have to do */ + if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FIRMWARE && + gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION)) { + AsScreenshot *ss = gs_app_get_action_screenshot (app); + if (ss != NULL && as_screenshot_get_caption (ss) != NULL) { + gs_page_needs_user_action (helper, ss); + return; + } + } + + /* generic fallback */ + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", TRUE, + "propagate-error", helper->propagate_error, + "app", app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + helper->cancellable, + gs_page_app_installed_cb, + helper); +} + +static void +gs_page_remove_app_response_cb (GtkDialog *dialog, + gint response, + gpointer user_data) +{ + g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data; + GsPagePrivate *priv = gs_page_get_instance_private (helper->page); + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* unmap the dialog */ + gtk_window_destroy (GTK_WINDOW (dialog)); + + /* not agreed */ + if (response != GTK_RESPONSE_OK) + return; + + g_debug ("uninstall %s", gs_app_get_id (helper->app)); + if (gs_app_get_kind (helper->app) == AS_COMPONENT_KIND_REPOSITORY) { + helper->action = GS_PLUGIN_ACTION_REMOVE_REPO; + plugin_job = gs_plugin_job_manage_repository_new (helper->app, + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_REMOVE | + GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE); + } else { + plugin_job = gs_plugin_job_newv (helper->action, + "interactive", TRUE, + "app", helper->app, + NULL); + } + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + helper->cancellable, + gs_page_app_removed_cb, + helper); + g_steal_pointer (&helper); +} + +void +gs_page_remove_app (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + GsPageHelper *helper; + GtkWidget *dialog; + g_autofree gchar *message = NULL; + g_autofree gchar *title = NULL; + GtkWidget *remove_button; + GtkStyleContext *context; + + /* Is the app actually removable? */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY)) + return; + + /* pending install */ + helper = g_slice_new0 (GsPageHelper); + helper->action = GS_PLUGIN_ACTION_REMOVE; + helper->app = g_object_ref (app); + helper->page = g_object_ref (page); + helper->cancellable = cancellable != NULL ? g_object_ref (cancellable) : NULL; + if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) { + g_autoptr(GsPluginJob) plugin_job = NULL; + + /* cancel any ongoing job, this allows to e.g. cancel pending + * installations, updates, or other ops that may have been queued + * in the plugin loader (due to reaching the max parallel ops allowed) */ + g_cancellable_cancel (gs_app_get_cancellable (app)); + + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REMOVE, + "interactive", TRUE, + "app", app, + NULL); + g_debug ("uninstall %s", gs_app_get_id (app)); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + helper->cancellable, + gs_page_app_removed_cb, + helper); + return; + } + + /* use different name and summary */ + switch (gs_app_get_kind (app)) { + case AS_COMPONENT_KIND_REPOSITORY: + /* TRANSLATORS: this is a prompt message, and '%s' is an + * repository name, e.g. 'GNOME Nightly' */ + title = g_strdup_printf (_("Are you sure you want to remove " + "the %s repository?"), + gs_app_get_name (app)); + /* TRANSLATORS: longer dialog text */ + message = g_strdup_printf (_("All applications from %s will be " + "uninstalled, and you will have to " + "re-install the repository to use them again."), + gs_app_get_name (app)); + break; + default: + /* TRANSLATORS: this is a prompt message, and '%s' is an + * application summary, e.g. 'GNOME Clocks' */ + title = g_strdup_printf (_("Are you sure you want to uninstall %s?"), + gs_app_get_name (app)); + /* TRANSLATORS: longer dialog text */ + message = g_strdup_printf (_("%s will be uninstalled, and you will " + "have to install it to use it again."), + gs_app_get_name (app)); + break; + } + + /* ask for confirmation */ + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (page), GTK_TYPE_WINDOW)), + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + "%s", title); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", message); + + /* TRANSLATORS: this is button text to remove the application */ + remove_button = gtk_dialog_add_button (GTK_DIALOG (dialog), _("Uninstall"), GTK_RESPONSE_OK); + context = gtk_widget_get_style_context (remove_button); + gtk_style_context_add_class (context, "destructive-action"); + + /* handle this async */ + g_signal_connect (dialog, "response", + G_CALLBACK (gs_page_remove_app_response_cb), helper); + gs_shell_modal_dialog_present (priv->shell, GTK_WINDOW (dialog)); +} + +static void +gs_page_app_launched_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); + g_autoptr(GError) error = NULL; + if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { + g_warning ("failed to launch GsApp: %s", error->message); + return; + } +} + +void +gs_page_launch_app (GsPage *page, GsApp *app, GCancellable *cancellable) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH, + "interactive", TRUE, + "app", app, + NULL); + gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, + cancellable, + gs_page_app_launched_cb, + NULL); +} + +gboolean +gs_page_is_active (GsPage *page) +{ + GsPagePrivate *priv = gs_page_get_instance_private (page); + g_return_val_if_fail (GS_IS_PAGE (page), FALSE); + return priv->is_active; +} + +/** + * gs_page_get_title: + * @page: a #GsPage + * + * Get the value of #GsPage:title. + * + * Returns: (nullable): human readable title for the page, or %NULL if one isn’t set + * + * Since: 40 + */ +const gchar * +gs_page_get_title (GsPage *page) +{ + g_auto(GValue) value = G_VALUE_INIT; + + g_return_val_if_fail (GS_IS_PAGE (page), NULL); + + /* The property is typically overridden by subclasses; the + * implementation in #GsPage itself is just a placeholder. */ + g_object_get_property (G_OBJECT (page), "title", &value); + + return g_value_get_string (&value); +} + +/** + * gs_page_get_counter: + * @page: a #GsPage + * + * Get the value of #GsPage:counter. + * + * Returns: a counter of the number of available updates, installed packages, + * etc. on this page + * + * Since: 40 + */ +guint +gs_page_get_counter (GsPage *page) +{ + g_auto(GValue) value = G_VALUE_INIT; + + g_return_val_if_fail (GS_IS_PAGE (page), 0); + + /* The property is typically overridden by subclasses; the + * implementation in #GsPage itself is just a placeholder. */ + g_object_get_property (G_OBJECT (page), "counter", &value); + + return g_value_get_uint (&value); +} + +/** + * gs_page_get_vadjustment: + * @page: a #GsPage + * + * Get the #GtkAdjustment used for vertical scrolling. + * + * Returns: (nullable) (transfer none): the #GtkAdjustment used for vertical scrolling + * + * Since: 41 + */ +GtkAdjustment * +gs_page_get_vadjustment (GsPage *page) +{ + g_auto(GValue) value = G_VALUE_INIT; + + g_return_val_if_fail (GS_IS_PAGE (page), NULL); + + /* The property is typically overridden by subclasses; the + * implementation in #GsPage itself is just a placeholder. */ + g_object_get_property (G_OBJECT (page), "vadjustment", &value); + + return g_value_get_object (&value); +} + +/** + * gs_page_switch_to: + * + * Pure virtual method that subclasses have to override to show page specific + * widgets. + */ +void +gs_page_switch_to (GsPage *page) +{ + GsPageClass *klass = GS_PAGE_GET_CLASS (page); + GsPagePrivate *priv = gs_page_get_instance_private (page); + priv->is_active = TRUE; + if (klass->switch_to != NULL) + klass->switch_to (page); +} + +/** + * gs_page_switch_from: + * + * Pure virtual method that subclasses have to override to show page specific + * widgets. + */ +void +gs_page_switch_from (GsPage *page) +{ + GsPageClass *klass = GS_PAGE_GET_CLASS (page); + GsPagePrivate *priv = gs_page_get_instance_private (page); + priv->is_active = FALSE; + if (klass->switch_from != NULL) + klass->switch_from (page); +} + +/** + * gs_page_scroll_up: + * @page: a #GsPage + * + * Scroll the page to the top of its content, if it supports scrolling. + * + * If it doesn’t support scrolling, this is a no-op. + * + * Since: 40 + */ +void +gs_page_scroll_up (GsPage *page) +{ + GtkAdjustment *adj; + + g_return_if_fail (GS_IS_PAGE (page)); + + adj = gs_page_get_vadjustment (page); + if (adj) + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); +} + +void +gs_page_reload (GsPage *page) +{ + GsPageClass *klass; + g_return_if_fail (GS_IS_PAGE (page)); + klass = GS_PAGE_GET_CLASS (page); + if (klass->reload != NULL) + klass->reload (page); +} + +gboolean +gs_page_setup (GsPage *page, + GsShell *shell, + GsPluginLoader *plugin_loader, + GCancellable *cancellable, + GError **error) +{ + GsPageClass *klass; + GsPagePrivate *priv = gs_page_get_instance_private (page); + + g_return_val_if_fail (GS_IS_PAGE (page), FALSE); + + klass = GS_PAGE_GET_CLASS (page); + g_assert (klass->setup != NULL); + + priv->plugin_loader = g_object_ref (plugin_loader); + priv->shell = shell; + + return klass->setup (page, shell, plugin_loader, cancellable, error); +} + +static void +gs_page_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch ((GsPageProperty) prop_id) { + case PROP_TITLE: + /* Should be overridden by subclasses. */ + g_value_set_string (value, NULL); + break; + case PROP_COUNTER: + /* Should be overridden by subclasses. */ + g_value_set_uint (value, 0); + break; + case PROP_VADJUSTMENT: + /* Should be overridden by subclasses. */ + g_value_set_object (value, NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_page_dispose (GObject *object) +{ + GsPage *page = GS_PAGE (object); + GsPagePrivate *priv = gs_page_get_instance_private (page); + + gs_widget_remove_all (GTK_WIDGET (page), NULL); + + g_clear_object (&priv->plugin_loader); + g_clear_object (&priv->header_start_widget); + g_clear_object (&priv->header_end_widget); + + G_OBJECT_CLASS (gs_page_parent_class)->dispose (object); +} + +static void +gs_page_init (GsPage *page) +{ + gtk_widget_set_hexpand (GTK_WIDGET (page), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (page), TRUE); +} + +static void +gs_page_class_init (GsPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gs_page_get_property; + object_class->dispose = gs_page_dispose; + + /** + * GsPage:title: (nullable) + * + * A human readable title for this page, or %NULL if one isn’t set or + * doesn’t make sense. + * + * Since: 40 + */ + obj_props[PROP_TITLE] = + g_param_spec_string ("title", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsPage:counter: + * + * A counter indicating the number of installed packages, available + * updates, etc. on this page. + * + * Since: 40 + */ + obj_props[PROP_COUNTER] = + g_param_spec_uint ("counter", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsPage:vadjustment: (nullable) + * + * The #GtkAdjustment used for vertical scrolling. + * This will be %NULL if the page is not vertically scrollable. + * + * Since: 41 + */ + obj_props[PROP_VADJUSTMENT] = + g_param_spec_object ("vadjustment", NULL, NULL, + GTK_TYPE_ADJUSTMENT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); +} + +GsPage * +gs_page_new (void) +{ + return GS_PAGE (g_object_new (GS_TYPE_PAGE, NULL)); +} |