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