diff --git a/gnucash/register/ledger-core/split-register-layout.c b/gnucash/register/ledger-core/split-register-layout.c index 93a6abcd25..bb6c3f76e9 100644 --- a/gnucash/register/ledger-core/split-register-layout.c +++ b/gnucash/register/ledger-core/split-register-layout.c @@ -677,7 +677,7 @@ gnc_split_register_layout_add_cells (SplitRegister* reg, gnc_register_add_cell (layout, DESC_CELL, - COMBO_CELL_TYPE_NAME, + COMPLETION_CELL_TYPE_NAME, C_ ("sample", "Description of a transaction"), CELL_ALIGN_LEFT, TRUE, diff --git a/gnucash/register/ledger-core/split-register-load.c b/gnucash/register/ledger-core/split-register-load.c index 8874ddacba..25a51facfe 100644 --- a/gnucash/register/ledger-core/split-register-load.c +++ b/gnucash/register/ledger-core/split-register-load.c @@ -32,6 +32,7 @@ #include "Transaction.h" #include "account-quickfill.h" #include "combocell.h" +#include "completioncell.h" #include "gnc-component-manager.h" #include "qof.h" #include "gnc-ui-util.h" @@ -54,7 +55,6 @@ static QofLogModule log_module = GNC_MOD_LEDGER; static void gnc_split_register_load_xfer_cells (SplitRegister* reg, Account* base_account); -static void gnc_split_register_load_desc_cells (SplitRegister* reg); static void gnc_split_register_load_recn_cells (SplitRegister* reg) { @@ -113,6 +113,21 @@ gnc_split_register_load_type_cells (SplitRegister* reg) gnc_recn_cell_set_read_only (cell, TRUE); } +static void +gnc_split_register_load_desc_cells (SplitRegister* reg) +{ + CompletionCell *cell; + + if (!reg) return; + + cell = (CompletionCell *) + gnc_table_layout_get_cell (reg->table->layout, DESC_CELL); + + if (!cell) return; + + gnc_completion_cell_set_sort_enabled (cell, TRUE); +} + /** Add a transaction to the register. * * Virtual cells are set up to hold the data, beginning at @a vcell_loc and @@ -609,6 +624,13 @@ gnc_split_register_load (SplitRegister* reg, GList* slist, added_blank_trans = TRUE; } + gnc_completion_cell_clear_menu ( + (CompletionCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL)); + + gnc_completion_cell_reverse_sort ( + (CompletionCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), + table->model->reverse_sort); + /* populate the table */ for (node = slist; node; node = node->next) { @@ -729,9 +751,10 @@ gnc_split_register_load (SplitRegister* reg, GList* slist, if (info->first_pass) add_quickfill_completions (reg->table->layout, trans, split, has_last_num); - gnc_combo_cell_add_menu_item_unique ( - (ComboCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), - xaccTransGetDescription (trans)); + gnc_completion_cell_add_menu_item ( + (CompletionCell*) gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), + xaccTransGetDescription (trans)); + if (trans == find_trans) new_trans_row = vcell_loc.virt_row; @@ -936,17 +959,4 @@ gnc_split_register_load_xfer_cells (SplitRegister* reg, Account* base_account) gnc_combo_cell_use_list_store_cache (cell, store); } -static void -gnc_split_register_load_desc_cells (SplitRegister* reg) -{ - ComboCell* cell; - GtkListStore* store = gtk_list_store_new (1, G_TYPE_STRING); - - cell = (ComboCell*) - gnc_table_layout_get_cell (reg->table->layout, DESC_CELL); - - gnc_combo_cell_use_type_ahead_only (cell); - - gnc_combo_cell_use_list_store_cache (cell, store); -} /* ====================== END OF FILE ================================== */ diff --git a/gnucash/register/ledger-core/split-register.c b/gnucash/register/ledger-core/split-register.c index 3d017d4ca4..e26671098d 100644 --- a/gnucash/register/ledger-core/split-register.c +++ b/gnucash/register/ledger-core/split-register.c @@ -29,6 +29,7 @@ #include #include "combocell.h" +#include "completioncell.h" #include "datecell.h" #include "dialog-utils.h" #include "gnc-component-manager.h" @@ -2665,10 +2666,10 @@ gnc_split_register_config_cells (SplitRegister* reg) ((ComboCell*) gnc_table_layout_get_cell (reg->table->layout, ACTN_CELL), TRUE); - /* the description cell */ - gnc_combo_cell_set_autosize - ((ComboCell*) - gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), TRUE); + /* the description cell */ + gnc_completion_cell_set_autosize + ((CompletionCell*) + gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), TRUE); /* Use GNC_COMMODITY_MAX_FRACTION for prices and "exchange rates" */ gnc_price_cell_set_fraction @@ -2697,10 +2698,10 @@ gnc_split_register_config_cells (SplitRegister* reg) ((ComboCell*) gnc_table_layout_get_cell (reg->table->layout, ACTN_CELL), FALSE); - /* The description cell should accept strings not in the list */ - gnc_combo_cell_set_strict - ((ComboCell*) - gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), FALSE); + /* The description cell should accept strings not in the list */ + gnc_completion_cell_set_strict + ((CompletionCell*) + gnc_table_layout_get_cell (reg->table->layout, DESC_CELL), FALSE); /* number format for share quantities in stock ledgers */ switch (reg->type) diff --git a/gnucash/register/register-core/CMakeLists.txt b/gnucash/register/register-core/CMakeLists.txt index 7ec7f63450..8c1afeee44 100644 --- a/gnucash/register/register-core/CMakeLists.txt +++ b/gnucash/register/register-core/CMakeLists.txt @@ -24,6 +24,7 @@ set (register_core_HEADERS cell-factory.h cellblock.h combocell.h + completioncell.h datecell.h formulacell.h gtable.h diff --git a/gnucash/register/register-core/combocell.h b/gnucash/register/register-core/combocell.h index ebdb0340a7..ceeb7b7dd0 100644 --- a/gnucash/register/register-core/combocell.h +++ b/gnucash/register/register-core/combocell.h @@ -63,10 +63,6 @@ void gnc_combo_cell_clear_menu (ComboCell* cell); void gnc_combo_cell_add_menu_item (ComboCell* cell, const char* menustr); -/** Add a unique menu item to the list. */ -void gnc_combo_cell_add_menu_item_unique (ComboCell* cell, - const char* menustr); - /** Add a 'account name' menu item to the list. When testing for * equality with the currently selected item, this function will * ignore the characters normally used to separate account names. */ @@ -108,9 +104,5 @@ void gnc_combo_cell_use_quickfill_cache (ComboCell* cell, QuickFill* shared_qf); void gnc_combo_cell_use_list_store_cache (ComboCell* cell, gpointer data); -/** Set the combocell to use only type ahead search. This will make the - * search to be more like a modified entry completion. */ -void gnc_combo_cell_use_type_ahead_only (ComboCell* cell); - /** @} */ #endif diff --git a/gnucash/register/register-core/completioncell.h b/gnucash/register/register-core/completioncell.h new file mode 100644 index 0000000000..8484eb428f --- /dev/null +++ b/gnucash/register/register-core/completioncell.h @@ -0,0 +1,79 @@ +/********************************************************************\ + * completion.h -- combo-box used for completion cell * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + * * +\********************************************************************/ + +/** @addtogroup Cell Cell + * @{ + * @file completion.h + * @struct CompletionCell + * @brief The CompletionCell object implements a cell handler with a + * "combination-box" pull-down menu in it. + * + * On output, the currently selected menu item is displayed. + * On input, the user can select from a list in the pull-down menu, + * or use the keyboard to select a menu entry by typing the first + * few menu characters. + * + * @author Copyright (c) 2023 Robert Fewell + */ + +#ifndef COMPLETION_CELL_H +#define COMPLETION_CELL_H + +#include + +#include "basiccell.h" + +typedef struct +{ + BasicCell cell; +} CompletionCell; + + +BasicCell * gnc_completion_cell_new (void); +void gnc_completion_cell_init (CompletionCell* cell); + +void gnc_completion_cell_set_value (CompletionCell* cell, const char* value); + +void gnc_completion_cell_clear_menu (CompletionCell* cell); + +/** Add a menu item to the hash table list. */ +void gnc_completion_cell_add_menu_item (CompletionCell* cell, + const char* menustr); + +/** Enable sorting of the menu item's contents. */ +void gnc_completion_cell_set_sort_enabled (CompletionCell* cell, + gboolean enabled); + +/** Determines whether the cell will accept strings not in the + * menu. Defaults to strict, i.e., only menu items are accepted. */ +void gnc_completion_cell_set_strict (CompletionCell* cell, gboolean strict); + +/** Determines whether the popup list autosizes itself or uses + * all available space. FALSE by default. */ +void gnc_completion_cell_set_autosize (CompletionCell* cell, gboolean autosize); + +/** Register the sort direction. Used to determine in what order the completion should + * present the list. FALSE by default */ +void gnc_completion_cell_reverse_sort (CompletionCell* cell, gboolean is_reversed); + +/** @} */ +#endif diff --git a/gnucash/register/register-core/register-common.c b/gnucash/register/register-core/register-common.c index 9ea9708b48..a1c11f455a 100644 --- a/gnucash/register/register-core/register-common.c +++ b/gnucash/register/register-core/register-common.c @@ -27,6 +27,7 @@ #include "basiccell.h" #include "cell-factory.h" #include "combocell.h" +#include "completioncell.h" #include "datecell.h" #include "formulacell.h" #include "numcell.h" diff --git a/gnucash/register/register-core/register-common.h b/gnucash/register/register-core/register-common.h index 31c427469a..9bb2d4007d 100644 --- a/gnucash/register/register-core/register-common.h +++ b/gnucash/register/register-core/register-common.h @@ -71,6 +71,7 @@ #define QUICKFILL_CELL_TYPE_NAME "quickfill-cell" #define FORMULA_CELL_TYPE_NAME "formula-cell" #define CHECKBOX_CELL_TYPE_NAME "checkbox-cell" +#define COMPLETION_CELL_TYPE_NAME "completion-cell" void gnc_register_init (void); void gnc_register_shutdown (void); diff --git a/gnucash/register/register-gnome/CMakeLists.txt b/gnucash/register/register-gnome/CMakeLists.txt index 51a288680c..6d893fe2aa 100644 --- a/gnucash/register/register-gnome/CMakeLists.txt +++ b/gnucash/register/register-gnome/CMakeLists.txt @@ -2,6 +2,7 @@ include(CheckSymbolExists) set (register_gnome_SOURCES combocell-gnome.c + completioncell-gnome.c datecell-gnome.c formulacell-gnome.c gnucash-color.c diff --git a/gnucash/register/register-gnome/combocell-gnome.c b/gnucash/register/register-gnome/combocell-gnome.c index fd6c039d0b..e55681cd05 100644 --- a/gnucash/register/register-gnome/combocell-gnome.c +++ b/gnucash/register/register-gnome/combocell-gnome.c @@ -75,9 +75,6 @@ typedef struct _PopBox GList* ignore_strings; - GHashTable *item_hash; - - gboolean use_type_ahead_only; } PopBox; @@ -168,10 +165,6 @@ gnc_combo_cell_init (ComboCell* cell) box->complete_char = '\0'; box->ignore_strings = NULL; - - box->item_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - - box->use_type_ahead_only = FALSE; } static void @@ -340,9 +333,6 @@ gnc_combo_cell_destroy (BasicCell* bcell) box->qf = NULL; } - if (box->item_hash) - g_hash_table_destroy (box->item_hash); - g_list_free_full (box->ignore_strings, g_free); box->ignore_strings = NULL; @@ -367,7 +357,7 @@ gnc_combo_cell_set_sort_enabled (ComboCell* cell, gboolean enabled) return; block_list_signals (cell); - gnc_item_list_set_sort_enabled (box->item_list, enabled); + gnc_item_list_set_sort_column (box->item_list, 0); unblock_list_signals (cell); } @@ -468,70 +458,6 @@ gnc_combo_cell_add_menu_item (ComboCell* cell, const char* menustr) } } -void -gnc_combo_cell_add_menu_item_unique (ComboCell* cell, const char* menustr) -{ - PopBox* box; - - if (cell == NULL) - return; - if (menustr == NULL) - return; - - box = cell->cell.gui_private; - - if (box->item_list != NULL) - { - block_list_signals (cell); - - /* check if menustr has already been added. */ - if (g_hash_table_lookup_extended (box->item_hash, menustr, NULL, NULL)) - return; - - g_hash_table_insert (box->item_hash, g_strdup (menustr), NULL); - - gchar *menustr_temp = g_strdup (menustr); - gnc_utf8_strip_invalid_and_controls (menustr_temp); - gnc_item_list_append (box->item_list, menustr_temp); - if (cell->cell.value && - (strcmp (menustr_temp, cell->cell.value) == 0)) - gnc_item_list_select (box->item_list, menustr_temp); - g_free (menustr_temp); - unblock_list_signals (cell); - } - else - { - GtkTreeIter iter; - - // add a blank entry as the first entry in store - if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL(cell->shared_store), NULL) == 0) - { - gtk_list_store_append (cell->shared_store, &iter); - gtk_list_store_set (cell->shared_store, &iter, 0, "", -1); - g_hash_table_insert (box->item_hash, g_strdup (""), NULL); - } - - /* check if menustr has already been added. */ - if (g_hash_table_lookup_extended (box->item_hash, menustr, NULL, NULL)) - return; - - g_hash_table_insert (box->item_hash, g_strdup (menustr), NULL); - - gchar *menustr_temp = g_strdup (menustr); - gnc_utf8_strip_invalid_and_controls (menustr_temp); - gtk_list_store_append (cell->shared_store, &iter); - gtk_list_store_set (cell->shared_store, &iter, 0, menustr_temp, -1); - g_free (menustr_temp); - } - - /* If we're going to be using a pre-fab quickfill, - * then don't fill it in here */ - if (FALSE == box->use_quickfill_cache) - { - gnc_quickfill_insert (box->qf, menustr, QUICKFILL_ALPHA); - } -} - void gnc_combo_cell_add_account_menu_item (ComboCell* cell, char* menustr) { @@ -713,8 +639,7 @@ gnc_combo_cell_modify_verify (BasicCell* _cell, return; } - if (!box->use_type_ahead_only) // Do we only want to use type-ahead - match_str = quickfill_match (box->qf, newval); + match_str = quickfill_match (box->qf, newval); if (match_str != NULL) // Do we have a quickfill match { @@ -937,19 +862,6 @@ gnc_combo_cell_direct_update (BasicCell* bcell, return TRUE; } -void -gnc_combo_cell_use_type_ahead_only (ComboCell* cell) -{ - PopBox* box; - - if (cell == NULL) return; - - box = cell->cell.gui_private; - - box->use_type_ahead_only = TRUE; - -} - static void gnc_combo_cell_gui_realize (BasicCell* bcell, gpointer data) { diff --git a/gnucash/register/register-gnome/completioncell-gnome.c b/gnucash/register/register-gnome/completioncell-gnome.c new file mode 100644 index 0000000000..3fdf827f06 --- /dev/null +++ b/gnucash/register/register-gnome/completioncell-gnome.c @@ -0,0 +1,960 @@ +/********************************************************************\ + * completioncell-gnome.c -- completion combobox cell for gnome * + * * + * 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, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + * * +\********************************************************************/ + +/* + * FILE: completioncell-gnome.c + * + * FUNCTION: Implement gnome portion of a entry completion combo widget + * embedded in a table cell. + * + * HISTORY: + * @author Copyright (c) 2023 Robert Fewell + */ + +#include + +#include +#include + +#include "completioncell.h" +#include "gnc-prefs.h" +#include "gnucash-item-edit.h" +#include "gnucash-item-list.h" +#include "gnucash-sheet.h" +#include "gnucash-sheetP.h" +#include "table-allgui.h" +#include "gnc-glib-utils.h" + +typedef struct _PopBox +{ + GnucashSheet* sheet; + GncItemEdit* item_edit; + GncItemList* item_list; + + GHashTable* item_hash; // the item hash table + GtkListStore* item_store; // the item list store + + gchar* newval; // string value to find + + gboolean signals_connected; // list signals connected + gboolean list_popped; // list is popped up + + gboolean autosize; // autosize the popup width + + gboolean sort_enabled; // sort of list store enabled + gboolean register_is_reversed; // whether the register is reversed + gboolean stop_searching; // set when there are no results + + gboolean strict; // text entry must be in the list + gboolean in_list_select; // item selected in the list + + gint occurrence; // the position in the list + +} PopBox; + +#define DONT_TEXT N_("Don't autocomplete") + +/** Enumeration for the list-store */ +enum GncCompletionColumn +{ + TEXT_COL, //0 + TEXT_MARKUP_COL, //1 + WEIGHT_COL, //2 +}; + +static void gnc_completion_cell_gui_realize (BasicCell* bcell, gpointer w); +static void gnc_completion_cell_gui_move (BasicCell* bcell); +static void gnc_completion_cell_gui_destroy (BasicCell* bcell); +static gboolean gnc_completion_cell_enter (BasicCell* bcell, + int* cursor_position, + int* start_selection, + int* end_selection); +static void gnc_completion_cell_leave (BasicCell* bcell); +static void gnc_completion_cell_destroy (BasicCell* bcell); + +BasicCell* +gnc_completion_cell_new (void) +{ + CompletionCell* cell = g_new0 (CompletionCell, 1); + gnc_completion_cell_init (cell); + return &cell->cell; +} + +void +gnc_completion_cell_init (CompletionCell* cell) +{ + gnc_basic_cell_init (& (cell->cell)); + + cell->cell.is_popup = TRUE; + + cell->cell.destroy = gnc_completion_cell_destroy; + + cell->cell.gui_realize = gnc_completion_cell_gui_realize; + cell->cell.gui_destroy = gnc_completion_cell_gui_destroy; + + PopBox* box = g_new0 (PopBox, 1); + + box->sheet = NULL; + box->item_edit = NULL; + box->item_list = NULL; + box->item_store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_INT); + box->signals_connected = FALSE; + box->list_popped = FALSE; + box->autosize = FALSE; + box->register_is_reversed = FALSE; + + box->sort_enabled = FALSE; + + cell->cell.gui_private = box; + + box->stop_searching = FALSE; + + box->strict = FALSE; + box->in_list_select = FALSE; + box->occurrence = 0; + + box->item_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); +} + +static void +hide_popup (PopBox* box) +{ + gnc_item_edit_hide_popup (box->item_edit); + box->list_popped = FALSE; +} + +static void +select_item_cb (GncItemList* item_list, char* item_string, gpointer user_data) +{ + CompletionCell* cell = user_data; + PopBox* box = cell->cell.gui_private; + + box->in_list_select = TRUE; + gnucash_sheet_modify_current_cell (box->sheet, item_string); + box->in_list_select = FALSE; + + hide_popup (box); +} + +static void +change_item_cb (GncItemList* item_list, char* item_string, gpointer user_data) +{ + CompletionCell* cell = user_data; + PopBox* box = cell->cell.gui_private; + + box->in_list_select = TRUE; + gnucash_sheet_modify_current_cell (box->sheet, item_string); + box->in_list_select = FALSE; +} + +static void +activate_item_cb (GncItemList* item_list, char* item_string, gpointer user_data) +{ + CompletionCell* cell = user_data; + PopBox* box = cell->cell.gui_private; + hide_popup (box); +} + +static void +block_list_signals (CompletionCell* cell) +{ + PopBox* box = cell->cell.gui_private; + + if (!box->signals_connected) + return; + + g_signal_handlers_block_matched (G_OBJECT(box->item_list), + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, cell); +} + +static void +unblock_list_signals (CompletionCell* cell) +{ + PopBox* box = cell->cell.gui_private; + + if (!box->signals_connected) + return; + + g_signal_handlers_unblock_matched (G_OBJECT(box->item_list), + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, cell); +} + +static void +key_press_item_cb (GncItemList* item_list, GdkEventKey* event, gpointer user_data) +{ + CompletionCell* cell = user_data; + PopBox* box = cell->cell.gui_private; + + switch (event->keyval) + { + case GDK_KEY_Escape: + block_list_signals (cell); // Prevent recursion, unselect all + gnc_item_list_select (box->item_list, NULL); + unblock_list_signals (cell); + hide_popup (box); + break; + + default: + gtk_widget_event (GTK_WIDGET (box->sheet), + (GdkEvent*) event); + break; + } +} + +static void +completion_disconnect_signals (CompletionCell* cell) +{ + PopBox* box = cell->cell.gui_private; + + if (!box->signals_connected) + return; + + g_signal_handlers_disconnect_matched (G_OBJECT(box->item_list), + G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, cell); + + box->signals_connected = FALSE; +} + +static void +completion_connect_signals (CompletionCell* cell) +{ + PopBox* box = cell->cell.gui_private; + + if (box->signals_connected) + return; + + g_signal_connect (G_OBJECT(box->item_list), "select_item", + G_CALLBACK(select_item_cb), cell); + + g_signal_connect (G_OBJECT(box->item_list), "change_item", + G_CALLBACK(change_item_cb), cell); + + g_signal_connect (G_OBJECT(box->item_list), "activate_item", + G_CALLBACK(activate_item_cb), cell); + + g_signal_connect (G_OBJECT(box->item_list), "key_press_event", + G_CALLBACK(key_press_item_cb), cell); + + box->signals_connected = TRUE; +} + +static void +gnc_completion_cell_gui_destroy (BasicCell* bcell) +{ + CompletionCell* cell = (CompletionCell*) bcell; + + if (cell->cell.gui_realize) + { + PopBox* box = bcell->gui_private; + if (box && box->item_list) + { + completion_disconnect_signals (cell); + g_object_unref (box->item_list); + box->item_list = NULL; + } + /* allow the widget to be shown again */ + cell->cell.gui_realize = gnc_completion_cell_gui_realize; + cell->cell.gui_move = NULL; + cell->cell.enter_cell = NULL; + cell->cell.leave_cell = NULL; + cell->cell.gui_destroy = NULL; + } +} + +static void +gnc_completion_cell_destroy (BasicCell* bcell) +{ + CompletionCell* cell = (CompletionCell*) bcell; + PopBox* box = cell->cell.gui_private; + + gnc_completion_cell_gui_destroy (& (cell->cell)); + + if (box) + { + if (box->item_hash) + g_hash_table_destroy (box->item_hash); + + g_free (box); + cell->cell.gui_private = NULL; + } + cell->cell.gui_private = NULL; + cell->cell.gui_realize = NULL; +} + +static gint +sort_func (GtkTreeModel* model, GtkTreeIter* iter_a, GtkTreeIter* iter_b, gpointer user_data) +{ + gint a_weight, b_weight; + gint ret = 0; + + gtk_tree_model_get (model, iter_a, WEIGHT_COL, &a_weight, -1); + gtk_tree_model_get (model, iter_b, WEIGHT_COL, &b_weight, -1); + + if (a_weight < b_weight) + ret = -1; + else if (a_weight > b_weight) + ret = 1; + + return ret; +} + +void +gnc_completion_cell_set_sort_enabled (CompletionCell* cell, + gboolean enabled) +{ + if (!cell) + return; + + PopBox* box = cell->cell.gui_private; + box->sort_enabled = enabled; +} + +static void +set_sort_column_enabled (PopBox* box, gboolean enable) +{ + if (enable) + { + gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE(box->item_list->list_store), + WEIGHT_COL, sort_func, box->item_list, NULL); + + gnc_item_list_set_sort_column (box->item_list, WEIGHT_COL); + } + else + gnc_item_list_set_sort_column (box->item_list, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID); +} + +static void +item_store_clear (CompletionCell* cell) +{ + if (!cell) + return; + + PopBox* box = cell->cell.gui_private; + + // disconnect list store from tree view + GtkListStore *store = gnc_item_list_disconnect_store (box->item_list); + + block_list_signals (cell); + + if (box->sort_enabled) // if sorting, disable it + set_sort_column_enabled (box, FALSE); + + box->stop_searching = FALSE; + gtk_list_store_clear (box->item_store); + + if (box->sort_enabled) // if sorting, enable it + set_sort_column_enabled (box, TRUE); + + unblock_list_signals (cell); + + // reconect list store to tree view + gnc_item_list_connect_store (box->item_list, store); + + hide_popup (box); +} + +void +gnc_completion_cell_clear_menu (CompletionCell* cell) +{ + if (!cell) + return; + + PopBox* box = cell->cell.gui_private; + if (!box) + return; + + if (box->item_list) + { + g_hash_table_remove_all (box->item_hash); + item_store_clear (cell); + box->occurrence = 0; + } +} + +void +gnc_completion_cell_add_menu_item (CompletionCell* cell, + const char* menustr) +{ + if (!cell || !menustr) + return; + + PopBox* box = cell->cell.gui_private; + + if (box->item_hash) + { + gpointer value = g_hash_table_lookup (box->item_hash, menustr); + gboolean update = FALSE; + if (value) + { + if (!box->register_is_reversed) + update = TRUE; + } + else + update = TRUE; + + if (update) + { + g_hash_table_insert (box->item_hash, g_strdup (menustr), + GINT_TO_POINTER(box->occurrence)); + } + box->occurrence++; + } +} + +void +gnc_completion_cell_set_value (CompletionCell* cell, const char* str) +{ + if (!cell || !str) + + gnc_basic_cell_set_value (&cell->cell, str); +} + +static inline void +list_store_append (GtkListStore *store, char* string, + char* markup, gint weight) +{ + GtkTreeIter iter; + + g_return_if_fail (store); + g_return_if_fail (string); + g_return_if_fail (markup); + + gtk_list_store_append (store, &iter); + + gtk_list_store_set (store, &iter, TEXT_COL, string, + TEXT_MARKUP_COL, markup, + WEIGHT_COL, weight, -1); +} + +static char* +normalize_and_fold (char* utf8_string) +{ + char *normalized = g_utf8_normalize (utf8_string, -1, G_NORMALIZE_NFC); + if (!normalized) + return NULL; + + char *folded = g_utf8_casefold (normalized, -1); + g_free (normalized); + return folded; +} + +static gint +test_and_add (PopBox* box, const gchar *text, gint start_pos, + gpointer key, gint occurrence_difference) +{ + gint ret_value = -1; + gint text_length = g_utf8_strlen (text, -1); + + if (start_pos > text_length) + return ret_value; + + gchar *sub_text = g_utf8_substring (text, start_pos, text_length); + gchar *sub_text_norm_fold = normalize_and_fold (sub_text); + gchar *found_text_ptr = g_strstr_len (sub_text_norm_fold, -1, box->newval); + + if (found_text_ptr) + { + gchar *markup = NULL, *prefix = NULL, *match = NULL, *suffix = NULL; + glong newval_length = g_utf8_strlen (box->newval, -1); + gulong found_location = g_utf8_pointer_to_offset (sub_text_norm_fold, + found_text_ptr) + start_pos; + gboolean have_boundary = FALSE; + gint prefix_length; + gint weight; + + if (found_location > 0) + prefix = g_utf8_substring (text, 0, found_location); + else + prefix = g_strdup (""); + + prefix_length = g_utf8_strlen (prefix, -1); + + match = g_utf8_substring (text, found_location, found_location + newval_length); + + if (found_location >= 1) + { + gunichar prev = g_utf8_get_char (g_utf8_offset_to_pointer (sub_text, found_location - start_pos - 1)); + if (prev && (g_unichar_isspace (prev) || g_unichar_ispunct (prev))) + have_boundary = TRUE; + else + ret_value = found_location + 1; + } + + suffix = g_utf8_substring (text, found_location + newval_length, text_length); + + markup = g_markup_printf_escaped ("%s%s%s%s", prefix, match, suffix, " "); + + if ((prefix_length == 0 ) || have_boundary) + { + weight = occurrence_difference; // sorted by recent first + + if (g_strcmp0 (sub_text_norm_fold, box->newval) == 0) // exact match + weight = 1; + + list_store_append (box->item_store, key, markup, weight); + } + g_free (markup); + g_free (prefix); + g_free (match); + g_free (suffix); + } + g_free (sub_text_norm_fold); + g_free (sub_text); + return ret_value; +} + +static void +add_item (gpointer key, gpointer value, gpointer user_data) +{ + PopBox* box = user_data; + gchar *hash_entry = g_strdup (key); + + if (hash_entry && *hash_entry) + { + gint start_pos = 0; + gint occurrence_difference; + gnc_utf8_strip_invalid_and_controls (hash_entry); + + if (box->register_is_reversed) + occurrence_difference = GPOINTER_TO_INT(value) + 1; + else + occurrence_difference = box->occurrence - GPOINTER_TO_INT(value); + + do + { + start_pos = test_and_add (box, hash_entry, start_pos, key, occurrence_difference); + } + while (start_pos != -1); + } + g_free (hash_entry); +} + +static void +select_first_entry_in_list (PopBox* box) +{ + GtkTreeModel *model = gtk_tree_view_get_model (box->item_list->tree_view); + GtkTreeIter iter; + gchar* string; + + if (!gtk_tree_model_get_iter_first (model, &iter)) + return; + + if (!gtk_tree_model_iter_next (model, &iter)) + return; + + gtk_tree_model_get (model, &iter, TEXT_COL, &string, -1); + + gnc_item_list_select (box->item_list, string); + + GtkTreePath* path = gtk_tree_path_new_first (); + gtk_tree_view_scroll_to_cell (box->item_list->tree_view, + path, NULL, TRUE, 0.5, 0.0); + gtk_tree_path_free (path); + g_free (string); +} + +static void +populate_list_store (CompletionCell* cell, const gchar* str) +{ + PopBox* box = cell->cell.gui_private; + + box->in_list_select = FALSE; + + if (box->stop_searching) + return; + + if (str && *str) + box->newval = normalize_and_fold ((gchar*)str); + else + return; + + // disconnect list store from tree view + box->item_store = gnc_item_list_disconnect_store (box->item_list); + + block_list_signals (cell); + + if (box->sort_enabled) // if sorting, disable it + set_sort_column_enabled (box, FALSE); + + gtk_list_store_clear (box->item_store); + + // add the don't first entry + gchar *markup = g_markup_printf_escaped ("%s", DONT_TEXT); + list_store_append (box->item_store, DONT_TEXT, markup, 0); + g_free (markup); + + // add to the list store + g_hash_table_foreach (box->item_hash, add_item, box); + + if (box->sort_enabled) // if sorting, enable it + set_sort_column_enabled (box, TRUE); + + unblock_list_signals (cell); + + // reconnect list store to tree view + gnc_item_list_connect_store (box->item_list, box->item_store); + + // if no entries, do not show popup + if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL(box->item_store), NULL) == 1) + { + box->stop_searching = TRUE; + hide_popup (box); + } + else + gnc_item_edit_show_popup (box->item_edit); + + block_list_signals (cell); // Prevent recursion, select first entry + select_first_entry_in_list (box); + unblock_list_signals (cell); + + g_free (box->newval); +} + +static void +gnc_completion_cell_modify_verify (BasicCell* bcell, + const char* change, + int change_len, + const char* newval, + int newval_len, + int* cursor_position, + int* start_selection, + int* end_selection) +{ + CompletionCell* cell = (CompletionCell*) bcell; + PopBox* box = cell->cell.gui_private; + glong newval_chars = g_utf8_strlen (newval, newval_len); + + if (box->in_list_select) + { + if (g_strcmp0 (newval, DONT_TEXT) == 0) + return; + gnc_basic_cell_set_value_internal (bcell, newval); + *cursor_position = -1; + *start_selection = 0; + *end_selection = 0; + return; + } + + // check to enable searching + if (((*cursor_position < newval_chars) && + (g_utf8_strlen (bcell->value, -1) < newval_chars)) || + (g_utf8_strlen (bcell->value, -1) > newval_chars)) + { + box->stop_searching = FALSE; + } + + populate_list_store (cell, newval); + + if (g_strcmp0 (newval, "") == 0) + { + block_list_signals (cell); // Prevent recursion, unselect all + gnc_item_list_select (box->item_list, NULL); + unblock_list_signals (cell); + hide_popup (box); + } + gnc_basic_cell_set_value_internal (bcell, newval); +} + +static gboolean +gnc_completion_cell_direct_update (BasicCell* bcell, + int* cursor_position, + int* start_selection, + int* end_selection, + void* gui_data) +{ + CompletionCell* cell = (CompletionCell*) bcell; + PopBox* box = cell->cell.gui_private; + GdkEventKey* event = gui_data; + + if (event->type != GDK_KEY_PRESS) + return FALSE; + + switch (event->keyval) + { + case GDK_KEY_Tab: + case GDK_KEY_ISO_Left_Tab: + { + char* string = gnc_item_list_get_selection (box->item_list); + + if (!string) + break; + + g_signal_emit_by_name (G_OBJECT(box->item_list), "change_item", + string, (gpointer)bcell); + + g_free (string); + break; + } + } + + if (box->strict) + box->in_list_select = gnc_item_in_list (box->item_list, bcell->value); + + if (!bcell->value) + item_store_clear (cell); + + return FALSE; +} + +void +gnc_completion_cell_reverse_sort (CompletionCell* cell, gboolean is_reversed) +{ + if (!cell) + return; + + PopBox* box = cell->cell.gui_private; + + if (is_reversed != box->register_is_reversed) + { + gnc_completion_cell_clear_menu (cell); + box->register_is_reversed = is_reversed; + box->occurrence = 0; + } +} + +static void +gnc_completion_cell_gui_realize (BasicCell* bcell, gpointer data) +{ + GnucashSheet* sheet = data; + GncItemEdit* item_edit = gnucash_sheet_get_item_edit (sheet); + CompletionCell* cell = (CompletionCell*) bcell; + PopBox* box = cell->cell.gui_private; + + /* initialize gui-specific, private data */ + box->sheet = sheet; + box->item_edit = item_edit; + box->item_list = GNC_ITEM_LIST(gnc_item_list_new (box->item_store)); + + block_list_signals (cell); + set_sort_column_enabled (box, FALSE); + unblock_list_signals (cell); + + gtk_widget_show_all (GTK_WIDGET(box->item_list)); + g_object_ref_sink (box->item_list); + + /* to mark cell as realized, remove the realize method */ + cell->cell.gui_realize = NULL; + cell->cell.gui_move = gnc_completion_cell_gui_move; + cell->cell.enter_cell = gnc_completion_cell_enter; + cell->cell.leave_cell = gnc_completion_cell_leave; + cell->cell.gui_destroy = gnc_completion_cell_gui_destroy; + cell->cell.modify_verify = gnc_completion_cell_modify_verify; + cell->cell.direct_update = gnc_completion_cell_direct_update; +} + +static void +reset_item_list_to_default_setup (BasicCell* bcell) +{ + PopBox* box = bcell->gui_private; + PopupToggle popup_toggle; + + item_store_clear ((CompletionCell*) bcell); + + popup_toggle = box->item_edit->popup_toggle; + gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton), TRUE); + gtk_widget_set_visible (GTK_WIDGET(popup_toggle.tbutton), TRUE); + + GtkTreeViewColumn *column = gtk_tree_view_get_column ( + GTK_TREE_VIEW(box->item_list->tree_view), TEXT_COL); + gtk_tree_view_column_clear_attributes (column,box->item_list->renderer); + gtk_tree_view_column_add_attribute (column, box->item_list->renderer, + "text", TEXT_COL); + box->list_popped = FALSE; +} + +static void +gnc_completion_cell_gui_move (BasicCell* bcell) +{ + PopBox* box = bcell->gui_private; + + completion_disconnect_signals ((CompletionCell*) bcell); + + gnc_item_edit_set_popup (box->item_edit, NULL, NULL, + NULL, NULL, NULL, NULL, NULL); + + reset_item_list_to_default_setup (bcell); +} + +static int +popup_get_height (G_GNUC_UNUSED GtkWidget* widget, + int space_available, + int row_height, + gpointer user_data) +{ + PopBox* box = user_data; + GtkScrolledWindow* scrollwin = GNC_ITEM_LIST(widget)->scrollwin; + GtkWidget *hsbar = gtk_scrolled_window_get_hscrollbar (scrollwin); + GtkStyleContext *context = gtk_widget_get_style_context (hsbar); + /* Note: gtk_scrolled_window_get_overlay_scrolling (scrollwin) always returns + TRUE so look for style class "overlay-indicator" on the scrollbar. */ + gboolean overlay = gtk_style_context_has_class (context, "overlay-indicator"); + int count = gnc_item_list_num_entries (box->item_list); + int height = count * (gnc_item_list_get_cell_height (box->item_list) + 2); + + if (!overlay) + { + gint minh, nath; + gtk_widget_get_preferred_height (hsbar, &minh, &nath); + height = height + minh; + } + + if (height < space_available) + { + // if the list is empty height would be 0 so return 1 instead to + // satisfy the check_popup_height_is_true function + gint ret_height = height ? height : 1; + + gtk_widget_set_size_request (GTK_WIDGET(scrollwin), -1, ret_height); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrollwin), + GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER); + return ret_height; + } + else + gtk_widget_set_size_request (GTK_WIDGET(scrollwin), -1, -1); + + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrollwin), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + return space_available; +} + +static int +popup_autosize (GtkWidget* widget, + int max_width, + gpointer user_data) +{ + PopBox* box = user_data; + + if (!box || !box->autosize) + return max_width; + + return gnc_item_list_autosize (GNC_ITEM_LIST(widget)) + 20; +} + +static void +popup_set_focus (GtkWidget* widget, + G_GNUC_UNUSED gpointer user_data) +{ + /* An empty GtkTreeView grabbing focus causes the key_press events to be + * lost because there's no entry cell to handle them. + */ + if (gnc_item_list_num_entries (GNC_ITEM_LIST(widget))) + gtk_widget_grab_focus (GTK_WIDGET (GNC_ITEM_LIST(widget)->tree_view)); +} + +static void +popup_post_show (GtkWidget* widget, + G_GNUC_UNUSED gpointer user_data) +{ + gnc_item_list_autosize (GNC_ITEM_LIST(widget)); + gnc_item_list_show_selected (GNC_ITEM_LIST(widget)); +} + +static int +popup_get_width (GtkWidget* widget, + G_GNUC_UNUSED gpointer user_data) +{ + GtkAllocation alloc; + gtk_widget_get_allocation (GTK_WIDGET (GNC_ITEM_LIST(widget)->tree_view), + &alloc); + return alloc.width; +} + +static gboolean +gnc_completion_cell_enter (BasicCell* bcell, + int* cursor_position, + int* start_selection, + int* end_selection) +{ + CompletionCell* cell = (CompletionCell*) bcell; + PopBox* box = bcell->gui_private; + PopupToggle popup_toggle; + + gnc_item_edit_set_popup (box->item_edit, + GTK_WIDGET(box->item_list), + popup_get_height, popup_autosize, + popup_set_focus, popup_post_show, + popup_get_width, box); + + popup_toggle = box->item_edit->popup_toggle; + gtk_widget_set_sensitive (GTK_WIDGET(popup_toggle.tbutton), FALSE); + gtk_widget_set_visible (GTK_WIDGET(popup_toggle.tbutton), FALSE); + + GtkTreeViewColumn *column = gtk_tree_view_get_column ( + GTK_TREE_VIEW(box->item_list->tree_view), TEXT_COL); + gtk_tree_view_column_clear_attributes (column, box->item_list->renderer); + gtk_tree_view_column_add_attribute (column, box->item_list->renderer, + "markup", TEXT_MARKUP_COL); + + completion_connect_signals (cell); + + *cursor_position = -1; + *start_selection = 0; + *end_selection = -1; + + return TRUE; +} + +static void +gnc_completion_cell_leave (BasicCell* bcell) +{ + PopBox* box = bcell->gui_private; + + completion_disconnect_signals ((CompletionCell*) bcell); + + gnc_item_edit_set_popup (box->item_edit, NULL, NULL, + NULL, NULL, NULL, NULL, NULL); + + reset_item_list_to_default_setup (bcell); + + if (box->strict && !box->in_list_select) + gnc_basic_cell_set_value_internal (bcell, ""); +} + +void +gnc_completion_cell_set_strict (CompletionCell* cell, gboolean strict) +{ + if (!cell) + return; + + PopBox* box = cell->cell.gui_private; + if (!box) + return; + + box->strict = strict; +} + +void +gnc_completion_cell_set_autosize (CompletionCell* cell, gboolean autosize) +{ + if (!cell) + return; + + PopBox* box = cell->cell.gui_private; + if (!box) + return; + + box->autosize = autosize; +} diff --git a/gnucash/register/register-gnome/gnucash-item-list.c b/gnucash/register/register-gnome/gnucash-item-list.c index febc0b2189..0b1eee4296 100644 --- a/gnucash/register/register-gnome/gnucash-item-list.c +++ b/gnucash/register/register-gnome/gnucash-item-list.c @@ -98,22 +98,14 @@ gnc_item_list_append (GncItemList* item_list, const char* string) void -gnc_item_list_set_sort_enabled (GncItemList* item_list, gboolean enabled) +gnc_item_list_set_sort_column (GncItemList* item_list, gint column_id) { - if (enabled) - { - gtk_tree_sortable_set_sort_column_id + g_return_if_fail (IS_GNC_ITEM_LIST (item_list)); + + gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (item_list->list_store), - 0, + column_id, GTK_SORT_ASCENDING); - } - else - { - gtk_tree_sortable_set_sort_column_id - (GTK_TREE_SORTABLE (item_list->list_store), - GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, - GTK_SORT_ASCENDING); - } } @@ -203,6 +195,7 @@ gnc_item_list_select (GncItemList* item_list, const char* string) g_free (to_find_data); } + char* gnc_item_list_get_selection (GncItemList *item_list) { @@ -277,6 +270,29 @@ gnc_item_list_using_temp (GncItemList *item_list) return item_list && item_list->temp_store; } +GtkListStore * +gnc_item_list_disconnect_store (GncItemList *item_list) +{ + GtkListStore *store; + + g_return_val_if_fail (item_list != NULL, NULL); + + store = GTK_LIST_STORE(gtk_tree_view_get_model (item_list->tree_view)); + + gtk_tree_view_set_model (item_list->tree_view, NULL); + + return store; +} + +void +gnc_item_list_connect_store (GncItemList *item_list, GtkListStore *list_store) +{ + g_return_if_fail (item_list != 0); + + gtk_tree_view_set_model (item_list->tree_view, + GTK_TREE_MODEL (list_store)); +} + static void gnc_item_list_init (GncItemList* item_list) { @@ -493,9 +509,9 @@ gnc_item_list_new (GtkListStore* list_store) g_object_unref (list_store); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE); - gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW ( - tree_view)), - GTK_SELECTION_BROWSE); + gtk_tree_selection_set_mode ( + gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)), + GTK_SELECTION_BROWSE); gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (list_store), 0, GTK_SORT_ASCENDING); diff --git a/gnucash/register/register-gnome/gnucash-item-list.h b/gnucash/register/register-gnome/gnucash-item-list.h index 31ab1eaf3a..4cf903c339 100644 --- a/gnucash/register/register-gnome/gnucash-item-list.h +++ b/gnucash/register/register-gnome/gnucash-item-list.h @@ -77,7 +77,7 @@ void gnc_item_list_clear (GncItemList *item_list); void gnc_item_list_append (GncItemList *item_list, const char *string); -void gnc_item_list_set_sort_enabled(GncItemList *item_list, gboolean enabled); +void gnc_item_list_set_sort_column (GncItemList *item_list, gint column_id); gboolean gnc_item_in_list (GncItemList *item_list, const char *string); @@ -98,5 +98,9 @@ void gnc_item_list_set_temp_store (GncItemList *item_list, GtkListStore *store); gboolean gnc_item_list_using_temp (GncItemList *item_list); +GtkListStore * gnc_item_list_disconnect_store (GncItemList *item_list); + +void gnc_item_list_connect_store (GncItemList *item_list, GtkListStore *store); + /** @} */ #endif /* GNUCASH_ITEM_LIST_H */ diff --git a/gnucash/register/register-gnome/gnucash-register.c b/gnucash/register/register-gnome/gnucash-register.c index 1324581307..9a09ae89f4 100644 --- a/gnucash/register/register-gnome/gnucash-register.c +++ b/gnucash/register/register-gnome/gnucash-register.c @@ -47,6 +47,7 @@ #include "gnc-state.h" #include "combocell.h" +#include "completioncell.h" #include "datecell.h" #include "formulacell-gnome.h" #include "pricecell-gnome.h" @@ -100,6 +101,7 @@ void gnucash_register_add_cell_types (void) { gnc_register_add_cell_type (COMBO_CELL_TYPE_NAME, gnc_combo_cell_new); + gnc_register_add_cell_type (COMPLETION_CELL_TYPE_NAME, gnc_completion_cell_new); gnc_register_add_cell_type (DATE_CELL_TYPE_NAME, gnc_date_cell_new); gnc_register_add_cell_type (PRICE_CELL_TYPE_NAME, gnc_price_cell_gnome_new);