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