1 /********************************************************************\
2  * combocell-gnome.c -- implement combobox pull down 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: combocell-gnome.c
25  *
26  * FUNCTION: Implement gnome portion of a pull-down combo widget
27  *           embedded in a table cell.
28  *
29  * HISTORY:
30  * Copyright (c) 1998 Linas Vepstas <linas@linas.org>
31  * Copyright (c) 1998-1999 Rob Browning <rlb@cs.utexas.edu>
32  * Copyright (c) 2000 Linas Vepstas <linas@linas.org>
33  * Copyright (c) 2006 David Hampton <hampton@employees.org>
34  */
35 
36 #include <config.h>
37 
38 #include <string.h>
39 #include <gdk/gdkkeysyms.h>
40 
41 #include "QuickFill.h"
42 #include "combocell.h"
43 #include "gnc-prefs.h"
44 #include "gnucash-item-edit.h"
45 #include "gnucash-item-list.h"
46 #include "gnucash-sheet.h"
47 #include "gnucash-sheetP.h"
48 #include "table-allgui.h"
49 #include "Account.h"
50 
51 #define GNC_PREF_AUTO_RAISE_LISTS "auto-raise-lists"
52 
53 typedef struct _PopBox
54 {
55     GnucashSheet* sheet;
56     GncItemEdit*  item_edit;
57     GncItemList*  item_list;
58     GtkListStore* tmp_store;
59 
60     gboolean signals_connected; /* list signals connected? */
61 
62     gboolean list_popped;  /* list is popped up? */
63 
64     gboolean autosize;
65 
66     QuickFill* qf;
67     gboolean use_quickfill_cache;  /* If TRUE, we don't own the qf */
68 
69     gboolean in_list_select;
70 
71     gboolean strict;
72 
73     gunichar complete_char; /* char to be used for auto-completion */
74 
75     GList* ignore_strings;
76 } PopBox;
77 
78 
79 static void gnc_combo_cell_gui_realize (BasicCell* bcell, gpointer w);
80 static void gnc_combo_cell_gui_move (BasicCell* bcell);
81 static void gnc_combo_cell_gui_destroy (BasicCell* bcell);
82 static gboolean gnc_combo_cell_enter (BasicCell* bcell,
83                                       int* cursor_position,
84                                       int* start_selection,
85                                       int* end_selection);
86 static void gnc_combo_cell_leave (BasicCell* bcell);
87 static void gnc_combo_cell_destroy (BasicCell* bcell);
88 
89 static GOnce auto_pop_init_once = G_ONCE_INIT;
90 static gboolean auto_pop_combos = FALSE;
91 
92 
93 static void
gnc_combo_cell_set_autopop(gpointer prefs,gchar * pref,gpointer user_data)94 gnc_combo_cell_set_autopop (gpointer prefs, gchar* pref, gpointer user_data)
95 {
96     auto_pop_combos = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL_REGISTER,
97                                           GNC_PREF_AUTO_RAISE_LISTS);
98 }
99 
100 static gpointer
gnc_combo_cell_autopop_init(gpointer unused)101 gnc_combo_cell_autopop_init (gpointer unused)
102 {
103     gulong id;
104     auto_pop_combos = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL_REGISTER,
105                                           GNC_PREF_AUTO_RAISE_LISTS);
106 
107     id = gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL_REGISTER,
108                                 GNC_PREF_AUTO_RAISE_LISTS,
109                                 gnc_combo_cell_set_autopop,
110                                 NULL);
111 
112     gnc_prefs_set_reg_auto_raise_lists_id (id);
113     return NULL;
114 }
115 
116 BasicCell*
gnc_combo_cell_new(void)117 gnc_combo_cell_new (void)
118 {
119     ComboCell* cell;
120 
121     g_once (&auto_pop_init_once, gnc_combo_cell_autopop_init, NULL);
122 
123     cell = g_new0 (ComboCell, 1);
124 
125     gnc_combo_cell_init (cell);
126 
127     return &cell->cell;
128 }
129 
130 void
gnc_combo_cell_init(ComboCell * cell)131 gnc_combo_cell_init (ComboCell* cell)
132 {
133     PopBox* box;
134 
135     gnc_basic_cell_init (& (cell->cell));
136 
137     cell->cell.is_popup = TRUE;
138 
139     cell->cell.destroy = gnc_combo_cell_destroy;
140 
141     cell->cell.gui_realize = gnc_combo_cell_gui_realize;
142     cell->cell.gui_destroy = gnc_combo_cell_gui_destroy;
143 
144     box = g_new0 (PopBox, 1);
145 
146     box->sheet = NULL;
147     box->item_edit = NULL;
148     box->item_list = NULL;
149     box->tmp_store = gtk_list_store_new (1, G_TYPE_STRING);
150     box->signals_connected = FALSE;
151     box->list_popped = FALSE;
152     box->autosize = FALSE;
153 
154     cell->cell.gui_private = box;
155 
156     box->qf = gnc_quickfill_new();
157     box->use_quickfill_cache = FALSE;
158 
159     box->in_list_select = FALSE;
160 
161     box->strict = TRUE;
162 
163     box->complete_char = '\0';
164 
165     box->ignore_strings = NULL;
166 }
167 
168 static void
select_item_cb(GncItemList * item_list,char * item_string,gpointer data)169 select_item_cb (GncItemList* item_list, char* item_string, gpointer data)
170 {
171     ComboCell* cell = data;
172     PopBox* box = cell->cell.gui_private;
173 
174     box->in_list_select = TRUE;
175     gnucash_sheet_modify_current_cell (box->sheet, item_string);
176     box->in_list_select = FALSE;
177 
178     gnc_item_edit_hide_popup (box->item_edit);
179     box->list_popped = FALSE;
180 }
181 
182 static void
change_item_cb(GncItemList * item_list,char * item_string,gpointer data)183 change_item_cb (GncItemList* item_list, char* item_string, gpointer data)
184 {
185     ComboCell* cell = data;
186     PopBox* box = cell->cell.gui_private;
187 
188     box->in_list_select = TRUE;
189     gnucash_sheet_modify_current_cell (box->sheet, item_string);
190     box->in_list_select = FALSE;
191 }
192 
193 static void
activate_item_cb(GncItemList * item_list,char * item_string,gpointer data)194 activate_item_cb (GncItemList* item_list, char* item_string, gpointer data)
195 {
196     ComboCell* cell = data;
197     PopBox* box = cell->cell.gui_private;
198 
199     gnc_item_edit_hide_popup (box->item_edit);
200     box->list_popped = FALSE;
201 }
202 
203 static void
key_press_item_cb(GncItemList * item_list,GdkEventKey * event,gpointer data)204 key_press_item_cb (GncItemList* item_list, GdkEventKey* event, gpointer data)
205 {
206     ComboCell* cell = data;
207     PopBox* box = cell->cell.gui_private;
208 
209     switch (event->keyval)
210     {
211     case GDK_KEY_Escape:
212         gnc_item_edit_hide_popup (box->item_edit);
213         box->list_popped = FALSE;
214         break;
215 
216     default:
217         gtk_widget_event (GTK_WIDGET (box->sheet),
218                           (GdkEvent*) event);
219         break;
220     }
221 }
222 
223 static void
combo_disconnect_signals(ComboCell * cell)224 combo_disconnect_signals (ComboCell* cell)
225 {
226     PopBox* box = cell->cell.gui_private;
227 
228     if (!box->signals_connected)
229         return;
230 
231     g_signal_handlers_disconnect_matched (G_OBJECT (box->item_list),
232                                           G_SIGNAL_MATCH_DATA,
233                                           0, 0, NULL, NULL, cell);
234 
235     box->signals_connected = FALSE;
236 }
237 
238 static void
combo_connect_signals(ComboCell * cell)239 combo_connect_signals (ComboCell* cell)
240 {
241     PopBox* box = cell->cell.gui_private;
242 
243     if (box->signals_connected)
244         return;
245 
246     g_signal_connect (G_OBJECT (box->item_list), "select_item",
247                       G_CALLBACK (select_item_cb), cell);
248 
249     g_signal_connect (G_OBJECT (box->item_list), "change_item",
250                       G_CALLBACK (change_item_cb), cell);
251 
252     g_signal_connect (G_OBJECT (box->item_list), "activate_item",
253                       G_CALLBACK (activate_item_cb), cell);
254 
255     g_signal_connect (G_OBJECT (box->item_list), "key_press_event",
256                       G_CALLBACK (key_press_item_cb), cell);
257 
258     box->signals_connected = TRUE;
259 }
260 
261 static void
block_list_signals(ComboCell * cell)262 block_list_signals (ComboCell* cell)
263 {
264     PopBox* box = cell->cell.gui_private;
265 
266     if (!box->signals_connected)
267         return;
268 
269     g_signal_handlers_block_matched (G_OBJECT (box->item_list),
270                                      G_SIGNAL_MATCH_DATA,
271                                      0, 0, NULL, NULL, cell);
272 }
273 
274 static void
unblock_list_signals(ComboCell * cell)275 unblock_list_signals (ComboCell* cell)
276 {
277     PopBox* box = cell->cell.gui_private;
278 
279     if (!box->signals_connected)
280         return;
281 
282     g_signal_handlers_unblock_matched (G_OBJECT (box->item_list),
283                                        G_SIGNAL_MATCH_DATA,
284                                        0, 0, NULL, NULL, cell);
285 }
286 
287 static void
gnc_combo_cell_gui_destroy(BasicCell * bcell)288 gnc_combo_cell_gui_destroy (BasicCell* bcell)
289 {
290     PopBox* box = bcell->gui_private;
291     ComboCell* cell = (ComboCell*) bcell;
292 
293     if (cell->cell.gui_realize == NULL)
294     {
295         if (box != NULL && box->item_list != NULL)
296         {
297             combo_disconnect_signals (cell);
298             g_object_unref (box->item_list);
299             box->item_list = NULL;
300         }
301 
302         if (box && box->tmp_store)
303         {
304             g_object_unref (box->tmp_store);
305             box->tmp_store = NULL;
306         }
307 
308         /* allow the widget to be shown again */
309         cell->cell.gui_realize = gnc_combo_cell_gui_realize;
310         cell->cell.gui_move = NULL;
311         cell->cell.enter_cell = NULL;
312         cell->cell.leave_cell = NULL;
313         cell->cell.gui_destroy = NULL;
314     }
315 }
316 
317 static void
gnc_combo_cell_destroy(BasicCell * bcell)318 gnc_combo_cell_destroy (BasicCell* bcell)
319 {
320     ComboCell* cell = (ComboCell*) bcell;
321     PopBox* box = cell->cell.gui_private;
322 
323     gnc_combo_cell_gui_destroy (& (cell->cell));
324 
325     if (box != NULL)
326     {
327         GList* node;
328 
329         /* Don't destroy the qf if its not ours to destroy */
330         if (FALSE == box->use_quickfill_cache)
331         {
332             gnc_quickfill_destroy (box->qf);
333             box->qf = NULL;
334         }
335 
336         g_list_free_full (box->ignore_strings, g_free);
337         box->ignore_strings = NULL;
338 
339         g_free (box);
340         cell->cell.gui_private = NULL;
341     }
342 
343     cell->cell.gui_private = NULL;
344     cell->cell.gui_realize = NULL;
345 }
346 
347 void
gnc_combo_cell_set_sort_enabled(ComboCell * cell,gboolean enabled)348 gnc_combo_cell_set_sort_enabled (ComboCell* cell, gboolean enabled)
349 {
350     PopBox* box;
351 
352     if (cell == NULL)
353         return;
354 
355     box = cell->cell.gui_private;
356     if (box->item_list == NULL)
357         return;
358 
359     block_list_signals (cell);
360     gnc_item_list_set_sort_enabled (box->item_list, enabled);
361     unblock_list_signals (cell);
362 }
363 
364 void
gnc_combo_cell_clear_menu(ComboCell * cell)365 gnc_combo_cell_clear_menu (ComboCell* cell)
366 {
367     PopBox* box;
368 
369     if (cell == NULL)
370         return;
371 
372     box = cell->cell.gui_private;
373     if (box == NULL)
374         return;
375 
376     /* Don't destroy the qf if its not ours to destroy */
377     if (FALSE == box->use_quickfill_cache)
378     {
379         gnc_quickfill_destroy (box->qf);
380         box->qf = gnc_quickfill_new();
381     }
382 
383     if (box->item_list != NULL)
384     {
385         block_list_signals (cell);
386 
387         gnc_item_list_clear (box->item_list);
388         gnc_item_edit_hide_popup (box->item_edit);
389         box->list_popped = FALSE;
390         unblock_list_signals (cell);
391     }
392     else
393         gtk_list_store_clear (box->tmp_store);
394 }
395 
396 void
gnc_combo_cell_use_quickfill_cache(ComboCell * cell,QuickFill * shared_qf)397 gnc_combo_cell_use_quickfill_cache (ComboCell* cell, QuickFill* shared_qf)
398 {
399     PopBox* box;
400 
401     if (cell == NULL) return;
402 
403     box = cell->cell.gui_private;
404     if (NULL == box) return;
405 
406     if (FALSE == box->use_quickfill_cache)
407     {
408         box->use_quickfill_cache = TRUE;
409         gnc_quickfill_destroy (box->qf);
410     }
411     box->qf = shared_qf;
412 }
413 
414 void
gnc_combo_cell_use_list_store_cache(ComboCell * cell,gpointer data)415 gnc_combo_cell_use_list_store_cache (ComboCell* cell, gpointer data)
416 {
417     if (cell == NULL) return;
418 
419     cell->shared_store = data;
420 }
421 
422 void
gnc_combo_cell_add_menu_item(ComboCell * cell,const char * menustr)423 gnc_combo_cell_add_menu_item (ComboCell* cell, const char* menustr)
424 {
425     PopBox* box;
426 
427     if (cell == NULL)
428         return;
429     if (menustr == NULL)
430         return;
431 
432     box = cell->cell.gui_private;
433 
434     if (box->item_list != NULL)
435     {
436         block_list_signals (cell);
437 
438         gnc_item_list_append (box->item_list, menustr);
439         if (cell->cell.value &&
440             (strcmp (menustr, cell->cell.value) == 0))
441             gnc_item_list_select (box->item_list, menustr);
442 
443         unblock_list_signals (cell);
444     }
445     else
446     {
447         GtkTreeIter iter;
448 
449         gtk_list_store_append (box->tmp_store, &iter);
450         gtk_list_store_set (box->tmp_store, &iter, 0, menustr, -1);
451     }
452 
453     /* If we're going to be using a pre-fab quickfill,
454      * then don't fill it in here */
455     if (FALSE == box->use_quickfill_cache)
456     {
457         gnc_quickfill_insert (box->qf, menustr, QUICKFILL_ALPHA);
458     }
459 }
460 
461 void
gnc_combo_cell_add_account_menu_item(ComboCell * cell,char * menustr)462 gnc_combo_cell_add_account_menu_item (ComboCell* cell, char* menustr)
463 {
464     PopBox* box;
465 
466     if (cell == NULL)
467         return;
468     if (menustr == NULL)
469         return;
470 
471     box = cell->cell.gui_private;
472 
473     if (box->item_list != NULL)
474     {
475         block_list_signals (cell);
476 
477         gnc_item_list_append (box->item_list, menustr);
478         if (cell->cell.value)
479         {
480             gchar* menu_copy = g_strdup (menustr);
481             gchar* value_copy = g_strdup (cell->cell.value);
482             g_strdelimit (menu_copy, "-:/\\.", ' ');
483             g_strdelimit (value_copy, "-:/\\.", ' ');
484             if (strcmp (menu_copy, value_copy) == 0)
485             {
486                 gnc_combo_cell_set_value (cell, menustr);
487                 gnc_item_list_select (box->item_list, menustr);
488             }
489             g_free (value_copy);
490             g_free (menu_copy);
491         }
492         unblock_list_signals (cell);
493     }
494 
495     /* If we're going to be using a pre-fab quickfill,
496      * then don't fill it in here */
497     if (FALSE == box->use_quickfill_cache)
498     {
499         gnc_quickfill_insert (box->qf, menustr, QUICKFILL_ALPHA);
500     }
501 }
502 
503 void
gnc_combo_cell_set_value(ComboCell * cell,const char * str)504 gnc_combo_cell_set_value (ComboCell* cell, const char* str)
505 {
506     gnc_basic_cell_set_value (&cell->cell, str);
507 }
508 
509 static inline void
list_store_append(GtkListStore * store,char * string)510 list_store_append (GtkListStore *store, char* string)
511 {
512     GtkTreeIter iter;
513 
514     g_return_if_fail (store != NULL);
515     g_return_if_fail (string != NULL);
516     gtk_list_store_append (store, &iter);
517     gtk_list_store_set (store, &iter, 0, string, -1);
518 }
519 
520 /* This function looks through full_store for a partial match with newval and
521  * returns the first match (which must be subsequently freed). It fills out
522  * box->item_list with found matches.
523  */
524 static gchar*
gnc_combo_cell_type_ahead_search(const gchar * newval,GtkListStore * full_store,ComboCell * cell)525 gnc_combo_cell_type_ahead_search (const gchar* newval,
526                                   GtkListStore* full_store, ComboCell *cell)
527 {
528     GtkTreeIter iter;
529     PopBox* box = cell->cell.gui_private;
530     int num_found = 0;
531     gchar* match_str = NULL;
532     const char* sep = gnc_get_account_separator_string ();
533     char* escaped_sep = g_regex_escape_string (sep, -1);
534     char* escaped_newval = g_regex_escape_string (newval, -1);
535     gchar* newval_rep = g_strdup_printf (".*%s.*", escaped_sep);
536     GRegex* regex0 = g_regex_new (escaped_sep, 0, 0, NULL);
537     char* rep_str = g_regex_replace_literal (regex0, escaped_newval, -1, 0,
538                                              newval_rep, 0, NULL);
539     char* normal_rep_str = g_utf8_normalize (rep_str, -1, G_NORMALIZE_ALL);
540     GRegex *regex = g_regex_new (normal_rep_str, G_REGEX_CASELESS, 0, NULL);
541 
542     gboolean valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (full_store),
543                                                     &iter);
544 
545     /* Limit the number found to keep the combo box from getting unreasonably
546      * large.
547      */
548     static const gint MAX_NUM_MATCHES = 30;
549 
550     g_free (normal_rep_str);
551     g_free (rep_str);
552     g_free (newval_rep);
553     g_free (escaped_sep);
554     g_free (escaped_newval);
555     g_regex_unref (regex0);
556 
557     block_list_signals (cell); //Prevent recursion from gtk_tree_view signals.
558     gnc_item_edit_hide_popup (box->item_edit);
559     gtk_list_store_clear (box->tmp_store);
560     unblock_list_signals (cell);
561 
562     while (valid && num_found < MAX_NUM_MATCHES)
563     {
564         gchar* str_data = NULL;
565         gchar* normalized_str_data = NULL;
566         gtk_tree_model_get (GTK_TREE_MODEL (full_store), &iter, 0,
567                             &str_data, -1);
568         normalized_str_data = g_utf8_normalize (str_data, -1, G_NORMALIZE_ALL);
569 
570         if (g_regex_match (regex, normalized_str_data, 0, NULL))
571         {
572             if (!num_found)
573                 match_str = g_strdup (str_data);
574             ++num_found;
575             list_store_append (box->tmp_store, str_data);
576         }
577         g_free (str_data);
578         g_free (normalized_str_data);
579         valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (full_store), &iter);
580     }
581 
582     if (num_found)
583     {
584         gnc_item_list_set_temp_store (box->item_list, box->tmp_store);
585         gnc_item_edit_show_popup (box->item_edit);
586         box->list_popped = TRUE;
587     }
588     g_regex_unref (regex);
589     return match_str;
590 }
591 
592 static char*
quickfill_match(QuickFill * qf,const char * string)593 quickfill_match (QuickFill *qf, const char *string)
594 {
595     QuickFill *match = gnc_quickfill_get_string_match (qf, string);
596     return g_strdup (gnc_quickfill_string (match));
597 }
598 
599 
600 static void
gnc_combo_cell_modify_verify(BasicCell * _cell,const char * change,int change_len,const char * newval,int newval_len,int * cursor_position,int * start_selection,int * end_selection)601 gnc_combo_cell_modify_verify (BasicCell* _cell,
602                               const char* change,
603                               int change_len,
604                               const char* newval,
605                               int newval_len,
606                               int* cursor_position,
607                               int* start_selection,
608                               int* end_selection)
609 {
610     ComboCell* cell = (ComboCell*) _cell;
611     PopBox* box = cell->cell.gui_private;
612     gchar* match_str = NULL;
613     glong newval_chars;
614     glong change_chars;
615     const gchar* box_str = NULL;
616 
617     newval_chars = g_utf8_strlen (newval, newval_len);
618     change_chars = g_utf8_strlen (change, change_len);
619 
620     if (box->in_list_select)
621     {
622         gnc_basic_cell_set_value_internal (_cell, newval);
623         *cursor_position = -1;
624         *start_selection = 0;
625         *end_selection = -1;
626         return;
627     }
628 
629     /* If item_list is using temp then we're already partly matched by
630      * type-ahead and a quickfill_match won't work.
631      */
632     if (!gnc_item_list_using_temp (box->item_list))
633     {
634         // If we were deleting or inserting in the middle, just accept.
635         if (change == NULL || *cursor_position < _cell->value_chars)
636         {
637             gnc_basic_cell_set_value_internal (_cell, newval);
638             *start_selection = *end_selection = *cursor_position;
639             return;
640         }
641         match_str = quickfill_match (box->qf, newval);
642 
643         if (match_str != NULL) // Do we have a quickfill match
644         {
645             *start_selection = newval_chars;
646             *end_selection = -1;
647             *cursor_position += change_chars;
648             box_str = match_str;
649 
650             block_list_signals (cell); // Prevent recursion
651             gnc_item_list_select (box->item_list, match_str);
652             unblock_list_signals (cell);
653         }
654     }
655 
656     // Try using type-ahead
657     if (match_str == NULL && cell->shared_store)
658     {
659         // No start-of-name match, try type-ahead search, we match any substring of the full account name.
660         GtkListStore *store = cell->shared_store;
661         match_str = gnc_combo_cell_type_ahead_search (newval, store, cell);
662         *start_selection = newval_chars;
663         *end_selection = -1;
664         *cursor_position = newval_chars;
665 
666         // Do not change the string in the type-in box.
667         box_str = newval;
668     }
669 
670     // No type-ahead / quickfill entry found
671     if (match_str == NULL)
672     {
673         block_list_signals (cell); // Prevent recursion
674         if (cell->shared_store && gnc_item_list_using_temp (box->item_list))
675         {
676             gnc_item_list_set_temp_store (box->item_list, NULL);
677             gtk_list_store_clear (box->tmp_store);
678         }
679         gnc_item_list_select (box->item_list, NULL);
680         unblock_list_signals (cell);
681         gnc_basic_cell_set_value_internal (_cell, newval);
682         *cursor_position = *start_selection = newval_chars;
683         *end_selection = -1;
684         return;
685     }
686 
687     if (!box->list_popped && auto_pop_combos)
688     {
689         gnc_item_edit_show_popup (box->item_edit);
690         box->list_popped = TRUE;
691     }
692 
693     gnc_basic_cell_set_value_internal (_cell, box_str);
694     g_free (match_str);
695 }
696 
697 static gboolean
gnc_combo_cell_direct_update(BasicCell * bcell,int * cursor_position,int * start_selection,int * end_selection,void * gui_data)698 gnc_combo_cell_direct_update (BasicCell* bcell,
699                               int* cursor_position,
700                               int* start_selection,
701                               int* end_selection,
702                               void* gui_data)
703 {
704     ComboCell* cell = (ComboCell*) bcell;
705     PopBox* box = cell->cell.gui_private;
706     GdkEventKey* event = gui_data;
707     gboolean keep_on_going = FALSE;
708     gboolean extra_colon;
709     gunichar unicode_value;
710     QuickFill* match;
711     const char* match_str;
712     int prefix_len;
713     int find_pos;
714     int new_pos;
715 
716     if (event->type != GDK_KEY_PRESS)
717         return FALSE;
718 
719     unicode_value = gdk_keyval_to_unicode (event->keyval);
720     switch (event->keyval)
721     {
722     case GDK_KEY_slash:
723         if (! (event->state & GDK_MOD1_MASK))
724         {
725             if (unicode_value == box->complete_char)
726                 break;
727 
728             return FALSE;
729         }
730         keep_on_going = TRUE;
731     /* fall through */
732     case GDK_KEY_Tab:
733     case GDK_KEY_ISO_Left_Tab:
734         if (gnc_item_list_using_temp (box->item_list))
735         {
736             char* string = gnc_item_list_get_selection (box->item_list);
737             g_signal_emit_by_name (G_OBJECT (box->item_list), "change_item",
738                                    string, (gpointer)bcell);
739             g_free (string);
740             return FALSE;
741         }
742         if (! (event->state & GDK_CONTROL_MASK) &&
743             !keep_on_going)
744             return FALSE;
745 
746         match = gnc_quickfill_get_string_len_match
747                 (box->qf, bcell->value, *cursor_position);
748         if (match == NULL)
749             return TRUE;
750 
751         match = gnc_quickfill_get_unique_len_match
752                 (match, &prefix_len);
753         if (match == NULL)
754             return TRUE;
755 
756         match_str = gnc_quickfill_string (match);
757 
758         if ((match_str != NULL) &&
759             (strncmp (match_str, bcell->value,
760                       strlen (bcell->value)) == 0) &&
761             (strcmp (match_str, bcell->value) != 0))
762         {
763             gnc_basic_cell_set_value_internal (bcell,
764                                                match_str);
765 
766             block_list_signals (cell);
767             gnc_item_list_select (box->item_list,
768                                   match_str);
769             unblock_list_signals (cell);
770         }
771 
772         *cursor_position += prefix_len;
773         *start_selection = *cursor_position;
774         *end_selection = -1;
775         return TRUE;
776     }
777 
778     if (box->complete_char == 0)
779         return FALSE;
780 
781     if (unicode_value != box->complete_char)
782         return FALSE;
783 
784     if (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK))
785         return FALSE;
786 
787     if ((*cursor_position < bcell->value_chars) &&
788         ((*end_selection < bcell->value_chars) ||
789          (*cursor_position < *start_selection)))
790         return FALSE;
791 
792     if ((*cursor_position == bcell->value_chars) &&
793         (*start_selection != *end_selection) &&
794         (*end_selection < bcell->value_chars))
795         return FALSE;
796 
797     find_pos = -1;
798     if (*start_selection < bcell->value_chars)
799     {
800         int i = *start_selection;
801         const char* c;
802         gunichar uc;
803 
804         c = g_utf8_offset_to_pointer (bcell->value, i);
805         while (*c)
806         {
807             uc = g_utf8_get_char (c);
808             if (uc == box->complete_char)
809             {
810                 find_pos = (i + 1);
811                 break;
812             }
813             c = g_utf8_next_char (c);
814             i++;
815         }
816     }
817 
818     if (find_pos >= 0)
819     {
820         new_pos = find_pos;
821         extra_colon = FALSE;
822     }
823     else
824     {
825         new_pos = bcell->value_chars;
826         extra_colon = TRUE;
827     }
828 
829     match = gnc_quickfill_get_string_len_match (box->qf,
830                                                 bcell->value, new_pos);
831     if (match == NULL)
832         return FALSE;
833 
834     if (extra_colon)
835     {
836         match = gnc_quickfill_get_char_match (match,
837                                               box->complete_char);
838         if (match == NULL)
839             return FALSE;
840 
841         new_pos++;
842     }
843 
844     match_str = gnc_quickfill_string (match);
845 
846     if ((match_str != NULL) &&
847         (strncmp (match_str, bcell->value, strlen (bcell->value)) == 0) &&
848         (strcmp (match_str, bcell->value) != 0))
849     {
850         gnc_basic_cell_set_value_internal (bcell, match_str);
851 
852         block_list_signals (cell);
853         gnc_item_list_select (box->item_list, match_str);
854         unblock_list_signals (cell);
855     }
856 
857     *cursor_position = new_pos;
858     *start_selection = new_pos;
859     *end_selection = -1;
860 
861     return TRUE;
862 }
863 
864 static void
gnc_combo_cell_gui_realize(BasicCell * bcell,gpointer data)865 gnc_combo_cell_gui_realize (BasicCell* bcell, gpointer data)
866 {
867     GnucashSheet* sheet = data;
868     GncItemEdit* item_edit = gnucash_sheet_get_item_edit (sheet);
869     ComboCell* cell = (ComboCell*) bcell;
870     PopBox* box = cell->cell.gui_private;
871 
872     /* initialize gui-specific, private data */
873     box->sheet = sheet;
874     box->item_edit = item_edit;
875     if (cell->shared_store)
876         box->item_list = GNC_ITEM_LIST (gnc_item_list_new (cell->shared_store));
877     else
878         box->item_list = GNC_ITEM_LIST (gnc_item_list_new (box->tmp_store));
879     gtk_widget_show_all (GTK_WIDGET (box->item_list));
880     g_object_ref_sink (box->item_list);
881 
882     /* to mark cell as realized, remove the realize method */
883     cell->cell.gui_realize = NULL;
884     cell->cell.gui_move = gnc_combo_cell_gui_move;
885     cell->cell.enter_cell = gnc_combo_cell_enter;
886     cell->cell.leave_cell = gnc_combo_cell_leave;
887     cell->cell.gui_destroy = gnc_combo_cell_gui_destroy;
888     cell->cell.modify_verify = gnc_combo_cell_modify_verify;
889     cell->cell.direct_update = gnc_combo_cell_direct_update;
890 }
891 
892 static void
gnc_combo_cell_gui_move(BasicCell * bcell)893 gnc_combo_cell_gui_move (BasicCell* bcell)
894 {
895     PopBox* box = bcell->gui_private;
896 
897     combo_disconnect_signals ((ComboCell*) bcell);
898 
899     gnc_item_edit_set_popup (box->item_edit, NULL, NULL,
900                              NULL, NULL, NULL, NULL, NULL);
901 
902     box->list_popped = FALSE;
903 }
904 
905 static int
popup_get_height(G_GNUC_UNUSED GtkWidget * widget,int space_available,int row_height,gpointer user_data)906 popup_get_height (G_GNUC_UNUSED GtkWidget* widget,
907                   int space_available,
908                   int row_height,
909                   gpointer user_data)
910 {
911     PopBox* box = user_data;
912     GtkScrolledWindow* scrollwin = GNC_ITEM_LIST(widget)->scrollwin;
913     GtkWidget *hsbar = gtk_scrolled_window_get_hscrollbar (scrollwin);
914     GtkStyleContext *context = gtk_widget_get_style_context (hsbar);
915     /* Note: gtk_scrolled_window_get_overlay_scrolling (scrollwin) always returns
916        TRUE so look for style class "overlay-indicator" on the scrollbar. */
917     gboolean overlay = gtk_style_context_has_class (context, "overlay-indicator");
918     int count, height;
919 
920     count = gnc_item_list_num_entries (box->item_list);
921     height = count * (gnc_item_list_get_cell_height (box->item_list) + 2);
922 
923     if (!overlay)
924     {
925         gint minh, nath;
926         gtk_widget_get_preferred_height (hsbar, &minh, &nath);
927         height = height + minh;
928     }
929 
930     if (height < space_available)
931     {
932         gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrollwin),
933                                         GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER);
934         // if the list is empty height would be 0 so return 1 instead to
935         // satisfy the check_popup_height_is_true function
936         return height ? height : 1;
937     }
938     gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrollwin),
939                                     GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
940     return space_available;
941 }
942 
943 static int
popup_autosize(GtkWidget * widget,int max_width,gpointer user_data)944 popup_autosize (GtkWidget* widget,
945                 int max_width,
946                 gpointer user_data)
947 {
948     PopBox* box = user_data;
949 
950     if (!box || !box->autosize)
951         return max_width;
952 
953     return gnc_item_list_autosize (GNC_ITEM_LIST (widget)) + 20;
954 }
955 
956 static void
popup_set_focus(GtkWidget * widget,G_GNUC_UNUSED gpointer user_data)957 popup_set_focus (GtkWidget* widget,
958                  G_GNUC_UNUSED gpointer user_data)
959 {
960     /* An empty GtkTreeView grabbing focus causes the key_press events to be
961      * lost because there's no entry cell to handle them.
962      */
963     if (gnc_item_list_num_entries (GNC_ITEM_LIST (widget)))
964         gtk_widget_grab_focus (GTK_WIDGET (GNC_ITEM_LIST (widget)->tree_view));
965 }
966 
967 static void
popup_post_show(GtkWidget * widget,G_GNUC_UNUSED gpointer user_data)968 popup_post_show (GtkWidget* widget,
969                  G_GNUC_UNUSED gpointer user_data)
970 {
971     gnc_item_list_autosize (GNC_ITEM_LIST (widget));
972     gnc_item_list_show_selected (GNC_ITEM_LIST (widget));
973 }
974 
975 static int
popup_get_width(GtkWidget * widget,G_GNUC_UNUSED gpointer user_data)976 popup_get_width (GtkWidget* widget,
977                  G_GNUC_UNUSED gpointer user_data)
978 {
979     GtkAllocation alloc;
980     gtk_widget_get_allocation (GTK_WIDGET (GNC_ITEM_LIST (widget)->tree_view),
981                                &alloc);
982     return alloc.width;
983 }
984 
985 static gboolean
gnc_combo_cell_enter(BasicCell * bcell,int * cursor_position,int * start_selection,int * end_selection)986 gnc_combo_cell_enter (BasicCell* bcell,
987                       int* cursor_position,
988                       int* start_selection,
989                       int* end_selection)
990 {
991     ComboCell* cell = (ComboCell*) bcell;
992     PopBox* box = bcell->gui_private;
993     PopupToggle popup_toggle;
994     GList* find = NULL;
995 
996     if (bcell->value)
997         find = g_list_find_custom (box->ignore_strings,
998                                    bcell->value,
999                                    (GCompareFunc) strcmp);
1000     if (find)
1001         return FALSE;
1002 
1003     gnc_item_edit_set_popup (box->item_edit,
1004                              GTK_WIDGET (box->item_list),
1005                              popup_get_height, popup_autosize,
1006                              popup_set_focus, popup_post_show,
1007                              popup_get_width, box);
1008 
1009     block_list_signals (cell);
1010 
1011     if (cell->shared_store && gnc_item_list_using_temp (box->item_list))
1012     {
1013         // Clear the temp store to ensure we don't start in type-ahead mode.
1014         gnc_item_list_set_temp_store (box->item_list, NULL);
1015         gtk_list_store_clear (box->tmp_store);
1016     }
1017     gnc_item_list_select (box->item_list, bcell->value);
1018     unblock_list_signals (cell);
1019 
1020     popup_toggle = box->item_edit->popup_toggle;
1021 
1022     // if the list is empty disable the toggle button
1023     gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton),
1024                               gnc_item_list_num_entries (box->item_list));
1025 
1026     combo_connect_signals (cell);
1027 
1028     *cursor_position = -1;
1029     *start_selection = 0;
1030     *end_selection = -1;
1031 
1032     return TRUE;
1033 }
1034 
1035 static void
gnc_combo_cell_leave(BasicCell * bcell)1036 gnc_combo_cell_leave (BasicCell* bcell)
1037 {
1038     PopBox* box = bcell->gui_private;
1039 
1040     combo_disconnect_signals ((ComboCell*) bcell);
1041 
1042     gnc_item_edit_set_popup (box->item_edit, NULL, NULL,
1043                              NULL, NULL, NULL, NULL, NULL);
1044 
1045     box->list_popped = FALSE;
1046 
1047     if (box->strict)
1048     {
1049         if (bcell->value)
1050         {
1051             if (gnc_item_in_list (box->item_list, bcell->value))
1052                 return;
1053 
1054             if (g_list_find_custom (box->ignore_strings,
1055                                     bcell->value,
1056                                     (GCompareFunc) strcmp))
1057                 return;
1058         }
1059         gnc_basic_cell_set_value_internal (bcell, "");
1060     }
1061 }
1062 
1063 void
gnc_combo_cell_set_strict(ComboCell * cell,gboolean strict)1064 gnc_combo_cell_set_strict (ComboCell* cell, gboolean strict)
1065 {
1066     PopBox* box;
1067 
1068     if (cell == NULL)
1069         return;
1070 
1071     box = cell->cell.gui_private;
1072 
1073     box->strict = strict;
1074 }
1075 
1076 void
gnc_combo_cell_set_complete_char(ComboCell * cell,gunichar complete_char)1077 gnc_combo_cell_set_complete_char (ComboCell* cell, gunichar complete_char)
1078 {
1079     PopBox* box;
1080 
1081     if (cell == NULL)
1082         return;
1083 
1084     box = cell->cell.gui_private;
1085 
1086     box->complete_char = complete_char;
1087 }
1088 
1089 void
gnc_combo_cell_add_ignore_string(ComboCell * cell,const char * ignore_string)1090 gnc_combo_cell_add_ignore_string (ComboCell* cell,
1091                                   const char* ignore_string)
1092 {
1093     PopBox* box;
1094 
1095     if (cell == NULL)
1096         return;
1097 
1098     if (!ignore_string)
1099         return;
1100 
1101     box = cell->cell.gui_private;
1102 
1103     box->ignore_strings = g_list_prepend (box->ignore_strings,
1104                                           g_strdup (ignore_string));
1105 }
1106 
1107 void
gnc_combo_cell_set_autosize(ComboCell * cell,gboolean autosize)1108 gnc_combo_cell_set_autosize (ComboCell* cell, gboolean autosize)
1109 {
1110     PopBox* box;
1111 
1112     if (!cell)
1113         return;
1114 
1115     box = cell->cell.gui_private;
1116     if (!box)
1117         return;
1118 
1119     box->autosize = autosize;
1120 }
1121 
1122