Merge pull request #3823 from ntrel/msg-tree-search
[geany-mirror.git] / plugins / filebrowser.c
blob5358bb2b5f3824a1cacb4418178cab338f697a43
1 /*
2 * filebrowser.c - this file is part of Geany, a fast and lightweight IDE
4 * Copyright 2007 The Geany contributors
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 /* Sidebar file browser plugin. */
23 #ifdef HAVE_CONFIG_H
24 # include "config.h"
25 #endif
27 #include "geanyplugin.h"
28 #include "gtkcompat.h"
29 #include <string.h>
31 #include <gdk/gdkkeysyms.h>
33 #ifdef G_OS_WIN32
34 # include <windows.h>
36 # define OPEN_CMD "explorer \"%d\""
37 #elif defined(__APPLE__)
38 # define OPEN_CMD "open \"%d\""
39 #else
40 # define OPEN_CMD "xdg-open \"%d\""
41 #endif
43 GeanyPlugin *geany_plugin;
44 GeanyData *geany_data;
47 PLUGIN_VERSION_CHECK(GEANY_API_VERSION)
49 PLUGIN_SET_INFO(_("File Browser"), _("Adds a file browser tab to the sidebar."),
50 PACKAGE_VERSION, _("The Geany developer team"))
53 /* Keybinding(s) */
54 enum
56 KB_FOCUS_FILE_LIST,
57 KB_FOCUS_PATH_ENTRY,
58 KB_COUNT
62 enum
64 FILEVIEW_COLUMN_ICON = 0,
65 FILEVIEW_COLUMN_NAME,
66 FILEVIEW_COLUMN_FILENAME, /* the full filename, including path for display as tooltip */
67 FILEVIEW_COLUMN_IS_DIR,
68 FILEVIEW_N_COLUMNS
71 static gboolean fb_set_project_base_path = FALSE;
72 static gboolean fb_follow_path = FALSE;
73 static gboolean show_hidden_files = FALSE;
74 static gboolean hide_object_files = TRUE;
76 static GtkWidget *file_view_vbox;
77 static GtkWidget *file_view;
78 static GtkListStore *file_store;
79 static GtkTreeIter *last_dir_iter = NULL;
80 static GtkEntryCompletion *entry_completion = NULL;
82 static GtkWidget *filter_combo;
83 static GtkWidget *filter_entry;
84 static GtkWidget *path_combo;
85 static GtkWidget *path_entry;
86 static gchar *current_dir = NULL; /* in locale-encoding */
87 static gchar *open_cmd; /* in locale-encoding */
88 static gchar *config_file;
89 static gchar **filter = NULL;
90 static gchar *hidden_file_extensions = NULL;
92 static gint page_number = 0;
94 static struct
96 GtkWidget *open;
97 GtkWidget *open_external;
98 GtkWidget *find_in_files;
99 GtkWidget *show_hidden_files;
100 } popup_items;
103 static void project_open_cb(GObject *obj, GKeyFile *config, gpointer data);
105 /* note: other callbacks connected in plugin_init */
106 PluginCallback plugin_callbacks[] =
108 { "project-open", (GCallback) &project_open_cb, TRUE, NULL },
109 { NULL, NULL, FALSE, NULL }
113 #ifdef G_OS_WIN32
114 static gboolean win32_check_hidden(const gchar *filename)
116 DWORD attrs;
117 static wchar_t w_filename[MAX_PATH];
118 MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename));
119 attrs = GetFileAttributesW(w_filename);
120 if (attrs != INVALID_FILE_ATTRIBUTES && attrs & FILE_ATTRIBUTE_HIDDEN)
121 return TRUE;
122 return FALSE;
124 #endif
127 /* Returns: whether name should be hidden. */
128 static gboolean check_hidden(const gchar *filename, const gchar *base_name)
130 gsize len;
132 #ifdef G_OS_WIN32
133 if (win32_check_hidden(filename))
134 return TRUE;
135 #else
136 if (base_name[0] == '.')
137 return TRUE;
138 #endif
140 len = strlen(base_name);
141 return base_name[len - 1] == '~';
145 static gboolean check_object(const gchar *base_name)
147 gboolean ret = FALSE;
148 gchar **ptr;
149 gchar **exts = g_strsplit(hidden_file_extensions, " ", -1);
151 foreach_strv(ptr, exts)
153 if (g_str_has_suffix(base_name, *ptr))
155 ret = TRUE;
156 break;
159 g_strfreev(exts);
160 return ret;
164 /* Returns: whether filename should be removed. */
165 static gboolean check_filtered(const gchar *base_name)
167 gchar **filter_item;
169 if (filter == NULL)
170 return FALSE;
172 foreach_strv(filter_item, filter)
174 if (utils_str_equal(*filter_item, "*") || g_pattern_match_simple(*filter_item, base_name))
176 return FALSE;
179 return TRUE;
183 static GIcon *get_icon(const gchar *fname)
185 GIcon *icon = NULL;
186 gchar *ctype;
188 ctype = g_content_type_guess(fname, NULL, 0, NULL);
190 if (ctype)
192 icon = g_content_type_get_icon(ctype);
193 if (icon)
195 GtkIconInfo *icon_info;
197 icon_info = gtk_icon_theme_lookup_by_gicon(gtk_icon_theme_get_default(), icon, 16, 0);
198 if (!icon_info)
200 g_object_unref(icon);
201 icon = NULL;
203 else
204 gtk_icon_info_free(icon_info);
206 g_free(ctype);
209 if (!icon)
210 icon = g_themed_icon_new("text-x-generic");
212 return icon;
216 /* name is in locale encoding */
217 static void add_item(const gchar *name)
219 GtkTreeIter iter;
220 gchar *fname, *utf8_name, *utf8_fullname;
221 const gchar *sep;
222 gboolean dir;
223 GIcon *icon;
225 if (G_UNLIKELY(EMPTY(name)))
226 return;
228 /* root directory doesn't need separator */
229 sep = (utils_str_equal(current_dir, "/")) ? "" : G_DIR_SEPARATOR_S;
230 fname = g_strconcat(current_dir, sep, name, NULL);
231 dir = g_file_test(fname, G_FILE_TEST_IS_DIR);
232 utf8_fullname = utils_get_utf8_from_locale(fname);
233 utf8_name = utils_get_utf8_from_locale(name);
234 g_free(fname);
236 if (! show_hidden_files && check_hidden(utf8_fullname, utf8_name))
237 goto done;
239 if (dir)
241 if (last_dir_iter == NULL)
242 gtk_list_store_prepend(file_store, &iter);
243 else
245 gtk_list_store_insert_after(file_store, &iter, last_dir_iter);
246 gtk_tree_iter_free(last_dir_iter);
248 last_dir_iter = gtk_tree_iter_copy(&iter);
250 else
252 if (! show_hidden_files && hide_object_files && check_object(utf8_name))
253 goto done;
254 if (check_filtered(utf8_name))
255 goto done;
257 gtk_list_store_append(file_store, &iter);
260 icon = dir ? g_themed_icon_new("folder") : get_icon(utf8_name);
261 gtk_list_store_set(file_store, &iter,
262 FILEVIEW_COLUMN_ICON, icon,
263 FILEVIEW_COLUMN_NAME, utf8_name,
264 FILEVIEW_COLUMN_FILENAME, utf8_fullname,
265 FILEVIEW_COLUMN_IS_DIR, dir,
266 -1);
267 g_object_unref(icon);
268 done:
269 g_free(utf8_name);
270 g_free(utf8_fullname);
274 /* adds ".." to the start of the file list */
275 static void add_top_level_entry(void)
277 GtkTreeIter iter;
278 gchar *utf8_dir;
279 GIcon *icon;
281 if (EMPTY(g_path_skip_root(current_dir)))
282 return; /* ignore 'C:\' or '/' */
284 utf8_dir = g_path_get_dirname(current_dir);
285 SETPTR(utf8_dir, utils_get_utf8_from_locale(utf8_dir));
287 gtk_list_store_prepend(file_store, &iter);
288 last_dir_iter = gtk_tree_iter_copy(&iter);
290 icon = g_themed_icon_new("folder");
291 gtk_list_store_set(file_store, &iter,
292 FILEVIEW_COLUMN_ICON, icon,
293 FILEVIEW_COLUMN_NAME, "..",
294 FILEVIEW_COLUMN_FILENAME, utf8_dir,
295 FILEVIEW_COLUMN_IS_DIR, TRUE,
296 -1);
297 g_object_unref(icon);
298 g_free(utf8_dir);
302 static void clear(void)
304 gtk_list_store_clear(file_store);
306 /* reset the directory item pointer */
307 if (last_dir_iter != NULL)
308 gtk_tree_iter_free(last_dir_iter);
309 last_dir_iter = NULL;
313 /* recreate the tree model from current_dir. */
314 static void refresh(void)
316 gchar *utf8_dir;
317 GSList *list, *node;
319 /* don't clear when the new path doesn't exist */
320 if (! g_file_test(current_dir, G_FILE_TEST_EXISTS))
321 return;
323 clear();
325 utf8_dir = utils_get_utf8_from_locale(current_dir);
326 gtk_entry_set_text(GTK_ENTRY(path_entry), utf8_dir);
327 gtk_widget_set_tooltip_text(path_entry, utf8_dir);
328 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(path_combo), utf8_dir, 0);
329 g_free(utf8_dir);
331 add_top_level_entry(); /* ".." item */
333 list = utils_get_file_list(current_dir, NULL, NULL);
334 if (list != NULL)
336 /* free filenames as we go through the list */
337 foreach_slist(node, list)
339 gchar *fname = node->data;
341 add_item(fname);
342 g_free(fname);
344 g_slist_free(list);
346 gtk_entry_completion_set_model(entry_completion, GTK_TREE_MODEL(file_store));
350 static void on_go_home(void)
352 SETPTR(current_dir, g_strdup(g_get_home_dir()));
353 refresh();
357 /* TODO: use utils_get_default_dir_utf8() */
358 static gchar *get_default_dir(void)
360 const gchar *dir = NULL;
361 GeanyProject *project = geany->app->project;
363 if (project)
364 dir = project->base_path;
365 else
366 dir = geany->prefs->default_open_path;
368 if (!EMPTY(dir))
369 return utils_get_locale_from_utf8(dir);
371 return g_get_current_dir();
375 static void on_current_path(void)
377 gchar *fname;
378 gchar *dir;
379 GeanyDocument *doc = document_get_current();
381 if (doc == NULL || doc->file_name == NULL || ! g_path_is_absolute(doc->file_name))
383 SETPTR(current_dir, get_default_dir());
384 refresh();
385 return;
387 fname = doc->file_name;
388 fname = utils_get_locale_from_utf8(fname);
389 dir = g_path_get_dirname(fname);
390 g_free(fname);
392 SETPTR(current_dir, dir);
393 refresh();
397 static void on_realized(void)
399 GeanyProject *project = geany->app->project;
401 /* if fb_set_project_base_path and project open, the path has already been set */
402 if (! fb_set_project_base_path || project == NULL || EMPTY(project->base_path))
403 on_current_path();
407 static void on_go_up(void)
409 gsize len = strlen(current_dir);
410 if (current_dir[len-1] == G_DIR_SEPARATOR)
411 current_dir[len-1] = '\0';
412 /* remove the highest directory part (which becomes the basename of current_dir) */
413 SETPTR(current_dir, g_path_get_dirname(current_dir));
414 refresh();
418 static gboolean check_single_selection(GtkTreeSelection *treesel)
420 if (gtk_tree_selection_count_selected_rows(treesel) == 1)
421 return TRUE;
423 ui_set_statusbar(FALSE, _("Too many items selected!"));
424 return FALSE;
428 /* Returns: TRUE if at least one of selected_items is a folder. */
429 static gboolean is_folder_selected(GList *selected_items)
431 GList *item;
432 GtkTreeModel *model = GTK_TREE_MODEL(file_store);
433 gboolean dir_found = FALSE;
435 for (item = selected_items; item != NULL; item = g_list_next(item))
437 GtkTreeIter iter;
438 GtkTreePath *treepath;
440 treepath = (GtkTreePath*) item->data;
441 gtk_tree_model_get_iter(model, &iter, treepath);
442 gtk_tree_model_get(model, &iter, FILEVIEW_COLUMN_IS_DIR, &dir_found, -1);
444 if (dir_found)
445 break;
447 return dir_found;
451 /* Returns: the full filename in locale encoding. */
452 static gchar *get_tree_path_filename(GtkTreePath *treepath)
454 GtkTreeModel *model = GTK_TREE_MODEL(file_store);
455 GtkTreeIter iter;
456 gchar *name, *fname;
458 gtk_tree_model_get_iter(model, &iter, treepath);
459 gtk_tree_model_get(model, &iter, FILEVIEW_COLUMN_FILENAME, &name, -1);
461 fname = utils_get_locale_from_utf8(name);
462 g_free(name);
464 return fname;
468 static void open_external(const gchar *fname, gboolean dir_found)
470 gchar *cmd;
471 gchar *locale_cmd;
472 gchar *dir;
473 GString *cmd_str = g_string_new(open_cmd);
474 GError *error = NULL;
476 if (! dir_found)
477 dir = g_path_get_dirname(fname);
478 else
479 dir = g_strdup(fname);
481 utils_string_replace_all(cmd_str, "%f", fname);
482 utils_string_replace_all(cmd_str, "%d", dir);
484 cmd = g_string_free(cmd_str, FALSE);
485 locale_cmd = utils_get_locale_from_utf8(cmd);
486 if (! spawn_async(NULL, locale_cmd, NULL, NULL, NULL, &error))
488 gchar *c = strchr(cmd, ' ');
490 if (c != NULL)
491 *c = '\0';
492 ui_set_statusbar(TRUE,
493 _("Could not execute configured external command '%s' (%s)."),
494 cmd, error->message);
495 g_error_free(error);
497 g_free(locale_cmd);
498 g_free(cmd);
499 g_free(dir);
503 static void on_external_open(GtkMenuItem *menuitem, gpointer user_data)
505 GtkTreeSelection *treesel;
506 GtkTreeModel *model;
507 GList *list;
508 gboolean dir_found;
510 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
512 list = gtk_tree_selection_get_selected_rows(treesel, &model);
513 dir_found = is_folder_selected(list);
515 if (! dir_found || check_single_selection(treesel))
517 GList *item;
519 for (item = list; item != NULL; item = g_list_next(item))
521 GtkTreePath *treepath = item->data;
522 gchar *fname = get_tree_path_filename(treepath);
524 open_external(fname, dir_found);
525 g_free(fname);
529 g_list_free_full(list, (GDestroyNotify) gtk_tree_path_free);
533 /* We use document_open_files() as it's more efficient. */
534 static void open_selected_files(GList *list, gboolean do_not_focus)
536 GSList *files = NULL;
537 GList *item;
538 GeanyDocument *doc;
540 for (item = list; item != NULL; item = g_list_next(item))
542 GtkTreePath *treepath = item->data;
543 gchar *fname = get_tree_path_filename(treepath);
545 files = g_slist_prepend(files, fname);
547 files = g_slist_reverse(files);
548 document_open_files(files, FALSE, NULL, NULL);
549 doc = document_get_current();
550 if (doc != NULL && ! do_not_focus)
551 keybindings_send_command(GEANY_KEY_GROUP_FOCUS, GEANY_KEYS_FOCUS_EDITOR);
553 g_slist_free_full(files, g_free);
557 static void open_folder(GtkTreePath *treepath)
559 gchar *fname = get_tree_path_filename(treepath);
561 SETPTR(current_dir, fname);
562 refresh();
566 static void on_open_clicked(GtkMenuItem *menuitem, gpointer user_data)
568 GtkTreeSelection *treesel;
569 GtkTreeModel *model;
570 GList *list;
571 gboolean dir_found;
573 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
575 list = gtk_tree_selection_get_selected_rows(treesel, &model);
576 dir_found = is_folder_selected(list);
578 if (dir_found)
580 if (check_single_selection(treesel))
582 GtkTreePath *treepath = list->data; /* first selected item */
584 open_folder(treepath);
587 else
588 open_selected_files(list, GPOINTER_TO_INT(user_data));
590 g_list_free_full(list, (GDestroyNotify) gtk_tree_path_free);
594 static void on_find_in_files(GtkMenuItem *menuitem, gpointer user_data)
596 GtkTreeSelection *treesel;
597 GtkTreeModel *model;
598 GList *list;
599 gchar *dir;
600 gboolean is_dir = FALSE;
602 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
603 /* allow 0 or 1 selections */
604 if (gtk_tree_selection_count_selected_rows(treesel) > 0 &&
605 ! check_single_selection(treesel))
606 return;
608 list = gtk_tree_selection_get_selected_rows(treesel, &model);
609 is_dir = is_folder_selected(list);
611 if (is_dir)
613 GtkTreePath *treepath = list->data; /* first selected item */
615 dir = get_tree_path_filename(treepath);
617 else
618 dir = g_strdup(current_dir);
620 g_list_free_full(list, (GDestroyNotify) gtk_tree_path_free);
622 SETPTR(dir, utils_get_utf8_from_locale(dir));
623 search_show_find_in_files_dialog(dir);
624 g_free(dir);
628 static void on_hidden_files_clicked(GtkCheckMenuItem *item)
630 show_hidden_files = gtk_check_menu_item_get_active(item);
631 refresh();
635 static void on_hide_sidebar(void)
637 keybindings_send_command(GEANY_KEY_GROUP_VIEW, GEANY_KEYS_VIEW_SIDEBAR);
641 static void on_show_preferences(void)
643 plugin_show_configure(geany_plugin);
647 static GtkWidget *create_popup_menu(void)
649 GtkWidget *item, *menu;
651 menu = gtk_menu_new();
653 item = ui_image_menu_item_new(GTK_STOCK_OPEN, _("Open in _Geany"));
654 gtk_widget_show(item);
655 gtk_container_add(GTK_CONTAINER(menu), item);
656 g_signal_connect(item, "activate", G_CALLBACK(on_open_clicked), NULL);
657 popup_items.open = item;
659 item = ui_image_menu_item_new(GTK_STOCK_OPEN, _("Open _Externally"));
660 gtk_widget_show(item);
661 gtk_container_add(GTK_CONTAINER(menu), item);
662 g_signal_connect(item, "activate", G_CALLBACK(on_external_open), NULL);
663 popup_items.open_external = item;
665 item = gtk_separator_menu_item_new();
666 gtk_widget_show(item);
667 gtk_container_add(GTK_CONTAINER(menu), item);
669 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_REFRESH, NULL);
670 gtk_widget_show(item);
671 gtk_container_add(GTK_CONTAINER(menu), item);
672 g_signal_connect(item, "activate", G_CALLBACK(refresh), NULL);
674 item = ui_image_menu_item_new(GTK_STOCK_FIND, _("_Find in Files..."));
675 gtk_widget_show(item);
676 gtk_container_add(GTK_CONTAINER(menu), item);
677 g_signal_connect(item, "activate", G_CALLBACK(on_find_in_files), NULL);
678 popup_items.find_in_files = item;
680 item = gtk_separator_menu_item_new();
681 gtk_widget_show(item);
682 gtk_container_add(GTK_CONTAINER(menu), item);
684 item = gtk_check_menu_item_new_with_mnemonic(_("Show _Hidden Files"));
685 gtk_widget_show(item);
686 gtk_container_add(GTK_CONTAINER(menu), item);
687 g_signal_connect(item, "activate", G_CALLBACK(on_hidden_files_clicked), NULL);
688 popup_items.show_hidden_files = item;
690 item = gtk_separator_menu_item_new();
691 gtk_widget_show(item);
692 gtk_container_add(GTK_CONTAINER(menu), item);
694 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_PREFERENCES, NULL);
695 gtk_widget_show(item);
696 gtk_container_add(GTK_CONTAINER(menu), item);
697 g_signal_connect(item, "activate", G_CALLBACK(on_show_preferences), NULL);
699 item = gtk_separator_menu_item_new();
700 gtk_widget_show(item);
701 gtk_container_add(GTK_CONTAINER(menu), item);
703 item = ui_image_menu_item_new(GTK_STOCK_CLOSE, _("H_ide Sidebar"));
704 gtk_widget_show(item);
705 gtk_container_add(GTK_CONTAINER(menu), item);
706 g_signal_connect(item, "activate", G_CALLBACK(on_hide_sidebar), NULL);
708 return menu;
712 static void on_tree_selection_changed(GtkTreeSelection *selection, gpointer data)
714 gboolean have_sel = (gtk_tree_selection_count_selected_rows(selection) > 0);
715 gboolean multi_sel = (gtk_tree_selection_count_selected_rows(selection) > 1);
717 if (popup_items.open != NULL)
718 gtk_widget_set_sensitive(popup_items.open, have_sel);
719 if (popup_items.open_external != NULL)
720 gtk_widget_set_sensitive(popup_items.open_external, have_sel);
721 if (popup_items.find_in_files != NULL)
722 gtk_widget_set_sensitive(popup_items.find_in_files, have_sel && ! multi_sel);
726 static gboolean on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
728 if (event->button == 1 && event->type == GDK_2BUTTON_PRESS)
730 on_open_clicked(NULL, NULL);
731 return TRUE;
733 else if (event->button == 3)
735 static GtkWidget *popup_menu = NULL;
737 if (popup_menu == NULL)
738 popup_menu = create_popup_menu();
740 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(popup_items.show_hidden_files),
741 show_hidden_files);
742 gtk_menu_popup_at_pointer(GTK_MENU(popup_menu), (GdkEvent *) event);
743 /* don't return TRUE here, unless the selection won't be changed */
745 return FALSE;
749 static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
751 if (ui_is_keyval_enter_or_return(event->keyval))
753 on_open_clicked(NULL, NULL);
754 return TRUE;
757 if (event->keyval == GDK_KEY_space)
759 on_open_clicked(NULL, GINT_TO_POINTER(TRUE));
760 return TRUE;
763 if (( (event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up) && (event->state & GDK_MOD1_MASK)) || /* FIXME: Alt-Up doesn't seem to work! */
764 (event->keyval == GDK_KEY_BackSpace) )
766 on_go_up();
767 return TRUE;
770 if ((event->keyval == GDK_KEY_F10 && event->state & GDK_SHIFT_MASK) || event->keyval == GDK_KEY_Menu)
772 GdkEventButton button_event;
774 button_event.time = event->time;
775 button_event.button = 3;
777 on_button_press(widget, &button_event, data);
778 return TRUE;
781 return FALSE;
785 static void clear_filter(void)
787 if (filter != NULL)
789 g_strfreev(filter);
790 filter = NULL;
795 static void on_clear_filter(GtkEntry *entry, gpointer user_data)
797 clear_filter();
799 gtk_entry_set_text(GTK_ENTRY(filter_entry), "");
801 refresh();
805 static void on_path_entry_activate(GtkEntry *entry, gpointer user_data)
807 gchar *new_dir = (gchar*) gtk_entry_get_text(entry);
809 if (!EMPTY(new_dir))
811 if (g_str_has_suffix(new_dir, ".."))
813 on_go_up();
814 return;
816 else if (new_dir[0] == '~')
818 GString *str = g_string_new(new_dir);
819 utils_string_replace_first(str, "~", g_get_home_dir());
820 new_dir = g_string_free(str, FALSE);
822 else
823 new_dir = utils_get_locale_from_utf8(new_dir);
825 else
826 new_dir = g_strdup(g_get_home_dir());
828 SETPTR(current_dir, new_dir);
830 on_clear_filter(NULL, NULL);
834 static void ui_combo_box_changed(GtkComboBox *combo, gpointer user_data)
836 /* we get this callback on typing as well as choosing an item */
837 if (gtk_combo_box_get_active(combo) >= 0)
838 gtk_widget_activate(gtk_bin_get_child(GTK_BIN(combo)));
842 static void on_filter_activate(GtkEntry *entry, gpointer user_data)
844 /* We use spaces for consistency with Find in Files file patterns
845 * ';' also supported like original patch. */
846 filter = g_strsplit_set(gtk_entry_get_text(entry), "; ", -1);
847 if (filter == NULL || g_strv_length(filter) == 0)
849 clear_filter();
851 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(filter_combo), NULL, 0);
852 refresh();
856 static void on_filter_clear(GtkEntry *entry, gint icon_pos,
857 GdkEvent *event, gpointer data)
859 clear_filter();
860 refresh();
864 static void prepare_file_view(void)
866 GtkCellRenderer *text_renderer, *icon_renderer;
867 GtkTreeViewColumn *column;
868 GtkTreeSelection *selection;
870 file_store = gtk_list_store_new(FILEVIEW_N_COLUMNS, G_TYPE_ICON, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
872 gtk_tree_view_set_model(GTK_TREE_VIEW(file_view), GTK_TREE_MODEL(file_store));
873 g_object_unref(file_store);
875 icon_renderer = gtk_cell_renderer_pixbuf_new();
876 text_renderer = gtk_cell_renderer_text_new();
877 column = gtk_tree_view_column_new();
878 gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
879 gtk_tree_view_column_set_attributes(column, icon_renderer, "gicon", FILEVIEW_COLUMN_ICON, NULL);
880 gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
881 gtk_tree_view_column_set_attributes(column, text_renderer, "text", FILEVIEW_COLUMN_NAME, NULL);
882 gtk_tree_view_append_column(GTK_TREE_VIEW(file_view), column);
883 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(file_view), FALSE);
885 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(file_view), TRUE);
886 gtk_tree_view_set_search_column(GTK_TREE_VIEW(file_view), FILEVIEW_COLUMN_NAME);
888 ui_widget_modify_font_from_string(file_view, geany->interface_prefs->tagbar_font);
890 /* tooltips */
891 ui_tree_view_set_tooltip_text_column(GTK_TREE_VIEW(file_view), FILEVIEW_COLUMN_FILENAME);
893 /* selection handling */
894 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
895 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
897 /* Show the current path when the FB is first needed */
898 g_signal_connect(file_view, "realize", G_CALLBACK(on_realized), NULL);
899 g_signal_connect(selection, "changed", G_CALLBACK(on_tree_selection_changed), NULL);
900 g_signal_connect(file_view, "button-press-event", G_CALLBACK(on_button_press), NULL);
901 g_signal_connect(file_view, "key-press-event", G_CALLBACK(on_key_press), NULL);
905 static GtkWidget *make_toolbar(void)
907 GtkWidget *wid, *toolbar;
909 toolbar = gtk_toolbar_new();
910 gtk_toolbar_set_icon_size(GTK_TOOLBAR(toolbar), GTK_ICON_SIZE_MENU);
911 gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
913 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_GO_UP));
914 gtk_widget_set_tooltip_text(wid, _("Up"));
915 g_signal_connect(wid, "clicked", G_CALLBACK(on_go_up), NULL);
916 gtk_container_add(GTK_CONTAINER(toolbar), wid);
918 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_REFRESH));
919 gtk_widget_set_tooltip_text(wid, _("Refresh"));
920 g_signal_connect(wid, "clicked", G_CALLBACK(refresh), NULL);
921 gtk_container_add(GTK_CONTAINER(toolbar), wid);
923 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_HOME));
924 gtk_widget_set_tooltip_text(wid, _("Home"));
925 g_signal_connect(wid, "clicked", G_CALLBACK(on_go_home), NULL);
926 gtk_container_add(GTK_CONTAINER(toolbar), wid);
928 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_JUMP_TO));
929 gtk_widget_set_tooltip_text(wid, _("Set path from document"));
930 g_signal_connect(wid, "clicked", G_CALLBACK(on_current_path), NULL);
931 gtk_container_add(GTK_CONTAINER(toolbar), wid);
933 return toolbar;
937 static GtkWidget *make_filterbar(void)
939 GtkWidget *label, *filterbar;
941 filterbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
943 label = gtk_label_new(_("Filter:"));
945 filter_combo = gtk_combo_box_text_new_with_entry();
946 filter_entry = gtk_bin_get_child(GTK_BIN(filter_combo));
948 ui_entry_add_clear_icon(GTK_ENTRY(filter_entry));
949 g_signal_connect(filter_entry, "icon-release", G_CALLBACK(on_filter_clear), NULL);
951 gtk_widget_set_tooltip_text(filter_entry,
952 _("Filter your files with the usual wildcards. Separate multiple patterns with a space."));
953 g_signal_connect(filter_entry, "activate", G_CALLBACK(on_filter_activate), NULL);
954 g_signal_connect(filter_combo, "changed", G_CALLBACK(ui_combo_box_changed), NULL);
956 gtk_box_pack_start(GTK_BOX(filterbar), label, FALSE, FALSE, 0);
957 gtk_box_pack_start(GTK_BOX(filterbar), filter_combo, TRUE, TRUE, 0);
959 return filterbar;
963 static gboolean completion_match_func(GtkEntryCompletion *completion, const gchar *key,
964 GtkTreeIter *iter, gpointer user_data)
966 gchar *str;
967 gboolean is_dir;
968 gboolean result = FALSE;
970 gtk_tree_model_get(GTK_TREE_MODEL(file_store), iter,
971 FILEVIEW_COLUMN_IS_DIR, &is_dir, FILEVIEW_COLUMN_NAME, &str, -1);
973 if (str != NULL && is_dir && !g_str_has_suffix(key, G_DIR_SEPARATOR_S))
975 /* key is something like "/tmp/te" and str is a filename like "test",
976 * so strip the path from key to make them comparable */
977 gchar *base_name = g_path_get_basename(key);
978 gchar *str_lowered = g_utf8_strdown(str, -1);
979 result = g_str_has_prefix(str_lowered, base_name);
980 g_free(base_name);
981 g_free(str_lowered);
983 g_free(str);
985 return result;
989 static gboolean completion_match_selected(GtkEntryCompletion *widget, GtkTreeModel *model,
990 GtkTreeIter *iter, gpointer user_data)
992 gchar *str;
993 gtk_tree_model_get(model, iter, FILEVIEW_COLUMN_NAME, &str, -1);
994 if (str != NULL)
996 gchar *text = g_strconcat(current_dir, G_DIR_SEPARATOR_S, str, NULL);
997 gtk_entry_set_text(GTK_ENTRY(path_entry), text);
998 gtk_editable_set_position(GTK_EDITABLE(path_entry), -1);
999 /* force change of directory when completion is done */
1000 on_path_entry_activate(GTK_ENTRY(path_entry), NULL);
1001 g_free(text);
1003 g_free(str);
1005 return TRUE;
1009 static void completion_create(void)
1011 entry_completion = gtk_entry_completion_new();
1013 gtk_entry_completion_set_inline_completion(entry_completion, FALSE);
1014 gtk_entry_completion_set_popup_completion(entry_completion, TRUE);
1015 gtk_entry_completion_set_text_column(entry_completion, FILEVIEW_COLUMN_NAME);
1016 gtk_entry_completion_set_match_func(entry_completion, completion_match_func, NULL, NULL);
1018 g_signal_connect(entry_completion, "match-selected",
1019 G_CALLBACK(completion_match_selected), NULL);
1021 gtk_entry_set_completion(GTK_ENTRY(path_entry), entry_completion);
1025 static void load_settings(void)
1027 GKeyFile *config = g_key_file_new();
1029 config_file = g_strconcat(geany->app->configdir, G_DIR_SEPARATOR_S, "plugins", G_DIR_SEPARATOR_S,
1030 "filebrowser", G_DIR_SEPARATOR_S, "filebrowser.conf", NULL);
1031 g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
1033 open_cmd = utils_get_setting_string(config, "filebrowser", "open_command", OPEN_CMD);
1034 /* g_key_file_get_boolean defaults to FALSE */
1035 show_hidden_files = g_key_file_get_boolean(config, "filebrowser", "show_hidden_files", NULL);
1036 hide_object_files = utils_get_setting_boolean(config, "filebrowser", "hide_object_files", TRUE);
1037 hidden_file_extensions = utils_get_setting_string(config, "filebrowser", "hidden_file_extensions",
1038 ".o .obj .so .dll .a .lib .pyc");
1039 fb_follow_path = g_key_file_get_boolean(config, "filebrowser", "fb_follow_path", NULL);
1040 fb_set_project_base_path = g_key_file_get_boolean(config, "filebrowser", "fb_set_project_base_path", NULL);
1042 g_key_file_free(config);
1046 static void project_open_cb(G_GNUC_UNUSED GObject *obj, G_GNUC_UNUSED GKeyFile *config,
1047 G_GNUC_UNUSED gpointer data)
1049 gchar *new_dir;
1050 GeanyProject *project = geany->app->project;
1052 if (! fb_set_project_base_path || project == NULL || EMPTY(project->base_path))
1053 return;
1055 /* TODO this is a copy of project_get_base_path(), add it to the plugin API */
1056 if (g_path_is_absolute(project->base_path))
1057 new_dir = g_strdup(project->base_path);
1058 else
1059 { /* build base_path out of project file name's dir and base_path */
1060 gchar *dir = g_path_get_dirname(project->file_name);
1062 new_dir = g_strconcat(dir, G_DIR_SEPARATOR_S, project->base_path, NULL);
1063 g_free(dir);
1065 /* get it into locale encoding */
1066 SETPTR(new_dir, utils_get_locale_from_utf8(new_dir));
1068 if (! utils_str_equal(current_dir, new_dir))
1070 SETPTR(current_dir, new_dir);
1071 refresh();
1073 else
1074 g_free(new_dir);
1078 static gpointer last_activate_path = NULL;
1080 static void document_activate_cb(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc,
1081 G_GNUC_UNUSED gpointer data)
1083 gchar *new_dir;
1085 last_activate_path = doc->real_path;
1087 if (! fb_follow_path || doc->file_name == NULL || ! g_path_is_absolute(doc->file_name))
1088 return;
1090 new_dir = g_path_get_dirname(doc->file_name);
1091 SETPTR(new_dir, utils_get_locale_from_utf8(new_dir));
1093 if (! utils_str_equal(current_dir, new_dir))
1095 SETPTR(current_dir, new_dir);
1096 refresh();
1098 else
1099 g_free(new_dir);
1103 static void document_save_cb(GObject *obj, GeanyDocument *doc, gpointer user_data)
1105 if (!last_activate_path)
1106 document_activate_cb(obj, doc, user_data);
1110 static void kb_activate(guint key_id)
1112 gtk_notebook_set_current_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook), page_number);
1113 switch (key_id)
1115 case KB_FOCUS_FILE_LIST:
1116 gtk_widget_grab_focus(file_view);
1117 break;
1118 case KB_FOCUS_PATH_ENTRY:
1119 gtk_widget_grab_focus(path_entry);
1120 break;
1125 void plugin_init(GeanyData *data)
1127 GeanyKeyGroup *key_group;
1128 GtkWidget *scrollwin, *toolbar, *filterbar;
1130 filter = NULL;
1132 file_view_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1133 toolbar = make_toolbar();
1134 gtk_box_pack_start(GTK_BOX(file_view_vbox), toolbar, FALSE, FALSE, 0);
1136 filterbar = make_filterbar();
1137 gtk_box_pack_start(GTK_BOX(file_view_vbox), filterbar, FALSE, FALSE, 0);
1139 path_combo = gtk_combo_box_text_new_with_entry();
1140 gtk_box_pack_start(GTK_BOX(file_view_vbox), path_combo, FALSE, FALSE, 2);
1141 g_signal_connect(path_combo, "changed", G_CALLBACK(ui_combo_box_changed), NULL);
1142 path_entry = gtk_bin_get_child(GTK_BIN(path_combo));
1143 g_signal_connect(path_entry, "activate", G_CALLBACK(on_path_entry_activate), NULL);
1145 file_view = gtk_tree_view_new();
1146 prepare_file_view();
1147 completion_create();
1149 popup_items.open = popup_items.open_external = popup_items.find_in_files = NULL;
1151 scrollwin = gtk_scrolled_window_new(NULL, NULL);
1152 gtk_scrolled_window_set_policy(
1153 GTK_SCROLLED_WINDOW(scrollwin),
1154 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
1155 gtk_container_add(GTK_CONTAINER(scrollwin), file_view);
1156 gtk_box_pack_start(GTK_BOX(file_view_vbox), scrollwin, TRUE, TRUE, 0);
1158 /* load settings before file_view "realize" callback */
1159 load_settings();
1161 gtk_widget_show_all(file_view_vbox);
1162 page_number = gtk_notebook_append_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook),
1163 file_view_vbox, gtk_label_new(_("Files")));
1165 /* setup keybindings */
1166 key_group = plugin_set_key_group(geany_plugin, "file_browser", KB_COUNT, NULL);
1167 keybindings_set_item(key_group, KB_FOCUS_FILE_LIST, kb_activate,
1168 0, 0, "focus_file_list", _("Focus File List"), NULL);
1169 keybindings_set_item(key_group, KB_FOCUS_PATH_ENTRY, kb_activate,
1170 0, 0, "focus_path_entry", _("Focus Path Entry"), NULL);
1172 plugin_signal_connect(geany_plugin, NULL, "document-activate", TRUE,
1173 (GCallback) &document_activate_cb, NULL);
1174 plugin_signal_connect(geany_plugin, NULL, "document-save", TRUE,
1175 (GCallback) &document_save_cb, NULL);
1179 static void save_settings(void)
1181 GKeyFile *config = g_key_file_new();
1182 gchar *data;
1183 gchar *config_dir = g_path_get_dirname(config_file);
1185 g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
1187 g_key_file_set_string(config, "filebrowser", "open_command", open_cmd);
1188 g_key_file_set_boolean(config, "filebrowser", "show_hidden_files", show_hidden_files);
1189 g_key_file_set_boolean(config, "filebrowser", "hide_object_files", hide_object_files);
1190 g_key_file_set_string(config, "filebrowser", "hidden_file_extensions", hidden_file_extensions);
1191 g_key_file_set_boolean(config, "filebrowser", "fb_follow_path", fb_follow_path);
1192 g_key_file_set_boolean(config, "filebrowser", "fb_set_project_base_path",
1193 fb_set_project_base_path);
1195 if (! g_file_test(config_dir, G_FILE_TEST_IS_DIR) && utils_mkdir(config_dir, TRUE) != 0)
1197 dialogs_show_msgbox(GTK_MESSAGE_ERROR,
1198 _("Plugin configuration directory could not be created."));
1200 else
1202 /* write config to file */
1203 data = g_key_file_to_data(config, NULL, NULL);
1204 utils_write_file(config_file, data);
1205 g_free(data);
1207 g_free(config_dir);
1208 g_key_file_free(config);
1212 static struct
1214 GtkWidget *open_cmd_entry;
1215 GtkWidget *show_hidden_checkbox;
1216 GtkWidget *hide_objects_checkbox;
1217 GtkWidget *hidden_files_entry;
1218 GtkWidget *follow_path_checkbox;
1219 GtkWidget *set_project_base_path_checkbox;
1221 pref_widgets;
1223 static void
1224 on_configure_response(GtkDialog *dialog, gint response, gpointer user_data)
1226 if (response == GTK_RESPONSE_OK || response == GTK_RESPONSE_APPLY)
1228 g_free(open_cmd);
1229 open_cmd = g_strdup(gtk_entry_get_text(GTK_ENTRY(pref_widgets.open_cmd_entry)));
1230 show_hidden_files = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.show_hidden_checkbox));
1231 hide_object_files = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.hide_objects_checkbox));
1232 g_free(hidden_file_extensions);
1233 hidden_file_extensions = g_strdup(gtk_entry_get_text(GTK_ENTRY(pref_widgets.hidden_files_entry)));
1234 fb_follow_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.follow_path_checkbox));
1235 fb_set_project_base_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
1236 pref_widgets.set_project_base_path_checkbox));
1238 /* apply the changes */
1239 refresh();
1244 static void on_toggle_hidden(void)
1246 gboolean enabled = !gtk_toggle_button_get_active(
1247 GTK_TOGGLE_BUTTON(pref_widgets.show_hidden_checkbox));
1249 gtk_widget_set_sensitive(pref_widgets.hide_objects_checkbox, enabled);
1250 enabled &= gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.hide_objects_checkbox));
1251 gtk_widget_set_sensitive(pref_widgets.hidden_files_entry, enabled);
1255 GtkWidget *plugin_configure(GtkDialog *dialog)
1257 GtkWidget *label, *entry, *checkbox_of, *checkbox_hf, *checkbox_fp, *checkbox_pb, *vbox;
1258 GtkWidget *box, *align;
1260 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
1261 box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 3);
1263 label = gtk_label_new(_("External open command:"));
1264 gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
1265 gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
1267 entry = gtk_entry_new();
1268 if (open_cmd != NULL)
1269 gtk_entry_set_text(GTK_ENTRY(entry), open_cmd);
1270 gtk_widget_set_tooltip_text(entry,
1271 _("The command to execute when using \"Open with\". You can use %f and %d wildcards.\n"
1272 "%f will be replaced with the filename including full path\n"
1273 "%d will be replaced with the path name of the selected file without the filename"));
1274 gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
1275 pref_widgets.open_cmd_entry = entry;
1277 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, FALSE, 3);
1279 checkbox_hf = gtk_check_button_new_with_label(_("Show hidden files"));
1280 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_hf), FALSE);
1281 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_hf), show_hidden_files);
1282 gtk_box_pack_start(GTK_BOX(vbox), checkbox_hf, FALSE, FALSE, 0);
1283 pref_widgets.show_hidden_checkbox = checkbox_hf;
1284 g_signal_connect(checkbox_hf, "toggled", on_toggle_hidden, NULL);
1286 box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 3);
1287 checkbox_of = gtk_check_button_new_with_label(_("Hide file extensions:"));
1288 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_of), FALSE);
1289 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_of), hide_object_files);
1290 gtk_box_pack_start(GTK_BOX(box), checkbox_of, FALSE, FALSE, 0);
1291 pref_widgets.hide_objects_checkbox = checkbox_of;
1292 g_signal_connect(checkbox_of, "toggled", on_toggle_hidden, NULL);
1294 entry = gtk_entry_new();
1295 if (hidden_file_extensions != NULL)
1296 gtk_entry_set_text(GTK_ENTRY(entry), hidden_file_extensions);
1297 gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
1298 pref_widgets.hidden_files_entry = entry;
1300 align = gtk_alignment_new(1, 0.5, 1, 1);
1301 gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 0, 12, 0);
1302 gtk_container_add(GTK_CONTAINER(align), box);
1303 gtk_box_pack_start(GTK_BOX(vbox), align, FALSE, FALSE, 0);
1304 on_toggle_hidden();
1306 checkbox_fp = gtk_check_button_new_with_label(_("Follow the path of the current file"));
1307 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_fp), FALSE);
1308 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_fp), fb_follow_path);
1309 gtk_box_pack_start(GTK_BOX(vbox), checkbox_fp, FALSE, FALSE, 0);
1310 pref_widgets.follow_path_checkbox = checkbox_fp;
1312 checkbox_pb = gtk_check_button_new_with_label(_("Use the project's base directory"));
1313 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_pb), FALSE);
1314 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_pb), fb_set_project_base_path);
1315 gtk_widget_set_tooltip_text(checkbox_pb,
1316 _("Change the directory to the base directory of the currently opened project"));
1317 gtk_box_pack_start(GTK_BOX(vbox), checkbox_pb, FALSE, FALSE, 0);
1318 pref_widgets.set_project_base_path_checkbox = checkbox_pb;
1320 gtk_widget_show_all(vbox);
1322 g_signal_connect(dialog, "response", G_CALLBACK(on_configure_response), NULL);
1323 return vbox;
1327 void plugin_cleanup(void)
1329 save_settings();
1331 g_free(config_file);
1332 g_free(open_cmd);
1333 g_free(hidden_file_extensions);
1334 clear_filter();
1335 gtk_widget_destroy(file_view_vbox);
1336 g_object_unref(G_OBJECT(entry_completion));