Bump plugin API version
[geany-mirror.git] / plugins / filebrowser.c
blobc6fcfd8f574fc7a724ab7dc6c3abe3b96d7aed86
1 /*
2 * filebrowser.c - this file is part of Geany, a fast and lightweight IDE
4 * Copyright 2007-2011 Enrico Tröger <enrico(dot)troeger(at)uvena(dot)de>
5 * Copyright 2007-2011 Nick Treleaven <nick(dot)treleaven(at)btinternet(dot)com>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20 * MA 02110-1301, USA.
22 * $Id$
25 /* Sidebar file browser plugin. */
27 #include "geanyplugin.h"
28 #include <string.h>
30 #include <gdk/gdkkeysyms.h>
32 #ifdef G_OS_WIN32
33 # include <windows.h>
34 #endif
36 GeanyPlugin *geany_plugin;
37 GeanyData *geany_data;
38 GeanyFunctions *geany_functions;
41 PLUGIN_VERSION_CHECK(GEANY_API_VERSION)
43 PLUGIN_SET_INFO(_("File Browser"), _("Adds a file browser tab to the sidebar."), VERSION,
44 _("The Geany developer team"))
47 /* Keybinding(s) */
48 enum
50 KB_FOCUS_FILE_LIST,
51 KB_FOCUS_PATH_ENTRY,
52 KB_COUNT
55 PLUGIN_KEY_GROUP(file_browser, KB_COUNT)
58 enum
60 FILEVIEW_COLUMN_ICON = 0,
61 FILEVIEW_COLUMN_NAME,
62 FILEVIEW_COLUMN_FILENAME, /* the full filename, including path for display as tooltip */
63 FILEVIEW_N_COLUMNS
66 static gboolean fb_set_project_base_path = FALSE;
67 static gboolean fb_follow_path = FALSE;
68 static gboolean show_hidden_files = FALSE;
69 static gboolean hide_object_files = TRUE;
71 static GtkWidget *file_view_vbox;
72 static GtkWidget *file_view;
73 static GtkListStore *file_store;
74 static GtkTreeIter *last_dir_iter = NULL;
75 static GtkEntryCompletion *entry_completion = NULL;
77 static GtkWidget *filter_combo;
78 static GtkWidget *filter_entry;
79 static GtkWidget *path_combo;
80 static GtkWidget *path_entry;
81 static gchar *current_dir = NULL; /* in locale-encoding */
82 static gchar *open_cmd; /* in locale-encoding */
83 static gchar *config_file;
84 static gchar **filter = NULL;
85 static gchar *hidden_file_extensions = NULL;
87 static gint page_number = 0;
89 static struct
91 GtkWidget *open;
92 GtkWidget *open_external;
93 GtkWidget *find_in_files;
94 GtkWidget *show_hidden_files;
95 } popup_items;
98 static void project_change_cb(GObject *obj, GKeyFile *config, gpointer data);
100 /* note: other callbacks connected in plugin_init */
101 PluginCallback plugin_callbacks[] =
103 { "project-open", (GCallback) &project_change_cb, TRUE, NULL },
104 { "project-save", (GCallback) &project_change_cb, TRUE, NULL },
105 { NULL, NULL, FALSE, NULL }
109 #ifdef G_OS_WIN32
110 static gboolean win32_check_hidden(const gchar *filename)
112 DWORD attrs;
113 static wchar_t w_filename[MAX_PATH];
114 MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename));
115 attrs = GetFileAttributesW(w_filename);
116 if (attrs != INVALID_FILE_ATTRIBUTES && attrs & FILE_ATTRIBUTE_HIDDEN)
117 return TRUE;
118 return FALSE;
120 #endif
123 /* Returns: whether name should be hidden. */
124 static gboolean check_hidden(const gchar *filename, const gchar *base_name)
126 gsize len;
128 #ifdef G_OS_WIN32
129 if (win32_check_hidden(filename))
130 return TRUE;
131 #else
132 if (base_name[0] == '.')
133 return TRUE;
134 #endif
136 len = strlen(base_name);
137 return base_name[len - 1] == '~';
141 static gboolean check_object(const gchar *base_name)
143 gboolean ret = FALSE;
144 gchar **ptr;
145 gchar **exts = g_strsplit(hidden_file_extensions, " ", -1);
147 foreach_strv(ptr, exts)
149 if (g_str_has_suffix(base_name, *ptr))
151 ret = TRUE;
152 break;
155 g_strfreev(exts);
156 return ret;
160 /* Returns: whether filename should be removed. */
161 static gboolean check_filtered(const gchar *base_name)
163 gchar **filter_item;
165 if (filter == NULL)
166 return FALSE;
168 foreach_strv(filter_item, filter)
170 if (utils_str_equal(*filter_item, "*") || g_pattern_match_simple(*filter_item, base_name))
172 return FALSE;
175 return TRUE;
179 /* name is in locale encoding */
180 static void add_item(const gchar *name)
182 GtkTreeIter iter;
183 gchar *fname, *utf8_name, *utf8_fullname;
184 const gchar *sep;
185 gboolean dir;
187 if (G_UNLIKELY(! NZV(name)))
188 return;
190 /* root directory doesn't need separator */
191 sep = (utils_str_equal(current_dir, "/")) ? "" : G_DIR_SEPARATOR_S;
192 fname = g_strconcat(current_dir, sep, name, NULL);
193 dir = g_file_test(fname, G_FILE_TEST_IS_DIR);
194 utf8_fullname = utils_get_locale_from_utf8(fname);
195 utf8_name = utils_get_utf8_from_locale(name);
196 g_free(fname);
198 if (! show_hidden_files && check_hidden(utf8_fullname, utf8_name))
199 goto done;
201 if (dir)
203 if (last_dir_iter == NULL)
204 gtk_list_store_prepend(file_store, &iter);
205 else
207 gtk_list_store_insert_after(file_store, &iter, last_dir_iter);
208 gtk_tree_iter_free(last_dir_iter);
210 last_dir_iter = gtk_tree_iter_copy(&iter);
212 else
214 if (! show_hidden_files && hide_object_files && check_object(utf8_name))
215 goto done;
216 if (check_filtered(utf8_name))
217 goto done;
219 gtk_list_store_append(file_store, &iter);
221 gtk_list_store_set(file_store, &iter,
222 FILEVIEW_COLUMN_ICON, (dir) ? GTK_STOCK_DIRECTORY : GTK_STOCK_FILE,
223 FILEVIEW_COLUMN_NAME, utf8_name,
224 FILEVIEW_COLUMN_FILENAME, utf8_fullname,
225 -1);
226 done:
227 g_free(utf8_name);
228 g_free(utf8_fullname);
232 /* adds ".." to the start of the file list */
233 static void add_top_level_entry(void)
235 GtkTreeIter iter;
236 gchar *utf8_dir;
238 if (! NZV(g_path_skip_root(current_dir)))
239 return; /* ignore 'C:\' or '/' */
241 utf8_dir = g_path_get_dirname(current_dir);
242 setptr(utf8_dir, utils_get_utf8_from_locale(utf8_dir));
244 gtk_list_store_prepend(file_store, &iter);
245 last_dir_iter = gtk_tree_iter_copy(&iter);
247 gtk_list_store_set(file_store, &iter,
248 FILEVIEW_COLUMN_ICON, GTK_STOCK_DIRECTORY,
249 FILEVIEW_COLUMN_NAME, "..",
250 FILEVIEW_COLUMN_FILENAME, utf8_dir,
251 -1);
252 g_free(utf8_dir);
256 static void clear(void)
258 gtk_list_store_clear(file_store);
260 /* reset the directory item pointer */
261 if (last_dir_iter != NULL)
262 gtk_tree_iter_free(last_dir_iter);
263 last_dir_iter = NULL;
267 /* recreate the tree model from current_dir. */
268 static void refresh(void)
270 gchar *utf8_dir;
271 GSList *list, *node;
273 /* don't clear when the new path doesn't exist */
274 if (! g_file_test(current_dir, G_FILE_TEST_EXISTS))
275 return;
277 clear();
279 utf8_dir = utils_get_utf8_from_locale(current_dir);
280 gtk_entry_set_text(GTK_ENTRY(path_entry), utf8_dir);
281 ui_combo_box_add_to_history(GTK_COMBO_BOX_ENTRY(path_combo), utf8_dir, 0);
282 g_free(utf8_dir);
284 add_top_level_entry(); /* ".." item */
286 list = utils_get_file_list(current_dir, NULL, NULL);
287 if (list != NULL)
289 /* free filenames as we go through the list */
290 foreach_slist(node, list)
292 gchar *fname = node->data;
294 add_item(fname);
295 g_free(fname);
297 g_slist_free(list);
299 gtk_entry_completion_set_model(entry_completion, GTK_TREE_MODEL(file_store));
303 static void on_go_home(void)
305 setptr(current_dir, g_strdup(g_get_home_dir()));
306 refresh();
310 /* TODO: use utils_get_default_dir_utf8() */
311 static gchar *get_default_dir(void)
313 const gchar *dir = NULL;
314 GeanyProject *project = geany->app->project;
316 if (project)
317 dir = project->base_path;
318 else
319 dir = geany->prefs->default_open_path;
321 if (NZV(dir))
322 return utils_get_locale_from_utf8(dir);
324 return g_get_current_dir();
328 static void on_current_path(void)
330 gchar *fname;
331 gchar *dir;
332 GeanyDocument *doc = document_get_current();
334 if (doc == NULL || doc->file_name == NULL || ! g_path_is_absolute(doc->file_name))
336 setptr(current_dir, get_default_dir());
337 refresh();
338 return;
340 fname = doc->file_name;
341 fname = utils_get_locale_from_utf8(fname);
342 dir = g_path_get_dirname(fname);
343 g_free(fname);
345 setptr(current_dir, dir);
346 refresh();
350 static void on_go_up(void)
352 gsize len = strlen(current_dir);
353 if (current_dir[len-1] == G_DIR_SEPARATOR)
354 current_dir[len-1] = '\0';
355 /* remove the highest directory part (which becomes the basename of current_dir) */
356 setptr(current_dir, g_path_get_dirname(current_dir));
357 refresh();
361 static gboolean check_single_selection(GtkTreeSelection *treesel)
363 if (gtk_tree_selection_count_selected_rows(treesel) == 1)
364 return TRUE;
366 ui_set_statusbar(FALSE, _("Too many items selected!"));
367 return FALSE;
371 /* Returns: TRUE if at least one of selected_items is a folder. */
372 static gboolean is_folder_selected(GList *selected_items)
374 GList *item;
375 GtkTreeModel *model = GTK_TREE_MODEL(file_store);
376 gboolean dir_found = FALSE;
378 for (item = selected_items; item != NULL; item = g_list_next(item))
380 gchar *icon;
381 GtkTreeIter iter;
382 GtkTreePath *treepath;
384 treepath = (GtkTreePath*) item->data;
385 gtk_tree_model_get_iter(model, &iter, treepath);
386 gtk_tree_model_get(model, &iter, FILEVIEW_COLUMN_ICON, &icon, -1);
388 if (utils_str_equal(icon, GTK_STOCK_DIRECTORY))
390 dir_found = TRUE;
391 g_free(icon);
392 break;
394 g_free(icon);
396 return dir_found;
400 /* Returns: the full filename in locale encoding. */
401 static gchar *get_tree_path_filename(GtkTreePath *treepath)
403 GtkTreeModel *model = GTK_TREE_MODEL(file_store);
404 GtkTreeIter iter;
405 gchar *name, *fname;
407 gtk_tree_model_get_iter(model, &iter, treepath);
408 gtk_tree_model_get(model, &iter, FILEVIEW_COLUMN_FILENAME, &name, -1);
410 fname = utils_get_locale_from_utf8(name);
411 g_free(name);
413 return fname;
417 static void open_external(const gchar *fname, gboolean dir_found)
419 gchar *cmd;
420 gchar *locale_cmd;
421 gchar *dir;
422 GString *cmd_str = g_string_new(open_cmd);
423 GError *error = NULL;
425 if (! dir_found)
426 dir = g_path_get_dirname(fname);
427 else
428 dir = g_strdup(fname);
430 utils_string_replace_all(cmd_str, "%f", fname);
431 utils_string_replace_all(cmd_str, "%d", dir);
433 cmd = g_string_free(cmd_str, FALSE);
434 locale_cmd = utils_get_locale_from_utf8(cmd);
435 if (! g_spawn_command_line_async(locale_cmd, &error))
437 gchar *c = strchr(cmd, ' ');
439 if (c != NULL)
440 *c = '\0';
441 ui_set_statusbar(TRUE,
442 _("Could not execute configured external command '%s' (%s)."),
443 cmd, error->message);
444 g_error_free(error);
446 g_free(locale_cmd);
447 g_free(cmd);
448 g_free(dir);
452 static void on_external_open(GtkMenuItem *menuitem, gpointer user_data)
454 GtkTreeSelection *treesel;
455 GtkTreeModel *model;
456 GList *list;
457 gboolean dir_found;
459 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
461 list = gtk_tree_selection_get_selected_rows(treesel, &model);
462 dir_found = is_folder_selected(list);
464 if (! dir_found || check_single_selection(treesel))
466 GList *item;
468 for (item = list; item != NULL; item = g_list_next(item))
470 GtkTreePath *treepath = item->data;
471 gchar *fname = get_tree_path_filename(treepath);
473 open_external(fname, dir_found);
474 g_free(fname);
478 g_list_foreach(list, (GFunc) gtk_tree_path_free, NULL);
479 g_list_free(list);
483 /* We use document_open_files() as it's more efficient. */
484 static void open_selected_files(GList *list, gboolean do_not_focus)
486 GSList *files = NULL;
487 GList *item;
488 GeanyDocument *doc;
490 for (item = list; item != NULL; item = g_list_next(item))
492 GtkTreePath *treepath = item->data;
493 gchar *fname = get_tree_path_filename(treepath);
495 files = g_slist_prepend(files, fname);
497 files = g_slist_reverse(files);
498 document_open_files(files, FALSE, NULL, NULL);
499 doc = document_get_current();
500 if (doc != NULL && ! do_not_focus)
501 keybindings_send_command(GEANY_KEY_GROUP_FOCUS, GEANY_KEYS_FOCUS_EDITOR);
503 g_slist_foreach(files, (GFunc) g_free, NULL); /* free filenames */
504 g_slist_free(files);
508 static void open_folder(GtkTreePath *treepath)
510 gchar *fname = get_tree_path_filename(treepath);
512 setptr(current_dir, fname);
513 refresh();
517 static void on_open_clicked(GtkMenuItem *menuitem, gpointer user_data)
519 GtkTreeSelection *treesel;
520 GtkTreeModel *model;
521 GList *list;
522 gboolean dir_found;
524 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
526 list = gtk_tree_selection_get_selected_rows(treesel, &model);
527 dir_found = is_folder_selected(list);
529 if (dir_found)
531 if (check_single_selection(treesel))
533 GtkTreePath *treepath = list->data; /* first selected item */
535 open_folder(treepath);
538 else
539 open_selected_files(list, GPOINTER_TO_INT(user_data));
541 g_list_foreach(list, (GFunc) gtk_tree_path_free, NULL);
542 g_list_free(list);
546 static void on_find_in_files(GtkMenuItem *menuitem, gpointer user_data)
548 GtkTreeSelection *treesel;
549 GtkTreeModel *model;
550 GList *list;
551 gchar *dir;
552 gboolean is_dir = FALSE;
554 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
555 /* allow 0 or 1 selections */
556 if (gtk_tree_selection_count_selected_rows(treesel) > 0 &&
557 ! check_single_selection(treesel))
558 return;
560 list = gtk_tree_selection_get_selected_rows(treesel, &model);
561 is_dir = is_folder_selected(list);
563 if (is_dir)
565 GtkTreePath *treepath = list->data; /* first selected item */
567 dir = get_tree_path_filename(treepath);
569 else
570 dir = g_strdup(current_dir);
572 g_list_foreach(list, (GFunc) gtk_tree_path_free, NULL);
573 g_list_free(list);
575 setptr(dir, utils_get_utf8_from_locale(dir));
576 search_show_find_in_files_dialog(dir);
577 g_free(dir);
581 static void on_hidden_files_clicked(GtkCheckMenuItem *item)
583 show_hidden_files = gtk_check_menu_item_get_active(item);
584 refresh();
588 static void on_hide_sidebar(void)
590 keybindings_send_command(GEANY_KEY_GROUP_VIEW, GEANY_KEYS_VIEW_SIDEBAR);
594 static void on_show_preferences(void)
596 plugin_show_configure(geany_plugin);
600 static GtkWidget *create_popup_menu(void)
602 GtkWidget *item, *menu;
604 menu = gtk_menu_new();
606 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL);
607 gtk_widget_show(item);
608 gtk_container_add(GTK_CONTAINER(menu), item);
609 g_signal_connect(item, "activate", G_CALLBACK(on_open_clicked), NULL);
610 popup_items.open = item;
612 item = ui_image_menu_item_new(GTK_STOCK_OPEN, _("Open _externally"));
613 gtk_widget_show(item);
614 gtk_container_add(GTK_CONTAINER(menu), item);
615 g_signal_connect(item, "activate", G_CALLBACK(on_external_open), NULL);
616 popup_items.open_external = item;
618 item = gtk_separator_menu_item_new();
619 gtk_widget_show(item);
620 gtk_container_add(GTK_CONTAINER(menu), item);
622 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_REFRESH, NULL);
623 gtk_widget_show(item);
624 gtk_container_add(GTK_CONTAINER(menu), item);
625 g_signal_connect(item, "activate", G_CALLBACK(refresh), NULL);
627 item = ui_image_menu_item_new(GTK_STOCK_FIND, _("_Find in Files"));
628 gtk_widget_show(item);
629 gtk_container_add(GTK_CONTAINER(menu), item);
630 g_signal_connect(item, "activate", G_CALLBACK(on_find_in_files), NULL);
631 popup_items.find_in_files = item;
633 item = gtk_separator_menu_item_new();
634 gtk_widget_show(item);
635 gtk_container_add(GTK_CONTAINER(menu), item);
637 item = gtk_check_menu_item_new_with_mnemonic(_("Show _Hidden Files"));
638 gtk_widget_show(item);
639 gtk_container_add(GTK_CONTAINER(menu), item);
640 g_signal_connect(item, "activate", G_CALLBACK(on_hidden_files_clicked), NULL);
641 popup_items.show_hidden_files = item;
643 item = gtk_separator_menu_item_new();
644 gtk_widget_show(item);
645 gtk_container_add(GTK_CONTAINER(menu), item);
647 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_PREFERENCES, NULL);
648 gtk_widget_show(item);
649 gtk_container_add(GTK_CONTAINER(menu), item);
650 g_signal_connect(item, "activate", G_CALLBACK(on_show_preferences), NULL);
652 item = gtk_separator_menu_item_new();
653 gtk_widget_show(item);
654 gtk_container_add(GTK_CONTAINER(menu), item);
656 item = ui_image_menu_item_new(GTK_STOCK_CLOSE, _("H_ide Sidebar"));
657 gtk_widget_show(item);
658 gtk_container_add(GTK_CONTAINER(menu), item);
659 g_signal_connect(item, "activate", G_CALLBACK(on_hide_sidebar), NULL);
661 return menu;
665 static void on_tree_selection_changed(GtkTreeSelection *selection, gpointer data)
667 gboolean have_sel = (gtk_tree_selection_count_selected_rows(selection) > 0);
668 gboolean multi_sel = (gtk_tree_selection_count_selected_rows(selection) > 1);
670 if (popup_items.open != NULL)
671 gtk_widget_set_sensitive(popup_items.open, have_sel);
672 if (popup_items.open_external != NULL)
673 gtk_widget_set_sensitive(popup_items.open_external, have_sel);
674 if (popup_items.find_in_files != NULL)
675 gtk_widget_set_sensitive(popup_items.find_in_files, have_sel && ! multi_sel);
679 static gboolean on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
681 if (event->button == 1 && event->type == GDK_2BUTTON_PRESS)
683 on_open_clicked(NULL, NULL);
684 return TRUE;
686 else if (event->button == 3)
688 static GtkWidget *popup_menu = NULL;
690 if (popup_menu == NULL)
691 popup_menu = create_popup_menu();
693 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(popup_items.show_hidden_files),
694 show_hidden_files);
695 gtk_menu_popup(GTK_MENU(popup_menu), NULL, NULL, NULL, NULL, event->button, event->time);
696 /* don't return TRUE here, unless the selection won't be changed */
698 return FALSE;
702 static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
704 if (ui_is_keyval_enter_or_return(event->keyval))
706 on_open_clicked(NULL, NULL);
707 return TRUE;
710 if (event->keyval == GDK_space)
712 on_open_clicked(NULL, GINT_TO_POINTER(TRUE));
713 return TRUE;
716 if ((event->keyval == GDK_Up ||
717 event->keyval == GDK_KP_Up) &&
718 (event->state & GDK_MOD1_MASK)) /* FIXME: Alt-Up doesn't seem to work! */
720 on_go_up();
721 return TRUE;
724 if ((event->keyval == GDK_F10 && event->state & GDK_SHIFT_MASK) || event->keyval == GDK_Menu)
726 GdkEventButton button_event;
728 button_event.time = event->time;
729 button_event.button = 3;
731 on_button_press(widget, &button_event, data);
732 return TRUE;
735 return FALSE;
739 static void clear_filter(void)
741 if (filter != NULL)
743 g_strfreev(filter);
744 filter = NULL;
749 static void on_clear_filter(GtkEntry *entry, gpointer user_data)
751 clear_filter();
753 gtk_entry_set_text(GTK_ENTRY(filter_entry), "");
755 refresh();
759 static void on_path_entry_activate(GtkEntry *entry, gpointer user_data)
761 gchar *new_dir = (gchar*) gtk_entry_get_text(entry);
763 if (NZV(new_dir))
765 if (g_str_has_suffix(new_dir, ".."))
767 on_go_up();
768 return;
770 else if (new_dir[0] == '~')
772 GString *str = g_string_new(new_dir);
773 utils_string_replace_first(str, "~", g_get_home_dir());
774 new_dir = g_string_free(str, FALSE);
776 else
777 new_dir = utils_get_locale_from_utf8(new_dir);
779 else
780 new_dir = g_strdup(g_get_home_dir());
782 setptr(current_dir, new_dir);
784 on_clear_filter(NULL, NULL);
788 static void ui_combo_box_changed(GtkComboBox *combo, gpointer user_data)
790 /* we get this callback on typing as well as choosing an item */
791 if (gtk_combo_box_get_active(combo) >= 0)
792 gtk_widget_activate(GTK_BIN(combo)->child);
796 static void on_filter_activate(GtkEntry *entry, gpointer user_data)
798 /* We use spaces for consistency with Find in Files file patterns
799 * ';' also supported like original patch. */
800 filter = g_strsplit_set(gtk_entry_get_text(entry), "; ", -1);
801 if (filter == NULL || g_strv_length(filter) == 0)
803 clear_filter();
805 ui_combo_box_add_to_history(GTK_COMBO_BOX_ENTRY(filter_combo), NULL, 0);
806 refresh();
810 static void on_filter_clear(GtkEntry *entry, gint icon_pos,
811 GdkEvent *event, gpointer data)
813 clear_filter();
814 refresh();
818 static void prepare_file_view(void)
820 GtkCellRenderer *text_renderer, *icon_renderer;
821 GtkTreeViewColumn *column;
822 GtkTreeSelection *selection;
824 file_store = gtk_list_store_new(FILEVIEW_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
826 gtk_tree_view_set_model(GTK_TREE_VIEW(file_view), GTK_TREE_MODEL(file_store));
827 g_object_unref(file_store);
829 icon_renderer = gtk_cell_renderer_pixbuf_new();
830 text_renderer = gtk_cell_renderer_text_new();
831 column = gtk_tree_view_column_new();
832 gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
833 gtk_tree_view_column_set_attributes(column, icon_renderer, "stock-id", FILEVIEW_COLUMN_ICON, NULL);
834 gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
835 gtk_tree_view_column_set_attributes(column, text_renderer, "text", FILEVIEW_COLUMN_NAME, NULL);
836 gtk_tree_view_append_column(GTK_TREE_VIEW(file_view), column);
837 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(file_view), FALSE);
839 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(file_view), TRUE);
840 gtk_tree_view_set_search_column(GTK_TREE_VIEW(file_view), FILEVIEW_COLUMN_NAME);
842 ui_widget_modify_font_from_string(file_view, geany->interface_prefs->tagbar_font);
844 /* GTK 2.12 tooltips */
845 if (gtk_check_version(2, 12, 0) == NULL)
846 g_object_set(file_view, "has-tooltip", TRUE, "tooltip-column", FILEVIEW_COLUMN_FILENAME, NULL);
848 /* selection handling */
849 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
850 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
852 /* Show the current path when the FB is first needed */
853 g_signal_connect(file_view, "realize", G_CALLBACK(on_current_path), NULL);
854 g_signal_connect(selection, "changed", G_CALLBACK(on_tree_selection_changed), NULL);
855 g_signal_connect(file_view, "button-press-event", G_CALLBACK(on_button_press), NULL);
856 g_signal_connect(file_view, "key-press-event", G_CALLBACK(on_key_press), NULL);
860 static GtkWidget *make_toolbar(void)
862 GtkWidget *wid, *toolbar;
864 toolbar = gtk_toolbar_new();
865 gtk_toolbar_set_icon_size(GTK_TOOLBAR(toolbar), GTK_ICON_SIZE_MENU);
866 gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
868 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_GO_UP));
869 ui_widget_set_tooltip_text(wid, _("Up"));
870 g_signal_connect(wid, "clicked", G_CALLBACK(on_go_up), NULL);
871 gtk_container_add(GTK_CONTAINER(toolbar), wid);
873 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_REFRESH));
874 ui_widget_set_tooltip_text(wid, _("Refresh"));
875 g_signal_connect(wid, "clicked", G_CALLBACK(refresh), NULL);
876 gtk_container_add(GTK_CONTAINER(toolbar), wid);
878 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_HOME));
879 ui_widget_set_tooltip_text(wid, _("Home"));
880 g_signal_connect(wid, "clicked", G_CALLBACK(on_go_home), NULL);
881 gtk_container_add(GTK_CONTAINER(toolbar), wid);
883 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_JUMP_TO));
884 ui_widget_set_tooltip_text(wid, _("Set path from document"));
885 g_signal_connect(wid, "clicked", G_CALLBACK(on_current_path), NULL);
886 gtk_container_add(GTK_CONTAINER(toolbar), wid);
888 if (gtk_check_version(2, 15, 2) != NULL)
890 wid = GTK_WIDGET(gtk_separator_tool_item_new());
891 gtk_container_add(GTK_CONTAINER(toolbar), wid);
893 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_CLEAR));
894 ui_widget_set_tooltip_text(wid, _("Clear the filter"));
895 g_signal_connect(wid, "clicked", G_CALLBACK(on_clear_filter), NULL);
896 gtk_container_add(GTK_CONTAINER(toolbar), wid);
898 return toolbar;
902 static GtkWidget *make_filterbar(void)
904 GtkWidget *label, *filterbar;
906 filterbar = gtk_hbox_new(FALSE, 1);
908 label = gtk_label_new(_("Filter:"));
910 filter_combo = gtk_combo_box_entry_new_text();
911 filter_entry = GTK_BIN(filter_combo)->child;
913 if (gtk_check_version(2, 15, 2) == NULL)
915 ui_entry_add_clear_icon(GTK_ENTRY(filter_entry));
916 g_signal_connect(filter_entry, "icon-release", G_CALLBACK(on_filter_clear), NULL);
918 ui_widget_set_tooltip_text(filter_entry,
919 _("Filter your files with the usual wildcards. Separate multiple patterns with a space."));
920 g_signal_connect(filter_entry, "activate", G_CALLBACK(on_filter_activate), NULL);
921 g_signal_connect(filter_combo, "changed", G_CALLBACK(ui_combo_box_changed), NULL);
923 gtk_box_pack_start(GTK_BOX(filterbar), label, FALSE, FALSE, 0);
924 gtk_box_pack_start(GTK_BOX(filterbar), filter_combo, TRUE, TRUE, 0);
926 return filterbar;
930 static gboolean completion_match_func(GtkEntryCompletion *completion, const gchar *key,
931 GtkTreeIter *iter, gpointer user_data)
933 gchar *str, *icon;
934 gboolean result = FALSE;
936 gtk_tree_model_get(GTK_TREE_MODEL(file_store), iter,
937 FILEVIEW_COLUMN_ICON, &icon, FILEVIEW_COLUMN_NAME, &str, -1);
939 if (str != NULL && icon != NULL && utils_str_equal(icon, GTK_STOCK_DIRECTORY) &&
940 ! g_str_has_suffix(key, G_DIR_SEPARATOR_S))
942 /* key is something like "/tmp/te" and str is a filename like "test",
943 * so strip the path from key to make them comparable */
944 gchar *base_name = g_path_get_basename(key);
945 gchar *str_lowered = g_utf8_strdown(str, -1);
946 result = g_str_has_prefix(str_lowered, base_name);
947 g_free(base_name);
948 g_free(str_lowered);
950 g_free(str);
951 g_free(icon);
953 return result;
957 static gboolean completion_match_selected(GtkEntryCompletion *widget, GtkTreeModel *model,
958 GtkTreeIter *iter, gpointer user_data)
960 gchar *str;
961 gtk_tree_model_get(model, iter, FILEVIEW_COLUMN_NAME, &str, -1);
962 if (str != NULL)
964 gchar *text = g_strconcat(current_dir, G_DIR_SEPARATOR_S, str, NULL);
965 gtk_entry_set_text(GTK_ENTRY(path_entry), text);
966 gtk_editable_set_position(GTK_EDITABLE(path_entry), -1);
967 /* force change of directory when completion is done */
968 on_path_entry_activate(GTK_ENTRY(path_entry), NULL);
969 g_free(text);
971 g_free(str);
973 return TRUE;
977 static void completion_create(void)
979 entry_completion = gtk_entry_completion_new();
981 gtk_entry_completion_set_inline_completion(entry_completion, FALSE);
982 gtk_entry_completion_set_popup_completion(entry_completion, TRUE);
983 gtk_entry_completion_set_text_column(entry_completion, FILEVIEW_COLUMN_NAME);
984 gtk_entry_completion_set_match_func(entry_completion, completion_match_func, NULL, NULL);
986 g_signal_connect(entry_completion, "match-selected",
987 G_CALLBACK(completion_match_selected), NULL);
989 gtk_entry_set_completion(GTK_ENTRY(path_entry), entry_completion);
993 static void load_settings(void)
995 GKeyFile *config = g_key_file_new();
997 config_file = g_strconcat(geany->app->configdir, G_DIR_SEPARATOR_S, "plugins", G_DIR_SEPARATOR_S,
998 "filebrowser", G_DIR_SEPARATOR_S, "filebrowser.conf", NULL);
999 g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
1001 open_cmd = utils_get_setting_string(config, "filebrowser", "open_command", "nautilus \"%d\"");
1002 /* g_key_file_get_boolean defaults to FALSE */
1003 show_hidden_files = g_key_file_get_boolean(config, "filebrowser", "show_hidden_files", NULL);
1004 hide_object_files = utils_get_setting_boolean(config, "filebrowser", "hide_object_files", TRUE);
1005 hidden_file_extensions = utils_get_setting_string(config, "filebrowser", "hidden_file_extensions",
1006 ".o .obj .so .dll .a .lib .pyc");
1007 fb_follow_path = g_key_file_get_boolean(config, "filebrowser", "fb_follow_path", NULL);
1008 fb_set_project_base_path = g_key_file_get_boolean(config, "filebrowser", "fb_set_project_base_path", NULL);
1010 g_key_file_free(config);
1014 static void project_change_cb(G_GNUC_UNUSED GObject *obj, G_GNUC_UNUSED GKeyFile *config,
1015 G_GNUC_UNUSED gpointer data)
1017 gchar *new_dir;
1018 GeanyProject *project = geany->app->project;
1020 if (! fb_set_project_base_path || project == NULL || ! NZV(project->base_path))
1021 return;
1023 /* TODO this is a copy of project_get_base_path(), add it to the plugin API */
1024 if (g_path_is_absolute(project->base_path))
1025 new_dir = g_strdup(project->base_path);
1026 else
1027 { /* build base_path out of project file name's dir and base_path */
1028 gchar *dir = g_path_get_dirname(project->file_name);
1030 new_dir = g_strconcat(dir, G_DIR_SEPARATOR_S, project->base_path, NULL);
1031 g_free(dir);
1033 /* get it into locale encoding */
1034 setptr(new_dir, utils_get_locale_from_utf8(new_dir));
1036 if (! utils_str_equal(current_dir, new_dir))
1038 setptr(current_dir, new_dir);
1039 refresh();
1041 else
1042 g_free(new_dir);
1046 static gpointer last_activate_path = NULL;
1048 static void document_activate_cb(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc,
1049 G_GNUC_UNUSED gpointer data)
1051 gchar *new_dir;
1053 last_activate_path = doc->real_path;
1055 if (! fb_follow_path || doc->file_name == NULL || ! g_path_is_absolute(doc->file_name))
1056 return;
1058 new_dir = g_path_get_dirname(doc->file_name);
1059 setptr(new_dir, utils_get_locale_from_utf8(new_dir));
1061 if (! utils_str_equal(current_dir, new_dir))
1063 setptr(current_dir, new_dir);
1064 refresh();
1066 else
1067 g_free(new_dir);
1071 static void document_save_cb(GObject *obj, GeanyDocument *doc, gpointer user_data)
1073 if (!last_activate_path)
1074 document_activate_cb(obj, doc, user_data);
1078 static void kb_activate(guint key_id)
1080 gtk_notebook_set_current_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook), page_number);
1081 switch (key_id)
1083 case KB_FOCUS_FILE_LIST:
1084 gtk_widget_grab_focus(file_view);
1085 break;
1086 case KB_FOCUS_PATH_ENTRY:
1087 gtk_widget_grab_focus(path_entry);
1088 break;
1093 void plugin_init(GeanyData *data)
1095 GtkWidget *scrollwin, *toolbar, *filterbar;
1097 filter = NULL;
1099 file_view_vbox = gtk_vbox_new(FALSE, 0);
1100 toolbar = make_toolbar();
1101 gtk_box_pack_start(GTK_BOX(file_view_vbox), toolbar, FALSE, FALSE, 0);
1103 filterbar = make_filterbar();
1104 gtk_box_pack_start(GTK_BOX(file_view_vbox), filterbar, FALSE, FALSE, 0);
1106 path_combo = gtk_combo_box_entry_new_text();
1107 gtk_box_pack_start(GTK_BOX(file_view_vbox), path_combo, FALSE, FALSE, 2);
1108 g_signal_connect(path_combo, "changed", G_CALLBACK(ui_combo_box_changed), NULL);
1109 path_entry = GTK_BIN(path_combo)->child;
1110 g_signal_connect(path_entry, "activate", G_CALLBACK(on_path_entry_activate), NULL);
1112 file_view = gtk_tree_view_new();
1113 prepare_file_view();
1114 completion_create();
1116 popup_items.open = popup_items.open_external = popup_items.find_in_files = NULL;
1118 scrollwin = gtk_scrolled_window_new(NULL, NULL);
1119 gtk_scrolled_window_set_policy(
1120 GTK_SCROLLED_WINDOW(scrollwin),
1121 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
1122 gtk_container_add(GTK_CONTAINER(scrollwin), file_view);
1123 gtk_container_add(GTK_CONTAINER(file_view_vbox), scrollwin);
1125 /* load settings before file_view "realize" callback */
1126 load_settings();
1128 gtk_widget_show_all(file_view_vbox);
1129 page_number = gtk_notebook_append_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook),
1130 file_view_vbox, gtk_label_new(_("Files")));
1132 /* setup keybindings */
1133 keybindings_set_item(plugin_key_group, KB_FOCUS_FILE_LIST, kb_activate,
1134 0, 0, "focus_file_list", _("Focus File List"), NULL);
1135 keybindings_set_item(plugin_key_group, KB_FOCUS_PATH_ENTRY, kb_activate,
1136 0, 0, "focus_path_entry", _("Focus Path Entry"), NULL);
1138 plugin_signal_connect(geany_plugin, NULL, "document-activate", TRUE,
1139 (GCallback) &document_activate_cb, NULL);
1140 plugin_signal_connect(geany_plugin, NULL, "document-save", TRUE,
1141 (GCallback) &document_save_cb, NULL);
1145 static void save_settings(void)
1147 GKeyFile *config = g_key_file_new();
1148 gchar *data;
1149 gchar *config_dir = g_path_get_dirname(config_file);
1151 g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
1153 g_key_file_set_string(config, "filebrowser", "open_command", open_cmd);
1154 g_key_file_set_boolean(config, "filebrowser", "show_hidden_files", show_hidden_files);
1155 g_key_file_set_boolean(config, "filebrowser", "hide_object_files", hide_object_files);
1156 g_key_file_set_string(config, "filebrowser", "hidden_file_extensions", hidden_file_extensions);
1157 g_key_file_set_boolean(config, "filebrowser", "fb_follow_path", fb_follow_path);
1158 g_key_file_set_boolean(config, "filebrowser", "fb_set_project_base_path",
1159 fb_set_project_base_path);
1161 if (! g_file_test(config_dir, G_FILE_TEST_IS_DIR) && utils_mkdir(config_dir, TRUE) != 0)
1163 dialogs_show_msgbox(GTK_MESSAGE_ERROR,
1164 _("Plugin configuration directory could not be created."));
1166 else
1168 /* write config to file */
1169 data = g_key_file_to_data(config, NULL, NULL);
1170 utils_write_file(config_file, data);
1171 g_free(data);
1173 g_free(config_dir);
1174 g_key_file_free(config);
1178 static struct
1180 GtkWidget *open_cmd_entry;
1181 GtkWidget *show_hidden_checkbox;
1182 GtkWidget *hide_objects_checkbox;
1183 GtkWidget *hidden_files_entry;
1184 GtkWidget *follow_path_checkbox;
1185 GtkWidget *set_project_base_path_checkbox;
1187 pref_widgets;
1189 static void
1190 on_configure_response(GtkDialog *dialog, gint response, gpointer user_data)
1192 if (response == GTK_RESPONSE_OK || response == GTK_RESPONSE_APPLY)
1194 g_free(open_cmd);
1195 open_cmd = g_strdup(gtk_entry_get_text(GTK_ENTRY(pref_widgets.open_cmd_entry)));
1196 show_hidden_files = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.show_hidden_checkbox));
1197 hide_object_files = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.hide_objects_checkbox));
1198 g_free(hidden_file_extensions);
1199 hidden_file_extensions = g_strdup(gtk_entry_get_text(GTK_ENTRY(pref_widgets.hidden_files_entry)));
1200 fb_follow_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.follow_path_checkbox));
1201 fb_set_project_base_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
1202 pref_widgets.set_project_base_path_checkbox));
1204 /* apply the changes */
1205 refresh();
1210 static void on_toggle_hidden(void)
1212 gboolean enabled = !gtk_toggle_button_get_active(
1213 GTK_TOGGLE_BUTTON(pref_widgets.show_hidden_checkbox));
1215 gtk_widget_set_sensitive(pref_widgets.hide_objects_checkbox, enabled);
1216 enabled &= gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.hide_objects_checkbox));
1217 gtk_widget_set_sensitive(pref_widgets.hidden_files_entry, enabled);
1221 GtkWidget *plugin_configure(GtkDialog *dialog)
1223 GtkWidget *label, *entry, *checkbox_of, *checkbox_hf, *checkbox_fp, *checkbox_pb, *vbox;
1224 GtkWidget *box, *align;
1226 vbox = gtk_vbox_new(FALSE, 6);
1227 box = gtk_vbox_new(FALSE, 3);
1229 label = gtk_label_new(_("External open command:"));
1230 gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
1231 gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
1233 entry = gtk_entry_new();
1234 if (open_cmd != NULL)
1235 gtk_entry_set_text(GTK_ENTRY(entry), open_cmd);
1236 ui_widget_set_tooltip_text(entry,
1237 _("The command to execute when using \"Open with\". You can use %f and %d wildcards.\n"
1238 "%f will be replaced with the filename including full path\n"
1239 "%d will be replaced with the path name of the selected file without the filename"));
1240 gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
1241 pref_widgets.open_cmd_entry = entry;
1243 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, FALSE, 3);
1245 checkbox_hf = gtk_check_button_new_with_label(_("Show hidden files"));
1246 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_hf), FALSE);
1247 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_hf), show_hidden_files);
1248 gtk_box_pack_start(GTK_BOX(vbox), checkbox_hf, FALSE, FALSE, 0);
1249 pref_widgets.show_hidden_checkbox = checkbox_hf;
1250 g_signal_connect(checkbox_hf, "toggled", on_toggle_hidden, NULL);
1252 box = gtk_vbox_new(FALSE, 3);
1253 checkbox_of = gtk_check_button_new_with_label(_("Hide file extensions:"));
1254 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_of), FALSE);
1255 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_of), hide_object_files);
1256 gtk_box_pack_start(GTK_BOX(box), checkbox_of, FALSE, FALSE, 0);
1257 pref_widgets.hide_objects_checkbox = checkbox_of;
1258 g_signal_connect(checkbox_of, "toggled", on_toggle_hidden, NULL);
1260 entry = gtk_entry_new();
1261 if (hidden_file_extensions != NULL)
1262 gtk_entry_set_text(GTK_ENTRY(entry), hidden_file_extensions);
1263 gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
1264 pref_widgets.hidden_files_entry = entry;
1266 align = gtk_alignment_new(1, 0.5, 1, 1);
1267 gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 0, 12, 0);
1268 gtk_container_add(GTK_CONTAINER(align), box);
1269 gtk_box_pack_start(GTK_BOX(vbox), align, FALSE, FALSE, 0);
1270 on_toggle_hidden();
1272 checkbox_fp = gtk_check_button_new_with_label(_("Follow the path of the current file"));
1273 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_fp), FALSE);
1274 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_fp), fb_follow_path);
1275 gtk_box_pack_start(GTK_BOX(vbox), checkbox_fp, FALSE, FALSE, 0);
1276 pref_widgets.follow_path_checkbox = checkbox_fp;
1278 checkbox_pb = gtk_check_button_new_with_label(_("Use the project's base directory"));
1279 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_pb), FALSE);
1280 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_pb), fb_set_project_base_path);
1281 ui_widget_set_tooltip_text(checkbox_pb,
1282 _("Change the directory to the base directory of the currently opened project"));
1283 gtk_box_pack_start(GTK_BOX(vbox), checkbox_pb, FALSE, FALSE, 0);
1284 pref_widgets.set_project_base_path_checkbox = checkbox_pb;
1286 gtk_widget_show_all(vbox);
1288 g_signal_connect(dialog, "response", G_CALLBACK(on_configure_response), NULL);
1289 return vbox;
1293 void plugin_cleanup(void)
1295 save_settings();
1297 g_free(config_file);
1298 g_free(open_cmd);
1299 g_free(hidden_file_extensions);
1300 clear_filter();
1301 gtk_widget_destroy(file_view_vbox);
1302 g_object_unref(G_OBJECT(entry_completion));