diff --git a/data/com.mattjakeman.ExtensionManager.gschema.xml.in b/data/com.mattjakeman.ExtensionManager.gschema.xml.in index 0714ec00..2c14997d 100644 --- a/data/com.mattjakeman.ExtensionManager.gschema.xml.in +++ b/data/com.mattjakeman.ExtensionManager.gschema.xml.in @@ -16,5 +16,10 @@ Last-Used Version The last-used version of the app. If the saved version does not match, the "What's New" dialog is shown. + + false + Sort Enabled First + Display enabled extensions first in the installed view. + diff --git a/src/exm-application.c b/src/exm-application.c index 97e34f56..fd4418e6 100644 --- a/src/exm-application.c +++ b/src/exm-application.c @@ -197,6 +197,9 @@ exm_application_init (ExmApplication *self) g_signal_connect_swapped (logout_action, "activate", G_CALLBACK (request_logout), self); g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (logout_action)); + GAction *sort_enabled_first_action = g_settings_create_action (settings, "sort-enabled-first"); + g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (sort_enabled_first_action)); + GAction *style_variant_action = g_settings_create_action (settings, "style-variant"); g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (style_variant_action)); diff --git a/src/exm-extension-row.c b/src/exm-extension-row.c index bdfdc49a..cc07a6af 100644 --- a/src/exm-extension-row.c +++ b/src/exm-extension-row.c @@ -25,6 +25,8 @@ struct _ExmExtensionRow GtkImage *update_icon; GtkImage *error_icon; GtkImage *out_of_date_icon; + + guint signal_handler; }; G_DEFINE_FINAL_TYPE (ExmExtensionRow, exm_extension_row, ADW_TYPE_EXPANDER_ROW) @@ -37,6 +39,13 @@ enum { static GParamSpec *properties [N_PROPS]; +static void +bind_extension (ExmExtensionRow *self, + ExmExtension *extension); + +static void +unbind_extension (ExmExtensionRow *self); + ExmExtensionRow * exm_extension_row_new (ExmExtension *extension) { @@ -53,6 +62,16 @@ exm_extension_row_finalize (GObject *object) G_OBJECT_CLASS (exm_extension_row_parent_class)->finalize (object); } +static void +exm_extension_row_dispose (GObject *object) +{ + ExmExtensionRow *self = (ExmExtensionRow *)object; + + unbind_extension (self); + + G_OBJECT_CLASS (exm_extension_row_parent_class)->dispose (object); +} + static void exm_extension_row_get_property (GObject *object, guint prop_id, @@ -82,14 +101,7 @@ exm_extension_row_set_property (GObject *object, switch (prop_id) { case PROP_EXTENSION: - self->extension = g_value_get_object (value); - if (self->extension) - { - // TODO: Bind here, rather than in constructed() - g_object_get (self->extension, - "uuid", &self->uuid, - NULL); - } + bind_extension (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -106,6 +118,11 @@ update_state (ExmExtension *extension, // the extension. We do not want this behaviour as it messes with the global // extension toggle. + g_return_if_fail (EXM_IS_EXTENSION (extension)); + g_return_if_fail (EXM_IS_EXTENSION_ROW (row)); + + g_assert (row->extension == extension); + const gchar *uuid; ExmExtensionState new_state; GAction *action; @@ -168,14 +185,35 @@ set_error_label_visible (ExmExtensionRow *self, } static void -exm_extension_row_constructed (GObject *object) +unbind_extension (ExmExtensionRow *self) +{ + if (self->extension != NULL) + { + g_signal_handler_disconnect (self->extension, self->signal_handler); + g_clear_object (&self->extension); + g_clear_pointer (&self->uuid, g_free); + } +} + +static void +bind_extension (ExmExtensionRow *self, + ExmExtension *extension) { // TODO: This big block of property assignments is currently copy/pasted // from ExmExtension. We can replace this with GtkExpression lookups // once blueprint-compiler supports expressions. // (See https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/5) - ExmExtensionRow *self = EXM_EXTENSION_ROW (object); + g_return_if_fail (EXM_IS_EXTENSION_ROW (self)); + + // First, remove traces of the old extension + unbind_extension (self); + + // Now, bind the new one + self->extension = g_object_ref (extension); + + if (self->extension == NULL) + return; gchar *name, *uuid, *description, *version, *error_msg; gboolean has_prefs, has_update, is_user; @@ -192,6 +230,8 @@ exm_extension_row_constructed (GObject *object) "error-msg", &error_msg, NULL); + self->uuid = g_strdup (uuid); + g_object_set (self, "title", name, "subtitle", uuid, NULL); g_object_set (self->prefs_btn, "visible", has_prefs, NULL); g_object_set (self->remove_btn, "visible", is_user, NULL); @@ -208,11 +248,13 @@ exm_extension_row_constructed (GObject *object) gboolean has_error = (error_msg != NULL) && (strlen(error_msg) != 0); set_error_label_visible (self, has_error); - gtk_actionable_set_action_target (GTK_ACTIONABLE (self->details_btn), "s", uuid); // One way binding from extension ("source of truth") to switch - g_signal_connect (self->extension, "notify::state", G_CALLBACK (update_state), self); + self->signal_handler = g_signal_connect (self->extension, + "notify::state", + G_CALLBACK (update_state), + self); GAction *action; @@ -226,8 +268,6 @@ exm_extension_row_constructed (GObject *object) g_simple_action_set_enabled (G_SIMPLE_ACTION (action), is_user); update_state (self->extension, NULL, self); - - G_OBJECT_CLASS (exm_extension_row_parent_class)->constructed (object); } static void @@ -236,9 +276,9 @@ exm_extension_row_class_init (ExmExtensionRowClass *klass) GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = exm_extension_row_finalize; + object_class->dispose = exm_extension_row_dispose; object_class->get_property = exm_extension_row_get_property; object_class->set_property = exm_extension_row_set_property; - object_class->constructed = exm_extension_row_constructed; properties [PROP_EXTENSION] = g_param_spec_object ("extension", diff --git a/src/exm-installed-page.c b/src/exm-installed-page.c index eb65b55b..71230555 100644 --- a/src/exm-installed-page.c +++ b/src/exm-installed-page.c @@ -3,6 +3,10 @@ #include "exm-extension-row.h" #include "local/exm-manager.h" +#include "exm-enums.h" +#include "exm-types.h" + +#include "exm-config.h" #include @@ -17,6 +21,8 @@ struct _ExmInstalledPage GtkListBox *system_list_box; GtkLabel *num_updates_label; GtkRevealer *updates_action_bar; + + gboolean sort_enabled_first; }; G_DEFINE_FINAL_TYPE (ExmInstalledPage, exm_installed_page, GTK_TYPE_WIDGET) @@ -24,11 +30,15 @@ G_DEFINE_FINAL_TYPE (ExmInstalledPage, exm_installed_page, GTK_TYPE_WIDGET) enum { PROP_0, PROP_MANAGER, + PROP_SORT_ENABLED_FIRST, N_PROPS }; static GParamSpec *properties [N_PROPS]; +static void +invalidate_model_bindings (ExmInstalledPage *self); + ExmInstalledPage * exm_installed_page_new (void) { @@ -56,6 +66,9 @@ exm_installed_page_get_property (GObject *object, case PROP_MANAGER: g_value_set_object (value, self->manager); break; + case PROP_SORT_ENABLED_FIRST: + g_value_set_boolean (value, self->sort_enabled_first); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -74,6 +87,10 @@ exm_installed_page_set_property (GObject *object, case PROP_MANAGER: self->manager = g_value_get_object (value); break; + case PROP_SORT_ENABLED_FIRST: + self->sort_enabled_first = g_value_get_boolean (value); + invalidate_model_bindings (self); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -119,18 +136,60 @@ widget_factory (ExmExtension* extension) return GTK_WIDGET (row); } +static int +compare_enabled (ExmExtension *this, ExmExtension *other) +{ + g_return_if_fail (EXM_IS_EXTENSION (this)); + g_return_if_fail (EXM_IS_EXTENSION (other)); + + ExmExtensionState this_state; + ExmExtensionState other_state; + + g_object_get (this, "state", &this_state, NULL); + g_object_get (other, "state", &other_state, NULL); + + gboolean this_enabled = (this_state == EXM_EXTENSION_STATE_ENABLED); + gboolean other_enabled = (other_state == EXM_EXTENSION_STATE_ENABLED); + + if ((this_enabled && other_enabled) || (!this_enabled && !other_enabled)) + return 0; + else if (this_enabled && !other_enabled) + return -1; + else if (!this_enabled && other_enabled) + return 1; +} + static void bind_list_box (GtkListBox *list_box, - GListModel *model) + GListModel *model, + gboolean sort_enabled_first) { GtkExpression *expression; - GtkStringSorter *sorter; + GtkStringSorter *alphabetical_sorter; GtkSortListModel *sorted_model; // Sort alphabetically expression = gtk_property_expression_new (EXM_TYPE_EXTENSION, NULL, "display-name"); - sorter = gtk_string_sorter_new (expression); - sorted_model = gtk_sort_list_model_new (model, GTK_SORTER (sorter)); + alphabetical_sorter = gtk_string_sorter_new (expression); + + if (sort_enabled_first) + { + GtkCustomSorter *enabled_sorter; + GtkMultiSorter *multi_sorter; + + // Sort by enabled + enabled_sorter = gtk_custom_sorter_new ((GCompareDataFunc) compare_enabled, NULL, NULL); + + multi_sorter = gtk_multi_sorter_new (); + gtk_multi_sorter_append (multi_sorter, GTK_SORTER (enabled_sorter)); + gtk_multi_sorter_append (multi_sorter, GTK_SORTER (alphabetical_sorter)); + + sorted_model = gtk_sort_list_model_new (model, GTK_SORTER (multi_sorter)); + } + else + { + sorted_model = gtk_sort_list_model_new (model, GTK_SORTER (alphabetical_sorter)); + } gtk_list_box_bind_model (list_box, G_LIST_MODEL (sorted_model), (GtkListBoxCreateWidgetFunc) widget_factory, @@ -165,18 +224,24 @@ on_updates_available (ExmManager *manager, } static void -on_bind_manager (ExmInstalledPage *self) +invalidate_model_bindings (ExmInstalledPage *self) { GListModel *user_ext_model; GListModel *system_ext_model; + if (!self->manager) + return; + g_object_get (self->manager, "user-extensions", &user_ext_model, "system-extensions", &system_ext_model, NULL); - bind_list_box (self->user_list_box, user_ext_model); - bind_list_box (self->system_list_box, system_ext_model); + if (!user_ext_model || !system_ext_model) + return; + + bind_list_box (self->user_list_box, user_ext_model, self->sort_enabled_first); + bind_list_box (self->system_list_box, system_ext_model, self->sort_enabled_first); g_object_bind_property (self->manager, "extensions-enabled", @@ -189,6 +254,13 @@ on_bind_manager (ExmInstalledPage *self) self->system_list_box, "sensitive", G_BINDING_SYNC_CREATE); +} + +static void +on_bind_manager (ExmInstalledPage *self) +{ + // Bind (or rebind) models + invalidate_model_bindings (self); g_signal_connect (self->manager, "updates-available", @@ -217,6 +289,13 @@ exm_installed_page_class_init (ExmInstalledPageClass *klass) EXM_TYPE_MANAGER, G_PARAM_READWRITE); + properties [PROP_SORT_ENABLED_FIRST] + = g_param_spec_boolean ("sort-enabled-first", + "Sort Enabled First", + "Sort Enabled First", + FALSE, + G_PARAM_READWRITE); + g_object_class_install_properties (object_class, N_PROPS, properties); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); @@ -230,30 +309,21 @@ exm_installed_page_class_init (ExmInstalledPageClass *klass) gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); } -static GtkWidget * -create_placeholder (const gchar *text) -{ - GtkWidget *label; - - label = gtk_label_new (text); - gtk_widget_add_css_class (label, "placeholder"); - - return label; -} - static void exm_installed_page_init (ExmInstalledPage *self) { - gtk_widget_init_template (GTK_WIDGET (self)); - - gtk_list_box_set_placeholder (self->user_list_box, - create_placeholder (_("There are no user extensions installed."))); + GSettings *settings; - gtk_list_box_set_placeholder (self->system_list_box, - create_placeholder (_("There are no system extensions installed."))); + gtk_widget_init_template (GTK_WIDGET (self)); g_signal_connect (self, "notify::manager", G_CALLBACK (on_bind_manager), NULL); + + settings = g_settings_new (APP_ID); + + g_settings_bind (settings, "sort-enabled-first", + self, "sort-enabled-first", + G_SETTINGS_BIND_GET); } diff --git a/src/exm-window.blp b/src/exm-window.blp index 439c9507..2db632ed 100644 --- a/src/exm-window.blp +++ b/src/exm-window.blp @@ -91,6 +91,10 @@ menu primary_menu { label: _("Release Notes"); action: "win.show-release-notes"; } + item { + label: _("Sort Enabled First"); + action: "app.sort-enabled-first"; + } item { label: _("About Extension Manager"); action: "app.about"; diff --git a/src/local/exm-manager.c b/src/local/exm-manager.c index cac45260..5d3795d4 100644 --- a/src/local/exm-manager.c +++ b/src/local/exm-manager.c @@ -696,6 +696,13 @@ on_state_changed (ShellExtensions *object, return; } + // Emit items-changed signal to re-sort extension list + { + guint position; + if (g_list_store_find_with_equal_func (list_store, extension, (GEqualFunc)is_extension_equal, &position)) + g_list_model_items_changed (G_LIST_MODEL (list_store), position, 1, 1); + } + // If the extension that has changed has an update, then // one or more extensions have updates available. Lazily // check the exact number and emit the 'updates-available'