diff --git a/src/business/business-gnome/business-gnome-utils.c b/src/business/business-gnome/business-gnome-utils.c index 5670b0502f..4c1d4ef9f8 100644 --- a/src/business/business-gnome/business-gnome-utils.c +++ b/src/business/business-gnome/business-gnome-utils.c @@ -64,13 +64,16 @@ static GtkWidget * gnc_owner_new (GtkWidget *label, GtkWidget *hbox, GNCSearchCB search_cb = NULL; const char *type_name = NULL; const char *text = NULL; + gboolean text_editable = FALSE; switch (type) { case GNCSEARCH_TYPE_SELECT: text = _("Select..."); + text_editable = TRUE; break; case GNCSEARCH_TYPE_EDIT: text = _("Edit..."); + text_editable = FALSE; }; switch (owner->type) { @@ -115,7 +118,7 @@ static GtkWidget * gnc_owner_new (GtkWidget *label, GtkWidget *hbox, return NULL; } - edit = gnc_general_search_new (type_name, text, search_cb, book); + edit = gnc_general_search_new (type_name, text, text_editable, search_cb, book, book); if (!edit) return NULL; @@ -251,7 +254,7 @@ GtkWidget * gnc_invoice_select_create (GtkWidget *hbox, QofBook *book, isi->label = label; edit = gnc_general_search_new (GNC_INVOICE_MODULE_NAME, _("Select..."), - gnc_invoice_select_search_cb, isi); + TRUE, gnc_invoice_select_search_cb, isi, isi->book); if (!edit) { g_free(isi); return NULL; diff --git a/src/business/business-gnome/dialog-invoice.c b/src/business/business-gnome/dialog-invoice.c index b102502f59..686a3c2804 100644 --- a/src/business/business-gnome/dialog-invoice.c +++ b/src/business/business-gnome/dialog-invoice.c @@ -1110,8 +1110,8 @@ gnc_invoice_update_job_choice (InvoiceWindow *iw) case NEW_INVOICE: case MOD_INVOICE: iw->job_choice = - gnc_general_search_new (GNC_JOB_MODULE_NAME, _("Select..."), - gnc_invoice_select_job_cb, iw); + gnc_general_search_new (GNC_JOB_MODULE_NAME, _("Select..."), TRUE, + gnc_invoice_select_job_cb, iw, iw->book); gnc_general_search_set_selected (GNC_GENERAL_SEARCH (iw->job_choice), gncOwnerGetJob (&iw->job)); @@ -1181,8 +1181,8 @@ gnc_invoice_update_proj_job (InvoiceWindow *iw) iw->proj_job_choice = NULL; } else { iw->proj_job_choice = - gnc_general_search_new (GNC_JOB_MODULE_NAME, _("Select..."), - gnc_invoice_select_proj_job_cb, iw); + gnc_general_search_new (GNC_JOB_MODULE_NAME, _("Select..."), TRUE, + gnc_invoice_select_proj_job_cb, iw, iw->book); gnc_general_search_set_selected (GNC_GENERAL_SEARCH(iw->proj_job_choice), gncOwnerGetJob (&iw->proj_job)); diff --git a/src/gnome-search/gnc-general-search.c b/src/gnome-search/gnc-general-search.c index 261a195893..4b30bc777e 100644 --- a/src/gnome-search/gnc-general-search.c +++ b/src/gnome-search/gnc-general-search.c @@ -51,6 +51,12 @@ enum LAST_SIGNAL }; +/* Columns used in GtkEntryCompletion's model */ +enum { + GSL_COLUMN_TEXT, + GSL_COLUMN_QOFOBJECT, + GSL_N_COLUMNS +}; static void gnc_general_search_init (GNCGeneralSearch *gsl); static void gnc_general_search_class_init (GNCGeneralSearchClass *class); @@ -256,14 +262,179 @@ search_cb(GtkButton * button, gpointer user_data) } -static void -create_children (GNCGeneralSearch *gsl, const char *label) +/** The completion attached to search edit widget has selected a + * match. This function extracts the completed string from the + * completion code's temporary model, and uses that to set the iterator + * and object data of the selection for use when the user leaves the widget. + * This should always point to a valid iterator since the user + * made the selection from a list of available object names. + * + * @param completion Unused. + * + * @param comp_model A temporary model used by completion code that + * contains only the current matches. + * + * @param comp_iter The iter in the completion's temporary model + * that represents the user selected match. + * + * @param cbe A pointer to a currency entry widget. */ +static gboolean +gnc_gsl_match_selected_cb (GtkEntryCompletion *completion, + GtkTreeModel *comp_model, + GtkTreeIter *comp_iter, + GNCGeneralSearch *gsl) { + QofObject * qofobject; + + gtk_tree_model_get(comp_model, comp_iter, GSL_COLUMN_QOFOBJECT, &qofobject, -1); + gnc_general_search_set_selected (gsl, qofobject); + return FALSE; +} + +/** The focus left the general search edit widget, so reset the widget to + * its last known good value. If the widget value contained a valid + * value then this is a noop. Otherwise the widget will be reset + * to the last user selected value. This latter state will occur + * if the user has typed characters directly into the widget but not + * selected a completion. + * + * @param entry The entry widget in which the user is typing. + * + * @param event Unused. + * + * @param gsl A pointer to a general search widget. */ +static gboolean +gnc_gsl_focus_out_cb (GtkEntry *entry, + GdkEventFocus *event, + GNCGeneralSearch *gsl) +{ + const gchar *text; + GtkEntryCompletion *completion; + GtkTreeModel *model; + GtkTreeIter iter; + gchar *lc_text, *tree_string, *lc_tree_string; + gboolean match, valid_iter; + QofObject *qofobject; + gpointer selected_item=NULL; + + /* Attempt to match the current text to a qofobject. */ + completion = gtk_entry_get_completion(entry); + model = gtk_entry_completion_get_model(completion); + + /* Return if completion tree is empty */ + valid_iter = gtk_tree_model_get_iter_first(model, &iter); + if (!valid_iter) + return FALSE; + + text = gtk_entry_get_text(entry); + lc_text = g_utf8_strdown(text, -1); + + /* The last, valid selected entry can match the entered text + * No need to search further in that case */ + if (gsl->selected_item) + { + GNCGeneralSearchPrivate * priv; + + priv=_PRIVATE(gsl); + tree_string = g_strdup(qof_object_printable(priv->type, gsl->selected_item)); + lc_tree_string = g_utf8_strdown(tree_string, -1); + match = g_utf8_collate(lc_text, lc_tree_string) == 0; + g_free(tree_string); + g_free(lc_tree_string); + if (match) + selected_item = gsl->selected_item; + } + + /* Otherwise, find a match in the completion list */ + while (valid_iter && !selected_item) + { + gtk_tree_model_get(model, &iter, GSL_COLUMN_TEXT, &tree_string, -1); + lc_tree_string = g_utf8_strdown(tree_string, -1); + match = g_utf8_collate(lc_text, lc_tree_string) == 0; + g_free(tree_string); + g_free(lc_tree_string); + if (match) + { + gtk_tree_model_get(model, &iter, GSL_COLUMN_QOFOBJECT, &qofobject, -1); + selected_item = qofobject; + } else + valid_iter = gtk_tree_model_iter_next(model, &iter); + } + + g_free(lc_text); + gnc_general_search_set_selected (gsl, selected_item); + return FALSE; +} + +static void +create_children (GNCGeneralSearch *gsl, + const char *label, + gboolean text_editable, + GNCIdTypeConst type, + QofBook *book) +{ + GtkListStore * list_store; + QueryNew * q; + GtkTreeIter iter; + GList * list, * it; + GtkEntryCompletion *completion; + + /* Add a text entry box */ gsl->entry = gtk_entry_new (); - gtk_editable_set_editable (GTK_EDITABLE (gsl->entry), FALSE); + if (!text_editable) + gtk_editable_set_editable (GTK_EDITABLE (gsl->entry), FALSE); gtk_box_pack_start (GTK_BOX (gsl), gsl->entry, TRUE, TRUE, 0); + + + /* Setup a GtkEntryCompletion auxiliary widget for our Entry box + * This requires an internal table ("model") with the possible + * auto-completion text entries */ + + /* Query for the requested object type */ + q = qof_query_create_for (type); + qof_query_add_boolean_match(q, g_slist_prepend + (NULL, QOF_PARAM_ACTIVE), TRUE, QOF_QUERY_AND); + qof_query_set_book (q, book); + list = qof_query_run(q); + + /* Setup the internal model */ + list_store = gtk_list_store_new (GSL_N_COLUMNS, G_TYPE_STRING, G_TYPE_OBJECT); + for (it = list; it != NULL ; it = it->next) + { + char * name; + + name = g_strdup(qof_object_printable(type, it->data)); + /* Add a new row to the model */ + if (name) + { + gtk_list_store_append (list_store, &iter); + gtk_list_store_set (list_store, &iter, + GSL_COLUMN_TEXT, name, + GSL_COLUMN_QOFOBJECT, G_OBJECT(it->data), + -1); + g_free(name); + } + + } + + gncQueryDestroy(q); + + /* Add the GtkEntryCompletion widget */ + completion = gtk_entry_completion_new(); + gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(list_store)); + gtk_entry_completion_set_text_column(completion, 0); + gtk_entry_completion_set_inline_completion(completion, TRUE); + gtk_entry_set_completion(GTK_ENTRY(gsl->entry), completion); + + g_signal_connect (G_OBJECT (completion), "match_selected", + G_CALLBACK (gnc_gsl_match_selected_cb), gsl); + g_signal_connect (G_OBJECT (gsl->entry), "focus-out-event", + G_CALLBACK (gnc_gsl_focus_out_cb), gsl); + + g_object_unref(completion); gtk_widget_show (gsl->entry); + /* Add the search button */ gsl->button = gtk_button_new_with_label (label); gtk_box_pack_start (GTK_BOX (gsl), gsl->button, FALSE, FALSE, 0); g_signal_connect (G_OBJECT (gsl->button), "clicked", @@ -275,13 +446,30 @@ create_children (GNCGeneralSearch *gsl, const char *label) * gnc_general_search_new: * * Creates a new GNCGeneralSearch widget which can be used to provide - * an easy way to choose selections + * an easy way to choose selections. * - * Returns a GNCGeneralSearch widget. + * @param type The type of object that this widget will be used for. + * This parameter is a GNCIdTypeConst. + * @param label The label for the GtkButton child widget. + * @param text_editable switch to enable or disable direct text entry + * @param search_cb The callback function to use when an object has been + * selected in the search dialog. This dialog is created when clicking on + * the GtkButton child widget. + * @param user_data Generic pointer to context relevant data that can be + * used by callback functions later on. At present, depending on the context + * this can be a QofBook, a GncISI structure or a InvoiceWindow structure. + * @param book Pointer to the QofBook for this search widget. This is used for + * the autocompletion in the text entry widget. + * + * @return a GNCGeneralSearch widget. */ GtkWidget * -gnc_general_search_new (GNCIdTypeConst type, const char *label, - GNCSearchCB search_cb, gpointer user_data) +gnc_general_search_new (GNCIdTypeConst type, + const char *label, + gboolean text_editable, + GNCSearchCB search_cb, + gpointer user_data, + QofBook *book) { GNCGeneralSearch *gsl; GNCGeneralSearchPrivate *priv; @@ -294,7 +482,7 @@ gnc_general_search_new (GNCIdTypeConst type, const char *label, gsl = g_object_new (GNC_TYPE_GENERAL_SEARCH, NULL); - create_children (gsl, label); + create_children (gsl, label, text_editable, type, book); priv = _PRIVATE(gsl); priv->type = type; @@ -328,10 +516,10 @@ gnc_general_search_set_selected (GNCGeneralSearch *gsl, gpointer selection) priv = _PRIVATE(gsl); if (selection != gsl->selected_item) { gsl->selected_item = selection; - reset_selection_text (gsl); g_signal_emit(gsl, general_search_signals[SELECTION_CHANGED], 0); } + reset_selection_text (gsl); gnc_gui_component_clear_watches (priv->component_id); diff --git a/src/gnome-search/gnc-general-search.h b/src/gnome-search/gnc-general-search.h index 3f2c30326b..e323ee5a98 100644 --- a/src/gnome-search/gnc-general-search.h +++ b/src/gnome-search/gnc-general-search.h @@ -74,8 +74,10 @@ typedef struct { GtkWidget *gnc_general_search_new (GNCIdTypeConst type, const char *label, + gboolean text_editable, GNCSearchCB search_cb, - gpointer user_data); + gpointer user_data, + QofBook *book); void gnc_general_search_allow_clear (GNCGeneralSearch *gsl, gboolean allow_clear);