diff options
Diffstat (limited to '')
-rw-r--r-- | src/gs-repo-row.c | 463 |
1 files changed, 463 insertions, 0 deletions
diff --git a/src/gs-repo-row.c b/src/gs-repo-row.c new file mode 100644 index 0000000..0a3a3b9 --- /dev/null +++ b/src/gs-repo-row.c @@ -0,0 +1,463 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <libsoup/soup.h> + +#include "gs-repo-row.h" + +typedef struct +{ + GsApp *repo; + GtkWidget *name_label; + GtkWidget *hostname_label; + GtkWidget *comment_label; + GtkWidget *remove_button; + GtkWidget *disable_switch; + gulong switch_handler_id; + guint refresh_idle_id; + guint busy_counter; + gboolean supports_remove; + gboolean supports_enable_disable; + gboolean always_allow_enable_disable; +} GsRepoRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsRepoRow, gs_repo_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + SIGNAL_REMOVE_CLICKED, + SIGNAL_SWITCH_CLICKED, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +static void +refresh_ui (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + GtkListBox *listbox; + gboolean active = FALSE; + gboolean state_sensitive = FALSE; + gboolean busy = priv->busy_counter> 0; + gboolean is_provenance; + gboolean is_compulsory; + + if (priv->repo == NULL) { + gtk_widget_set_sensitive (priv->disable_switch, FALSE); + gtk_switch_set_active (GTK_SWITCH (priv->disable_switch), FALSE); + return; + } + + g_signal_handler_block (priv->disable_switch, priv->switch_handler_id); + gtk_widget_set_sensitive (priv->disable_switch, TRUE); + + switch (gs_app_get_state (priv->repo)) { + case GS_APP_STATE_AVAILABLE: + case GS_APP_STATE_AVAILABLE_LOCAL: + active = FALSE; + state_sensitive = TRUE; + break; + case GS_APP_STATE_INSTALLED: + active = TRUE; + break; + case GS_APP_STATE_INSTALLING: + active = TRUE; + busy = TRUE; + break; + case GS_APP_STATE_REMOVING: + active = FALSE; + busy = TRUE; + break; + case GS_APP_STATE_UNAVAILABLE: + g_signal_handler_unblock (priv->disable_switch, priv->switch_handler_id); + listbox = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (row))); + g_assert (listbox != NULL); + gtk_list_box_remove (listbox, GTK_WIDGET (row)); + return; + default: + state_sensitive = TRUE; + break; + } + + is_provenance = gs_app_has_quirk (priv->repo, GS_APP_QUIRK_PROVENANCE); + is_compulsory = gs_app_has_quirk (priv->repo, GS_APP_QUIRK_COMPULSORY); + + /* Disable for the system repos, if installed */ + gtk_widget_set_sensitive (priv->disable_switch, priv->supports_enable_disable && (state_sensitive || !is_compulsory || priv->always_allow_enable_disable)); + gtk_widget_set_visible (priv->remove_button, priv->supports_remove && !is_provenance && !is_compulsory); + + /* Set only the 'state' to visually indicate the state is not saved yet */ + if (busy) + gtk_switch_set_state (GTK_SWITCH (priv->disable_switch), active); + else + gtk_switch_set_active (GTK_SWITCH (priv->disable_switch), active); + + g_signal_handler_unblock (priv->disable_switch, priv->switch_handler_id); +} + +static gboolean +refresh_idle (gpointer user_data) +{ + g_autoptr(GsRepoRow) row = (GsRepoRow *) user_data; + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + priv->refresh_idle_id = 0; + + refresh_ui (row); + + return G_SOURCE_REMOVE; +} + +static void +repo_state_changed_cb (GsApp *repo, GParamSpec *pspec, GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + if (priv->refresh_idle_id > 0) + return; + priv->refresh_idle_id = g_idle_add (refresh_idle, g_object_ref (row)); +} + +static gchar * +get_repo_installed_text (GsApp *repo) +{ + GsAppList *related; + guint cnt_addon = 0; + guint cnt_apps = 0; + g_autofree gchar *addons_text = NULL; + g_autofree gchar *apps_text = NULL; + + related = gs_app_get_related (repo); + for (guint i = 0; i < gs_app_list_length (related); i++) { + GsApp *app_tmp = gs_app_list_index (related, i); + switch (gs_app_get_kind (app_tmp)) { + case AS_COMPONENT_KIND_WEB_APP: + case AS_COMPONENT_KIND_DESKTOP_APP: + cnt_apps++; + break; + case AS_COMPONENT_KIND_FONT: + case AS_COMPONENT_KIND_CODEC: + case AS_COMPONENT_KIND_INPUT_METHOD: + case AS_COMPONENT_KIND_ADDON: + cnt_addon++; + break; + default: + break; + } + } + + if (cnt_addon == 0) { + /* TRANSLATORS: This string is used to construct the 'X applications + installed' sentence, describing a software repository. */ + return g_strdup_printf (ngettext ("%u application installed", + "%u applications installed", + cnt_apps), cnt_apps); + } + if (cnt_apps == 0) { + /* TRANSLATORS: This string is used to construct the 'X add-ons + installed' sentence, describing a software repository. */ + return g_strdup_printf (ngettext ("%u add-on installed", + "%u add-ons installed", + cnt_addon), cnt_addon); + } + + /* TRANSLATORS: This string is used to construct the 'X applications + and y add-ons installed' sentence, describing a software repository. + The correct form here depends on the number of applications. */ + apps_text = g_strdup_printf (ngettext ("%u application", + "%u applications", + cnt_apps), cnt_apps); + /* TRANSLATORS: This string is used to construct the 'X applications + and y add-ons installed' sentence, describing a software repository. + The correct form here depends on the number of add-ons. */ + addons_text = g_strdup_printf (ngettext ("%u add-on", + "%u add-ons", + cnt_addon), cnt_addon); + /* TRANSLATORS: This string is used to construct the 'X applications + and y add-ons installed' sentence, describing a software repository. + The correct form here depends on the total number of + applications and add-ons. */ + return g_strdup_printf (ngettext ("%s and %s installed", + "%s and %s installed", + cnt_apps + cnt_addon), + apps_text, addons_text); +} + +static void +gs_repo_row_set_repo (GsRepoRow *self, GsApp *repo) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self); + g_autoptr(GsPlugin) plugin = NULL; + g_autofree gchar *comment = NULL; + const gchar *tmp; + + g_assert (priv->repo == NULL); + + priv->repo = g_object_ref (repo); + g_signal_connect_object (priv->repo, "notify::state", + G_CALLBACK (repo_state_changed_cb), + self, 0); + + plugin = gs_app_dup_management_plugin (repo); + if (plugin) { + GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin); + priv->supports_remove = plugin_class != NULL && plugin_class->remove_repository_async != NULL; + priv->supports_enable_disable = plugin_class != NULL && + plugin_class->enable_repository_async != NULL && + plugin_class->disable_repository_async != NULL; + } else { + priv->supports_remove = FALSE; + priv->supports_enable_disable = FALSE; + } + + gtk_label_set_label (GTK_LABEL (priv->name_label), gs_app_get_name (repo)); + + gtk_widget_set_visible (priv->hostname_label, FALSE); + + tmp = gs_app_get_url (repo, AS_URL_KIND_HOMEPAGE); + if (tmp != NULL && *tmp != '\0') { + g_autoptr(GUri) uri = NULL; + + uri = g_uri_parse (tmp, SOUP_HTTP_URI_FLAGS, NULL); + if (uri && g_uri_get_host (uri) != NULL && *g_uri_get_host (uri) != '\0') { + gtk_label_set_label (GTK_LABEL (priv->hostname_label), g_uri_get_host (uri)); + gtk_widget_set_visible (priv->hostname_label, TRUE); + } + } + + comment = get_repo_installed_text (repo); + tmp = gs_app_get_metadata_item (priv->repo, "GnomeSoftware::InstallationKind"); + if (tmp != NULL && *tmp != '\0') { + gchar *cnt; + + /* Translators: The first '%s' is replaced with a text like '10 applications installed', + the second '%s' is replaced with installation kind, like in case of Flatpak 'User Installation'. */ + cnt = g_strdup_printf (C_("repo-row", "%s • %s"), comment, tmp); + g_clear_pointer (&comment, g_free); + comment = cnt; + } + + gtk_label_set_label (GTK_LABEL (priv->comment_label), comment); + + refresh_ui (self); +} + +GsApp * +gs_repo_row_get_repo (GsRepoRow *self) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self); + g_return_val_if_fail (GS_IS_REPO_ROW (self), NULL); + return priv->repo; +} + +static void +disable_switch_clicked_cb (GtkWidget *widget, + GParamSpec *param, + GsRepoRow *row) +{ + g_return_if_fail (GS_IS_REPO_ROW (row)); + gs_repo_row_emit_switch_clicked (row); +} + +static void +gs_repo_row_remove_button_clicked_cb (GtkWidget *button, + GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + g_return_if_fail (GS_IS_REPO_ROW (row)); + + if (priv->repo == NULL || priv->busy_counter) + return; + + g_signal_emit (row, signals[SIGNAL_REMOVE_CLICKED], 0); +} + +static void +gs_repo_row_dispose (GObject *object) +{ + GsRepoRow *self = GS_REPO_ROW (object); + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self); + + if (priv->repo != NULL) { + g_signal_handlers_disconnect_by_func (priv->repo, repo_state_changed_cb, self); + g_clear_object (&priv->repo); + } + + if (priv->refresh_idle_id != 0) { + g_source_remove (priv->refresh_idle_id); + priv->refresh_idle_id = 0; + } + + G_OBJECT_CLASS (gs_repo_row_parent_class)->dispose (object); +} + +static void +gs_repo_row_init (GsRepoRow *self) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self); + GtkWidget *image; + + gtk_widget_init_template (GTK_WIDGET (self)); + priv->switch_handler_id = g_signal_connect (priv->disable_switch, "notify::active", + G_CALLBACK (disable_switch_clicked_cb), self); + image = gtk_image_new_from_icon_name ("user-trash-symbolic"); + gtk_button_set_child (GTK_BUTTON (priv->remove_button), image); + g_signal_connect (priv->remove_button, "clicked", + G_CALLBACK (gs_repo_row_remove_button_clicked_cb), self); +} + +static void +gs_repo_row_class_init (GsRepoRowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gs_repo_row_dispose; + + signals [SIGNAL_REMOVE_CLICKED] = + g_signal_new ("remove-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsRepoRowClass, remove_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0, G_TYPE_NONE); + + signals [SIGNAL_SWITCH_CLICKED] = + g_signal_new ("switch-clicked", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsRepoRowClass, switch_clicked), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0, G_TYPE_NONE); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-repo-row.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, name_label); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, hostname_label); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, comment_label); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, remove_button); + gtk_widget_class_bind_template_child_private (widget_class, GsRepoRow, disable_switch); +} + +/* + * gs_repo_row_new: + * @repo: a #GsApp to represent the repo in the new row + * @always_allow_enable_disable: always allow enabled/disable of the @repo + * + * The @always_allow_enable_disable, when %TRUE, means that the @repo in this row + * can be always enabled/disabled by the user, if supported by the related plugin, + * regardless of the other heuristics, which can avoid the repo enable/disable. + * + * Returns: (transfer full): a newly created #GsRepoRow + */ +GtkWidget * +gs_repo_row_new (GsApp *repo, + gboolean always_allow_enable_disable) +{ + GsRepoRow *row = g_object_new (GS_TYPE_REPO_ROW, NULL); + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + priv->always_allow_enable_disable = always_allow_enable_disable; + gs_repo_row_set_repo (row, repo); + return GTK_WIDGET (row); +} + +static void +gs_repo_row_change_busy (GsRepoRow *self, + gboolean value) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self); + + g_return_if_fail (GS_IS_REPO_ROW (self)); + + if (value) + g_return_if_fail (priv->busy_counter + 1 > priv->busy_counter); + else + g_return_if_fail (priv->busy_counter > 0); + + priv->busy_counter += (value ? 1 : -1); + + if (value && priv->busy_counter == 1) + gtk_widget_set_sensitive (priv->disable_switch, FALSE); + else if (!value && !priv->busy_counter) + refresh_ui (self); +} + +/** + * gs_repo_row_mark_busy: + * @row: a #GsRepoRow + * + * Mark the @row as busy, that is the @row has pending operation(s). + * Unmark the @row as busy with gs_repo_row_unmark_busy() once + * the operation is done. This can be called mutliple times, only call + * the gs_repo_row_unmark_busy() as many times as this function had + * been called. + * + * Since: 41 + **/ +void +gs_repo_row_mark_busy (GsRepoRow *row) +{ + gs_repo_row_change_busy (row, TRUE); +} + +/** + * gs_repo_row_unmark_busy: + * @row: a #GsRepoRow + * + * A pair function for gs_repo_row_mark_busy(). + * + * Since: 41 + **/ +void +gs_repo_row_unmark_busy (GsRepoRow *row) +{ + gs_repo_row_change_busy (row, FALSE); +} + +/** + * gs_repo_row_get_is_busy: + * @row: a #GsRepoRow + * + * Returns: %TRUE, when there is any pending operation for the @row + * + * Since: 41 + **/ +gboolean +gs_repo_row_get_is_busy (GsRepoRow *row) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (row); + + g_return_val_if_fail (GS_IS_REPO_ROW (row), FALSE); + + return priv->busy_counter > 0; +} + +/** + * gs_repo_row_emit_switch_clicked: + * @self: a #GsRepoRow + * + * Emits the GsRepoRow:switch-clicked signal, if applicable. + * + * Since: 41 + **/ +void +gs_repo_row_emit_switch_clicked (GsRepoRow *self) +{ + GsRepoRowPrivate *priv = gs_repo_row_get_instance_private (self); + + g_return_if_fail (GS_IS_REPO_ROW (self)); + + if (priv->repo == NULL || priv->busy_counter > 0 || + !gtk_widget_get_visible (priv->disable_switch) || + !gtk_widget_get_sensitive (priv->disable_switch)) + return; + + g_signal_emit (self, signals[SIGNAL_SWITCH_CLICKED], 0); +} |