Drop 'already' from the message in project close confirmation dialog
[geany-mirror.git] / plugins / filebrowser.c
blob35e5109d07ab1dbf201e517d6ea72ee7851f49cf
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.
23 /* Sidebar file browser plugin. */
25 #ifdef HAVE_CONFIG_H
26 # include "config.h"
27 #endif
29 #include "geanyplugin.h"
30 #include <string.h>
32 #include <gdk/gdkkeysyms.h>
34 #ifdef G_OS_WIN32
35 # include <windows.h>
36 #endif
38 GeanyPlugin *geany_plugin;
39 GeanyData *geany_data;
40 GeanyFunctions *geany_functions;
43 PLUGIN_VERSION_CHECK(GEANY_API_VERSION)
45 PLUGIN_SET_INFO(_("File Browser"), _("Adds a file browser tab to the sidebar."), VERSION,
46 _("The Geany developer team"))
49 /* Keybinding(s) */
50 enum
52 KB_FOCUS_FILE_LIST,
53 KB_FOCUS_PATH_ENTRY,
54 KB_COUNT
57 PLUGIN_KEY_GROUP(file_browser, KB_COUNT)
60 enum
62 FILEVIEW_COLUMN_ICON = 0,
63 FILEVIEW_COLUMN_NAME,
64 FILEVIEW_COLUMN_FILENAME, /* the full filename, including path for display as tooltip */
65 FILEVIEW_N_COLUMNS
68 static gboolean fb_set_project_base_path = FALSE;
69 static gboolean fb_follow_path = FALSE;
70 static gboolean show_hidden_files = FALSE;
71 static gboolean hide_object_files = TRUE;
73 static GtkWidget *file_view_vbox;
74 static GtkWidget *file_view;
75 static GtkListStore *file_store;
76 static GtkTreeIter *last_dir_iter = NULL;
77 static GtkEntryCompletion *entry_completion = NULL;
79 static GtkWidget *filter_combo;
80 static GtkWidget *filter_entry;
81 static GtkWidget *path_combo;
82 static GtkWidget *path_entry;
83 static gchar *current_dir = NULL; /* in locale-encoding */
84 static gchar *open_cmd; /* in locale-encoding */
85 static gchar *config_file;
86 static gchar **filter = NULL;
87 static gchar *hidden_file_extensions = NULL;
89 static gint page_number = 0;
91 static struct
93 GtkWidget *open;
94 GtkWidget *open_external;
95 GtkWidget *find_in_files;
96 GtkWidget *show_hidden_files;
97 } popup_items;
100 static void project_change_cb(GObject *obj, GKeyFile *config, gpointer data);
102 /* note: other callbacks connected in plugin_init */
103 PluginCallback plugin_callbacks[] =
105 { "project-open", (GCallback) &project_change_cb, TRUE, NULL },
106 { "project-save", (GCallback) &project_change_cb, TRUE, NULL },
107 { NULL, NULL, FALSE, NULL }
111 #ifdef G_OS_WIN32
112 static gboolean win32_check_hidden(const gchar *filename)
114 DWORD attrs;
115 static wchar_t w_filename[MAX_PATH];
116 MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename));
117 attrs = GetFileAttributesW(w_filename);
118 if (attrs != INVALID_FILE_ATTRIBUTES && attrs & FILE_ATTRIBUTE_HIDDEN)
119 return TRUE;
120 return FALSE;
122 #endif
125 /* Returns: whether name should be hidden. */
126 static gboolean check_hidden(const gchar *filename, const gchar *base_name)
128 gsize len;
130 #ifdef G_OS_WIN32
131 if (win32_check_hidden(filename))
132 return TRUE;
133 #else
134 if (base_name[0] == '.')
135 return TRUE;
136 #endif
138 len = strlen(base_name);
139 return base_name[len - 1] == '~';
143 static gboolean check_object(const gchar *base_name)
145 gboolean ret = FALSE;
146 gchar **ptr;
147 gchar **exts = g_strsplit(hidden_file_extensions, " ", -1);
149 foreach_strv(ptr, exts)
151 if (g_str_has_suffix(base_name, *ptr))
153 ret = TRUE;
154 break;
157 g_strfreev(exts);
158 return ret;
162 /* Returns: whether filename should be removed. */
163 static gboolean check_filtered(const gchar *base_name)
165 gchar **filter_item;
167 if (filter == NULL)
168 return FALSE;
170 foreach_strv(filter_item, filter)
172 if (utils_str_equal(*filter_item, "*") || g_pattern_match_simple(*filter_item, base_name))
174 return FALSE;
177 return TRUE;
181 /* name is in locale encoding */
182 static void add_item(const gchar *name)
184 GtkTreeIter iter;
185 gchar *fname, *utf8_name, *utf8_fullname;
186 const gchar *sep;
187 gboolean dir;
189 if (G_UNLIKELY(! NZV(name)))
190 return;
192 /* root directory doesn't need separator */
193 sep = (utils_str_equal(current_dir, "/")) ? "" : G_DIR_SEPARATOR_S;
194 fname = g_strconcat(current_dir, sep, name, NULL);
195 dir = g_file_test(fname, G_FILE_TEST_IS_DIR);
196 utf8_fullname = utils_get_locale_from_utf8(fname);
197 utf8_name = utils_get_utf8_from_locale(name);
198 g_free(fname);
200 if (! show_hidden_files && check_hidden(utf8_fullname, utf8_name))
201 goto done;
203 if (dir)
205 if (last_dir_iter == NULL)
206 gtk_list_store_prepend(file_store, &iter);
207 else
209 gtk_list_store_insert_after(file_store, &iter, last_dir_iter);
210 gtk_tree_iter_free(last_dir_iter);
212 last_dir_iter = gtk_tree_iter_copy(&iter);
214 else
216 if (! show_hidden_files && hide_object_files && check_object(utf8_name))
217 goto done;
218 if (check_filtered(utf8_name))
219 goto done;
221 gtk_list_store_append(file_store, &iter);
223 gtk_list_store_set(file_store, &iter,
224 FILEVIEW_COLUMN_ICON, (dir) ? GTK_STOCK_DIRECTORY : GTK_STOCK_FILE,
225 FILEVIEW_COLUMN_NAME, utf8_name,
226 FILEVIEW_COLUMN_FILENAME, utf8_fullname,
227 -1);
228 done:
229 g_free(utf8_name);
230 g_free(utf8_fullname);
234 /* adds ".." to the start of the file list */
235 static void add_top_level_entry(void)
237 GtkTreeIter iter;
238 gchar *utf8_dir;
240 if (! NZV(g_path_skip_root(current_dir)))
241 return; /* ignore 'C:\' or '/' */
243 utf8_dir = g_path_get_dirname(current_dir);
244 SETPTR(utf8_dir, utils_get_utf8_from_locale(utf8_dir));
246 gtk_list_store_prepend(file_store, &iter);
247 last_dir_iter = gtk_tree_iter_copy(&iter);
249 gtk_list_store_set(file_store, &iter,
250 FILEVIEW_COLUMN_ICON, GTK_STOCK_DIRECTORY,
251 FILEVIEW_COLUMN_NAME, "..",
252 FILEVIEW_COLUMN_FILENAME, utf8_dir,
253 -1);
254 g_free(utf8_dir);
258 static void clear(void)
260 gtk_list_store_clear(file_store);
262 /* reset the directory item pointer */
263 if (last_dir_iter != NULL)
264 gtk_tree_iter_free(last_dir_iter);
265 last_dir_iter = NULL;
269 /* recreate the tree model from current_dir. */
270 static void refresh(void)
272 gchar *utf8_dir;
273 GSList *list, *node;
275 /* don't clear when the new path doesn't exist */
276 if (! g_file_test(current_dir, G_FILE_TEST_EXISTS))
277 return;
279 clear();
281 utf8_dir = utils_get_utf8_from_locale(current_dir);
282 gtk_entry_set_text(GTK_ENTRY(path_entry), utf8_dir);
283 gtk_widget_set_tooltip_text(path_entry, utf8_dir);
284 ui_combo_box_add_to_history(GTK_COMBO_BOX_ENTRY(path_combo), utf8_dir, 0);
285 g_free(utf8_dir);
287 add_top_level_entry(); /* ".." item */
289 list = utils_get_file_list(current_dir, NULL, NULL);
290 if (list != NULL)
292 /* free filenames as we go through the list */
293 foreach_slist(node, list)
295 gchar *fname = node->data;
297 add_item(fname);
298 g_free(fname);
300 g_slist_free(list);
302 gtk_entry_completion_set_model(entry_completion, GTK_TREE_MODEL(file_store));
306 static void on_go_home(void)
308 SETPTR(current_dir, g_strdup(g_get_home_dir()));
309 refresh();
313 /* TODO: use utils_get_default_dir_utf8() */
314 static gchar *get_default_dir(void)
316 const gchar *dir = NULL;
317 GeanyProject *project = geany->app->project;
319 if (project)
320 dir = project->base_path;
321 else
322 dir = geany->prefs->default_open_path;
324 if (NZV(dir))
325 return utils_get_locale_from_utf8(dir);
327 return g_get_current_dir();
331 static void on_current_path(void)
333 gchar *fname;
334 gchar *dir;
335 GeanyDocument *doc = document_get_current();
337 if (doc == NULL || doc->file_name == NULL || ! g_path_is_absolute(doc->file_name))
339 SETPTR(current_dir, get_default_dir());
340 refresh();
341 return;
343 fname = doc->file_name;
344 fname = utils_get_locale_from_utf8(fname);
345 dir = g_path_get_dirname(fname);
346 g_free(fname);
348 SETPTR(current_dir, dir);
349 refresh();
353 static void on_go_up(void)
355 gsize len = strlen(current_dir);
356 if (current_dir[len-1] == G_DIR_SEPARATOR)
357 current_dir[len-1] = '\0';
358 /* remove the highest directory part (which becomes the basename of current_dir) */
359 SETPTR(current_dir, g_path_get_dirname(current_dir));
360 refresh();
364 static gboolean check_single_selection(GtkTreeSelection *treesel)
366 if (gtk_tree_selection_count_selected_rows(treesel) == 1)
367 return TRUE;
369 ui_set_statusbar(FALSE, _("Too many items selected!"));
370 return FALSE;
374 /* Returns: TRUE if at least one of selected_items is a folder. */
375 static gboolean is_folder_selected(GList *selected_items)
377 GList *item;
378 GtkTreeModel *model = GTK_TREE_MODEL(file_store);
379 gboolean dir_found = FALSE;
381 for (item = selected_items; item != NULL; item = g_list_next(item))
383 gchar *icon;
384 GtkTreeIter iter;
385 GtkTreePath *treepath;
387 treepath = (GtkTreePath*) item->data;
388 gtk_tree_model_get_iter(model, &iter, treepath);
389 gtk_tree_model_get(model, &iter, FILEVIEW_COLUMN_ICON, &icon, -1);
391 if (utils_str_equal(icon, GTK_STOCK_DIRECTORY))
393 dir_found = TRUE;
394 g_free(icon);
395 break;
397 g_free(icon);
399 return dir_found;
403 /* Returns: the full filename in locale encoding. */
404 static gchar *get_tree_path_filename(GtkTreePath *treepath)
406 GtkTreeModel *model = GTK_TREE_MODEL(file_store);
407 GtkTreeIter iter;
408 gchar *name, *fname;
410 gtk_tree_model_get_iter(model, &iter, treepath);
411 gtk_tree_model_get(model, &iter, FILEVIEW_COLUMN_FILENAME, &name, -1);
413 fname = utils_get_locale_from_utf8(name);
414 g_free(name);
416 return fname;
420 static void open_external(const gchar *fname, gboolean dir_found)
422 gchar *cmd;
423 gchar *locale_cmd;
424 gchar *dir;
425 GString *cmd_str = g_string_new(open_cmd);
426 GError *error = NULL;
428 if (! dir_found)
429 dir = g_path_get_dirname(fname);
430 else
431 dir = g_strdup(fname);
433 utils_string_replace_all(cmd_str, "%f", fname);
434 utils_string_replace_all(cmd_str, "%d", dir);
436 cmd = g_string_free(cmd_str, FALSE);
437 locale_cmd = utils_get_locale_from_utf8(cmd);
438 if (! g_spawn_command_line_async(locale_cmd, &error))
440 gchar *c = strchr(cmd, ' ');
442 if (c != NULL)
443 *c = '\0';
444 ui_set_statusbar(TRUE,
445 _("Could not execute configured external command '%s' (%s)."),
446 cmd, error->message);
447 g_error_free(error);
449 g_free(locale_cmd);
450 g_free(cmd);
451 g_free(dir);
455 static void on_external_open(GtkMenuItem *menuitem, gpointer user_data)
457 GtkTreeSelection *treesel;
458 GtkTreeModel *model;
459 GList *list;
460 gboolean dir_found;
462 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
464 list = gtk_tree_selection_get_selected_rows(treesel, &model);
465 dir_found = is_folder_selected(list);
467 if (! dir_found || check_single_selection(treesel))
469 GList *item;
471 for (item = list; item != NULL; item = g_list_next(item))
473 GtkTreePath *treepath = item->data;
474 gchar *fname = get_tree_path_filename(treepath);
476 open_external(fname, dir_found);
477 g_free(fname);
481 g_list_foreach(list, (GFunc) gtk_tree_path_free, NULL);
482 g_list_free(list);
486 /* We use document_open_files() as it's more efficient. */
487 static void open_selected_files(GList *list, gboolean do_not_focus)
489 GSList *files = NULL;
490 GList *item;
491 GeanyDocument *doc;
493 for (item = list; item != NULL; item = g_list_next(item))
495 GtkTreePath *treepath = item->data;
496 gchar *fname = get_tree_path_filename(treepath);
498 files = g_slist_prepend(files, fname);
500 files = g_slist_reverse(files);
501 document_open_files(files, FALSE, NULL, NULL);
502 doc = document_get_current();
503 if (doc != NULL && ! do_not_focus)
504 keybindings_send_command(GEANY_KEY_GROUP_FOCUS, GEANY_KEYS_FOCUS_EDITOR);
506 g_slist_foreach(files, (GFunc) g_free, NULL); /* free filenames */
507 g_slist_free(files);
511 static void open_folder(GtkTreePath *treepath)
513 gchar *fname = get_tree_path_filename(treepath);
515 SETPTR(current_dir, fname);
516 refresh();
520 static void on_open_clicked(GtkMenuItem *menuitem, gpointer user_data)
522 GtkTreeSelection *treesel;
523 GtkTreeModel *model;
524 GList *list;
525 gboolean dir_found;
527 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
529 list = gtk_tree_selection_get_selected_rows(treesel, &model);
530 dir_found = is_folder_selected(list);
532 if (dir_found)
534 if (check_single_selection(treesel))
536 GtkTreePath *treepath = list->data; /* first selected item */
538 open_folder(treepath);
541 else
542 open_selected_files(list, GPOINTER_TO_INT(user_data));
544 g_list_foreach(list, (GFunc) gtk_tree_path_free, NULL);
545 g_list_free(list);
549 static void on_find_in_files(GtkMenuItem *menuitem, gpointer user_data)
551 GtkTreeSelection *treesel;
552 GtkTreeModel *model;
553 GList *list;
554 gchar *dir;
555 gboolean is_dir = FALSE;
557 treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
558 /* allow 0 or 1 selections */
559 if (gtk_tree_selection_count_selected_rows(treesel) > 0 &&
560 ! check_single_selection(treesel))
561 return;
563 list = gtk_tree_selection_get_selected_rows(treesel, &model);
564 is_dir = is_folder_selected(list);
566 if (is_dir)
568 GtkTreePath *treepath = list->data; /* first selected item */
570 dir = get_tree_path_filename(treepath);
572 else
573 dir = g_strdup(current_dir);
575 g_list_foreach(list, (GFunc) gtk_tree_path_free, NULL);
576 g_list_free(list);
578 SETPTR(dir, utils_get_utf8_from_locale(dir));
579 search_show_find_in_files_dialog(dir);
580 g_free(dir);
584 static void on_hidden_files_clicked(GtkCheckMenuItem *item)
586 show_hidden_files = gtk_check_menu_item_get_active(item);
587 refresh();
591 static void on_hide_sidebar(void)
593 keybindings_send_command(GEANY_KEY_GROUP_VIEW, GEANY_KEYS_VIEW_SIDEBAR);
597 static void on_show_preferences(void)
599 plugin_show_configure(geany_plugin);
603 static GtkWidget *create_popup_menu(void)
605 GtkWidget *item, *menu;
607 menu = gtk_menu_new();
609 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL);
610 gtk_widget_show(item);
611 gtk_container_add(GTK_CONTAINER(menu), item);
612 g_signal_connect(item, "activate", G_CALLBACK(on_open_clicked), NULL);
613 popup_items.open = item;
615 item = ui_image_menu_item_new(GTK_STOCK_OPEN, _("Open _externally"));
616 gtk_widget_show(item);
617 gtk_container_add(GTK_CONTAINER(menu), item);
618 g_signal_connect(item, "activate", G_CALLBACK(on_external_open), NULL);
619 popup_items.open_external = item;
621 item = gtk_separator_menu_item_new();
622 gtk_widget_show(item);
623 gtk_container_add(GTK_CONTAINER(menu), item);
625 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_REFRESH, NULL);
626 gtk_widget_show(item);
627 gtk_container_add(GTK_CONTAINER(menu), item);
628 g_signal_connect(item, "activate", G_CALLBACK(refresh), NULL);
630 item = ui_image_menu_item_new(GTK_STOCK_FIND, _("_Find in Files"));
631 gtk_widget_show(item);
632 gtk_container_add(GTK_CONTAINER(menu), item);
633 g_signal_connect(item, "activate", G_CALLBACK(on_find_in_files), NULL);
634 popup_items.find_in_files = item;
636 item = gtk_separator_menu_item_new();
637 gtk_widget_show(item);
638 gtk_container_add(GTK_CONTAINER(menu), item);
640 item = gtk_check_menu_item_new_with_mnemonic(_("Show _Hidden Files"));
641 gtk_widget_show(item);
642 gtk_container_add(GTK_CONTAINER(menu), item);
643 g_signal_connect(item, "activate", G_CALLBACK(on_hidden_files_clicked), NULL);
644 popup_items.show_hidden_files = item;
646 item = gtk_separator_menu_item_new();
647 gtk_widget_show(item);
648 gtk_container_add(GTK_CONTAINER(menu), item);
650 item = gtk_image_menu_item_new_from_stock(GTK_STOCK_PREFERENCES, NULL);
651 gtk_widget_show(item);
652 gtk_container_add(GTK_CONTAINER(menu), item);
653 g_signal_connect(item, "activate", G_CALLBACK(on_show_preferences), NULL);
655 item = gtk_separator_menu_item_new();
656 gtk_widget_show(item);
657 gtk_container_add(GTK_CONTAINER(menu), item);
659 item = ui_image_menu_item_new(GTK_STOCK_CLOSE, _("H_ide Sidebar"));
660 gtk_widget_show(item);
661 gtk_container_add(GTK_CONTAINER(menu), item);
662 g_signal_connect(item, "activate", G_CALLBACK(on_hide_sidebar), NULL);
664 return menu;
668 static void on_tree_selection_changed(GtkTreeSelection *selection, gpointer data)
670 gboolean have_sel = (gtk_tree_selection_count_selected_rows(selection) > 0);
671 gboolean multi_sel = (gtk_tree_selection_count_selected_rows(selection) > 1);
673 if (popup_items.open != NULL)
674 gtk_widget_set_sensitive(popup_items.open, have_sel);
675 if (popup_items.open_external != NULL)
676 gtk_widget_set_sensitive(popup_items.open_external, have_sel);
677 if (popup_items.find_in_files != NULL)
678 gtk_widget_set_sensitive(popup_items.find_in_files, have_sel && ! multi_sel);
682 static gboolean on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
684 if (event->button == 1 && event->type == GDK_2BUTTON_PRESS)
686 on_open_clicked(NULL, NULL);
687 return TRUE;
689 else if (event->button == 3)
691 static GtkWidget *popup_menu = NULL;
693 if (popup_menu == NULL)
694 popup_menu = create_popup_menu();
696 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(popup_items.show_hidden_files),
697 show_hidden_files);
698 gtk_menu_popup(GTK_MENU(popup_menu), NULL, NULL, NULL, NULL, event->button, event->time);
699 /* don't return TRUE here, unless the selection won't be changed */
701 return FALSE;
705 static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
707 if (ui_is_keyval_enter_or_return(event->keyval))
709 on_open_clicked(NULL, NULL);
710 return TRUE;
713 if (event->keyval == GDK_space)
715 on_open_clicked(NULL, GINT_TO_POINTER(TRUE));
716 return TRUE;
719 if ((event->keyval == GDK_Up ||
720 event->keyval == GDK_KP_Up) &&
721 (event->state & GDK_MOD1_MASK)) /* FIXME: Alt-Up doesn't seem to work! */
723 on_go_up();
724 return TRUE;
727 if ((event->keyval == GDK_F10 && event->state & GDK_SHIFT_MASK) || event->keyval == GDK_Menu)
729 GdkEventButton button_event;
731 button_event.time = event->time;
732 button_event.button = 3;
734 on_button_press(widget, &button_event, data);
735 return TRUE;
738 return FALSE;
742 static void clear_filter(void)
744 if (filter != NULL)
746 g_strfreev(filter);
747 filter = NULL;
752 static void on_clear_filter(GtkEntry *entry, gpointer user_data)
754 clear_filter();
756 gtk_entry_set_text(GTK_ENTRY(filter_entry), "");
758 refresh();
762 static void on_path_entry_activate(GtkEntry *entry, gpointer user_data)
764 gchar *new_dir = (gchar*) gtk_entry_get_text(entry);
766 if (NZV(new_dir))
768 if (g_str_has_suffix(new_dir, ".."))
770 on_go_up();
771 return;
773 else if (new_dir[0] == '~')
775 GString *str = g_string_new(new_dir);
776 utils_string_replace_first(str, "~", g_get_home_dir());
777 new_dir = g_string_free(str, FALSE);
779 else
780 new_dir = utils_get_locale_from_utf8(new_dir);
782 else
783 new_dir = g_strdup(g_get_home_dir());
785 SETPTR(current_dir, new_dir);
787 on_clear_filter(NULL, NULL);
791 static void ui_combo_box_changed(GtkComboBox *combo, gpointer user_data)
793 /* we get this callback on typing as well as choosing an item */
794 if (gtk_combo_box_get_active(combo) >= 0)
795 gtk_widget_activate(gtk_bin_get_child(GTK_BIN(combo)));
799 static void on_filter_activate(GtkEntry *entry, gpointer user_data)
801 /* We use spaces for consistency with Find in Files file patterns
802 * ';' also supported like original patch. */
803 filter = g_strsplit_set(gtk_entry_get_text(entry), "; ", -1);
804 if (filter == NULL || g_strv_length(filter) == 0)
806 clear_filter();
808 ui_combo_box_add_to_history(GTK_COMBO_BOX_ENTRY(filter_combo), NULL, 0);
809 refresh();
813 static void on_filter_clear(GtkEntry *entry, gint icon_pos,
814 GdkEvent *event, gpointer data)
816 clear_filter();
817 refresh();
821 static void prepare_file_view(void)
823 GtkCellRenderer *text_renderer, *icon_renderer;
824 GtkTreeViewColumn *column;
825 GtkTreeSelection *selection;
827 file_store = gtk_list_store_new(FILEVIEW_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
829 gtk_tree_view_set_model(GTK_TREE_VIEW(file_view), GTK_TREE_MODEL(file_store));
830 g_object_unref(file_store);
832 icon_renderer = gtk_cell_renderer_pixbuf_new();
833 text_renderer = gtk_cell_renderer_text_new();
834 column = gtk_tree_view_column_new();
835 gtk_tree_view_column_pack_start(column, icon_renderer, FALSE);
836 gtk_tree_view_column_set_attributes(column, icon_renderer, "stock-id", FILEVIEW_COLUMN_ICON, NULL);
837 gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
838 gtk_tree_view_column_set_attributes(column, text_renderer, "text", FILEVIEW_COLUMN_NAME, NULL);
839 gtk_tree_view_append_column(GTK_TREE_VIEW(file_view), column);
840 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(file_view), FALSE);
842 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(file_view), TRUE);
843 gtk_tree_view_set_search_column(GTK_TREE_VIEW(file_view), FILEVIEW_COLUMN_NAME);
845 ui_widget_modify_font_from_string(file_view, geany->interface_prefs->tagbar_font);
847 /* tooltips */
848 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(file_view), FILEVIEW_COLUMN_FILENAME);
850 /* selection handling */
851 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(file_view));
852 gtk_tree_selection_set_mode(selection, GTK_SELECTION_MULTIPLE);
854 /* Show the current path when the FB is first needed */
855 g_signal_connect(file_view, "realize", G_CALLBACK(on_current_path), NULL);
856 g_signal_connect(selection, "changed", G_CALLBACK(on_tree_selection_changed), NULL);
857 g_signal_connect(file_view, "button-press-event", G_CALLBACK(on_button_press), NULL);
858 g_signal_connect(file_view, "key-press-event", G_CALLBACK(on_key_press), NULL);
862 static GtkWidget *make_toolbar(void)
864 GtkWidget *wid, *toolbar;
866 toolbar = gtk_toolbar_new();
867 gtk_toolbar_set_icon_size(GTK_TOOLBAR(toolbar), GTK_ICON_SIZE_MENU);
868 gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
870 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_GO_UP));
871 gtk_widget_set_tooltip_text(wid, _("Up"));
872 g_signal_connect(wid, "clicked", G_CALLBACK(on_go_up), NULL);
873 gtk_container_add(GTK_CONTAINER(toolbar), wid);
875 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_REFRESH));
876 gtk_widget_set_tooltip_text(wid, _("Refresh"));
877 g_signal_connect(wid, "clicked", G_CALLBACK(refresh), NULL);
878 gtk_container_add(GTK_CONTAINER(toolbar), wid);
880 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_HOME));
881 gtk_widget_set_tooltip_text(wid, _("Home"));
882 g_signal_connect(wid, "clicked", G_CALLBACK(on_go_home), NULL);
883 gtk_container_add(GTK_CONTAINER(toolbar), wid);
885 wid = GTK_WIDGET(gtk_tool_button_new_from_stock(GTK_STOCK_JUMP_TO));
886 gtk_widget_set_tooltip_text(wid, _("Set path from document"));
887 g_signal_connect(wid, "clicked", G_CALLBACK(on_current_path), NULL);
888 gtk_container_add(GTK_CONTAINER(toolbar), wid);
890 return toolbar;
894 static GtkWidget *make_filterbar(void)
896 GtkWidget *label, *filterbar;
898 filterbar = gtk_hbox_new(FALSE, 1);
900 label = gtk_label_new(_("Filter:"));
902 filter_combo = gtk_combo_box_entry_new_text();
903 filter_entry = gtk_bin_get_child(GTK_BIN(filter_combo));
905 ui_entry_add_clear_icon(GTK_ENTRY(filter_entry));
906 g_signal_connect(filter_entry, "icon-release", G_CALLBACK(on_filter_clear), NULL);
908 gtk_widget_set_tooltip_text(filter_entry,
909 _("Filter your files with the usual wildcards. Separate multiple patterns with a space."));
910 g_signal_connect(filter_entry, "activate", G_CALLBACK(on_filter_activate), NULL);
911 g_signal_connect(filter_combo, "changed", G_CALLBACK(ui_combo_box_changed), NULL);
913 gtk_box_pack_start(GTK_BOX(filterbar), label, FALSE, FALSE, 0);
914 gtk_box_pack_start(GTK_BOX(filterbar), filter_combo, TRUE, TRUE, 0);
916 return filterbar;
920 static gboolean completion_match_func(GtkEntryCompletion *completion, const gchar *key,
921 GtkTreeIter *iter, gpointer user_data)
923 gchar *str, *icon;
924 gboolean result = FALSE;
926 gtk_tree_model_get(GTK_TREE_MODEL(file_store), iter,
927 FILEVIEW_COLUMN_ICON, &icon, FILEVIEW_COLUMN_NAME, &str, -1);
929 if (str != NULL && icon != NULL && utils_str_equal(icon, GTK_STOCK_DIRECTORY) &&
930 ! g_str_has_suffix(key, G_DIR_SEPARATOR_S))
932 /* key is something like "/tmp/te" and str is a filename like "test",
933 * so strip the path from key to make them comparable */
934 gchar *base_name = g_path_get_basename(key);
935 gchar *str_lowered = g_utf8_strdown(str, -1);
936 result = g_str_has_prefix(str_lowered, base_name);
937 g_free(base_name);
938 g_free(str_lowered);
940 g_free(str);
941 g_free(icon);
943 return result;
947 static gboolean completion_match_selected(GtkEntryCompletion *widget, GtkTreeModel *model,
948 GtkTreeIter *iter, gpointer user_data)
950 gchar *str;
951 gtk_tree_model_get(model, iter, FILEVIEW_COLUMN_NAME, &str, -1);
952 if (str != NULL)
954 gchar *text = g_strconcat(current_dir, G_DIR_SEPARATOR_S, str, NULL);
955 gtk_entry_set_text(GTK_ENTRY(path_entry), text);
956 gtk_editable_set_position(GTK_EDITABLE(path_entry), -1);
957 /* force change of directory when completion is done */
958 on_path_entry_activate(GTK_ENTRY(path_entry), NULL);
959 g_free(text);
961 g_free(str);
963 return TRUE;
967 static void completion_create(void)
969 entry_completion = gtk_entry_completion_new();
971 gtk_entry_completion_set_inline_completion(entry_completion, FALSE);
972 gtk_entry_completion_set_popup_completion(entry_completion, TRUE);
973 gtk_entry_completion_set_text_column(entry_completion, FILEVIEW_COLUMN_NAME);
974 gtk_entry_completion_set_match_func(entry_completion, completion_match_func, NULL, NULL);
976 g_signal_connect(entry_completion, "match-selected",
977 G_CALLBACK(completion_match_selected), NULL);
979 gtk_entry_set_completion(GTK_ENTRY(path_entry), entry_completion);
983 static void load_settings(void)
985 GKeyFile *config = g_key_file_new();
987 config_file = g_strconcat(geany->app->configdir, G_DIR_SEPARATOR_S, "plugins", G_DIR_SEPARATOR_S,
988 "filebrowser", G_DIR_SEPARATOR_S, "filebrowser.conf", NULL);
989 g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
991 open_cmd = utils_get_setting_string(config, "filebrowser", "open_command", "nautilus \"%d\"");
992 /* g_key_file_get_boolean defaults to FALSE */
993 show_hidden_files = g_key_file_get_boolean(config, "filebrowser", "show_hidden_files", NULL);
994 hide_object_files = utils_get_setting_boolean(config, "filebrowser", "hide_object_files", TRUE);
995 hidden_file_extensions = utils_get_setting_string(config, "filebrowser", "hidden_file_extensions",
996 ".o .obj .so .dll .a .lib .pyc");
997 fb_follow_path = g_key_file_get_boolean(config, "filebrowser", "fb_follow_path", NULL);
998 fb_set_project_base_path = g_key_file_get_boolean(config, "filebrowser", "fb_set_project_base_path", NULL);
1000 g_key_file_free(config);
1004 static void project_change_cb(G_GNUC_UNUSED GObject *obj, G_GNUC_UNUSED GKeyFile *config,
1005 G_GNUC_UNUSED gpointer data)
1007 gchar *new_dir;
1008 GeanyProject *project = geany->app->project;
1010 if (! fb_set_project_base_path || project == NULL || ! NZV(project->base_path))
1011 return;
1013 /* TODO this is a copy of project_get_base_path(), add it to the plugin API */
1014 if (g_path_is_absolute(project->base_path))
1015 new_dir = g_strdup(project->base_path);
1016 else
1017 { /* build base_path out of project file name's dir and base_path */
1018 gchar *dir = g_path_get_dirname(project->file_name);
1020 new_dir = g_strconcat(dir, G_DIR_SEPARATOR_S, project->base_path, NULL);
1021 g_free(dir);
1023 /* get it into locale encoding */
1024 SETPTR(new_dir, utils_get_locale_from_utf8(new_dir));
1026 if (! utils_str_equal(current_dir, new_dir))
1028 SETPTR(current_dir, new_dir);
1029 refresh();
1031 else
1032 g_free(new_dir);
1036 static gpointer last_activate_path = NULL;
1038 static void document_activate_cb(G_GNUC_UNUSED GObject *obj, GeanyDocument *doc,
1039 G_GNUC_UNUSED gpointer data)
1041 gchar *new_dir;
1043 last_activate_path = doc->real_path;
1045 if (! fb_follow_path || doc->file_name == NULL || ! g_path_is_absolute(doc->file_name))
1046 return;
1048 new_dir = g_path_get_dirname(doc->file_name);
1049 SETPTR(new_dir, utils_get_locale_from_utf8(new_dir));
1051 if (! utils_str_equal(current_dir, new_dir))
1053 SETPTR(current_dir, new_dir);
1054 refresh();
1056 else
1057 g_free(new_dir);
1061 static void document_save_cb(GObject *obj, GeanyDocument *doc, gpointer user_data)
1063 if (!last_activate_path)
1064 document_activate_cb(obj, doc, user_data);
1068 static void kb_activate(guint key_id)
1070 gtk_notebook_set_current_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook), page_number);
1071 switch (key_id)
1073 case KB_FOCUS_FILE_LIST:
1074 gtk_widget_grab_focus(file_view);
1075 break;
1076 case KB_FOCUS_PATH_ENTRY:
1077 gtk_widget_grab_focus(path_entry);
1078 break;
1083 void plugin_init(GeanyData *data)
1085 GtkWidget *scrollwin, *toolbar, *filterbar;
1087 filter = NULL;
1089 file_view_vbox = gtk_vbox_new(FALSE, 0);
1090 toolbar = make_toolbar();
1091 gtk_box_pack_start(GTK_BOX(file_view_vbox), toolbar, FALSE, FALSE, 0);
1093 filterbar = make_filterbar();
1094 gtk_box_pack_start(GTK_BOX(file_view_vbox), filterbar, FALSE, FALSE, 0);
1096 path_combo = gtk_combo_box_entry_new_text();
1097 gtk_box_pack_start(GTK_BOX(file_view_vbox), path_combo, FALSE, FALSE, 2);
1098 g_signal_connect(path_combo, "changed", G_CALLBACK(ui_combo_box_changed), NULL);
1099 path_entry = gtk_bin_get_child(GTK_BIN(path_combo));
1100 g_signal_connect(path_entry, "activate", G_CALLBACK(on_path_entry_activate), NULL);
1102 file_view = gtk_tree_view_new();
1103 prepare_file_view();
1104 completion_create();
1106 popup_items.open = popup_items.open_external = popup_items.find_in_files = NULL;
1108 scrollwin = gtk_scrolled_window_new(NULL, NULL);
1109 gtk_scrolled_window_set_policy(
1110 GTK_SCROLLED_WINDOW(scrollwin),
1111 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
1112 gtk_container_add(GTK_CONTAINER(scrollwin), file_view);
1113 gtk_container_add(GTK_CONTAINER(file_view_vbox), scrollwin);
1115 /* load settings before file_view "realize" callback */
1116 load_settings();
1118 gtk_widget_show_all(file_view_vbox);
1119 page_number = gtk_notebook_append_page(GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook),
1120 file_view_vbox, gtk_label_new(_("Files")));
1122 /* setup keybindings */
1123 keybindings_set_item(plugin_key_group, KB_FOCUS_FILE_LIST, kb_activate,
1124 0, 0, "focus_file_list", _("Focus File List"), NULL);
1125 keybindings_set_item(plugin_key_group, KB_FOCUS_PATH_ENTRY, kb_activate,
1126 0, 0, "focus_path_entry", _("Focus Path Entry"), NULL);
1128 plugin_signal_connect(geany_plugin, NULL, "document-activate", TRUE,
1129 (GCallback) &document_activate_cb, NULL);
1130 plugin_signal_connect(geany_plugin, NULL, "document-save", TRUE,
1131 (GCallback) &document_save_cb, NULL);
1135 static void save_settings(void)
1137 GKeyFile *config = g_key_file_new();
1138 gchar *data;
1139 gchar *config_dir = g_path_get_dirname(config_file);
1141 g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
1143 g_key_file_set_string(config, "filebrowser", "open_command", open_cmd);
1144 g_key_file_set_boolean(config, "filebrowser", "show_hidden_files", show_hidden_files);
1145 g_key_file_set_boolean(config, "filebrowser", "hide_object_files", hide_object_files);
1146 g_key_file_set_string(config, "filebrowser", "hidden_file_extensions", hidden_file_extensions);
1147 g_key_file_set_boolean(config, "filebrowser", "fb_follow_path", fb_follow_path);
1148 g_key_file_set_boolean(config, "filebrowser", "fb_set_project_base_path",
1149 fb_set_project_base_path);
1151 if (! g_file_test(config_dir, G_FILE_TEST_IS_DIR) && utils_mkdir(config_dir, TRUE) != 0)
1153 dialogs_show_msgbox(GTK_MESSAGE_ERROR,
1154 _("Plugin configuration directory could not be created."));
1156 else
1158 /* write config to file */
1159 data = g_key_file_to_data(config, NULL, NULL);
1160 utils_write_file(config_file, data);
1161 g_free(data);
1163 g_free(config_dir);
1164 g_key_file_free(config);
1168 static struct
1170 GtkWidget *open_cmd_entry;
1171 GtkWidget *show_hidden_checkbox;
1172 GtkWidget *hide_objects_checkbox;
1173 GtkWidget *hidden_files_entry;
1174 GtkWidget *follow_path_checkbox;
1175 GtkWidget *set_project_base_path_checkbox;
1177 pref_widgets;
1179 static void
1180 on_configure_response(GtkDialog *dialog, gint response, gpointer user_data)
1182 if (response == GTK_RESPONSE_OK || response == GTK_RESPONSE_APPLY)
1184 g_free(open_cmd);
1185 open_cmd = g_strdup(gtk_entry_get_text(GTK_ENTRY(pref_widgets.open_cmd_entry)));
1186 show_hidden_files = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.show_hidden_checkbox));
1187 hide_object_files = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.hide_objects_checkbox));
1188 g_free(hidden_file_extensions);
1189 hidden_file_extensions = g_strdup(gtk_entry_get_text(GTK_ENTRY(pref_widgets.hidden_files_entry)));
1190 fb_follow_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.follow_path_checkbox));
1191 fb_set_project_base_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
1192 pref_widgets.set_project_base_path_checkbox));
1194 /* apply the changes */
1195 refresh();
1200 static void on_toggle_hidden(void)
1202 gboolean enabled = !gtk_toggle_button_get_active(
1203 GTK_TOGGLE_BUTTON(pref_widgets.show_hidden_checkbox));
1205 gtk_widget_set_sensitive(pref_widgets.hide_objects_checkbox, enabled);
1206 enabled &= gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_widgets.hide_objects_checkbox));
1207 gtk_widget_set_sensitive(pref_widgets.hidden_files_entry, enabled);
1211 GtkWidget *plugin_configure(GtkDialog *dialog)
1213 GtkWidget *label, *entry, *checkbox_of, *checkbox_hf, *checkbox_fp, *checkbox_pb, *vbox;
1214 GtkWidget *box, *align;
1216 vbox = gtk_vbox_new(FALSE, 6);
1217 box = gtk_vbox_new(FALSE, 3);
1219 label = gtk_label_new(_("External open command:"));
1220 gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
1221 gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
1223 entry = gtk_entry_new();
1224 if (open_cmd != NULL)
1225 gtk_entry_set_text(GTK_ENTRY(entry), open_cmd);
1226 gtk_widget_set_tooltip_text(entry,
1227 _("The command to execute when using \"Open with\". You can use %f and %d wildcards.\n"
1228 "%f will be replaced with the filename including full path\n"
1229 "%d will be replaced with the path name of the selected file without the filename"));
1230 gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
1231 pref_widgets.open_cmd_entry = entry;
1233 gtk_box_pack_start(GTK_BOX(vbox), box, FALSE, FALSE, 3);
1235 checkbox_hf = gtk_check_button_new_with_label(_("Show hidden files"));
1236 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_hf), FALSE);
1237 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_hf), show_hidden_files);
1238 gtk_box_pack_start(GTK_BOX(vbox), checkbox_hf, FALSE, FALSE, 0);
1239 pref_widgets.show_hidden_checkbox = checkbox_hf;
1240 g_signal_connect(checkbox_hf, "toggled", on_toggle_hidden, NULL);
1242 box = gtk_vbox_new(FALSE, 3);
1243 checkbox_of = gtk_check_button_new_with_label(_("Hide file extensions:"));
1244 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_of), FALSE);
1245 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_of), hide_object_files);
1246 gtk_box_pack_start(GTK_BOX(box), checkbox_of, FALSE, FALSE, 0);
1247 pref_widgets.hide_objects_checkbox = checkbox_of;
1248 g_signal_connect(checkbox_of, "toggled", on_toggle_hidden, NULL);
1250 entry = gtk_entry_new();
1251 if (hidden_file_extensions != NULL)
1252 gtk_entry_set_text(GTK_ENTRY(entry), hidden_file_extensions);
1253 gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0);
1254 pref_widgets.hidden_files_entry = entry;
1256 align = gtk_alignment_new(1, 0.5, 1, 1);
1257 gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 0, 12, 0);
1258 gtk_container_add(GTK_CONTAINER(align), box);
1259 gtk_box_pack_start(GTK_BOX(vbox), align, FALSE, FALSE, 0);
1260 on_toggle_hidden();
1262 checkbox_fp = gtk_check_button_new_with_label(_("Follow the path of the current file"));
1263 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_fp), FALSE);
1264 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_fp), fb_follow_path);
1265 gtk_box_pack_start(GTK_BOX(vbox), checkbox_fp, FALSE, FALSE, 0);
1266 pref_widgets.follow_path_checkbox = checkbox_fp;
1268 checkbox_pb = gtk_check_button_new_with_label(_("Use the project's base directory"));
1269 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox_pb), FALSE);
1270 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox_pb), fb_set_project_base_path);
1271 gtk_widget_set_tooltip_text(checkbox_pb,
1272 _("Change the directory to the base directory of the currently opened project"));
1273 gtk_box_pack_start(GTK_BOX(vbox), checkbox_pb, FALSE, FALSE, 0);
1274 pref_widgets.set_project_base_path_checkbox = checkbox_pb;
1276 gtk_widget_show_all(vbox);
1278 g_signal_connect(dialog, "response", G_CALLBACK(on_configure_response), NULL);
1279 return vbox;
1283 void plugin_cleanup(void)
1285 save_settings();
1287 g_free(config_file);
1288 g_free(open_cmd);
1289 g_free(hidden_file_extensions);
1290 clear_filter();
1291 gtk_widget_destroy(file_view_vbox);
1292 g_object_unref(G_OBJECT(entry_completion));