diff --git a/data/geany.glade b/data/geany.glade index 29b719250a..b5805dc651 100644 --- a/data/geany.glade +++ b/data/geany.glade @@ -104,6 +104,12 @@ gtk-open 1 + + True + False + gtk-open + 1 + True False @@ -6368,6 +6374,16 @@ + + + Open Files Recursivel_y + True + False + True + image_open_files_recursively1 + + + True diff --git a/doc/geany.txt b/doc/geany.txt index bf007c2e14..ca90ee155e 100644 --- a/doc/geany.txt +++ b/doc/geany.txt @@ -3313,6 +3313,8 @@ New Ctrl-N (C) Creates a new file. Open Ctrl-O (C) Opens a file. +Open files recursively Opens files recursively. + Open selected file Ctrl-Shift-O Opens the selected filename. Re-open last closed tab Re-opens the last closed document tab. diff --git a/src/callbacks.c b/src/callbacks.c index 1177925a0e..b09d0de785 100644 --- a/src/callbacks.c +++ b/src/callbacks.c @@ -313,7 +313,14 @@ static void on_info1_activate(GtkMenuItem *menuitem, gpointer user_data) /* open file */ void on_open1_activate(GtkMenuItem *menuitem, gpointer user_data) { - dialogs_show_open_file(); + dialogs_show_open_file(FALSE); +} + + +/* open files recursively */ +void on_open_files_recursively1_activate(GtkMenuItem *menuitem, gpointer user_data) +{ + dialogs_show_open_file(TRUE); } diff --git a/src/callbacks.h b/src/callbacks.h index c77b781a10..1117004a8c 100644 --- a/src/callbacks.h +++ b/src/callbacks.h @@ -41,6 +41,8 @@ void on_quit1_activate(GtkMenuItem *menuitem, gpointer user_data); void on_open1_activate(GtkMenuItem *menuitem, gpointer user_data); +void on_open_files_recursively1_activate(GtkMenuItem *menuitem, gpointer user_data); + void on_save_all1_activate(GtkMenuItem *menuitem, gpointer user_data); void on_close1_activate(GtkMenuItem *menuitem, gpointer user_data); diff --git a/src/dialogs.c b/src/dialogs.c index c1a390f17e..506b6026f8 100644 --- a/src/dialogs.c +++ b/src/dialogs.c @@ -62,7 +62,8 @@ enum { GEANY_RESPONSE_RENAME, - GEANY_RESPONSE_VIEW + GEANY_RESPONSE_VIEW, + GEANY_RESPONSE_OPEN_RECURSIVELY }; @@ -124,11 +125,11 @@ static void file_chooser_set_filter_idx(GtkFileChooser *chooser, guint idx) } -static gboolean open_file_dialog_handle_response(GtkWidget *dialog, gint response) +static gboolean open_file_dialog_handle_response(GtkWidget *dialog, gint response, gboolean recursive) { gboolean ret = TRUE; - if (response == GTK_RESPONSE_ACCEPT || response == GEANY_RESPONSE_VIEW) + if (response == GTK_RESPONSE_ACCEPT || response == GEANY_RESPONSE_VIEW || response == GEANY_RESPONSE_OPEN_RECURSIVELY) { GSList *filelist; GeanyFiletype *ft = NULL; @@ -151,6 +152,15 @@ static gboolean open_file_dialog_handle_response(GtkWidget *dialog, gint respons charset = encodings[filesel_state.open.encoding_idx].charset; filelist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); + + if (filelist == NULL && recursive) + { + gchar *path = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(dialog)); + + if (path) + filelist = g_slist_append(filelist, path); + } + if (filelist != NULL) { const gchar *first = filelist->data; @@ -163,7 +173,50 @@ static gboolean open_file_dialog_handle_response(GtkWidget *dialog, gint respons } else { - document_open_files(filelist, ro, ft, charset); + const gchar *only_dir = NULL; + gboolean opens_many = FALSE; + + if (filelist->next) + opens_many = TRUE; + else if (g_file_test(filelist->data, G_FILE_TEST_IS_DIR)) + { + opens_many = TRUE; + only_dir = filelist->data; + } + + if (recursive && opens_many) + { + GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(dialog)); + const gchar *filter_name = gtk_file_filter_get_name(filter); + GError *error; + gchar *message; + + if (only_dir) + message = g_strdup_printf(_("Open files in \"%s\" using filter \"%s\"?"), + only_dir, filter_name); + else + message = g_strdup_printf(_("Open files using filter \"%s\"?"), + filter_name); + + if (dialogs_show_question_full(dialog, GTK_STOCK_YES, GTK_STOCK_NO, + _("This may take a while depending on the files and the filter."), + "%s", message)) + { + document_open_files_recursively(filelist, ro, ft, charset, filter, &error); + + if (error) + { + dialogs_show_msgbox(GTK_MESSAGE_ERROR, "%s", error->message); + g_error_free(error); + } + } + else + ret = FALSE; + + g_free(message); + } + else + document_open_files(filelist, ro, ft, charset); } g_slist_foreach(filelist, (GFunc) g_free, NULL); /* free filenames */ } @@ -368,23 +421,40 @@ static GtkWidget *add_file_open_extra_widget(GtkWidget *dialog) } -static GtkWidget *create_open_file_dialog(void) +static GtkWidget *create_open_file_dialog(gboolean recursive) { GtkWidget *dialog; GtkWidget *viewbtn; GSList *node; + const gchar *title, *open_button_text, *view_button_text; + gint open_response_id; + + if (recursive) + { + title = _("Open Files Recursively"); + open_button_text = _("_Open Recursively"); + open_response_id = GEANY_RESPONSE_OPEN_RECURSIVELY; + view_button_text = _("_View Recursively"); + } + else + { + title = _("Open File"); + open_button_text = GTK_STOCK_OPEN; + open_response_id = GTK_RESPONSE_ACCEPT; + view_button_text = C_("Open dialog action", "_View"); + } - dialog = gtk_file_chooser_dialog_new(_("Open File"), GTK_WINDOW(main_widgets.window), + dialog = gtk_file_chooser_dialog_new(title, GTK_WINDOW(main_widgets.window), GTK_FILE_CHOOSER_ACTION_OPEN, NULL, NULL); gtk_widget_set_name(dialog, "GeanyDialog"); - viewbtn = gtk_dialog_add_button(GTK_DIALOG(dialog), C_("Open dialog action", "_View"), GEANY_RESPONSE_VIEW); + viewbtn = gtk_dialog_add_button(GTK_DIALOG(dialog), view_button_text, GEANY_RESPONSE_VIEW); gtk_widget_set_tooltip_text(viewbtn, _("Opens the file in read-only mode. If you choose more than one file to open, all files will be opened read-only.")); gtk_dialog_add_buttons(GTK_DIALOG(dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, - GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL); + open_button_text, open_response_id, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); gtk_widget_set_size_request(dialog, -1, 460); @@ -448,7 +518,7 @@ static void open_file_dialog_apply_settings(GtkWidget *dialog) /* This shows the file selection dialog to open a file. */ -void dialogs_show_open_file(void) +void dialogs_show_open_file(gboolean recursive) { gchar *initdir; @@ -463,12 +533,12 @@ void dialogs_show_open_file(void) SETPTR(initdir, utils_get_locale_from_utf8(initdir)); #ifdef G_OS_WIN32 - if (interface_prefs.use_native_windows_dialogs) + if (interface_prefs.use_native_windows_dialogs && !recursive) win32_show_document_open_dialog(GTK_WINDOW(main_widgets.window), _("Open File"), initdir); else #endif { - GtkWidget *dialog = create_open_file_dialog(); + GtkWidget *dialog = create_open_file_dialog(recursive); open_file_dialog_apply_settings(dialog); @@ -479,8 +549,9 @@ void dialogs_show_open_file(void) gtk_file_chooser_add_shortcut_folder(GTK_FILE_CHOOSER(dialog), app->project->base_path, NULL); - while (!open_file_dialog_handle_response(dialog, - gtk_dialog_run(GTK_DIALOG(dialog)))); + while (!open_file_dialog_handle_response(dialog, gtk_dialog_run(GTK_DIALOG(dialog)), + recursive)) + ; gtk_widget_destroy(dialog); } g_free(initdir); diff --git a/src/dialogs.h b/src/dialogs.h index 6198d38e67..439569474c 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -52,7 +52,7 @@ gchar *dialogs_show_input(const gchar *title, GtkWindow *parent, typedef void (*GeanyInputCallback)(const gchar *text, gpointer data); -void dialogs_show_open_file(void); +void dialogs_show_open_file(gboolean recursive); gboolean dialogs_show_unsaved_file(GeanyDocument *doc); diff --git a/src/document.c b/src/document.c index 9fad86fca4..66086f9e6d 100644 --- a/src/document.c +++ b/src/document.c @@ -1565,6 +1565,138 @@ void document_open_files(const GSList *filenames, gboolean readonly, GeanyFilety } +static GFileEnumerator *enumerate_children(const char *dir_path, GError **error) +{ + GFile *file = g_file_new_for_path(dir_path); + + const gchar *attributes = G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE; + + GFileEnumerator *enumerator = g_file_enumerate_children(file, attributes, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, error); + + g_object_unref(file); + return enumerator; +} + + +void document_open_files_recursively(const GSList *filenames, gboolean readonly, GeanyFiletype *ft, + const gchar *forced_enc, GtkFileFilter *filter, GError **error) +{ + const GSList *item; + gchar *file_path, *dir_path; + const gchar *filename; + GFileEnumerator *enumerator; + GFileInfo *file_info; + GFileType file_type; + GError *my_error = NULL; + + typedef struct EnumeratorData + { + GFileEnumerator *enumerator; + gchar *dir_path; + } EnumeratorData; + + const guint RECURSION_LIMIT = 100; + const guint ENUM_STACK_SIZE = RECURSION_LIMIT - 1; + EnumeratorData enum_stack[ENUM_STACK_SIZE]; + guint enum_stack_index = 0; + + GtkFileFilterInfo filter_info; + filter_info.contains = GTK_FILE_FILTER_FILENAME|GTK_FILE_FILTER_DISPLAY_NAME|GTK_FILE_FILTER_MIME_TYPE; + filter_info.uri = NULL; + + for (item = filenames; item != NULL; item = g_slist_next(item)) + { + file_path = item->data; + + if (g_file_test(file_path, G_FILE_TEST_IS_DIR)) + { + dir_path = file_path; + enumerator = enumerate_children(dir_path, &my_error); + + while (TRUE) + { + while (my_error == NULL) + { + file_info = g_file_enumerator_next_file(enumerator, NULL, &my_error); + + if (file_info == NULL) + break; + + file_type = g_file_info_get_file_type(file_info); + + if (file_type != G_FILE_TYPE_SYMBOLIC_LINK) + { + filename = g_file_info_get_name(file_info); + + if (filename == NULL) + my_error = g_error_new(0, 0, "Failed to get filename of a file"); + else + { + if (file_type == G_FILE_TYPE_DIRECTORY) + { + if (enum_stack_index == ENUM_STACK_SIZE) + my_error = g_error_new(0, 0, "Recursion depth limit reached"); + else + { + enum_stack[enum_stack_index].enumerator = enumerator; + enum_stack[enum_stack_index++].dir_path = dir_path; + + dir_path = g_build_filename(dir_path, filename, NULL); + enumerator = enumerate_children(dir_path, &my_error); + } + } + else + { + file_path = g_build_filename(dir_path, filename, NULL); + + if (file_path) + { + filter_info.filename = filename; + filter_info.display_name = g_file_info_get_display_name(file_info); + filter_info.mime_type = g_file_info_get_content_type(file_info); + + if (gtk_file_filter_filter(filter, &filter_info)) + document_open_file(file_path, readonly, ft, forced_enc); + + g_free(file_path); + } + } + } + } + + g_object_unref(file_info); + } + + g_object_unref(enumerator); + + if (enum_stack_index == 0) + break; + + g_free(dir_path); + enumerator = enum_stack[--enum_stack_index].enumerator; + dir_path = enum_stack[enum_stack_index].dir_path; + } + } + else + document_open_file(file_path, readonly, ft, forced_enc); + } + + if (my_error) + { + if (error) + *error = g_error_new(my_error->domain, my_error->code, + "Failed to open files recursively: %s", my_error->message); + + g_error_free(my_error); + } + else + if (error) + *error = NULL; +} + + static void on_keep_edit_history_on_reload_response(GtkWidget *bar, gint response_id, GeanyDocument *doc) { if (response_id == GTK_RESPONSE_NO) diff --git a/src/document.h b/src/document.h index ad3716b7b1..628a0ea1be 100644 --- a/src/document.h +++ b/src/document.h @@ -323,6 +323,9 @@ void document_set_data(GeanyDocument *doc, const gchar *key, gpointer data); void document_set_data_full(GeanyDocument *doc, const gchar *key, gpointer data, GDestroyNotify free_func); +void document_open_files_recursively(const GSList *filenames, gboolean readonly, GeanyFiletype *ft, + const gchar *forced_enc, GtkFileFilter *filter, GError **error); + #endif /* GEANY_PRIVATE */ G_END_DECLS diff --git a/src/keybindings.c b/src/keybindings.c index 72a9e4a8bf..fc3499e8e2 100644 --- a/src/keybindings.c +++ b/src/keybindings.c @@ -333,6 +333,8 @@ static void init_default_kb(void) GDK_n, GEANY_PRIMARY_MOD_MASK, "menu_new", _("New"), "menu_new1"); add_kb(group, GEANY_KEYS_FILE_OPEN, NULL, GDK_o, GEANY_PRIMARY_MOD_MASK, "menu_open", _("Open"), "menu_open1"); + add_kb(group, GEANY_KEYS_FILE_OPENRECURSIVE, NULL, + 0, 0, "menu_open_files_recursively", _("Open files recursively"), "menu_open_files_recursively1"); add_kb(group, GEANY_KEYS_FILE_OPENSELECTED, NULL, GDK_o, GDK_SHIFT_MASK | GEANY_PRIMARY_MOD_MASK, "menu_open_selected", _("Open selected file"), "menu_open_selected_file1"); @@ -1429,6 +1431,9 @@ static gboolean cb_func_file_action(guint key_id) case GEANY_KEYS_FILE_OPEN: on_open1_activate(NULL, NULL); break; + case GEANY_KEYS_FILE_OPENRECURSIVE: + on_open_files_recursively1_activate(NULL, NULL); + break; case GEANY_KEYS_FILE_OPENSELECTED: on_menu_open_selected_file1_activate(NULL, NULL); break; diff --git a/src/keybindings.h b/src/keybindings.h index aa2afef096..147325db6e 100644 --- a/src/keybindings.h +++ b/src/keybindings.h @@ -274,6 +274,7 @@ enum GeanyKeyBindingID GEANY_KEYS_FORMAT_SENDTOCMD8, /**< Keybinding. */ GEANY_KEYS_FORMAT_SENDTOCMD9, /**< Keybinding. */ GEANY_KEYS_EDITOR_DELETELINETOBEGINNING, /**< Keybinding. */ + GEANY_KEYS_FILE_OPENRECURSIVE, /**< Keybinding. */ GEANY_KEYS_COUNT /* must not be used by plugins */ };