gnucash stable: Account tree: make numeric columns selectable via GncCellRendererLabel

Robert Fewell bobit at code.gnucash.org
Sat May 23 08:19:27 EDT 2026


Updated	 via  https://github.com/Gnucash/gnucash/commit/25e0f214 (commit)
	from  https://github.com/Gnucash/gnucash/commit/ec4a85a5 (commit)



commit 25e0f2144a16b2dc31822b9ba277c43265aee05a
Author: galbarm <galbarm at gmail.com>
Date:   Thu May 7 20:23:18 2026 +0300

    Account tree: make numeric columns selectable via GncCellRendererLabel
    
    Implements the approach suggested by jralls in PR #2222: instead of a
    context menu action, introduce GncCellRendererLabel - a
    GtkCellRendererText subclass that returns a read-only, selectable
    GtkEntry when a cell is activated.
    
    All numeric columns (Balance, Total, Cleared, Reconciled, Present,
    Future Minimum, and their report/period variants) are affected because
    they all go through gnc_tree_view_add_numeric_column.
    
    Usage:
    - Click once to select the row (unchanged behaviour)
    - Click again on a numeric cell to activate it; the value appears in a
      read-only entry field with the text pre-selected
    - Ctrl+C copies the value to the clipboard; Escape or clicking away
      dismisses the field without modifying the model
    - Unicode bidi control characters (added by GnuCash for RTL display)
      are stripped before showing the value so the copied text is clean

diff --git a/gnucash/gnome-utils/CMakeLists.txt b/gnucash/gnome-utils/CMakeLists.txt
index 5131bf4a3a..a6908d3c55 100644
--- a/gnucash/gnome-utils/CMakeLists.txt
+++ b/gnucash/gnome-utils/CMakeLists.txt
@@ -49,6 +49,7 @@ set (gnome_utils_SOURCES
   gnc-account-sel.c
   gnc-amount-edit.c
   gnc-autosave.c
+  gnc-cell-renderer-label.c
   gnc-cell-renderer-text-flag.c
   gnc-cell-renderer-text-view.c
   gnc-cell-view.c
@@ -134,6 +135,7 @@ set (gnome_utils_HEADERS
   dialog-utils.h
   gnc-account-sel.h
   gnc-amount-edit.h
+  gnc-cell-renderer-label.h
   gnc-cell-renderer-text-flag.h
   gnc-cell-renderer-text-view.h
   gnc-cell-view.h
diff --git a/gnucash/gnome-utils/gnc-cell-renderer-label.c b/gnucash/gnome-utils/gnc-cell-renderer-label.c
new file mode 100644
index 0000000000..1b5709ccbd
--- /dev/null
+++ b/gnucash/gnome-utils/gnc-cell-renderer-label.c
@@ -0,0 +1,169 @@
+/********************************************************************
+ * gnc-cell-renderer-label.c -- A GtkCellRendererText subclass that
+ * shows a selectable (but not editable) GtkEntry when activated,
+ * allowing the user to select and copy cell text via Ctrl+C.
+ * The entry has no frame and zero minimum width so it fits exactly
+ * within the cell area without overflowing into adjacent columns.
+ *
+ * Copyright (C) 2026 GnuCash contributors
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ *******************************************************************/
+
+#include <config.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "gnc-cell-renderer-label.h"
+#include "gnc-ui-util.h"
+
+/* ================================================================
+ * Selectable entry helpers
+ *
+ * GtkEntry already implements GtkCellEditable.  We use a plain
+ * non-editable GtkEntry (no wrapper widget) so its size exactly
+ * matches the cell area allocated by the tree view.  Callbacks
+ * always set editing-canceled so nothing is written back to the
+ * model.
+ * ================================================================ */
+
+static void
+gsl_dismiss (GtkEntry *entry)
+{
+    g_object_set (entry, "editing-canceled", TRUE, NULL);
+    gtk_cell_editable_editing_done (GTK_CELL_EDITABLE (entry));
+    gtk_cell_editable_remove_widget (GTK_CELL_EDITABLE (entry));
+}
+
+static gboolean
+gsl_key_press_cb (GtkWidget *widget,
+                  GdkEventKey *event,
+                  gpointer   user_data)
+{
+    if (event->keyval == GDK_KEY_Escape)
+    {
+        gsl_dismiss (GTK_ENTRY (widget));
+        return TRUE;
+    }
+    /* GtkEntry handles Ctrl+C natively (copies to GDK_SELECTION_CLIPBOARD). */
+    return FALSE;
+}
+
+static gboolean
+gsl_focus_in_cb (GtkWidget     *widget,
+                 GdkEventFocus *event,
+                 gpointer       user_data)
+{
+    gtk_editable_select_region (GTK_EDITABLE (widget), 0, -1);
+    return FALSE;
+}
+
+static gboolean
+gsl_focus_out_cb (GtkWidget   *widget,
+                  GdkEventFocus *event,
+                  gpointer     user_data)
+{
+    gsl_dismiss (GTK_ENTRY (widget));
+    return FALSE;
+}
+
+static GtkWidget *
+gnc_selectable_entry_new (const gchar *text, gfloat xalign)
+{
+    GtkEntry *entry = GTK_ENTRY (gtk_entry_new ());
+
+    /* Non-editable so the user can select but not modify. */
+    gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE);
+    /* No frame: matches the cell visual and avoids extra padding. */
+    gtk_entry_set_has_frame (entry, FALSE);
+    /* Allow the entry to shrink to the cell width. */
+    gtk_entry_set_width_chars (entry, 0);
+    gtk_entry_set_alignment (entry, xalign);
+
+    gchar *clean = gnc_filter_text_for_bidi_marks (text ? text : "");
+    gtk_entry_set_text (entry, clean ? clean : "");
+    g_free (clean);
+
+    g_signal_connect (entry, "focus-in-event",
+                      G_CALLBACK (gsl_focus_in_cb), NULL);
+    g_signal_connect (entry, "key-press-event",
+                      G_CALLBACK (gsl_key_press_cb), NULL);
+    g_signal_connect (entry, "focus-out-event",
+                      G_CALLBACK (gsl_focus_out_cb), NULL);
+
+    gtk_widget_show (GTK_WIDGET (entry));
+    return GTK_WIDGET (entry);
+}
+
+/* ================================================================
+ * GncCellRendererLabel
+ *
+ * A GtkCellRendererText subclass whose start_editing returns a
+ * non-editable GtkEntry (no frame, zero minimum width) so clicking
+ * a cell shows the value in a selectable widget that fits exactly
+ * within the cell area.
+ * ================================================================ */
+
+struct _GncCellRendererLabel
+{
+    GtkCellRendererText parent;
+};
+
+G_DEFINE_TYPE (GncCellRendererLabel, gnc_cell_renderer_label, GTK_TYPE_CELL_RENDERER_TEXT)
+
+static GtkCellEditable *
+gnc_cell_renderer_label_start_editing (GtkCellRenderer      *cell,
+                                       GdkEvent             *event,
+                                       GtkWidget            *widget,
+                                       const gchar          *path,
+                                       const GdkRectangle   *background_area,
+                                       const GdkRectangle   *cell_area,
+                                       GtkCellRendererState  flags)
+{
+    gchar  *text = NULL;
+    gfloat  xalign = 0.0;
+    GtkWidget *editable;
+
+    g_object_get (cell, "text", &text, "xalign", &xalign, NULL);
+
+    editable = gnc_selectable_entry_new (text, xalign);
+    g_free (text);
+
+    return GTK_CELL_EDITABLE (editable);
+}
+
+static void
+gnc_cell_renderer_label_class_init (GncCellRendererLabelClass *klass)
+{
+    GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (klass);
+
+    cell_class->start_editing = gnc_cell_renderer_label_start_editing;
+}
+
+static void
+gnc_cell_renderer_label_init (GncCellRendererLabel *self)
+{
+    /* GTK_CELL_RENDERER_MODE_EDITABLE causes start_editing() to be
+     * called when the user activates the cell. */
+    g_object_set (self, "mode", GTK_CELL_RENDERER_MODE_EDITABLE, NULL);
+}
+
+GtkCellRenderer *
+gnc_cell_renderer_label_new (void)
+{
+    return g_object_new (GNC_TYPE_CELL_RENDERER_LABEL, NULL);
+}
diff --git a/gnucash/gnome-utils/gnc-cell-renderer-label.h b/gnucash/gnome-utils/gnc-cell-renderer-label.h
new file mode 100644
index 0000000000..52c43bb065
--- /dev/null
+++ b/gnucash/gnome-utils/gnc-cell-renderer-label.h
@@ -0,0 +1,35 @@
+/********************************************************************
+ * gnc-cell-renderer-label.h -- A GtkCellRendererText subclass that
+ * shows a selectable (but not editable) GtkLabel when activated,
+ * allowing the user to select and copy cell text via Ctrl+C.
+ *
+ * Copyright (C) 2026 GnuCash contributors
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ *******************************************************************/
+
+#ifndef __GNC_CELL_RENDERER_LABEL_H__
+#define __GNC_CELL_RENDERER_LABEL_H__
+
+#include <gtk/gtk.h>
+
+#define GNC_TYPE_CELL_RENDERER_LABEL (gnc_cell_renderer_label_get_type ())
+G_DECLARE_FINAL_TYPE (GncCellRendererLabel, gnc_cell_renderer_label,
+                      GNC, CELL_RENDERER_LABEL, GtkCellRendererText)
+
+GtkCellRenderer *gnc_cell_renderer_label_new (void);
+
+#endif /* __GNC_CELL_RENDERER_LABEL_H__ */
diff --git a/gnucash/gnome-utils/gnc-tree-view.c b/gnucash/gnome-utils/gnc-tree-view.c
index 95e206c9fe..38af5ec221 100644
--- a/gnucash/gnome-utils/gnc-tree-view.c
+++ b/gnucash/gnome-utils/gnc-tree-view.c
@@ -43,6 +43,7 @@
 #include "gnc-glib-utils.h"
 #include "gnc-gnome-utils.h"
 #include "gnc-gobject-utils.h"
+#include "gnc-cell-renderer-label.h"
 #include "gnc-cell-renderer-text-view.h"
 #include "gnc-state.h"
 #include "gnc-prefs.h"
@@ -2070,12 +2071,13 @@ gnc_tree_view_add_numeric_column (GncTreeView *view,
     GtkCellRenderer *renderer;
     gfloat alignment = 1.0;
 
-    column = gnc_tree_view_add_text_column (view, column_title, pref_name,
-                                            NULL, sizing_text, model_data_column,
-                                            model_visibility_column,
-                                            column_sort_fn);
-
-    renderer = gnc_tree_view_column_get_renderer (column);
+    /* Use GncCellRendererLabel so the user can click a cell and select
+     * its text (e.g. to copy it to the clipboard), without being able
+     * to modify the underlying data. */
+    renderer = gnc_cell_renderer_label_new ();
+    column = add_text_column_variant (view, renderer, column_title, pref_name,
+                                      NULL, sizing_text, model_data_column,
+                                      model_visibility_column, column_sort_fn);
 
     /* Right align the column title and data for both ltr and rtl */
     if (gtk_widget_get_direction (GTK_WIDGET(view)) == GTK_TEXT_DIR_RTL)
diff --git a/libgnucash/app-utils/gnc-ui-util.cpp b/libgnucash/app-utils/gnc-ui-util.cpp
index 48ecf1aadf..a1736b8a35 100644
--- a/libgnucash/app-utils/gnc-ui-util.cpp
+++ b/libgnucash/app-utils/gnc-ui-util.cpp
@@ -42,6 +42,8 @@
 #include <stdio.h>
 #include <string.h>
 #include <cinttypes>
+#include <unicode/uchar.h>
+#include <unicode/utf8.h>
 #include <unicode/listformatter.h>
 
 #include "qof.h"
@@ -2152,6 +2154,32 @@ gnc_ui_util_remove_registered_prefs (void)
                                  (void*)gnc_set_auto_decimal_places, nullptr);
 }
 
+char*
+gnc_filter_text_for_bidi_marks (const char* text)
+{
+    if (!text)
+        return nullptr;
+
+    int32_t len = static_cast<int32_t> (strlen (text));
+    std::string result;
+    result.reserve (len);
+
+    const char* p = text;
+    int32_t i = 0;
+
+    while (i < len)
+    {
+        UChar32 c;
+        int32_t start = i;
+        U8_NEXT (p, i, len, c);
+
+        if (c >= 0 && !u_hasBinaryProperty (c, UCHAR_BIDI_CONTROL))
+            result.append (p + start, i - start);
+    }
+
+    return g_strdup (result.c_str ());
+}
+
 static inline bool
 unichar_is_cntrl (gunichar uc)
 {
diff --git a/libgnucash/app-utils/gnc-ui-util.h b/libgnucash/app-utils/gnc-ui-util.h
index 51b8edb42e..3002e80e8f 100644
--- a/libgnucash/app-utils/gnc-ui-util.h
+++ b/libgnucash/app-utils/gnc-ui-util.h
@@ -408,6 +408,20 @@ void gnc_ui_util_init (void);
 
 void gnc_ui_util_remove_registered_prefs (void);
 
+/** Returns the incoming text with Unicode bidi control characters removed.
+ *
+ * Strips directional marks (U+200E, U+200F), embedding/override codes
+ * (U+202A–U+202E), isolate codes (U+2066–U+2069) and the Arabic letter
+ * mark (U+061C) that GnuCash inserts for RTL display, so that values
+ * copied to the clipboard are clean plain text.
+ *
+ * @param incoming_text The text to filter (may be NULL)
+ *
+ * @return A newly-allocated string with bidi marks removed, or NULL when
+ *         incoming_text is NULL.  Must be freed by the caller.
+ */
+char* gnc_filter_text_for_bidi_marks (const char* incoming_text);
+
 /** Returns the incoming text removed of control characters
  *
  * @param incoming_text The text to filter
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2fc80422f9..6aa4fe2df2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -150,6 +150,7 @@ gnucash/gnome-utils/dialog-utils.c
 gnucash/gnome-utils/gnc-account-sel.c
 gnucash/gnome-utils/gnc-amount-edit.c
 gnucash/gnome-utils/gnc-autosave.c
+gnucash/gnome-utils/gnc-cell-renderer-label.c
 gnucash/gnome-utils/gnc-cell-renderer-text-flag.c
 gnucash/gnome-utils/gnc-cell-renderer-text-view.c
 gnucash/gnome-utils/gnc-cell-view.c



Summary of changes:
 gnucash/gnome-utils/CMakeLists.txt            |   2 +
 gnucash/gnome-utils/gnc-cell-renderer-label.c | 169 ++++++++++++++++++++++++++
 gnucash/gnome-utils/gnc-cell-renderer-label.h |  35 ++++++
 gnucash/gnome-utils/gnc-tree-view.c           |  14 ++-
 libgnucash/app-utils/gnc-ui-util.cpp          |  28 +++++
 libgnucash/app-utils/gnc-ui-util.h            |  14 +++
 po/POTFILES.in                                |   1 +
 7 files changed, 257 insertions(+), 6 deletions(-)
 create mode 100644 gnucash/gnome-utils/gnc-cell-renderer-label.c
 create mode 100644 gnucash/gnome-utils/gnc-cell-renderer-label.h



More information about the gnucash-changes mailing list