[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
Re: gEDA-user: command autocomplete (gtk)
This one should be a complete autocomplete implementation,
including history callback. I would like some feedback
before commiting anything.
diff --git a/src/hid/common/actions.c b/src/hid/common/actions.c
index 43c50f8..f511cc4 100644
--- a/src/hid/common/actions.c
+++ b/src/hid/common/actions.c
@@ -37,6 +37,19 @@ check_action_name (const char *s)
return NULL;
}
+/* Accessors needed by autocomplete list */
+int
+get_n_actions (void)
+{
+ return n_actions;
+}
+
+HID_Action **
+get_all_actions (void)
+{
+ return all_actions;
+}
+
void
hid_register_actions (HID_Action * a, int n)
{
diff --git a/src/hid/common/actions.h b/src/hid/common/actions.h
index 1cc6f24..a90d923 100644
--- a/src/hid/common/actions.h
+++ b/src/hid/common/actions.h
@@ -3,5 +3,7 @@
#define __HID_ACTIONS_INCLUDED__
void print_actions (void);
+HID_Action **get_all_actions (void);
+int get_n_actions (void);
#endif
diff --git a/src/hid/gtk/gui-command-window.c b/src/hid/gtk/gui-command-window.c
index b08836e..f95febf 100644
--- a/src/hid/gtk/gui-command-window.c
+++ b/src/hid/gtk/gui-command-window.c
@@ -35,6 +35,7 @@
#include "gui.h"
#include <gdk/gdkkeysyms.h>
+#include "hid/common/actions.h"
#include "command.h"
#include "crosshair.h"
@@ -46,10 +47,20 @@ RCSID ("$Id$");
static GtkWidget *command_window;
static GtkWidget *combo_vbox;
-static GList *history_list;
static gchar *command_entered;
static GMainLoop *loop;
+#define AUTO_MAX_HEIGHT 250
+static GtkListStore *auto_complete_list;
+static GtkTreeModel *auto_complete;
+static gint auto_row_height;
+static bool is_auto_complete_change;
+static bool is_history_change;
+static const char *auto_complete_prefix;
+
+static GList *history;
+static GList *history_cursor;
+
/* gui-command-window.c provides two interfaces for getting user input
| for executing a command.
@@ -135,60 +146,65 @@ static gchar *command_ref_text[] = {
N_("\tLoad a vendor file. If 'filename' omitted, pop up file select dialog.\n"),
};
-
- /* Put an allocated string on the history list and combo text list
- | if it is not a duplicate. The history_list is just a shadow of the
- | combo list, but I think is needed because I don't see an api for reading
- | the combo strings. The combo box strings take "const gchar *", so the
- | same allocated string can go in both the history list and the combo list.
- | If removed from both lists, a string can be freed.
- */
-static void
-command_history_add (gchar * cmd)
+static gboolean
+autocomplete_visible_func (GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
{
- GList *list;
- gchar *s;
- gint i;
-
- if (!cmd || !*cmd)
- return;
+ /* Visible if row is non-empty and first column is "HI" */
+ gchar *str;
+ gboolean visible = FALSE;
- /* Check for a duplicate command. If found, move it to the
- | top of the list and similarly modify the combo box strings.
- */
- for (i = 0, list = history_list; list; list = list->next, ++i)
- {
- s = (gchar *) list->data;
- if (!strcmp (cmd, s))
- {
- history_list = g_list_remove (history_list, s);
- history_list = g_list_prepend (history_list, s);
- gtk_combo_box_remove_text (GTK_COMBO_BOX
- (ghidgui->command_combo_box), i);
- gtk_combo_box_prepend_text (GTK_COMBO_BOX
- (ghidgui->command_combo_box), s);
- return;
- }
- }
+ gtk_tree_model_get (model, iter, 0, &str, -1);
+ if (str && auto_complete_prefix &&
+ strncasecmp (str, auto_complete_prefix, strlen (auto_complete_prefix)) == 0)
+ visible = TRUE;
+ g_free (str);
- /* Not a duplicate, so put first in history list and combo box text list.
- */
- s = g_strdup (cmd);
- history_list = g_list_prepend (history_list, s);
- gtk_combo_box_prepend_text (GTK_COMBO_BOX (ghidgui->command_combo_box), s);
+ return visible;
+}
- /* And keep the lists trimmed!
- */
- if (g_list_length (history_list) > ghidgui->history_size)
+static GtkTreeModel *
+create_autocomplete_model (void)
+{
+ int i;
+ int n = get_n_actions ();
+ HID_Action **actions = get_all_actions ();
+ GtkTreeIter iter;
+
+ auto_complete_prefix = NULL;
+ auto_complete_list = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
+ auto_complete = gtk_tree_model_filter_new (GTK_TREE_MODEL (auto_complete_list), NULL);
+ gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (auto_complete),
+ autocomplete_visible_func, NULL, NULL);
+ /* First entry will be filled with textentry contents */
+ gtk_list_store_append (auto_complete_list, &iter);
+ gtk_list_store_set (auto_complete_list, &iter, 0, "", 1, "", -1);
+ for (i = 0; i < n; ++i)
{
- s = (gchar *) g_list_nth_data (history_list, ghidgui->history_size);
- history_list = g_list_remove (history_list, s);
- gtk_combo_box_remove_text (GTK_COMBO_BOX (ghidgui->command_combo_box),
- ghidgui->history_size);
- g_free (s);
+ gtk_list_store_append (auto_complete_list, &iter);
+ gtk_list_store_set (auto_complete_list, &iter,
+ 0, actions[i]->name,
+ 1, actions[i]->description,
+ -1);
}
+
+ return GTK_TREE_MODEL (auto_complete);
}
+/* Populates the autocomplete model with all actions
+ * that start with prefix; returns the number of actions
+ * added. */
+static int
+populate_autocomplete_model (const char *prefix)
+{
+ GtkTreeIter first;
+ auto_complete_prefix = prefix;
+
+ gtk_tree_model_get_iter_first (GTK_TREE_MODEL (auto_complete_list), &first);
+ gtk_list_store_set (auto_complete_list, &first, 0, prefix, 1, "", -1);
+
+ gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (auto_complete));
+ return 1;
+}
/* Called when user hits "Enter" key in command entry. The action to take
| depends on where the combo box is. If it's in the command window, we can
@@ -202,12 +218,11 @@ command_entry_activate_cb (GtkWidget * widget, gpointer data)
{
gchar *command;
- command =
- g_strdup (ghid_entry_get_text (GTK_WIDGET (ghidgui->command_entry)));
- gtk_entry_set_text (ghidgui->command_entry, "");
-
+ command = g_strdup (ghid_entry_get_text (ghidgui->command_entry));
+ gtk_entry_set_text (GTK_ENTRY (ghidgui->command_entry), "");
if (*command)
- command_history_add (command);
+ history = g_list_prepend (history, g_strdup (command));
+ history_cursor = history;
if (ghidgui->use_command_window)
{
@@ -222,29 +237,209 @@ command_entry_activate_cb (GtkWidget * widget, gpointer data)
}
}
- /* Create the command_combo_box. Called once, either by
+static gboolean
+command_changed_cb (GtkEntry *entry, GtkTreeSelection *select)
+{
+ gchar *text;
+ gint x, y, height;
+ GtkAllocation allocation;
+
+ text = ghid_entry_get_text (GTK_WIDGET (entry));
+
+ /* Copy width, origin from text entry */
+ gtk_widget_get_allocation (ghidgui->command_entry, &allocation);
+ gdk_window_get_origin (gtk_widget_get_window (ghidgui->command_entry), &x, &y);
+
+ if (is_auto_complete_change)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (ghidgui->command_entry), -1);
+ return FALSE;
+ }
+ else if (is_history_change)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (ghidgui->command_entry), -1);
+ return FALSE;
+ }
+
+ if (text)
+ {
+ int n;
+ if (*text)
+ populate_autocomplete_model (text);
+ else
+ populate_autocomplete_model (NULL);
+ n = gtk_tree_model_iter_n_children (auto_complete, NULL);
+ if (*text && n > 1)
+ {
+ GtkTreePath *first = gtk_tree_path_new_first ();
+ height = MIN (AUTO_MAX_HEIGHT, n * auto_row_height);
+ /* Position and show autocomplete */
+ gtk_widget_set_size_request (ghidgui->command_hist_window, allocation.width, height);
+ gtk_window_resize (GTK_WINDOW (ghidgui->command_hist_window), allocation.width, height);
+ gtk_window_move (GTK_WINDOW (ghidgui->command_hist_window), x, y - height);
+ gtk_widget_show_all (ghidgui->command_hist_window);
+ /* Select first entry so that up/down can work cleanly. */
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ first, NULL, FALSE);
+ }
+ else
+ gtk_widget_hide (ghidgui->command_hist_window);
+ }
+ else
+ gtk_widget_hide (ghidgui->command_hist_window);
+
+ return FALSE;
+}
+
+static void
+tree_selection_changed_cb (GtkTreeSelection *selection, gpointer data)
+{
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+ gchar *text;
+
+ if (gtk_tree_selection_get_selected (selection, &model, &iter))
+ {
+ is_auto_complete_change = true;
+ gtk_tree_model_get (model, &iter, 0, &text, -1);
+ gtk_entry_set_text (GTK_ENTRY (ghidgui->command_entry), text);
+ is_auto_complete_change = false;
+ g_free (text);
+ }
+}
+
+static gboolean
+command_keyboard_cb (GtkWidget * widget, GdkEventKey * kev, gpointer data)
+{
+ GtkTreePath *path;
+ gint ksym = kev->keyval;
+ gchar *text = ghid_entry_get_text (GTK_WIDGET (ghidgui->command_entry));
+
+ if (ksym == GDK_Tab)
+ ksym = (kev->state & GDK_SHIFT_MASK) ? GDK_Up : GDK_Down;
+ if (ksym == GDK_ISO_Left_Tab)
+ ksym = GDK_Up;
+
+ /* Up and down scroll the autocomplete list, unless this list
+ * is empty. In this case they scroll the history. */
+ switch (ksym)
+ {
+ case GDK_Up:
+ if (*text == '\0' || history_cursor != history)
+ {
+ if (history_cursor)
+ {
+ is_history_change = true;
+ gtk_entry_set_text (GTK_ENTRY (ghidgui->command_entry), history_cursor->data);
+ if (history_cursor->next)
+ history_cursor = history_cursor->next;
+ is_history_change = false;
+ }
+ }
+ else if (auto_complete_prefix)
+ {
+ gtk_tree_view_get_cursor (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ &path, NULL);
+ gtk_tree_path_prev (path);
+ if (gtk_tree_row_reference_new (GTK_TREE_MODEL (auto_complete), path) != NULL)
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ path, NULL, FALSE);
+ }
+ return TRUE;
+ case GDK_Down:
+ if (history_cursor != history)
+ {
+ if (history_cursor)
+ {
+ is_history_change = true;
+ if (history_cursor->prev)
+ {
+ history_cursor = history_cursor->prev;
+ gtk_entry_set_text (GTK_ENTRY (ghidgui->command_entry), history_cursor->data);
+ }
+ is_history_change = false;
+ }
+ }
+ else if (auto_complete_prefix)
+ {
+ gtk_tree_view_get_cursor (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ &path, NULL);
+ gtk_tree_path_next (path);
+ if (gtk_tree_row_reference_new (GTK_TREE_MODEL (auto_complete), path) != NULL)
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ path, NULL, FALSE);
+ }
+ else
+ gtk_entry_set_text (GTK_ENTRY (ghidgui->command_entry), "");
+ return TRUE;
+ case GDK_Escape:
+ if (loop && g_main_loop_is_running (loop)) /* should always be */
+ g_main_loop_quit (loop);
+ history_cursor = history;
+ command_entered = NULL; /* We are aborting */
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+ /* Create the command_entry. Called once, either by
| ghid_command_window_show() or ghid_command_entry_get(). Then as long as
- | ghidgui->use_command_window is TRUE, the command_combo_box will live
+ | ghidgui->use_command_window is TRUE, the command_enty will live
| in a command window vbox or float if the command window is not up.
- | But if ghidgui->use_command_window is FALSE, the command_combo_box
+ | But if ghidgui->use_command_window is FALSE, the command_entry
| will live in the status_line_hbox either shown or hidden.
- | Since it's never destroyed, the combo history strings never need
- | rebuilding and history is maintained if the combo box location is moved.
*/
static void
-command_combo_box_entry_create (void)
+command_text_entry_create (void)
{
- ghidgui->command_combo_box = gtk_combo_box_entry_new_text ();
- ghidgui->command_entry =
- GTK_ENTRY (GTK_BIN (ghidgui->command_combo_box)->child);
-
- gtk_entry_set_width_chars (ghidgui->command_entry, 40);
- gtk_entry_set_activates_default (ghidgui->command_entry, TRUE);
+ GtkWidget *scroll;
+ GtkAdjustment *hadj, *vadj;
+ GtkTreeSelection *select;
+ GtkCellRenderer *renderer1 = gtk_cell_renderer_text_new ();
+ GtkCellRenderer *renderer2 = gtk_cell_renderer_text_new ();
+
+ /* Build textbox and autocomplete popup */
+ ghidgui->command_entry = gtk_entry_new ();
+ ghidgui->command_hist_window = gtk_window_new (GTK_WINDOW_POPUP);
+ ghidgui->command_hist_tree_view = gtk_tree_view_new ();
+
+ hadj = gtk_tree_view_get_hadjustment (GTK_TREE_VIEW (ghidgui->command_hist_tree_view));
+ vadj = gtk_tree_view_get_vadjustment (GTK_TREE_VIEW (ghidgui->command_hist_tree_view));
+ scroll = gtk_scrolled_window_new (hadj, vadj);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
+
+ gtk_container_add (GTK_CONTAINER (ghidgui->command_hist_window), scroll);
+ gtk_container_add (GTK_CONTAINER (scroll), ghidgui->command_hist_tree_view);
+
+ /* Configure treeview for minimalist look */
+ gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scroll), GTK_SHADOW_OUT);
+ gtk_cell_renderer_set_padding (renderer2, 15, 0);
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ FALSE);
+ gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ -1, "", renderer1, "text", 0, NULL);
+ gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ -1, "", renderer2, "text", 1, NULL);
+ gtk_cell_renderer_get_size (renderer1, ghidgui->command_hist_tree_view,
+ NULL, NULL, NULL, NULL, &auto_row_height);
+
+ gtk_entry_set_activates_default (GTK_ENTRY (ghidgui->command_entry), TRUE);
+
+ /* Hookup autocomplete model */
+ gtk_tree_view_set_model (GTK_TREE_VIEW (ghidgui->command_hist_tree_view),
+ create_autocomplete_model());
+ select = gtk_tree_view_get_selection (GTK_TREE_VIEW (ghidgui->command_hist_tree_view));
+ gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE);
g_signal_connect (G_OBJECT (ghidgui->command_entry), "activate",
G_CALLBACK (command_entry_activate_cb), NULL);
+ g_signal_connect (G_OBJECT (ghidgui->command_entry), "changed",
+ G_CALLBACK (command_changed_cb), select);
+ g_signal_connect (G_OBJECT (select), "changed",
+ G_CALLBACK (tree_selection_changed_cb), NULL);
- g_object_ref (G_OBJECT (ghidgui->command_combo_box)); /* so can move it */
+ g_object_ref (G_OBJECT (ghidgui->command_entry)); /* so can move it */
}
static void
@@ -253,7 +448,7 @@ command_window_close_cb (void)
if (command_window)
{
gtk_container_remove (GTK_CONTAINER (combo_vbox), /* Float it */
- ghidgui->command_combo_box);
+ ghidgui->command_entry);
gtk_widget_destroy (command_window);
}
combo_vbox = NULL;
@@ -267,7 +462,7 @@ command_destroy_cb (GtkWidget * widget, gpointer data)
}
/* If ghidgui->use_command_window toggles, the config code calls
- | this to ensure the command_combo_box is set up for living in the
+ | this to ensure the command_entry is set up for living in the
| right place.
*/
void
@@ -276,22 +471,22 @@ ghid_command_use_command_window_sync (void)
/* The combo box will be NULL and not living anywhere until the
| first command entry.
*/
- if (!ghidgui->command_combo_box)
+ if (!ghidgui->command_entry)
return;
if (ghidgui->use_command_window)
gtk_container_remove (GTK_CONTAINER (ghidgui->status_line_hbox),
- ghidgui->command_combo_box);
+ ghidgui->command_entry);
else
{
- /* Destroy the window (if it's up) which floats the command_combo_box
+ /* Destroy the window (if it's up) which floats the command_entry
| so we can pack it back into the status line hbox. If the window
- | wasn't up, the command_combo_box was already floating.
+ | wasn't up, the command_entry was already floating.
*/
command_window_close_cb ();
- gtk_widget_hide (ghidgui->command_combo_box);
+ gtk_widget_hide (ghidgui->command_entry);
gtk_box_pack_start (GTK_BOX (ghidgui->status_line_hbox),
- ghidgui->command_combo_box, FALSE, FALSE, 0);
+ ghidgui->command_entry, FALSE, FALSE, 5);
}
}
@@ -321,10 +516,10 @@ ghid_command_window_show (gboolean raise)
gtk_container_set_border_width (GTK_CONTAINER (vbox), 6);
gtk_container_add (GTK_CONTAINER (command_window), vbox);
- if (!ghidgui->command_combo_box)
- command_combo_box_entry_create ();
+ if (!ghidgui->command_entry)
+ command_text_entry_create ();
- gtk_box_pack_start (GTK_BOX (vbox), ghidgui->command_combo_box,
+ gtk_box_pack_start (GTK_BOX (vbox), ghidgui->command_entry,
FALSE, FALSE, 0);
combo_vbox = vbox;
@@ -355,42 +550,26 @@ ghid_command_window_show (gboolean raise)
gtk_widget_show_all (command_window);
}
-
-static gboolean
-command_escape_cb (GtkWidget * widget, GdkEventKey * kev, gpointer data)
-{
- gint ksym = kev->keyval;
-
- if (ksym != GDK_Escape)
- return FALSE;
-
- if (loop && g_main_loop_is_running (loop)) /* should always be */
- g_main_loop_quit (loop);
- command_entered = NULL; /* We are aborting */
-
- return TRUE;
-}
-
-
/* This is the command entry function called from ActionCommand() when
- | ghidgui->use_command_window is FALSE. The command_combo_box is already
+ | ghidgui->use_command_window is FALSE. The command_text_entry is already
| packed into the status line label hbox in this case.
*/
gchar *
ghid_command_entry_get (gchar * prompt, gchar * command)
{
gchar *s;
- gint escape_sig_id;
+ gint keyboard_sig_id;
GHidPort *out = &ghid_port;
/* If this is the first user command entry, we have to create the
| command_combo_box and pack it into the status_line_hbox.
*/
- if (!ghidgui->command_combo_box)
+ if (!ghidgui->command_entry)
{
- command_combo_box_entry_create ();
+ command_text_entry_create ();
gtk_box_pack_start (GTK_BOX (ghidgui->status_line_hbox),
- ghidgui->command_combo_box, FALSE, FALSE, 0);
+ GTK_WIDGET (ghidgui->command_entry),
+ TRUE, TRUE, 0);
}
/* Make the prompt bold and set the label before showing the combo to
@@ -402,12 +581,12 @@ ghid_command_entry_get (gchar * prompt, gchar * command)
/* Flag so output drawing area won't try to get focus away from us and
| so resetting the status line label can be blocked when resize
- | callbacks are invokded from the resize caused by showing the combo box.
+ | callbacks are invoked (from the resize caused by showing the entry!).
*/
ghidgui->command_entry_status_line_active = TRUE;
- gtk_entry_set_text (ghidgui->command_entry, command ? command : "");
- gtk_widget_show_all (ghidgui->command_combo_box);
+ gtk_entry_set_text (GTK_ENTRY (ghidgui->command_entry), command ? command : "");
+ gtk_widget_show (ghidgui->command_entry);
/* Remove the top window accel group so keys intended for the entry
| don't get intercepted by the menu system. Set the interface
@@ -420,9 +599,9 @@ ghid_command_entry_get (gchar * prompt, gchar * command)
ghid_interface_input_signals_disconnect ();
ghid_interface_set_sensitive (FALSE);
gtk_widget_grab_focus (GTK_WIDGET (ghidgui->command_entry));
- escape_sig_id = g_signal_connect (G_OBJECT (ghidgui->command_entry),
+ keyboard_sig_id = g_signal_connect (G_OBJECT (ghidgui->command_entry),
"key_press_event",
- G_CALLBACK (command_escape_cb), NULL);
+ G_CALLBACK (command_keyboard_cb), NULL);
loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (loop);
@@ -434,7 +613,7 @@ ghid_command_entry_get (gchar * prompt, gchar * command)
/* Restore the damage we did before entering the loop.
*/
- g_signal_handler_disconnect (ghidgui->command_entry, escape_sig_id);
+ g_signal_handler_disconnect (ghidgui->command_entry, keyboard_sig_id);
ghid_interface_input_signals_connect ();
ghid_interface_set_sensitive (TRUE);
gtk_window_add_accel_group (GTK_WINDOW (out->top_window),
@@ -443,7 +622,8 @@ ghid_command_entry_get (gchar * prompt, gchar * command)
/* Restore the status line label and give focus back to the drawing area
*/
- gtk_widget_hide (ghidgui->command_combo_box);
+ gtk_widget_hide (ghidgui->command_hist_window);
+ gtk_widget_hide (ghidgui->command_entry);
gtk_widget_grab_focus (out->drawing_area);
return command_entered;
@@ -460,7 +640,7 @@ ghid_handle_user_command (gboolean raise)
ghid_command_window_show (raise);
else
{
- command = ghid_command_entry_get (_("Enter command:"),
+ command = ghid_command_entry_get (_("Command:"),
(Settings.SaveLastCommand && previous) ? previous : (gchar *)"");
if (command != NULL)
{
diff --git a/src/hid/gtk/gui.h b/src/hid/gtk/gui.h
index 31ee636..2e1bc40 100644
--- a/src/hid/gtk/gui.h
+++ b/src/hid/gtk/gui.h
@@ -106,7 +106,7 @@ typedef struct
*cursor_position_relative_label,
*cursor_position_absolute_label,
*grid_units_label, *status_line_hbox, *command_combo_box;
- GtkEntry *command_entry;
+ GtkWidget *command_entry, *command_hist_window, *command_hist_tree_view;
GtkWidget *top_hbox,
*menu_hbox, *compact_vbox, *compact_hbox, *position_hbox, *label_hbox,
_______________________________________________
geda-user mailing list
geda-user@xxxxxxxxxxxxxx
http://www.seul.org/cgi-bin/mailman/listinfo/geda-user