Convert GtkTreeView in status editor to GtkColumnView

Tue, 17 Jan 2023 02:25:21 -0600

author
Elliott Sales de Andrade <quantum.analyst@gmail.com>
date
Tue, 17 Jan 2023 02:25:21 -0600
changeset 42026
14aae1aaeb98
parent 42025
f37c11d0200a
child 42027
b1826d4885af

Convert GtkTreeView in status editor to GtkColumnView

There is a (big or small, up to you) hack in this implementation: `PidginSavedStatus` is a boxed type and so cannot be placed in a `GListModel`. Thus the model uses a dummy `GObject` instance for each row that works similar to each `GtkTreeModel` row. This is annoying, but means the rest of the `.ui` remains _similar_ to how it would be with a non-boxed type.

Compared to before, this is only missing search.

Like the account manager, we might want to redesign this entirely, but this is just a straight move away from `GtkTreeView` due to its impending deprecation. Also, now I have some idea how `GtkColumnView` works at least.

Testing Done:
Double-clicked a few saved statuses to confirm the editor dialogs opened, then clicked Modify to confirm that it opened the existing editor dialog.
Added a new saved status and confirmed that they were in the view.
Deleted a saved status and confirmed it was no longer in the view.
Closed the manager window with editor dialogs opened and confirmed all the editor dialogs closed as they used to.
Confirmed that Remove button was disabled when selecting an in-use status.
Clicked the column headers and confirmed that the statuses were sorted by them.

Reviewed at https://reviews.imfreedom.org/r/2199/

pidgin/pidginstatusmanager.c file | annotate | diff | comparison | revisions
pidgin/resources/Status/manager.ui file | annotate | diff | comparison | revisions
--- a/pidgin/pidginstatusmanager.c	Tue Jan 17 01:41:05 2023 -0600
+++ b/pidgin/pidginstatusmanager.c	Tue Jan 17 02:25:21 2023 -0600
@@ -44,8 +44,8 @@
 struct _PidginStatusManager {
 	GtkDialog parent;
 
-	GtkListStore *model;
-	GtkTreeSelection *selection;
+	GListStore *model;
+	GtkSingleSelection *selection;
 
 	GtkWidget *use_button;
 	GtkWidget *modify_button;
@@ -62,23 +62,15 @@
  *****************************************************************************/
 static void
 pidgin_status_manager_show_editor(PidginStatusManager *manager) {
+	GObject *wrapper = NULL;
 	PurpleSavedStatus *status = NULL;
 	GtkWidget *editor = NULL;
-	GtkTreeIter iter;
 
-	if(gtk_tree_selection_count_selected_rows(manager->selection) == 0) {
-		return;
-	}
-
-	gtk_tree_selection_get_selected(manager->selection, NULL, &iter);
-	gtk_tree_model_get(GTK_TREE_MODEL(manager->model), &iter,
-		COLUMN_STATUS, &status,
-		COLUMN_EDITOR, &editor,
-		-1);
+	wrapper = gtk_single_selection_get_selected_item(manager->selection);
+	status = g_object_get_data(wrapper, "savedstatus");
+	editor = g_object_get_data(wrapper, "editor");
 
 	if(status == NULL) {
-		g_clear_object(&editor);
-
 		return;
 	}
 
@@ -87,7 +79,7 @@
 
 		gtk_window_set_transient_for(GTK_WINDOW(editor), GTK_WINDOW(manager));
 
-		gtk_list_store_set(manager->model, &iter, COLUMN_EDITOR, editor, -1);
+		g_object_set_data(wrapper, "editor", editor);
 		g_signal_connect_object(editor, "destroy",
 		                        G_CALLBACK(pidgin_status_editor_destroy_cb),
 		                        manager, 0);
@@ -100,24 +92,16 @@
 
 static void
 pidgin_status_manager_remove_selected(PidginStatusManager *manager) {
+	GObject *wrapper = NULL;
 	PurpleSavedStatus *status = NULL;
 	GtkWidget *editor = NULL;
-	GtkTreeIter iter;
 
-	if(gtk_tree_selection_count_selected_rows(manager->selection) == 0) {
-		return;
-	}
-
-	gtk_tree_selection_get_selected(manager->selection, NULL, &iter);
-	gtk_tree_model_get(GTK_TREE_MODEL(manager->model), &iter,
-		COLUMN_STATUS, &status,
-		COLUMN_EDITOR, &editor,
-		-1);
+	wrapper = gtk_single_selection_get_selected_item(manager->selection);
+	status = g_object_get_data(wrapper, "savedstatus");
+	editor = g_object_get_data(wrapper, "editor");
 
 	if(GTK_IS_WIDGET(editor)) {
 		gtk_window_destroy(GTK_WINDOW(editor));
-
-		g_clear_object(&editor);
 	}
 
 	purple_savedstatus_delete_by_status(status);
@@ -125,17 +109,11 @@
 
 static PurpleSavedStatus *
 pidgin_status_manager_get_selected_status(PidginStatusManager *manager) {
+	GObject *wrapper = NULL;
 	PurpleSavedStatus *status = NULL;
-	GtkTreeIter iter;
 
-	if(gtk_tree_selection_count_selected_rows(manager->selection) == 0) {
-		return NULL;
-	}
-
-	gtk_tree_selection_get_selected(manager->selection, NULL, &iter);
-	gtk_tree_model_get(GTK_TREE_MODEL(manager->model), &iter,
-	                   COLUMN_STATUS, &status,
-	                   -1);
+	wrapper = gtk_single_selection_get_selected_item(manager->selection);
+	status = g_object_get_data(wrapper, "savedstatus");
 
 	return status;
 }
@@ -144,8 +122,8 @@
 pidgin_status_manager_add(PidginStatusManager *manager,
                           PurpleSavedStatus *status)
 {
+	GObject *wrapper = NULL;
 	PurpleStatusPrimitive primitive;
-	GtkTreeIter iter;
 	gchar *message = NULL;
 	const gchar *icon_name = NULL, *type = NULL;
 
@@ -155,16 +133,20 @@
 	icon_name = pidgin_icon_name_from_status_primitive(primitive, NULL);
 	type = purple_primitive_get_name_from_type(primitive);
 
-	gtk_list_store_append(manager->model, &iter);
-	gtk_list_store_set(manager->model, &iter,
-	                   COLUMN_TITLE, purple_savedstatus_get_title(status),
-	                   COLUMN_ICON_NAME, icon_name,
-	                   COLUMN_TYPE, type,
-	                   COLUMN_MESSAGE, message,
-	                   COLUMN_STATUS, status,
-	                   -1);
+	/* PurpleSavedStatus is a boxed type, so it can't be put in a GListModel;
+	 * instead create a wrapper GObject instance to hold its information. */
+	wrapper = g_object_new(G_TYPE_OBJECT, NULL);
+	g_object_set_data(wrapper, "savedstatus", status);
+	g_object_set_data_full(wrapper, "title",
+	                       g_strdup(purple_savedstatus_get_title(status)),
+	                       g_free);
+	g_object_set_data_full(wrapper, "icon-name", g_strdup(icon_name), g_free);
+	g_object_set_data_full(wrapper, "type", g_strdup(type), g_free);
+	g_object_set_data_full(wrapper, "message", g_strdup(message), g_free);
 
 	g_free(message);
+
+	g_list_store_append(manager->model, wrapper);
 }
 
 static void
@@ -181,7 +163,7 @@
 pidgin_status_manager_refresh(PidginStatusManager *manager) {
 	GList *statuses = NULL;
 
-	gtk_list_store_clear(manager->model);
+	g_list_store_remove_all(manager->model);
 
 	statuses = purple_savedstatuses_get_all();
 	g_list_foreach(statuses, pidgin_status_manager_populate_helper, manager);
@@ -225,30 +207,52 @@
 	}
 }
 
-static void
-pidgin_status_manager_row_activated_cb(G_GNUC_UNUSED GtkTreeView *tree_view,
-                                       GtkTreePath *path,
-                                       G_GNUC_UNUSED GtkTreeViewColumn *column,
-                                       gpointer data)
+static char *
+pidgin_status_manager_sort_data_cb(GObject *wrapper, const char *name,
+                                   G_GNUC_UNUSED gpointer data)
 {
-	PidginStatusManager *manager = data;
-	GtkTreeIter iter;
+	const char *value = NULL;
+
+	if(G_IS_OBJECT(wrapper)) {
+		value = g_object_get_data(wrapper, name);
+	}
 
-	if(gtk_tree_model_get_iter(GTK_TREE_MODEL(manager->model), &iter, path)) {
-		gtk_tree_selection_select_iter(manager->selection, &iter);
+	/* NOTE: Most GTK widget properties don't care if you return NULL, but the
+	 * GtkStringSorter does some string comparisons without checking for NULL,
+	 * so we need to ensure that non-NULL is returned to prevent runtime
+	 * warnings. */
+	return g_strdup(value ? value : "");
+}
 
-		pidgin_status_manager_show_editor(manager);
-	}
+/* A closure from within a GtkBuilderListItemFactory passes an extra first
+ * argument, so we need to drop that to re-use the above callback. */
+static char *
+pidgin_status_manager_lookup_text_data_cb(G_GNUC_UNUSED GObject *self,
+                                          GObject *wrapper, const char *name,
+                                          gpointer data)
+{
+	return pidgin_status_manager_sort_data_cb(wrapper, name, data);
 }
 
 static void
-pidgin_status_manager_selection_changed_cb(GtkTreeSelection *selection,
+pidgin_status_manager_row_activated_cb(G_GNUC_UNUSED GtkColumnView *self,
+                                       guint position, gpointer data)
+{
+	PidginStatusManager *manager = data;
+
+	gtk_single_selection_set_selected(manager->selection, position);
+	pidgin_status_manager_show_editor(manager);
+}
+
+static void
+pidgin_status_manager_selection_changed_cb(G_GNUC_UNUSED GObject *object,
+                                           G_GNUC_UNUSED GParamSpec *pspec,
                                            gpointer data)
 {
 	PidginStatusManager *manager = data;
 	gboolean sensitive = TRUE;
 
-	if(gtk_tree_selection_count_selected_rows(selection) == 0) {
+	if(g_list_model_get_n_items(G_LIST_MODEL(manager->model)) == 0) {
 		sensitive = FALSE;
 	}
 
@@ -300,32 +304,25 @@
 static void
 pidgin_status_editor_destroy_cb(GtkWidget *widget, gpointer data) {
 	PidginStatusManager *manager = data;
-	GtkTreeIter iter;
+	GListModel *model = G_LIST_MODEL(manager->model);
+	guint n_items = 0;
 
-	if(!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(manager->model), &iter)) {
-		return;
-	}
-
-	do {
+	n_items = g_list_model_get_n_items(model);
+	for(guint index = 0; index < n_items; index++) {
+		GObject *wrapper = NULL;
 		GtkWidget *editor = NULL;
 
-		gtk_tree_model_get(GTK_TREE_MODEL(manager->model), &iter,
-			COLUMN_EDITOR, &editor,
-			-1);
+		wrapper = g_list_model_get_item(model, index);
+		editor = g_object_get_data(wrapper, "editor");
 
 		/* Check if editor is the widget being destroyed. */
 		if(editor == widget) {
-			/* It is, so set it back to NULL and unreference the copy we just
-			 * got.
-			 */
-			gtk_list_store_set(manager->model, &iter, COLUMN_EDITOR, NULL, -1);
-			g_clear_object(&editor);
+			/* It is, so set it back to NULL to remove it from the wrapper. */
+			g_object_set_data(wrapper, "editor", NULL);
 
 			break;
 		}
-
-		g_clear_object(&editor);
-	} while(gtk_tree_model_iter_next(GTK_TREE_MODEL(manager->model), &iter));
+	}
 }
 
 /******************************************************************************
@@ -387,6 +384,10 @@
 	gtk_widget_class_bind_template_callback(widget_class,
 	                                        pidgin_status_manager_response_cb);
 	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_status_manager_lookup_text_data_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
+	                                        pidgin_status_manager_sort_data_cb);
+	gtk_widget_class_bind_template_callback(widget_class,
 	                                        pidgin_status_manager_row_activated_cb);
 	gtk_widget_class_bind_template_callback(widget_class,
 	                                        pidgin_status_manager_selection_changed_cb);
--- a/pidgin/resources/Status/manager.ui	Tue Jan 17 01:41:05 2023 -0600
+++ b/pidgin/resources/Status/manager.ui	Tue Jan 17 02:25:21 2023 -0600
@@ -24,22 +24,6 @@
   <!-- interface-name Pidgin -->
   <!-- interface-description Internet Messenger -->
   <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
-  <object class="GtkListStore" id="model">
-    <columns>
-      <!-- column-name title -->
-      <column type="gchararray"/>
-      <!-- column-name icon-name -->
-      <column type="gchararray"/>
-      <!-- column-name type -->
-      <column type="gchararray"/>
-      <!-- column-name message -->
-      <column type="gchararray"/>
-      <!-- column-name status -->
-      <column type="gpointer"/>
-      <!-- column-name editor -->
-      <column type="GObject"/>
-    </columns>
-  </object>
   <template class="PidginStatusManager" parent="GtkDialog">
     <property name="title" translatable="1">Saved Statuses</property>
     <property name="default-width">550</property>
@@ -50,67 +34,159 @@
         <property name="vexpand">1</property>
         <property name="focusable">1</property>
         <child>
-          <object class="GtkTreeView">
+          <object class="GtkColumnView" id="columnview">
             <property name="focusable">1</property>
-            <property name="model">model</property>
-            <property name="search-column">0</property>
-            <signal name="row-activated" handler="pidgin_status_manager_row_activated_cb" object="PidginStatusManager" swapped="no"/>
-            <child internal-child="selection">
-              <object class="GtkTreeSelection" id="selection">
-                <signal name="changed" handler="pidgin_status_manager_selection_changed_cb" object="PidginStatusManager" swapped="no"/>
+            <property name="model">
+              <object class="GtkSingleSelection" id="selection">
+                <property name="model">
+                  <object class="GtkSortListModel">
+                    <property name="model">
+                      <object class="GListStore" id="model">
+                        <property name="item-type">GObject</property>
+                      </object>
+                    </property>
+                    <binding name="sorter">
+                      <lookup name="sorter">columnview</lookup>
+                    </binding>
+                  </object>
+                </property>
+                <signal name="notify::selected" handler="pidgin_status_manager_selection_changed_cb" swapped="no"/>
               </object>
-            </child>
+            </property>
+            <property name="vexpand">1</property>
+            <signal name="activate" handler="pidgin_status_manager_row_activated_cb" swapped="no"/>
             <child>
-              <object class="GtkTreeViewColumn">
+              <object class="GtkColumnViewColumn">
                 <property name="resizable">1</property>
-                <property name="min-width">100</property>
                 <property name="title" translatable="1">Title</property>
-                <property name="clickable">1</property>
-                <property name="sort-column-id">0</property>
-                <child>
-                  <object class="GtkCellRendererText">
-                    <property name="ellipsize">end</property>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="bytes">
+<![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkLabel">
+        <property name="ellipsize">end</property>
+        <property name="xalign">0</property>
+        <binding name="label">
+          <closure type="gchararray" function="pidgin_status_manager_lookup_text_data_cb">
+            <lookup name="item">GtkListItem</lookup>
+            <constant type="gchararray">title</constant>
+          </closure>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
+]]>
+                    </property>
                   </object>
-                  <attributes>
-                    <attribute name="markup">0</attribute>
-                  </attributes>
-                </child>
+                </property>
+                <property name="sorter">
+                  <object class="GtkStringSorter">
+                    <property name="expression">
+                      <closure type="gchararray" function="pidgin_status_manager_sort_data_cb">
+                        <constant type="gchararray">title</constant>
+                      </closure>
+                    </property>
+                  </object>
+                </property>
               </object>
             </child>
             <child>
-              <object class="GtkTreeViewColumn">
+              <object class="GtkColumnViewColumn">
                 <property name="resizable">1</property>
                 <property name="title" translatable="1">Type</property>
-                <property name="clickable">1</property>
-                <property name="sort-column-id">2</property>
-                <child>
-                  <object class="GtkCellRendererPixbuf"/>
-                  <attributes>
-                    <attribute name="icon-name">1</attribute>
-                  </attributes>
-                </child>
-                <child>
-                  <object class="GtkCellRendererText"/>
-                  <attributes>
-                    <attribute name="markup">2</attribute>
-                  </attributes>
-                </child>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="bytes">
+<![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <child>
+          <object class="GtkImage">
+            <binding name="icon-name">
+              <closure type="gchararray" function="pidgin_status_manager_lookup_text_data_cb">
+                <lookup name="item">GtkListItem</lookup>
+                <constant type="gchararray">icon-name</constant>
+              </closure>
+            </binding>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <binding name="label">
+              <closure type="gchararray" function="pidgin_status_manager_lookup_text_data_cb">
+                <lookup name="item">GtkListItem</lookup>
+                <constant type="gchararray">type</constant>
+              </closure>
+            </binding>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
+]]>
+                    </property>
+                  </object>
+                </property>
+                <property name="sorter">
+                  <object class="GtkStringSorter">
+                    <property name="expression">
+                      <closure type="gchararray" function="pidgin_status_manager_sort_data_cb">
+                        <constant type="gchararray">type</constant>
+                      </closure>
+                    </property>
+                  </object>
+                </property>
               </object>
             </child>
             <child>
-              <object class="GtkTreeViewColumn">
+              <object class="GtkColumnViewColumn">
+                <property name="expand">1</property>
                 <property name="resizable">1</property>
                 <property name="title" translatable="1">Message</property>
-                <property name="clickable">1</property>
-                <property name="sort-column-id">4</property>
-                <child>
-                  <object class="GtkCellRendererText">
-                    <property name="ellipsize">end</property>
+                <property name="factory">
+                  <object class="GtkBuilderListItemFactory">
+                    <property name="bytes">
+<![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GtkListItem">
+    <property name="child">
+      <object class="GtkLabel">
+        <property name="xalign">0</property>
+        <property name="ellipsize">end</property>
+        <binding name="label">
+          <closure type="gchararray" function="pidgin_status_manager_lookup_text_data_cb">
+            <lookup name="item">GtkListItem</lookup>
+            <constant type="gchararray">message</constant>
+          </closure>
+        </binding>
+      </object>
+    </property>
+  </template>
+</interface>
+]]>
+                    </property>
                   </object>
-                  <attributes>
-                    <attribute name="markup">3</attribute>
-                  </attributes>
-                </child>
+                </property>
+                <property name="sorter">
+                  <object class="GtkStringSorter">
+                    <property name="expression">
+                      <closure type="gchararray" function="pidgin_status_manager_sort_data_cb">
+                        <constant type="gchararray">message</constant>
+                      </closure>
+                    </property>
+                  </object>
+                </property>
               </object>
             </child>
           </object>

mercurial