1 /*
2  *
3  *  Copyright (C) 2012  Colomban Wendling <ban@herbesfolles.org>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #include "config.h"
21 
22 #include <string.h>
23 #include <glib.h>
24 #include <glib/gi18n-lib.h>
25 #include <gtk/gtk.h>
26 #include <gdk/gdkkeysyms.h>
27 
28 #include <geanyplugin.h>
29 
30 
31 /* uncomment to display each row score (for debugging sort) */
32 /*#define DISPLAY_SCORE 1*/
33 
34 
35 GeanyPlugin      *geany_plugin;
36 GeanyData        *geany_data;
37 
38 PLUGIN_VERSION_CHECK(226)
39 
40 PLUGIN_SET_TRANSLATABLE_INFO (
41   LOCALEDIR, GETTEXT_PACKAGE,
42   _("Commander"),
43   _("Provides a command panel for quick access to actions, files and more"),
44   VERSION,
45   "Colomban Wendling <ban@herbesfolles.org>"
46 )
47 
48 
49 /* GTK compatibility functions/macros */
50 
51 #if ! GTK_CHECK_VERSION (2, 18, 0)
52 # define gtk_widget_get_visible(w) \
53   (GTK_WIDGET_VISIBLE (w))
54 # define gtk_widget_set_can_focus(w, v)               \
55   G_STMT_START {                                      \
56     GtkWidget *widget = (w);                          \
57     if (v) {                                          \
58       GTK_WIDGET_SET_FLAGS (widget, GTK_CAN_FOCUS);   \
59     } else {                                          \
60       GTK_WIDGET_UNSET_FLAGS (widget, GTK_CAN_FOCUS); \
61     }                                                 \
62   } G_STMT_END
63 #endif
64 
65 #if ! GTK_CHECK_VERSION (2, 21, 8)
66 # define GDK_KEY_Down       GDK_Down
67 # define GDK_KEY_Escape     GDK_Escape
68 # define GDK_KEY_ISO_Enter  GDK_ISO_Enter
69 # define GDK_KEY_KP_Enter   GDK_KP_Enter
70 # define GDK_KEY_Page_Down  GDK_Page_Down
71 # define GDK_KEY_Page_Up    GDK_Page_Up
72 # define GDK_KEY_Return     GDK_Return
73 # define GDK_KEY_Tab        GDK_Tab
74 # define GDK_KEY_Up         GDK_Up
75 #endif
76 
77 
78 /* Plugin */
79 
80 enum {
81   KB_SHOW_PANEL,
82   KB_SHOW_PANEL_COMMANDS,
83   KB_SHOW_PANEL_FILES,
84   KB_COUNT
85 };
86 
87 struct {
88   GtkWidget    *panel;
89   GtkWidget    *entry;
90   GtkWidget    *view;
91   GtkListStore *store;
92   GtkTreeModel *sort;
93 
94   GtkTreePath  *last_path;
95 } plugin_data = {
96   NULL, NULL, NULL,
97   NULL, NULL,
98   NULL
99 };
100 
101 typedef enum {
102   COL_TYPE_MENU_ITEM  = 1 << 0,
103   COL_TYPE_FILE       = 1 << 1,
104   COL_TYPE_ANY        = 0xffff
105 } ColType;
106 
107 enum {
108   COL_LABEL,
109   COL_PATH,
110   COL_TYPE,
111   COL_WIDGET,
112   COL_DOCUMENT,
113   COL_COUNT
114 };
115 
116 
117 #define PATH_SEPARATOR " \342\206\222 " /* right arrow */
118 
119 #define SEPARATORS        " -_./\\\"'"
120 #define IS_SEPARATOR(c)   (strchr (SEPARATORS, (c)) != NULL)
121 #define next_separator(p) (strpbrk (p, SEPARATORS))
122 
123 /* TODO: be more tolerant regarding unmatched character in the needle.
124  * Right now, we implicitly accept unmatched characters at the end of the
125  * needle but absolutely not at the start.  e.g. "xpy" won't match "python" at
126  * all, though "pyx" will. */
127 static inline gint
get_score(const gchar * needle,const gchar * haystack)128 get_score (const gchar *needle,
129            const gchar *haystack)
130 {
131   if (! needle || ! haystack) {
132     return needle == NULL;
133   } else if (! *needle || ! *haystack) {
134     return *needle == 0;
135   }
136 
137   if (IS_SEPARATOR (*haystack)) {
138     return get_score (needle + IS_SEPARATOR (*needle), haystack + 1);
139   }
140 
141   if (IS_SEPARATOR (*needle)) {
142     return get_score (needle + 1, next_separator (haystack));
143   }
144 
145   if (*needle == *haystack) {
146     gint a = get_score (needle + 1, haystack + 1) + 1 + IS_SEPARATOR (haystack[1]);
147     gint b = get_score (needle, next_separator (haystack));
148 
149     return MAX (a, b);
150   } else {
151     return get_score (needle, next_separator (haystack));
152   }
153 }
154 
155 static const gchar *
path_basename(const gchar * path)156 path_basename (const gchar *path)
157 {
158   const gchar *p1 = strrchr (path, '/');
159   const gchar *p2 = g_strrstr (path, PATH_SEPARATOR);
160 
161   if (! p1 && ! p2) {
162     return path;
163   } else if (p1 > p2) {
164     return p1;
165   } else {
166     return p2;
167   }
168 }
169 
170 static gint
key_score(const gchar * key_,const gchar * text_)171 key_score (const gchar *key_,
172            const gchar *text_)
173 {
174   gchar  *text  = g_utf8_casefold (text_, -1);
175   gchar  *key   = g_utf8_casefold (key_, -1);
176   gint    score;
177 
178   score = get_score (key, text) + get_score (key, path_basename (text)) / 2;
179 
180   g_free (text);
181   g_free (key);
182 
183   return score;
184 }
185 
186 static const gchar *
get_key(gint * type_)187 get_key (gint *type_)
188 {
189   gint          type  = COL_TYPE_ANY;
190   const gchar  *key   = gtk_entry_get_text (GTK_ENTRY (plugin_data.entry));
191 
192   if (g_str_has_prefix (key, "f:")) {
193     key += 2;
194     type = COL_TYPE_FILE;
195   } else if (g_str_has_prefix (key, "c:")) {
196     key += 2;
197     type = COL_TYPE_MENU_ITEM;
198   }
199 
200   if (type_) {
201     *type_ = type;
202   }
203 
204   return key;
205 }
206 
207 static void
tree_view_set_cursor_from_iter(GtkTreeView * view,GtkTreeIter * iter)208 tree_view_set_cursor_from_iter (GtkTreeView *view,
209                                 GtkTreeIter *iter)
210 {
211   GtkTreePath *path;
212 
213   path = gtk_tree_model_get_path (gtk_tree_view_get_model (view), iter);
214   gtk_tree_view_set_cursor (view, path, NULL, FALSE);
215   gtk_tree_path_free (path);
216 }
217 
218 static void
tree_view_move_focus(GtkTreeView * view,GtkMovementStep step,gint amount)219 tree_view_move_focus (GtkTreeView    *view,
220                       GtkMovementStep step,
221                       gint            amount)
222 {
223   GtkTreeIter   iter;
224   GtkTreePath  *path;
225   GtkTreeModel *model = gtk_tree_view_get_model (view);
226   gboolean      valid = FALSE;
227 
228   gtk_tree_view_get_cursor (view, &path, NULL);
229   if (! path) {
230     valid = gtk_tree_model_get_iter_first (model, &iter);
231   } else {
232     switch (step) {
233       case GTK_MOVEMENT_BUFFER_ENDS:
234         valid = gtk_tree_model_get_iter_first (model, &iter);
235         if (valid && amount > 0) {
236           GtkTreeIter prev;
237 
238           do {
239             prev = iter;
240           } while (gtk_tree_model_iter_next (model, &iter));
241           iter = prev;
242         }
243         break;
244 
245       case GTK_MOVEMENT_PAGES:
246         /* FIXME: move by page */
247       case GTK_MOVEMENT_DISPLAY_LINES:
248         gtk_tree_model_get_iter (model, &iter, path);
249         if (amount > 0) {
250           while ((valid = gtk_tree_model_iter_next (model, &iter)) &&
251                  --amount > 0)
252             ;
253         } else if (amount < 0) {
254           while ((valid = gtk_tree_path_prev (path)) && --amount > 0)
255             ;
256 
257           if (valid) {
258             gtk_tree_model_get_iter (model, &iter, path);
259           }
260         }
261         break;
262 
263       default:
264         g_assert_not_reached ();
265     }
266     gtk_tree_path_free (path);
267   }
268 
269   if (valid) {
270     tree_view_set_cursor_from_iter (view, &iter);
271   } else {
272     gtk_widget_error_bell (GTK_WIDGET (view));
273   }
274 }
275 
276 static void
tree_view_activate_focused_row(GtkTreeView * view)277 tree_view_activate_focused_row (GtkTreeView *view)
278 {
279   GtkTreePath        *path;
280   GtkTreeViewColumn  *column;
281 
282   gtk_tree_view_get_cursor (view, &path, &column);
283   if (path) {
284     gtk_tree_view_row_activated (view, path, column);
285     gtk_tree_path_free (path);
286   }
287 }
288 
289 static void
store_populate_menu_items(GtkListStore * store,GtkMenuShell * menu,const gchar * parent_path)290 store_populate_menu_items (GtkListStore  *store,
291                            GtkMenuShell  *menu,
292                            const gchar   *parent_path)
293 {
294   GList  *children;
295   GList  *node;
296 
297   children = gtk_container_get_children (GTK_CONTAINER (menu));
298   for (node = children; node; node = node->next) {
299     if (GTK_IS_SEPARATOR_MENU_ITEM (node->data) ||
300         ! gtk_widget_get_visible (node->data)) {
301       /* skip that */
302     } else if (GTK_IS_MENU_ITEM (node->data)) {
303       GtkWidget    *submenu;
304       gchar        *path;
305       gchar        *item_label;
306       gboolean      use_underline;
307       GtkStockItem  item;
308 
309       if (GTK_IS_IMAGE_MENU_ITEM (node->data) &&
310           gtk_image_menu_item_get_use_stock (node->data) &&
311           gtk_stock_lookup (gtk_menu_item_get_label (node->data), &item)) {
312         item_label = g_strdup (item.label);
313         use_underline = TRUE;
314       } else {
315         item_label = g_strdup (gtk_menu_item_get_label (node->data));
316         use_underline = gtk_menu_item_get_use_underline (node->data);
317       }
318 
319       /* remove underlines */
320       if (use_underline) {
321         gchar  *p   = item_label;
322         gsize   len = strlen (p);
323 
324         while ((p = strchr (p, '_')) != NULL) {
325           len -= (gsize) (p - item_label);
326 
327           memmove (p, p + 1, len);
328         }
329       }
330 
331       if (parent_path) {
332         path = g_strconcat (parent_path, PATH_SEPARATOR, item_label, NULL);
333       } else {
334         path = g_strdup (item_label);
335       }
336 
337       submenu = gtk_menu_item_get_submenu (node->data);
338       if (submenu) {
339         /* go deeper in the menus... */
340         store_populate_menu_items (store, GTK_MENU_SHELL (submenu), path);
341       } else {
342         gchar *tmp;
343         gchar *tooltip;
344         gchar *label = g_markup_printf_escaped ("<big>%s</big>", item_label);
345 
346         tooltip = gtk_widget_get_tooltip_markup (node->data);
347         if (tooltip) {
348           SETPTR (label, g_strconcat (label, "\n<small>", tooltip, "</small>", NULL));
349           g_free (tooltip);
350         }
351 
352         tmp = g_markup_escape_text (path, -1);
353         SETPTR (label, g_strconcat (label, "\n<small><i>", tmp, "</i></small>", NULL));
354         g_free (tmp);
355 
356         gtk_list_store_insert_with_values (store, NULL, -1,
357                                            COL_LABEL, label,
358                                            COL_PATH, path,
359                                            COL_TYPE, COL_TYPE_MENU_ITEM,
360                                            COL_WIDGET, node->data,
361                                            -1);
362 
363         g_free (label);
364       }
365 
366       g_free (item_label);
367       g_free (path);
368     } else {
369       g_warning ("Unknown widget type in the menu: %s",
370                  G_OBJECT_TYPE_NAME (node->data));
371     }
372   }
373   g_list_free (children);
374 }
375 
376 static GtkWidget *
find_menubar(GtkContainer * container)377 find_menubar (GtkContainer *container)
378 {
379   GList      *children;
380   GList      *node;
381   GtkWidget  *menubar = NULL;
382 
383   children = gtk_container_get_children (container);
384   for (node = children; ! menubar && node; node = node->next) {
385     if (GTK_IS_MENU_BAR (node->data)) {
386       menubar = node->data;
387     } else if (GTK_IS_CONTAINER (node->data)) {
388       menubar = find_menubar (node->data);
389     }
390   }
391   g_list_free (children);
392 
393   return menubar;
394 }
395 
396 static void
fill_store(GtkListStore * store)397 fill_store (GtkListStore *store)
398 {
399   GtkWidget  *menubar;
400   guint       i = 0;
401 
402   /* menu items */
403   menubar = find_menubar (GTK_CONTAINER (geany_data->main_widgets->window));
404   store_populate_menu_items (store, GTK_MENU_SHELL (menubar), NULL);
405 
406   /* open files */
407   foreach_document (i) {
408     gchar *basename = g_path_get_basename (DOC_FILENAME (documents[i]));
409     gchar *label = g_markup_printf_escaped ("<big>%s</big>\n"
410                                             "<small><i>%s</i></small>",
411                                             basename,
412                                             DOC_FILENAME (documents[i]));
413 
414     gtk_list_store_insert_with_values (store, NULL, -1,
415                                        COL_LABEL, label,
416                                        COL_PATH, DOC_FILENAME (documents[i]),
417                                        COL_TYPE, COL_TYPE_FILE,
418                                        COL_DOCUMENT, documents[i],
419                                        -1);
420     g_free (basename);
421     g_free (label);
422   }
423 }
424 
425 static gint
sort_func(GtkTreeModel * model,GtkTreeIter * a,GtkTreeIter * b,gpointer dummy)426 sort_func (GtkTreeModel  *model,
427            GtkTreeIter   *a,
428            GtkTreeIter   *b,
429            gpointer       dummy)
430 {
431   gint          scorea;
432   gint          scoreb;
433   gchar        *patha;
434   gchar        *pathb;
435   gint          typea;
436   gint          typeb;
437   gint          type;
438   const gchar  *key = get_key (&type);
439 
440   gtk_tree_model_get (model, a, COL_PATH, &patha, COL_TYPE, &typea, -1);
441   gtk_tree_model_get (model, b, COL_PATH, &pathb, COL_TYPE, &typeb, -1);
442 
443   scorea = key_score (key, patha);
444   scoreb = key_score (key, pathb);
445 
446   if (! (typea & type)) {
447     scorea -= 0xf000;
448   }
449   if (! (typeb & type)) {
450     scoreb -= 0xf000;
451   }
452 
453   g_free (patha);
454   g_free (pathb);
455 
456   return scoreb - scorea;
457 }
458 
459 static gboolean
on_panel_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer dummy)460 on_panel_key_press_event (GtkWidget    *widget,
461                           GdkEventKey  *event,
462                           gpointer      dummy)
463 {
464   switch (event->keyval) {
465     case GDK_KEY_Escape:
466       gtk_widget_hide (widget);
467       return TRUE;
468 
469     case GDK_KEY_Tab:
470       /* avoid leaving the entry */
471       return TRUE;
472 
473     case GDK_KEY_Return:
474     case GDK_KEY_KP_Enter:
475     case GDK_KEY_ISO_Enter:
476       tree_view_activate_focused_row (GTK_TREE_VIEW (plugin_data.view));
477       return TRUE;
478 
479     case GDK_KEY_Page_Up:
480     case GDK_KEY_Page_Down:
481       tree_view_move_focus (GTK_TREE_VIEW (plugin_data.view),
482                             GTK_MOVEMENT_PAGES,
483                             event->keyval == GDK_KEY_Page_Up ? -1 : 1);
484       return TRUE;
485 
486     case GDK_KEY_Up:
487     case GDK_KEY_Down: {
488       tree_view_move_focus (GTK_TREE_VIEW (plugin_data.view),
489                             GTK_MOVEMENT_DISPLAY_LINES,
490                             event->keyval == GDK_KEY_Up ? -1 : 1);
491       return TRUE;
492     }
493   }
494 
495   return FALSE;
496 }
497 
498 static void
on_entry_text_notify(GObject * object,GParamSpec * pspec,gpointer dummy)499 on_entry_text_notify (GObject    *object,
500                       GParamSpec *pspec,
501                       gpointer    dummy)
502 {
503   GtkTreeIter   iter;
504   GtkTreeView  *view  = GTK_TREE_VIEW (plugin_data.view);
505   GtkTreeModel *model = gtk_tree_view_get_model (view);
506 
507   /* we force re-sorting the whole model from how it was before, and the
508    * back to the new filter.  this is somewhat hackish but since we don't
509    * know the original sorting order, and GtkTreeSortable don't have a
510    * resort() API anyway. */
511   gtk_tree_model_sort_reset_default_sort_func (GTK_TREE_MODEL_SORT (model));
512   gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (model),
513                                            sort_func, NULL, NULL);
514 
515   if (gtk_tree_model_get_iter_first (model, &iter)) {
516     tree_view_set_cursor_from_iter (view, &iter);
517   }
518 }
519 
520 static void
on_entry_activate(GtkEntry * entry,gpointer dummy)521 on_entry_activate (GtkEntry  *entry,
522                    gpointer   dummy)
523 {
524   tree_view_activate_focused_row (GTK_TREE_VIEW (plugin_data.view));
525 }
526 
527 static void
on_panel_hide(GtkWidget * widget,gpointer dummy)528 on_panel_hide (GtkWidget *widget,
529                gpointer   dummy)
530 {
531   GtkTreeView  *view = GTK_TREE_VIEW (plugin_data.view);
532 
533   if (plugin_data.last_path) {
534     gtk_tree_path_free (plugin_data.last_path);
535     plugin_data.last_path = NULL;
536   }
537   gtk_tree_view_get_cursor (view, &plugin_data.last_path, NULL);
538 
539   gtk_list_store_clear (plugin_data.store);
540 }
541 
542 static void
on_panel_show(GtkWidget * widget,gpointer dummy)543 on_panel_show (GtkWidget *widget,
544                gpointer   dummy)
545 {
546   GtkTreePath *path;
547   GtkTreeView *view = GTK_TREE_VIEW (plugin_data.view);
548 
549   fill_store (plugin_data.store);
550 
551   gtk_widget_grab_focus (plugin_data.entry);
552 
553   if (plugin_data.last_path) {
554     gtk_tree_view_set_cursor (view, plugin_data.last_path, NULL, FALSE);
555     gtk_tree_view_scroll_to_cell (view, plugin_data.last_path, NULL,
556                                   TRUE, 0.5, 0.5);
557   }
558   /* make sure the cursor is set (e.g. if plugin_data.last_path wasn't valid) */
559   gtk_tree_view_get_cursor (view, &path, NULL);
560   if (path) {
561     gtk_tree_path_free (path);
562   } else {
563     GtkTreeIter iter;
564 
565     if (gtk_tree_model_get_iter_first (gtk_tree_view_get_model (view), &iter)) {
566       tree_view_set_cursor_from_iter (GTK_TREE_VIEW (plugin_data.view), &iter);
567     }
568   }
569 }
570 
571 static void
on_view_row_activated(GtkTreeView * view,GtkTreePath * path,GtkTreeViewColumn * column,gpointer dummy)572 on_view_row_activated (GtkTreeView       *view,
573                        GtkTreePath       *path,
574                        GtkTreeViewColumn *column,
575                        gpointer           dummy)
576 {
577   GtkTreeModel *model = gtk_tree_view_get_model (view);
578   GtkTreeIter   iter;
579 
580   if (gtk_tree_model_get_iter (model, &iter, path)) {
581     gint type;
582 
583     gtk_tree_model_get (model, &iter, COL_TYPE, &type, -1);
584 
585     switch (type) {
586       case COL_TYPE_FILE: {
587         GeanyDocument  *doc;
588         gint            page;
589 
590         gtk_tree_model_get (model, &iter, COL_DOCUMENT, &doc, -1);
591         page = document_get_notebook_page (doc);
592         gtk_notebook_set_current_page (GTK_NOTEBOOK (geany_data->main_widgets->notebook),
593                                        page);
594         break;
595       }
596 
597       case COL_TYPE_MENU_ITEM: {
598         GtkMenuItem *item;
599 
600         gtk_tree_model_get (model, &iter, COL_WIDGET, &item, -1);
601         gtk_menu_item_activate (item);
602         g_object_unref (item);
603 
604         break;
605       }
606     }
607     gtk_widget_hide (plugin_data.panel);
608   }
609 }
610 
611 #ifdef DISPLAY_SCORE
612 static void
score_cell_data(GtkTreeViewColumn * column,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer col)613 score_cell_data (GtkTreeViewColumn *column,
614                  GtkCellRenderer   *cell,
615                  GtkTreeModel      *model,
616                  GtkTreeIter       *iter,
617                  gpointer           col)
618 {
619   gint          score;
620   gchar        *text;
621   gchar        *path;
622   gint          pathtype;
623   gint          type;
624   gint          width, old_width;
625   const gchar  *key = get_key (&type);
626 
627   gtk_tree_model_get (model, iter, COL_PATH, &path, COL_TYPE, &pathtype, -1);
628 
629   score = key_score (key, path);
630   if (! (pathtype & type)) {
631     score -= 0xf000;
632   }
633 
634   text = g_strdup_printf ("%d", score);
635   g_object_set (cell, "text", text, NULL);
636 
637   /* automatic column sizing is buggy, so just make an acceptable wild guess */
638   width = 8 + strlen (text) * 10;
639   old_width = gtk_tree_view_column_get_fixed_width (col);
640   if (old_width < width) {
641     gtk_tree_view_column_set_fixed_width (col, width);
642   }
643 
644   g_free (text);
645   g_free (path);
646 }
647 #endif
648 
649 static void
create_panel(void)650 create_panel (void)
651 {
652   GtkWidget          *frame;
653   GtkWidget          *box;
654   GtkWidget          *scroll;
655   GtkTreeViewColumn  *col;
656   GtkCellRenderer    *cell;
657 
658   plugin_data.panel = g_object_new (GTK_TYPE_WINDOW,
659                                     "decorated", FALSE,
660                                     "default-width", 500,
661                                     "default-height", 200,
662                                     "transient-for", geany_data->main_widgets->window,
663                                     "window-position", GTK_WIN_POS_CENTER_ON_PARENT,
664                                     "type-hint", GDK_WINDOW_TYPE_HINT_DIALOG,
665                                     "skip-taskbar-hint", TRUE,
666                                     "skip-pager-hint", TRUE,
667                                     NULL);
668   g_signal_connect (plugin_data.panel, "focus-out-event",
669                     G_CALLBACK (gtk_widget_hide), NULL);
670   g_signal_connect (plugin_data.panel, "show",
671                     G_CALLBACK (on_panel_show), NULL);
672   g_signal_connect (plugin_data.panel, "hide",
673                     G_CALLBACK (on_panel_hide), NULL);
674   g_signal_connect (plugin_data.panel, "key-press-event",
675                     G_CALLBACK (on_panel_key_press_event), NULL);
676 
677   frame = gtk_frame_new (NULL);
678   gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
679   gtk_container_add (GTK_CONTAINER (plugin_data.panel), frame);
680 
681   box = gtk_vbox_new (FALSE, 0);
682   gtk_container_add (GTK_CONTAINER (frame), box);
683 
684   plugin_data.entry = gtk_entry_new ();
685   gtk_box_pack_start (GTK_BOX (box), plugin_data.entry, FALSE, TRUE, 0);
686 
687   plugin_data.store = gtk_list_store_new (COL_COUNT,
688                                           G_TYPE_STRING,
689                                           G_TYPE_STRING,
690                                           G_TYPE_INT,
691                                           GTK_TYPE_WIDGET,
692                                           G_TYPE_POINTER);
693 
694   plugin_data.sort = gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (plugin_data.store));
695   gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (plugin_data.sort),
696                                         GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID,
697                                         GTK_SORT_ASCENDING);
698 
699   scroll = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
700                          "hscrollbar-policy", GTK_POLICY_AUTOMATIC,
701                          "vscrollbar-policy", GTK_POLICY_AUTOMATIC,
702                          NULL);
703   gtk_box_pack_start (GTK_BOX (box), scroll, TRUE, TRUE, 0);
704 
705   plugin_data.view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (plugin_data.sort));
706   gtk_widget_set_can_focus (plugin_data.view, FALSE);
707   gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (plugin_data.view), FALSE);
708 #ifdef DISPLAY_SCORE
709   cell = gtk_cell_renderer_text_new ();
710   col = gtk_tree_view_column_new_with_attributes (NULL, cell, NULL);
711   gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED);
712   gtk_tree_view_column_set_cell_data_func(col, cell, score_cell_data, col, NULL);
713   gtk_tree_view_append_column (GTK_TREE_VIEW (plugin_data.view), col);
714 #endif
715   cell = gtk_cell_renderer_text_new ();
716   g_object_set (cell, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
717   col = gtk_tree_view_column_new_with_attributes (NULL, cell,
718                                                   "markup", COL_LABEL,
719                                                   NULL);
720   gtk_tree_view_append_column (GTK_TREE_VIEW (plugin_data.view), col);
721   g_signal_connect (plugin_data.view, "row-activated",
722                     G_CALLBACK (on_view_row_activated), NULL);
723   gtk_container_add (GTK_CONTAINER (scroll), plugin_data.view);
724 
725   /* connect entry signals after the view is created as they use it */
726   g_signal_connect (plugin_data.entry, "notify::text",
727                     G_CALLBACK (on_entry_text_notify), NULL);
728   g_signal_connect (plugin_data.entry, "activate",
729                     G_CALLBACK (on_entry_activate), NULL);
730 
731   gtk_widget_show_all (frame);
732 }
733 
734 static gboolean
on_kb_show_panel(GeanyKeyBinding * kb,guint key_id,gpointer data)735 on_kb_show_panel (GeanyKeyBinding  *kb,
736                   guint             key_id,
737                   gpointer          data)
738 {
739   const gchar *prefix = data;
740 
741   gtk_widget_show (plugin_data.panel);
742 
743   if (prefix) {
744     const gchar *key = gtk_entry_get_text (GTK_ENTRY (plugin_data.entry));
745 
746     if (! g_str_has_prefix (key, prefix)) {
747       gtk_entry_set_text (GTK_ENTRY (plugin_data.entry), prefix);
748     }
749     /* select the non-prefix part */
750     gtk_editable_select_region (GTK_EDITABLE (plugin_data.entry),
751                                 g_utf8_strlen (prefix, -1), -1);
752   }
753 
754   return TRUE;
755 }
756 
757 static gboolean
on_plugin_idle_init(gpointer dummy)758 on_plugin_idle_init (gpointer dummy)
759 {
760   create_panel ();
761 
762   return FALSE;
763 }
764 
765 void
plugin_init(GeanyData * data)766 plugin_init (GeanyData *data)
767 {
768   GeanyKeyGroup *group;
769 
770   group = plugin_set_key_group (geany_plugin, "commander", KB_COUNT, NULL);
771   keybindings_set_item_full (group, KB_SHOW_PANEL, 0, 0, "show_panel",
772                              _("Show Command Panel"), NULL,
773                              on_kb_show_panel, NULL, NULL);
774   keybindings_set_item_full (group, KB_SHOW_PANEL_COMMANDS, 0, 0,
775                              "show_panel_commands",
776                              _("Show Command Panel (Commands Only)"), NULL,
777                              on_kb_show_panel, (gpointer) "c:", NULL);
778   keybindings_set_item_full (group, KB_SHOW_PANEL_FILES, 0, 0,
779                              "show_panel_files",
780                              _("Show Command Panel (Files Only)"), NULL,
781                              on_kb_show_panel, (gpointer) "f:", NULL);
782 
783   /* delay for other plugins to have a chance to load before, so we will
784    * include their items */
785   plugin_idle_add (geany_plugin, on_plugin_idle_init, NULL);
786 }
787 
788 void
plugin_cleanup(void)789 plugin_cleanup (void)
790 {
791   if (plugin_data.panel) {
792     gtk_widget_destroy (plugin_data.panel);
793   }
794   if (plugin_data.last_path) {
795     gtk_tree_path_free (plugin_data.last_path);
796   }
797 }
798 
799 void
plugin_help(void)800 plugin_help (void)
801 {
802   utils_open_browser (DOCDIR "/" PLUGIN "/README");
803 }
804