GnuCash  5.6-150-g038405b370+
completioncell-gnome.c
1 /********************************************************************\
2  * completioncell-gnome.c -- completion combobox cell for gnome *
3  * *
4  * This program is free software; you can redistribute it and/or *
5  * modify it under the terms of the GNU General Public License as *
6  * published by the Free Software Foundation; either version 2 of *
7  * the License, or (at your option) any later version. *
8  * *
9  * This program is distributed in the hope that it will be useful, *
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12  * GNU General Public License for more details. *
13  * *
14  * You should have received a copy of the GNU General Public License*
15  * along with this program; if not, contact: *
16  * *
17  * Free Software Foundation Voice: +1-617-542-5942 *
18  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
19  * Boston, MA 02110-1301, USA gnu@gnu.org *
20  * *
21 \********************************************************************/
22 
23 /*
24  * FILE: completioncell-gnome.c
25  *
26  * FUNCTION: Implement gnome portion of a entry completion combo widget
27  * embedded in a table cell.
28  *
29  * HISTORY:
30  * @author Copyright (c) 2023 Robert Fewell
31  */
32 
33 #include <config.h>
34 
35 #include <string.h>
36 #include <gdk/gdkkeysyms.h>
37 
38 #include "completioncell.h"
39 #include "gnc-prefs.h"
40 #include "gnucash-item-edit.h"
41 #include "gnucash-item-list.h"
42 #include "gnucash-sheet.h"
43 #include "gnucash-sheetP.h"
44 #include "table-allgui.h"
45 #include "gnc-glib-utils.h"
46 
47 typedef struct _PopBox
48 {
49  GnucashSheet* sheet;
50  GncItemEdit* item_edit;
51  GncItemList* item_list;
52 
53  GHashTable* item_hash; // the item hash table
54  GtkListStore* item_store; // the item list store
55 
56  gchar* newval; // string value to find
57  gint newval_len; // length of string value to find
58 
59  gboolean signals_connected; // list signals connected
60  gboolean list_popped; // list is popped up
61 
62  gboolean autosize; // autosize the popup width
63 
64  gboolean sort_enabled; // sort of list store enabled
65  gboolean register_is_reversed; // whether the register is reversed
66  gboolean stop_searching; // set when there are no results
67 
68  gboolean strict; // text entry must be in the list
69  gboolean in_list_select; // item selected in the list
70 
71  gint occurrence; // the position in the list
72 
73 } PopBox;
74 
75 #define DONT_TEXT N_("Don't autocomplete")
76 
78 enum GncCompletionColumn
79 {
80  TEXT_COL, //0
81  TEXT_MARKUP_COL, //1
82  WEIGHT_COL, //2
83  FOUND_LOCATION_COL, //3
84 };
85 
86 static void gnc_completion_cell_gui_realize (BasicCell* bcell, gpointer w);
87 static void gnc_completion_cell_gui_move (BasicCell* bcell);
88 static void gnc_completion_cell_gui_destroy (BasicCell* bcell);
89 static gboolean gnc_completion_cell_enter (BasicCell* bcell,
90  int* cursor_position,
91  int* start_selection,
92  int* end_selection);
93 static void gnc_completion_cell_leave (BasicCell* bcell);
94 static void gnc_completion_cell_destroy (BasicCell* bcell);
95 
96 BasicCell*
97 gnc_completion_cell_new (void)
98 {
99  CompletionCell* cell = g_new0 (CompletionCell, 1);
100  gnc_completion_cell_init (cell);
101  return &cell->cell;
102 }
103 
104 void
105 gnc_completion_cell_init (CompletionCell* cell)
106 {
107  gnc_basic_cell_init (& (cell->cell));
108 
109  cell->cell.is_popup = TRUE;
110 
111  cell->cell.destroy = gnc_completion_cell_destroy;
112 
113  cell->cell.gui_realize = gnc_completion_cell_gui_realize;
114  cell->cell.gui_destroy = gnc_completion_cell_gui_destroy;
115 
116  PopBox* box = g_new0 (PopBox, 1);
117 
118  box->sheet = NULL;
119  box->item_edit = NULL;
120  box->item_list = NULL;
121  box->item_store = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_STRING,
122  G_TYPE_INT, G_TYPE_INT);
123  box->signals_connected = FALSE;
124  box->list_popped = FALSE;
125  box->autosize = FALSE;
126  box->register_is_reversed = FALSE;
127 
128  box->sort_enabled = FALSE;
129 
130  cell->cell.gui_private = box;
131 
132  box->stop_searching = FALSE;
133 
134  box->strict = FALSE;
135  box->in_list_select = FALSE;
136  box->occurrence = 0;
137 
138  box->item_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
139 }
140 
141 static void
142 hide_popup (PopBox* box)
143 {
144  gnc_item_edit_hide_popup (box->item_edit);
145  box->list_popped = FALSE;
146 }
147 
148 static void
149 select_item_cb (GncItemList* item_list, char* item_string, gpointer user_data)
150 {
151  CompletionCell* cell = user_data;
152  PopBox* box = cell->cell.gui_private;
153 
154  box->in_list_select = TRUE;
155  gnucash_sheet_modify_current_cell (box->sheet, item_string);
156  box->in_list_select = FALSE;
157 
158  hide_popup (box);
159 }
160 
161 static gint
162 text_width (PangoLayout *layout)
163 {
164  PangoRectangle logical_rect;
165  pango_layout_set_width (layout, -1);
166  pango_layout_get_pixel_extents (layout, NULL, &logical_rect);
167  return logical_rect.width;
168 }
169 
170 static void
171 horizontal_scroll_to_found_text (PopBox* box, char* item_string, gint found_location)
172 {
173  if (!gtk_widget_get_realized (GTK_WIDGET(box->item_list->tree_view)))
174  return;
175 
176  GtkAllocation alloc;
177  gtk_widget_get_allocation (GTK_WIDGET(box->item_list->tree_view), &alloc);
178  gint scroll_point = 0;
179  gchar *start_string = g_utf8_substring (item_string, 0, found_location + box->newval_len);
180 
181  PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET(box->item_list->tree_view), item_string);
182  PangoAttrList *atlist = pango_attr_list_new ();
183  PangoAttribute *bold_weight = pango_attr_weight_new (PANGO_WEIGHT_BOLD);
184  bold_weight->start_index = found_location;
185  bold_weight->end_index = found_location + box->newval_len;
186  pango_attr_list_insert (atlist, bold_weight);
187  pango_layout_set_attributes (layout, atlist);
188 
189  gint item_string_width = text_width (layout);
190 
191  pango_layout_set_text (layout, start_string, -1);
192 
193  gint start_string_width = text_width (layout);
194 
195  pango_attr_list_unref (atlist);
196  g_object_unref (layout);
197  g_free (start_string);
198 
199  if (item_string_width <= alloc.width)
200  scroll_point = 0;
201  else
202  scroll_point = start_string_width - alloc.width / 2;
203 
204  if (scroll_point < 0)
205  scroll_point = 0;
206 
207  gtk_tree_view_scroll_to_point (box->item_list->tree_view, scroll_point, -1);
208 }
209 
210 static void
211 change_item_cb (GncItemList* item_list, char* item_string, gpointer user_data)
212 {
213  CompletionCell* cell = user_data;
214  PopBox* box = cell->cell.gui_private;
215 
216  box->in_list_select = TRUE;
217  gnucash_sheet_modify_current_cell (box->sheet, item_string);
218  box->in_list_select = FALSE;
219 
220  GtkTreeModel *model = gtk_tree_view_get_model (item_list->tree_view);
221  GtkTreeSelection *selection = gtk_tree_view_get_selection (item_list->tree_view);
222  GtkTreeIter iter;
223  if (gtk_tree_selection_get_selected (selection, &model, &iter))
224  {
225  gint found_location;
226  gtk_tree_model_get (model, &iter, FOUND_LOCATION_COL, &found_location, -1);
227  horizontal_scroll_to_found_text (box, item_string, found_location);
228  }
229 }
230 
231 static void
232 activate_item_cb (GncItemList* item_list, char* item_string, gpointer user_data)
233 {
234  CompletionCell* cell = user_data;
235  PopBox* box = cell->cell.gui_private;
236  hide_popup (box);
237 }
238 
239 static void
240 block_list_signals (CompletionCell* cell)
241 {
242  PopBox* box = cell->cell.gui_private;
243 
244  if (!box->signals_connected)
245  return;
246 
247  g_signal_handlers_block_matched (G_OBJECT(box->item_list),
248  G_SIGNAL_MATCH_DATA,
249  0, 0, NULL, NULL, cell);
250 }
251 
252 static void
253 unblock_list_signals (CompletionCell* cell)
254 {
255  PopBox* box = cell->cell.gui_private;
256 
257  if (!box->signals_connected)
258  return;
259 
260  g_signal_handlers_unblock_matched (G_OBJECT(box->item_list),
261  G_SIGNAL_MATCH_DATA,
262  0, 0, NULL, NULL, cell);
263 }
264 
265 static void
266 key_press_item_cb (GncItemList* item_list, GdkEventKey* event, gpointer user_data)
267 {
268  CompletionCell* cell = user_data;
269  PopBox* box = cell->cell.gui_private;
270 
271  switch (event->keyval)
272  {
273  case GDK_KEY_Escape:
274  block_list_signals (cell); // Prevent recursion, unselect all
275  gnc_item_list_select (box->item_list, NULL);
276  unblock_list_signals (cell);
277  hide_popup (box);
278  break;
279 
280  default:
281  gtk_widget_event (GTK_WIDGET (box->sheet),
282  (GdkEvent*) event);
283  break;
284  }
285 }
286 
287 static void
288 completion_disconnect_signals (CompletionCell* cell)
289 {
290  PopBox* box = cell->cell.gui_private;
291 
292  if (!box->signals_connected)
293  return;
294 
295  g_signal_handlers_disconnect_matched (G_OBJECT(box->item_list),
296  G_SIGNAL_MATCH_DATA,
297  0, 0, NULL, NULL, cell);
298 
299  box->signals_connected = FALSE;
300 }
301 
302 static void
303 completion_connect_signals (CompletionCell* cell)
304 {
305  PopBox* box = cell->cell.gui_private;
306 
307  if (box->signals_connected)
308  return;
309 
310  g_signal_connect (G_OBJECT(box->item_list), "select_item",
311  G_CALLBACK(select_item_cb), cell);
312 
313  g_signal_connect (G_OBJECT(box->item_list), "change_item",
314  G_CALLBACK(change_item_cb), cell);
315 
316  g_signal_connect (G_OBJECT(box->item_list), "activate_item",
317  G_CALLBACK(activate_item_cb), cell);
318 
319  g_signal_connect (G_OBJECT(box->item_list), "key_press_event",
320  G_CALLBACK(key_press_item_cb), cell);
321 
322  box->signals_connected = TRUE;
323 }
324 
325 static void
326 gnc_completion_cell_gui_destroy (BasicCell* bcell)
327 {
328  CompletionCell* cell = (CompletionCell*) bcell;
329 
330  if (cell->cell.gui_realize)
331  {
332  PopBox* box = bcell->gui_private;
333  if (box && box->item_list)
334  {
335  completion_disconnect_signals (cell);
336  g_object_unref (box->item_list);
337  box->item_list = NULL;
338  }
339  /* allow the widget to be shown again */
340  cell->cell.gui_realize = gnc_completion_cell_gui_realize;
341  cell->cell.gui_move = NULL;
342  cell->cell.enter_cell = NULL;
343  cell->cell.leave_cell = NULL;
344  cell->cell.gui_destroy = NULL;
345  }
346 }
347 
348 static void
349 gnc_completion_cell_destroy (BasicCell* bcell)
350 {
351  CompletionCell* cell = (CompletionCell*) bcell;
352  PopBox* box = cell->cell.gui_private;
353 
354  gnc_completion_cell_gui_destroy (& (cell->cell));
355 
356  if (box)
357  {
358  if (box->item_hash)
359  g_hash_table_destroy (box->item_hash);
360 
361  g_free (box);
362  cell->cell.gui_private = NULL;
363  }
364  cell->cell.gui_private = NULL;
365  cell->cell.gui_realize = NULL;
366 }
367 
368 static gint
369 sort_func (GtkTreeModel* model, GtkTreeIter* iter_a, GtkTreeIter* iter_b, gpointer user_data)
370 {
371  gint a_weight, b_weight;
372  gint ret = 0;
373 
374  gtk_tree_model_get (model, iter_a, WEIGHT_COL, &a_weight, -1);
375  gtk_tree_model_get (model, iter_b, WEIGHT_COL, &b_weight, -1);
376 
377  if (a_weight < b_weight)
378  ret = -1;
379  else if (a_weight > b_weight)
380  ret = 1;
381 
382  return ret;
383 }
384 
385 void
387  gboolean enabled)
388 {
389  if (!cell)
390  return;
391 
392  PopBox* box = cell->cell.gui_private;
393  box->sort_enabled = enabled;
394 }
395 
396 static void
397 set_sort_column_enabled (PopBox* box, gboolean enable)
398 {
399  if (enable)
400  {
401  gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE(box->item_list->list_store),
402  WEIGHT_COL, sort_func, box->item_list, NULL);
403 
404  gnc_item_list_set_sort_column (box->item_list, WEIGHT_COL);
405  }
406  else
407  gnc_item_list_set_sort_column (box->item_list, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
408 }
409 
410 static void
411 item_store_clear (CompletionCell* cell)
412 {
413  if (!cell)
414  return;
415 
416  PopBox* box = cell->cell.gui_private;
417 
418  // disconnect list store from tree view
419  GtkListStore *store = gnc_item_list_disconnect_store (box->item_list);
420 
421  block_list_signals (cell);
422 
423  if (box->sort_enabled) // if sorting, disable it
424  set_sort_column_enabled (box, FALSE);
425 
426  box->stop_searching = FALSE;
427  gtk_list_store_clear (box->item_store);
428 
429  if (box->sort_enabled) // if sorting, enable it
430  set_sort_column_enabled (box, TRUE);
431 
432  unblock_list_signals (cell);
433 
434  // reconect list store to tree view
435  gnc_item_list_connect_store (box->item_list, store);
436 
437  hide_popup (box);
438 }
439 
440 void
441 gnc_completion_cell_clear_menu (CompletionCell* cell)
442 {
443  if (!cell)
444  return;
445 
446  PopBox* box = cell->cell.gui_private;
447  if (!box)
448  return;
449 
450  if (box->item_list)
451  {
452  g_hash_table_remove_all (box->item_hash);
453  item_store_clear (cell);
454  box->occurrence = 0;
455  }
456 }
457 
458 void
460  const char* menustr)
461 {
462  if (!cell || !menustr)
463  return;
464 
465  PopBox* box = cell->cell.gui_private;
466 
467  if (box->item_hash)
468  {
469  gpointer value = g_hash_table_lookup (box->item_hash, menustr);
470  gboolean update = FALSE;
471  if (value)
472  {
473  if (!box->register_is_reversed)
474  update = TRUE;
475  }
476  else
477  update = TRUE;
478 
479  if (update)
480  {
481  g_hash_table_insert (box->item_hash, g_strdup (menustr),
482  GINT_TO_POINTER(box->occurrence));
483  }
484  box->occurrence++;
485  }
486 }
487 
488 void
489 gnc_completion_cell_set_value (CompletionCell* cell, const char* str)
490 {
491  if (!cell || !str)
492 
493  gnc_basic_cell_set_value (&cell->cell, str);
494 }
495 
496 static inline void
497 list_store_append (GtkListStore *store, char* string,
498  char* markup, gint weight, gint found_location)
499 {
500  GtkTreeIter iter;
501 
502  g_return_if_fail (store);
503  g_return_if_fail (string);
504  g_return_if_fail (markup);
505 
506  gtk_list_store_append (store, &iter);
507 
508  gtk_list_store_set (store, &iter, TEXT_COL, string,
509  TEXT_MARKUP_COL, markup,
510  WEIGHT_COL, weight,
511  FOUND_LOCATION_COL, found_location, -1);
512 }
513 
514 static char*
515 normalize_and_fold (char* utf8_string)
516 {
517  char *normalized = g_utf8_normalize (utf8_string, -1, G_NORMALIZE_NFC);
518  if (!normalized)
519  return NULL;
520 
521  char *folded = g_utf8_casefold (normalized, -1);
522  g_free (normalized);
523  return folded;
524 }
525 
526 static gint
527 test_and_add (PopBox* box, const gchar *text, gint start_pos,
528  gpointer key, gint occurrence_difference)
529 {
530  gint ret_value = -1;
531  gint text_length = g_utf8_strlen (text, -1);
532 
533  if (start_pos > text_length)
534  return ret_value;
535 
536  gchar *sub_text = g_utf8_substring (text, start_pos, text_length);
537  gchar *sub_text_norm_fold = normalize_and_fold (sub_text);
538  gchar *found_text_ptr = g_strstr_len (sub_text_norm_fold, -1, box->newval);
539 
540  if (found_text_ptr)
541  {
542  gchar *markup = NULL, *prefix = NULL, *match = NULL, *suffix = NULL;
543  glong newval_length = g_utf8_strlen (box->newval, -1);
544  gulong found_location = g_utf8_pointer_to_offset (sub_text_norm_fold,
545  found_text_ptr) + start_pos;
546  gboolean have_boundary = FALSE;
547  gint prefix_length;
548  gint weight;
549 
550  if (found_location > 0)
551  prefix = g_utf8_substring (text, 0, found_location);
552  else
553  prefix = g_strdup ("");
554 
555  prefix_length = g_utf8_strlen (prefix, -1);
556 
557  match = g_utf8_substring (text, found_location, found_location + newval_length);
558 
559  if (found_location - start_pos >= 1)
560  {
561  gunichar prev = g_utf8_get_char (g_utf8_offset_to_pointer (sub_text, found_location - start_pos - 1));
562  if (prev && (g_unichar_isspace (prev) || g_unichar_ispunct (prev)))
563  have_boundary = TRUE;
564  else
565  ret_value = found_location + 1;
566  }
567 
568  suffix = g_utf8_substring (text, found_location + newval_length, text_length);
569 
570  markup = g_markup_printf_escaped ("%s<b>%s</b>%s%s", prefix, match, suffix, " ");
571 
572  if ((prefix_length == 0 ) || have_boundary)
573  {
574  weight = occurrence_difference; // sorted by recent first
575 
576  if (g_strcmp0 (sub_text_norm_fold, box->newval) == 0) // exact match
577  weight = 1;
578 
579  list_store_append (box->item_store, key, markup, weight, found_location);
580  }
581  g_free (markup);
582  g_free (prefix);
583  g_free (match);
584  g_free (suffix);
585  }
586  g_free (sub_text_norm_fold);
587  g_free (sub_text);
588  return ret_value;
589 }
590 
591 static void
592 add_item (gpointer key, gpointer value, gpointer user_data)
593 {
594  PopBox* box = user_data;
595  gchar *hash_entry = g_strdup (key);
596 
597  if (hash_entry && *hash_entry)
598  {
599  gint start_pos = 0;
600  gint occurrence_difference;
602 
603  if (box->register_is_reversed)
604  occurrence_difference = GPOINTER_TO_INT(value) + 1;
605  else
606  occurrence_difference = box->occurrence - GPOINTER_TO_INT(value);
607 
608  do
609  {
610  start_pos = test_and_add (box, hash_entry, start_pos, key, occurrence_difference);
611  }
612  while (start_pos != -1);
613  }
614  g_free (hash_entry);
615 }
616 
617 static void
618 select_first_entry_in_list (PopBox* box)
619 {
620  GtkTreeModel *model = gtk_tree_view_get_model (box->item_list->tree_view);
621  GtkTreeIter iter;
622  gchar* string;
623 
624  if (!gtk_tree_model_get_iter_first (model, &iter))
625  return;
626 
627  if (!gtk_tree_model_iter_next (model, &iter))
628  return;
629 
630  gtk_tree_model_get (model, &iter, TEXT_COL, &string, -1);
631 
632  gnc_item_list_select (box->item_list, string);
633 
634  GtkTreePath* path = gtk_tree_path_new_first ();
635  gtk_tree_view_scroll_to_cell (box->item_list->tree_view,
636  path, NULL, TRUE, 0.5, 0.0);
637  gtk_tree_path_free (path);
638  g_free (string);
639 }
640 
641 static void
642 populate_list_store (CompletionCell* cell, const gchar* str)
643 {
644  PopBox* box = cell->cell.gui_private;
645 
646  box->in_list_select = FALSE;
647  box->item_edit->popup_allocation_height = -1;
648 
649  if (box->stop_searching)
650  return;
651 
652  if (str && *str)
653  box->newval = normalize_and_fold ((gchar*)str);
654  else
655  return;
656 
657  box->newval_len = g_utf8_strlen (str, -1);
658 
659  // disconnect list store from tree view
660  box->item_store = gnc_item_list_disconnect_store (box->item_list);
661 
662  block_list_signals (cell);
663 
664  if (box->sort_enabled) // if sorting, disable it
665  set_sort_column_enabled (box, FALSE);
666 
667  gtk_list_store_clear (box->item_store);
668 
669  // add the don't first entry
670  gchar *markup = g_markup_printf_escaped ("<i>%s</i>", DONT_TEXT);
671  list_store_append (box->item_store, DONT_TEXT, markup, 0, 0);
672  g_free (markup);
673 
674  // add to the list store
675  g_hash_table_foreach (box->item_hash, add_item, box);
676 
677  if (box->sort_enabled) // if sorting, enable it
678  set_sort_column_enabled (box, TRUE);
679 
680  unblock_list_signals (cell);
681 
682  // reconnect list store to tree view
683  gnc_item_list_connect_store (box->item_list, box->item_store);
684 
685  // reset horizontal scrolling
686  gtk_tree_view_column_queue_resize (gtk_tree_view_get_column (
687  box->item_list->tree_view, TEXT_COL));
688 
689  // if no entries, do not show popup
690  if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL(box->item_store), NULL) == 1)
691  {
692  box->stop_searching = TRUE;
693  hide_popup (box);
694  }
695  else
696  gnc_item_edit_show_popup (box->item_edit);
697 
698  block_list_signals (cell); // Prevent recursion, select first entry
699  select_first_entry_in_list (box);
700  unblock_list_signals (cell);
701 
702  g_free (box->newval);
703 }
704 
705 static void
706 gnc_completion_cell_modify_verify (BasicCell* bcell,
707  const char* change,
708  int change_len,
709  const char* newval,
710  int newval_len,
711  int* cursor_position,
712  int* start_selection,
713  int* end_selection)
714 {
715  CompletionCell* cell = (CompletionCell*) bcell;
716  PopBox* box = cell->cell.gui_private;
717  glong newval_chars = g_utf8_strlen (newval, newval_len);
718 
719  if (box->in_list_select)
720  {
721  if (g_strcmp0 (newval, DONT_TEXT) == 0)
722  return;
723  gnc_basic_cell_set_value_internal (bcell, newval);
724  *cursor_position = -1;
725  *start_selection = 0;
726  *end_selection = 0;
727  return;
728  }
729 
730  // check to enable searching
731  if (((*cursor_position < newval_chars) &&
732  (g_utf8_strlen (bcell->value, -1) < newval_chars)) ||
733  (g_utf8_strlen (bcell->value, -1) > newval_chars))
734  {
735  box->stop_searching = FALSE;
736  }
737 
738  // Are were deleting or inserting in the middle.
739  if (change == NULL || *cursor_position < bcell->value_chars)
740  *start_selection = *end_selection = *cursor_position;
741 
742  gchar *start_of_text = g_utf8_substring (newval, 0, *cursor_position);
743  populate_list_store (cell, start_of_text);
744  g_free (start_of_text);
745 
746  if (g_strcmp0 (newval, "") == 0)
747  {
748  block_list_signals (cell); // Prevent recursion, unselect all
749  gnc_item_list_select (box->item_list, NULL);
750  unblock_list_signals (cell);
751  hide_popup (box);
752  }
753  gnc_basic_cell_set_value_internal (bcell, newval);
754 }
755 
756 static char*
757 get_entry_from_hash_if_size_is_one (CompletionCell* cell)
758 {
759  if (!cell)
760  return NULL;
761 
762  PopBox* box = cell->cell.gui_private;
763 
764  if (box->item_hash && (g_hash_table_size (box->item_hash) == 1))
765  {
766  GList *keys = g_hash_table_get_keys (box->item_hash);
767  char *ret = g_strdup (keys->data);
768  g_list_free (keys);
769  return ret;
770  }
771  return NULL;
772 }
773 
774 static gboolean
775 gnc_completion_cell_direct_update (BasicCell* bcell,
776  int* cursor_position,
777  int* start_selection,
778  int* end_selection,
779  void* gui_data)
780 {
781  CompletionCell* cell = (CompletionCell*) bcell;
782  PopBox* box = cell->cell.gui_private;
783  GdkEventKey* event = gui_data;
784 
785  if (event->type != GDK_KEY_PRESS)
786  return FALSE;
787 
788  switch (event->keyval)
789  {
790  case GDK_KEY_Tab:
791  case GDK_KEY_ISO_Left_Tab:
792  {
793  if (event->state & GDK_CONTROL_MASK)
794  {
795  char* hash_string = get_entry_from_hash_if_size_is_one (cell);
796 
797  if (hash_string)
798  {
799  gnc_basic_cell_set_value_internal (bcell, hash_string);
800  *cursor_position = strlen (hash_string);
801  }
802  g_free (hash_string);
803  return TRUE;
804  }
805 
806  char* string = gnc_item_list_get_selection (box->item_list);
807 
808  if (!string)
809  break;
810 
811  g_signal_emit_by_name (G_OBJECT(box->item_list), "change_item",
812  string, (gpointer)bcell);
813 
814  g_free (string);
815  break;
816  }
817  }
818 
819  if (box->strict)
820  box->in_list_select = gnc_item_in_list (box->item_list, bcell->value);
821 
822  if (!bcell->value)
823  item_store_clear (cell);
824 
825  return FALSE;
826 }
827 
828 void
830 {
831  if (!cell)
832  return;
833 
834  PopBox* box = cell->cell.gui_private;
835 
836  if (is_reversed != box->register_is_reversed)
837  {
838  gnc_completion_cell_clear_menu (cell);
839  box->register_is_reversed = is_reversed;
840  box->occurrence = 0;
841  }
842 }
843 
844 static void
845 gnc_completion_cell_gui_realize (BasicCell* bcell, gpointer data)
846 {
847  GnucashSheet* sheet = data;
848  GncItemEdit* item_edit = gnucash_sheet_get_item_edit (sheet);
849  CompletionCell* cell = (CompletionCell*) bcell;
850  PopBox* box = cell->cell.gui_private;
851 
852  /* initialize gui-specific, private data */
853  box->sheet = sheet;
854  box->item_edit = item_edit;
855  box->item_list = GNC_ITEM_LIST(gnc_item_list_new (box->item_store));
856 
857  block_list_signals (cell);
858  set_sort_column_enabled (box, FALSE);
859  unblock_list_signals (cell);
860 
861  gtk_widget_show_all (GTK_WIDGET(box->item_list));
862  g_object_ref_sink (box->item_list);
863 
864  /* to mark cell as realized, remove the realize method */
865  cell->cell.gui_realize = NULL;
866  cell->cell.gui_move = gnc_completion_cell_gui_move;
867  cell->cell.enter_cell = gnc_completion_cell_enter;
868  cell->cell.leave_cell = gnc_completion_cell_leave;
869  cell->cell.gui_destroy = gnc_completion_cell_gui_destroy;
870  cell->cell.modify_verify = gnc_completion_cell_modify_verify;
871  cell->cell.direct_update = gnc_completion_cell_direct_update;
872 }
873 
874 static void
875 reset_item_list_to_default_setup (BasicCell* bcell)
876 {
877  PopBox* box = bcell->gui_private;
878  PopupToggle popup_toggle;
879 
880  item_store_clear ((CompletionCell*) bcell);
881 
882  popup_toggle = box->item_edit->popup_toggle;
883  gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton), TRUE);
884  gtk_widget_set_visible (GTK_WIDGET(popup_toggle.tbutton), TRUE);
885 
886  GtkTreeViewColumn *column = gtk_tree_view_get_column (
887  GTK_TREE_VIEW(box->item_list->tree_view), TEXT_COL);
888  gtk_tree_view_column_clear_attributes (column,box->item_list->renderer);
889  gtk_tree_view_column_add_attribute (column, box->item_list->renderer,
890  "text", TEXT_COL);
891  box->list_popped = FALSE;
892 }
893 
894 static void
895 gnc_completion_cell_gui_move (BasicCell* bcell)
896 {
897  PopBox* box = bcell->gui_private;
898 
899  completion_disconnect_signals ((CompletionCell*) bcell);
900 
901  gnc_item_edit_set_popup (box->item_edit, NULL, NULL,
902  NULL, NULL, NULL, NULL, NULL);
903 
904  reset_item_list_to_default_setup (bcell);
905 }
906 
907 static int
908 popup_get_height (GtkWidget* widget,
909  int space_available,
910  G_GNUC_UNUSED int row_height,
911  gpointer user_data)
912 {
913  PopBox* box = user_data;
914  GtkScrolledWindow* scrollwin = GNC_ITEM_LIST(widget)->scrollwin;
915  int height;
916 
917  // if popup_allocation_height set use that
918  if (box->item_edit->popup_allocation_height != -1)
919  height = box->item_edit->popup_allocation_height;
920  else
921  height = gnc_item_list_get_popup_height (GNC_ITEM_LIST(widget));
922 
923  if (height < space_available)
924  {
925  // if the list is empty height would be 0 so return 1 instead to
926  // satisfy the check_popup_height_is_true function
927  gint ret_height = height ? height : 1;
928 
929  gtk_widget_set_size_request (GTK_WIDGET(scrollwin), -1, ret_height);
930  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrollwin),
931  GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER);
932  return ret_height;
933  }
934  else
935  gtk_widget_set_size_request (GTK_WIDGET(scrollwin), -1, -1);
936 
937  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrollwin),
938  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
939  return space_available;
940 }
941 
942 static int
943 popup_autosize (GtkWidget* widget,
944  int max_width,
945  gpointer user_data)
946 {
947  PopBox* box = user_data;
948 
949  if (!box || !box->autosize)
950  return max_width;
951 
952  return gnc_item_list_autosize (GNC_ITEM_LIST(widget)) + 20;
953 }
954 
955 static void
956 popup_set_focus (GtkWidget* widget,
957  G_GNUC_UNUSED gpointer user_data)
958 {
959  /* An empty GtkTreeView grabbing focus causes the key_press events to be
960  * lost because there's no entry cell to handle them.
961  */
962  if (gnc_item_list_num_entries (GNC_ITEM_LIST(widget)))
963  gtk_widget_grab_focus (GTK_WIDGET (GNC_ITEM_LIST(widget)->tree_view));
964 }
965 
966 static void
967 popup_post_show (GtkWidget* widget,
968  G_GNUC_UNUSED gpointer user_data)
969 {
970  gnc_item_list_autosize (GNC_ITEM_LIST(widget));
971  gnc_item_list_show_selected (GNC_ITEM_LIST(widget));
972 }
973 
974 static int
975 popup_get_width (GtkWidget* widget,
976  G_GNUC_UNUSED gpointer user_data)
977 {
978  GtkAllocation alloc;
979  gtk_widget_get_allocation (GTK_WIDGET (GNC_ITEM_LIST(widget)->tree_view),
980  &alloc);
981  return alloc.width;
982 }
983 
984 static void
985 tree_view_size_allocate_cb (GtkWidget *widget,
986  G_GNUC_UNUSED GtkAllocation *allocation,
987  gpointer user_data)
988 {
989  GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW(widget));
990  GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(widget));
991  GtkTreeIter iter;
992  if (gtk_tree_selection_get_selected (selection, &model, &iter))
993  {
994  PopBox* box = user_data;
995  gint found_location;
996  gchar *item_text;
997  gtk_tree_model_get (model, &iter, TEXT_COL, &item_text,
998  FOUND_LOCATION_COL, &found_location, -1);
999  horizontal_scroll_to_found_text (box, item_text, found_location);
1000  g_free (item_text);
1001  }
1002 }
1003 
1004 static gboolean
1005 gnc_completion_cell_enter (BasicCell* bcell,
1006  int* cursor_position,
1007  int* start_selection,
1008  int* end_selection)
1009 {
1010  CompletionCell* cell = (CompletionCell*) bcell;
1011  PopBox* box = bcell->gui_private;
1012  PopupToggle popup_toggle;
1013 
1014  gnc_item_edit_set_popup (box->item_edit,
1015  GTK_WIDGET(box->item_list),
1016  popup_get_height, popup_autosize,
1017  popup_set_focus, popup_post_show,
1018  popup_get_width, box);
1019 
1020  popup_toggle = box->item_edit->popup_toggle;
1021  gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton), FALSE);
1022  gtk_widget_set_visible (GTK_WIDGET(popup_toggle.tbutton), FALSE);
1023 
1024  GtkTreeViewColumn *column = gtk_tree_view_get_column (
1025  GTK_TREE_VIEW(box->item_list->tree_view), TEXT_COL);
1026  gtk_tree_view_column_clear_attributes (column, box->item_list->renderer);
1027  gtk_tree_view_column_add_attribute (column, box->item_list->renderer,
1028  "markup", TEXT_MARKUP_COL);
1029 
1030  g_signal_connect (G_OBJECT(box->item_list->tree_view), "size-allocate",
1031  G_CALLBACK(tree_view_size_allocate_cb), box);
1032 
1033  completion_connect_signals (cell);
1034 
1035  *cursor_position = -1;
1036  *start_selection = 0;
1037  *end_selection = -1;
1038 
1039  return TRUE;
1040 }
1041 
1042 static void
1043 gnc_completion_cell_leave (BasicCell* bcell)
1044 {
1045  PopBox* box = bcell->gui_private;
1046 
1047  completion_disconnect_signals ((CompletionCell*) bcell);
1048 
1049  gnc_item_edit_set_popup (box->item_edit, NULL, NULL,
1050  NULL, NULL, NULL, NULL, NULL);
1051 
1052  reset_item_list_to_default_setup (bcell);
1053 
1054  if (box->strict && !box->in_list_select)
1055  gnc_basic_cell_set_value_internal (bcell, "");
1056 }
1057 
1058 void
1060 {
1061  if (!cell)
1062  return;
1063 
1064  PopBox* box = cell->cell.gui_private;
1065  if (!box)
1066  return;
1067 
1068  box->strict = strict;
1069 }
1070 
1071 void
1073 {
1074  if (!cell)
1075  return;
1076 
1077  PopBox* box = cell->cell.gui_private;
1078  if (!box)
1079  return;
1080 
1081  box->autosize = autosize;
1082 }
The CompletionCell object implements a cell handler with a "combination-box" pull-down menu in it...
void gnc_completion_cell_reverse_sort(CompletionCell *cell, gboolean is_reversed)
Register the sort direction.
void gnc_completion_cell_set_sort_enabled(CompletionCell *cell, gboolean enabled)
Enable sorting of the menu item&#39;s contents.
void gnc_utf8_strip_invalid_and_controls(gchar *str)
Strip any non-utf8 characters and any control characters (everything < 0x20, , , ...
Public Declarations for GncItemList class.
Public declarations of GnucashRegister class.
Private declarations for GnucashSheet class.
GLib helper routines.
Generic api to store and retrieve preferences.
void gnc_completion_cell_add_menu_item(CompletionCell *cell, const char *menustr)
Add a menu item to the hash table list.
Public declarations for GncItemEdit class.
void gnc_completion_cell_set_strict(CompletionCell *cell, gboolean strict)
Determines whether the cell will accept strings not in the menu.
Declarations for the Table object.
void gnc_completion_cell_set_autosize(CompletionCell *cell, gboolean autosize)
Determines whether the popup list autosizes itself or uses all available space.
char * gnc_item_list_get_selection(GncItemList *item_list)
Retrieve the selected string from the item_list&#39;s active GtkListStore.