diff --git a/codenav/src/goto_file.c b/codenav/src/goto_file.c index 1da44ec17..f95b59b71 100644 --- a/codenav/src/goto_file.c +++ b/codenav/src/goto_file.c @@ -32,7 +32,9 @@ /******************* Global variables for the feature *****************/ static GtkWidget* menu_item = NULL; -gchar *directory_ref = NULL; +static gchar *ref_dir = NULL; +static gchar *curr_dir = NULL; +static gint entry_len_before_completion = -1; /********************** Prototypes *********************/ static void @@ -44,8 +46,14 @@ build_file_list(const gchar*, const gchar*); static void directory_check(GtkEntry*, GtkEntryCompletion*); +static gboolean +entry_inline_completion_event(GtkEntryCompletion *, gchar *, GtkEntry *); + +static gboolean +entry_key_event(GtkEntry *, GdkEventKey *, GtkEntryCompletion *); + static GtkWidget* -create_dialog(GtkWidget**, GtkTreeModel*); +create_dialog(GtkWidget**); /********************** Functions for the feature *********************/ @@ -96,6 +104,27 @@ goto_file_cleanup(void) gtk_widget_destroy(menu_item); } +/** + * @brief Replace any special characters present in path_name + * @param gchar** path_name + * @return void + * + */ +static void +path_name_replace_special(gchar **path_name) +{ + GString *str; + + if(*path_name[0] == '~') + { + str = g_string_new(*path_name); + utils_string_replace_first(str, "~", g_get_home_dir()); + + free(*path_name); + *path_name = g_string_free(str, FALSE); + } +} + /** * @brief Populate the file list with file list of directory * @param const char* dirname the directory where to find files @@ -109,33 +138,33 @@ build_file_list(const gchar* dirname, const gchar* prefix) GtkListStore *ret_list; GtkTreeIter iter; ret_list = gtk_list_store_new (1, G_TYPE_STRING); - + GSList* file_iterator; - GSList* files_list; /* used to free later the sub-elements*/ + GSList* files_list; /* used to later free the sub-elements */ gchar *file; - gchar *pathfile; guint files_n; files_list = file_iterator = utils_get_file_list(dirname, &files_n, NULL); - + for( ; file_iterator; file_iterator = file_iterator->next) { - file = file_iterator->data; - - pathfile = g_build_filename(dirname,file,NULL); - - /* Append the element to model list */ - gtk_list_store_append (ret_list, &iter); - gtk_list_store_set (ret_list, &iter, 0, - g_strconcat(prefix, file, NULL), -1); - g_free(pathfile); + file = file_iterator->data; + + gchar *full_path = g_build_filename(dirname, file, NULL); + gboolean is_dir = g_file_test(full_path, G_FILE_TEST_IS_DIR); + + /* Append the element to model list */ + gtk_list_store_append (ret_list, &iter); + gtk_list_store_set (ret_list, &iter, 0, g_strconcat(prefix, + file, (is_dir ? "/" : NULL), NULL), -1); + + g_free(full_path); } - + g_slist_foreach(files_list, (GFunc) g_free, NULL); g_slist_free(files_list); - + return GTK_TREE_MODEL(ret_list); - } /** @@ -148,89 +177,160 @@ build_file_list(const gchar* dirname, const gchar* prefix) static void directory_check(GtkEntry* entry, GtkEntryCompletion* completion) { - static GtkTreeModel *old_model = NULL; - GtkTreeModel* completion_list; - static gchar *curr_dir = NULL; - gchar *new_dir, *new_dir_path = NULL; - const gchar *text; - - text = gtk_entry_get_text(entry); - gint dir_sep = strrpos(text, G_DIR_SEPARATOR_S); - - /* No subdir separator found */ - if (dir_sep == -1) - { - if (old_model != NULL) - { /* Restore the no-sub-directory model */ - log_debug("Restoring old model!"); - - gtk_entry_completion_set_model (completion, old_model); - g_object_unref(old_model); - old_model = NULL; - - g_free(curr_dir); - curr_dir = NULL; - } - return; - } - - new_dir = g_strndup (text, dir_sep+1); - /* I've already inserted new model completion for sub-dir elements? */ - if ( g_strcmp0 (new_dir, curr_dir) == 0 ) - { - g_free(new_dir); - return; - } - if ( curr_dir != NULL ) - g_free(curr_dir); - - curr_dir = new_dir; - - /* Save the completion_mode for future restore. */ - if (old_model == NULL) - { - old_model = gtk_entry_completion_get_model(completion); - g_object_ref(old_model); - } - - log_debug("New completion list!"); - - if ( g_path_is_absolute(new_dir) ) - new_dir_path = g_strdup(new_dir); - else - new_dir_path = g_build_filename(directory_ref, new_dir, NULL); - - /* Build the new file list for completion */ - completion_list = build_file_list(new_dir_path, new_dir); - gtk_entry_completion_set_model (completion, completion_list); - g_object_unref(completion_list); - - g_free(new_dir_path); + GtkTreeModel* completion_list; + gchar *text, *new_dir, *new_dir_exp, *new_dir_path; + + /* We need to discern between text written by the user and text filled by the + * autocomplete, and to only use what the user wrote for the completion model. + * Otherwise, when a directory is 'suggested', the completion model will start + * traversing it, even though the user has not yet 'accepted' the suggestion. + * One way to tell apart the autocompletion is to check if there is a selection. + * However, the "changed" event triggers before the selection occurs, so no dice. + * Instead, we watch the completion event (insert-prefix) and back-up (in entry_ + * inline_completion_event) the user's text. */ + text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, entry_len_before_completion); + entry_len_before_completion = -1; + + /* We are only interested in the directory part of the path that the + * user has given, to generate a file list for a completion model. */ + gint dir_sep = strrpos(text, G_DIR_SEPARATOR_S); + new_dir = g_strndup(text, dir_sep + 1); + g_free(text); + + /* We only create a new completion model when the directory changes */ + if(g_strcmp0(new_dir, curr_dir) == 0) + { + g_free(new_dir); + return; + } + + log_debug("New completion list!"); + + g_free(curr_dir); + curr_dir = new_dir; + + /* Replace special chars here (e.g. '~') */ + new_dir_exp = g_strdup(new_dir); + path_name_replace_special(&new_dir_exp); + + /* Assemble full path to the entered directory */ + new_dir_path = (g_path_is_absolute(new_dir_exp) ? g_strdup(new_dir_exp) + : g_build_filename(ref_dir, new_dir_exp, NULL)); + + /* Build the new file list for completion */ + completion_list = build_file_list(new_dir_path, new_dir); + gtk_entry_completion_set_model (completion, completion_list); + g_object_unref(completion_list); + + g_free(new_dir_exp); + g_free(new_dir_path); } +/** + * @brief GtkEntryCompletion insert-prefix callback + * @param GtkEntryCompletion *completion entry completion object + * @param gchar *prefix common prefix of all possible completions + * @param GtkEntry *entry attached data (path entry field object) + * @return gboolean whether the signal has been handled (i.e. run other handlers?) + * + */ +static gboolean +entry_inline_completion_event(GtkEntryCompletion *completion, gchar *prefix, GtkEntry *entry) +{ + gint entry_len = gtk_entry_get_text_length(entry); + gint prefix_len = g_utf8_strlen(prefix, -1); + + /* We want to mark the length of the entry before the completion, but + * only if an inline completion is actually about to be suggested. If + * a suggestion is not created, the entry text won't be modified, the + * 'changed' event won't fire, and thus entry_len_before_completion's + * value will be stale, as it won't get cleared in directory_check. */ + if(prefix_len > entry_len) + { + entry_len_before_completion = entry_len; + } + + return FALSE; +} + +/** + * @brief Widget key callback (path entry field) + * @param GtkEntry *entry + * @param GdkEventKey *key key that triggered event + * @param GtkEntryCompletion *completion attached data (entry completion object) + * @return gboolean whether the signal has been handled (i.e. run other handlers?) + * + */ +static gboolean +entry_key_event(GtkEntry *entry, GdkEventKey *key, GtkEntryCompletion *completion) +{ + gint end_pos; + + if(key->keyval == GDK_KEY_Tab) + { + /* The user can use the Tab key to 'accept' the autocomplete's suggestion, + * like in shells, and in GtkFileChoose. If there's a suggestion, it will + * be selected, and we may simply move the cursor to accept it. */ + if(gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), NULL, &end_pos)) + gtk_editable_set_position(GTK_EDITABLE(entry), end_pos); + + /* The Tab key may also be used to trigger the autocomplete list + * to show up. And this trigger right here after 'accepting' an + * autocomplete suggestion is also useful in order to calculate + * and show completions for the new path. */ + g_signal_emit_by_name(entry, "changed"); + + /* The 2nd purpose of the Tab key is to trigger the procedure that + * calculates and suggests an inline completion. FYI doing this here + * is still useful even if the Tab key was used to accept a suggestion + * above, as it will trigger an attempt for inline completion in the + * new/next directory. */ + gtk_entry_completion_insert_prefix(completion); + + /* Effectively reserve the Tab key for autocompletion purposes, + * so don't let any other handlers run. Do this even when there + * wasn't any actual suggestion, to make it safe for the user + * to 'spam' the key (mimics GtkFileChooser). */ + return TRUE; + } + else if(key->keyval == GDK_KEY_Return) + { + if(gtk_editable_get_selection_bounds(GTK_EDITABLE(entry), NULL, &end_pos)) + { + gtk_editable_set_position(GTK_EDITABLE(entry), end_pos); + g_signal_emit_by_name(entry, "changed"); + gtk_entry_completion_insert_prefix(completion); + + /* Unlike with the Tab key, only intercept Enter + * when there was a completion available. */ + return TRUE; + } + } + + return FALSE; +} /** * @brief Create the dialog, return the entry object to get the * response from user * @param GtkWidget **dialog entry object - * @param GtkTreeModel *completion_model completion object * @return GtkWidget* entry * */ static GtkWidget* -create_dialog(GtkWidget **dialog, GtkTreeModel *completion_model) +create_dialog(GtkWidget **dialog) { GtkWidget *entry; GtkWidget *label; GtkWidget *vbox; GtkEntryCompletion *completion; - + *dialog = gtk_dialog_new_with_buttons(_("Go to File..."), GTK_WINDOW(geany->main_widgets->window), - GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, - GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); - + GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL); + gtk_dialog_set_default_response(GTK_DIALOG(*dialog), GTK_RESPONSE_ACCEPT); - + gtk_widget_set_name(*dialog, "GotoFile"); vbox = ui_dialog_vbox_new(GTK_DIALOG(*dialog)); @@ -244,19 +344,35 @@ create_dialog(GtkWidget **dialog, GtkTreeModel *completion_model) gtk_entry_set_max_length(GTK_ENTRY(entry), MAX_FILENAME_LENGTH); gtk_entry_set_width_chars(GTK_ENTRY(entry), 40); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); /* 'enter' key */ - + /* Completion definition */ completion = gtk_entry_completion_new(); + + /* Installed here, so it'll get installed before the respective + * handler inside GtkEntryCompletion, and will thus be called first. */ + g_signal_connect(entry, "key-press-event", + G_CALLBACK(entry_key_event), completion); + gtk_entry_set_completion(GTK_ENTRY(entry), completion); - gtk_entry_completion_set_model (completion, completion_model); - + /* Completion options */ gtk_entry_completion_set_inline_completion(completion, 1); gtk_entry_completion_set_text_column (completion, 0); + gtk_entry_completion_set_minimum_key_length(completion, 0); + + /* Initialize a model for the completion, to avoid errors in code that + * might attempt to utilize it, before any 'changed' events are fired. */ + directory_check(GTK_ENTRY(entry), completion); /* Signals */ - g_signal_connect_after(GTK_ENTRY(entry), "changed", - G_CALLBACK(directory_check), completion); + g_signal_connect_after(entry, "changed", + G_CALLBACK(directory_check), completion); + g_signal_connect(completion, "insert-prefix", + G_CALLBACK(entry_inline_completion_event), entry); + + /* The completion object is tracked in the entry. We may release our local + * reference, and it will be deallocated when the entry is destroyed. */ + g_object_unref(completion); gtk_widget_show_all(*dialog); @@ -275,29 +391,31 @@ menu_item_activate(guint key_id) GtkWidget* dialog; GtkWidget* dialog_new = NULL; GtkWidget* dialog_entry; - GtkTreeModel* completion_list; GeanyDocument* current_doc = document_get_current(); - gchar *chosen_path; const gchar *chosen_file; + gchar *chosen_file_exp, *chosen_path; gint response; log_func(); - if(current_doc == NULL || current_doc->file_name == NULL || current_doc->file_name[0] == '\0') - return; - - /* Build current directory listing */ - directory_ref = g_path_get_dirname(current_doc->file_name); - completion_list = build_file_list(directory_ref, ""); + if(current_doc && !EMPTY(current_doc->file_name)) + ref_dir = g_path_get_dirname(current_doc->file_name); + else + ref_dir = g_strdup(g_get_home_dir()); /* Create the user dialog and get response */ - dialog_entry = create_dialog(&dialog, completion_list); + dialog_entry = create_dialog(&dialog); + +_show_dialog: response = gtk_dialog_run(GTK_DIALOG(dialog)); - /* Filename */ chosen_file = gtk_entry_get_text(GTK_ENTRY(dialog_entry)); - /* Path + Filename */ - chosen_path = g_build_filename(directory_ref, chosen_file, NULL); + + chosen_file_exp = g_strdup(chosen_file); + path_name_replace_special(&chosen_file_exp); + + chosen_path = (g_path_is_absolute(chosen_file_exp) ? g_strdup(chosen_file_exp) + : g_build_filename(ref_dir, chosen_file_exp, NULL)); if ( response == GTK_RESPONSE_ACCEPT ) { @@ -310,21 +428,57 @@ menu_item_activate(guint key_id) GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, - _("%s not found, create it?"), chosen_file); + _("%s not found, create it?"), chosen_path); gtk_window_set_title(GTK_WINDOW(dialog_new), "Geany"); - if(gtk_dialog_run(GTK_DIALOG(dialog_new)) == GTK_RESPONSE_OK) + response = gtk_dialog_run(GTK_DIALOG(dialog_new)); + + if (response == GTK_RESPONSE_OK) { - document_new_file(chosen_path, current_doc->file_type, NULL); + document_new_file(chosen_path, (current_doc ? + current_doc->file_type : NULL), NULL); document_set_text_changed(document_get_current(), TRUE); } + gtk_widget_destroy(dialog_new); + + /* File wasn't found and user denied creating it, + * go back to the initial "Go to file" dialog. */ + if (response != GTK_RESPONSE_OK) + { + g_free(chosen_path); + goto _show_dialog; + } + } + else if(g_file_test(chosen_path, G_FILE_TEST_IS_DIR)) + { + log_debug("File is a dir."); + + dialog_new = gtk_message_dialog_new(GTK_WINDOW(geany_data->main_widgets->window), + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + _("%s is a directory."), chosen_path); + gtk_window_set_title(GTK_WINDOW(dialog_new), "Geany"); + + gtk_dialog_run(GTK_DIALOG(dialog_new)); + gtk_widget_destroy(dialog_new); + + goto _show_dialog; } else document_open_file(chosen_path, FALSE, NULL, NULL); } /* Freeing memory */ + gtk_widget_destroy(dialog); - g_free(directory_ref); - g_object_unref (completion_list); + + g_free(chosen_file_exp); + g_free(chosen_path); + + g_free(ref_dir); + ref_dir = NULL; + + g_free(curr_dir); + curr_dir = NULL; }