-
- gtk_tree_model_get (model, iter,
- DISPLAY_NAME_COLUMN, &display_name,
- FILE_COLUMN, &file,
- -1);
-
- if (!display_name || !file)
- {
- if (file)
- g_object_unref (file);
-
- g_free (display_name);
- return FALSE;
- }
-
- pos = chooser_entry->file_part_pos;
-
- /* We don't set in_change here as we want to update the current_folder
- * variable */
- gtk_editable_delete_text (GTK_EDITABLE (chooser_entry),
- pos, -1);
- gtk_editable_insert_text (GTK_EDITABLE (chooser_entry),
- display_name, -1,
- &pos);
- gtk_editable_set_position (GTK_EDITABLE (chooser_entry), -1);
-
- g_object_unref (file);
- g_free (display_name);
-
- return TRUE;
-}
-
-/* Match function for the GtkEntryCompletion */
-static gboolean
-completion_match_func (GtkEntryCompletion *comp,
- const char *key_unused,
- GtkTreeIter *iter,
- gpointer data)
-{
- GtkFileChooserEntry *chooser_entry;
- char *name = NULL;
- gboolean result;
- char *norm_file_part;
- char *norm_name;
-
- chooser_entry = GTK_FILE_CHOOSER_ENTRY (data);
-
- /* We ignore the key because it is the contents of the entry. Instead, we
- * just use our precomputed file_part.
- */
- if (!chooser_entry->file_part)
- {
- return FALSE;
- }
-
- gtk_tree_model_get (GTK_TREE_MODEL (chooser_entry->completion_store), iter, DISPLAY_NAME_COLUMN, &name, -1);
- if (!name)
- {
- return FALSE; /* Uninitialized row, ugh */
- }
-
- /* If we have an empty file_part, then we're at the root of a directory. In
- * that case, we want to match all non-dot files. We might want to match
- * dot_files too if show_hidden is TRUE on the fileselector in the future.
- */
- /* Additionally, support for gnome .hidden files would be sweet, too */
- if (chooser_entry->file_part[0] == '\000')
- {
- if (name[0] == '.')
- result = FALSE;
- else
- result = TRUE;
- g_free (name);
-
- return result;
- }
-
-
- norm_file_part = g_utf8_normalize (chooser_entry->file_part, -1, G_NORMALIZE_ALL);
- norm_name = g_utf8_normalize (name, -1, G_NORMALIZE_ALL);
-
-#ifdef G_PLATFORM_WIN32
- {
- gchar *temp;
-
- temp = norm_file_part;
- norm_file_part = g_utf8_casefold (norm_file_part, -1);
- g_free (temp);
-
- temp = norm_name;
- norm_name = g_utf8_casefold (norm_name, -1);
- g_free (temp);
- }
-#endif
-
- result = (strncmp (norm_file_part, norm_name, strlen (norm_file_part)) == 0);
-
- g_free (norm_file_part);
- g_free (norm_name);
- g_free (name);
-
- return result;
-}
-
-static void
-clear_completions (GtkFileChooserEntry *chooser_entry)
-{
- chooser_entry->has_completion = FALSE;
- chooser_entry->load_complete_action = LOAD_COMPLETE_NOTHING;
-
- remove_completion_feedback (chooser_entry);
-}
-
-static void
-beep (GtkFileChooserEntry *chooser_entry)
-{
- gtk_widget_error_bell (GTK_WIDGET (chooser_entry));
-}
-
-/* This function will append a directory separator to paths to
- * display_name iff the path associated with it is a directory.
- * maybe_append_separator_to_file will g_free the display_name and
- * return a new one if needed. Otherwise, it will return the old one.
- * You should be safe calling
- *
- * display_name = maybe_append_separator_to_file (entry, file, display_name, &appended);
- * ...
- * g_free (display_name);
- */
-static char *
-maybe_append_separator_to_file (GtkFileChooserEntry *chooser_entry,
- GFile *file,
- gchar *display_name,
- gboolean *appended)
-{
- *appended = FALSE;
-
- if (!g_str_has_suffix (display_name, G_DIR_SEPARATOR_S) && file)
- {
- GFileInfo *info;
-
- info = _gtk_folder_get_info (chooser_entry->current_folder, file);
-
- if (info)
- {
- if (_gtk_file_info_consider_as_directory (info))
- {
- gchar *tmp = display_name;
- display_name = g_strconcat (tmp, G_DIR_SEPARATOR_S, NULL);
- *appended = TRUE;
- g_free (tmp);
- }
-
- g_object_unref (info);
- }
- }
-
- return display_name;
-}
-
-/* Determines if the completion model has entries with a common prefix relative
- * to the current contents of the entry. Also, if there's one and only one such
- * path, stores it in unique_path_ret.
- */
-static gboolean
-find_common_prefix (GtkFileChooserEntry *chooser_entry,
- gchar **common_prefix_ret,
- GFile **unique_file_ret,
- gboolean *is_complete_not_unique_ret,
- gboolean *prefix_expands_the_file_part_ret,
- GError **error)
-{
- GtkEditable *editable;
- GtkTreeIter iter;
- gboolean parsed;
- gboolean valid;
- char *text_up_to_cursor;
- GFile *parsed_folder_file;
- char *parsed_file_part;
-
- *common_prefix_ret = NULL;
- *unique_file_ret = NULL;
- *is_complete_not_unique_ret = FALSE;
- *prefix_expands_the_file_part_ret = FALSE;
-
- editable = GTK_EDITABLE (chooser_entry);
-
- text_up_to_cursor = gtk_editable_get_chars (editable, 0, gtk_editable_get_position (editable));
-
- parsed = _gtk_file_system_parse (chooser_entry->file_system,
- chooser_entry->base_folder,
- text_up_to_cursor,
- &parsed_folder_file,
- &parsed_file_part,
- error);
-
- g_free (text_up_to_cursor);
-
- if (!parsed)
- return FALSE;
-
- g_assert (parsed_folder_file != NULL
- && chooser_entry->current_folder != NULL
- && g_file_equal (parsed_folder_file, chooser_entry->current_folder_file));
-
- g_object_unref (parsed_folder_file);
-
- /* First pass: find the common prefix */
-
- valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (chooser_entry->completion_store), &iter);
-
- while (valid)
- {
- gchar *display_name;
- GFile *file;
-
- gtk_tree_model_get (GTK_TREE_MODEL (chooser_entry->completion_store),
- &iter,
- DISPLAY_NAME_COLUMN, &display_name,
- FILE_COLUMN, &file,
- -1);
-
- if (g_str_has_prefix (display_name, parsed_file_part))
- {
- if (!*common_prefix_ret)
- {
- *common_prefix_ret = g_strdup (display_name);
- *unique_file_ret = g_object_ref (file);
- }
- else
- {
- gchar *p = *common_prefix_ret;
- const gchar *q = display_name;
-
- while (*p && *p == *q)
- {
- p++;
- q++;
- }
-
- *p = '\0';
-
- if (*unique_file_ret)
- {
- g_object_unref (*unique_file_ret);
- *unique_file_ret = NULL;
- }
- }
- }
-
- g_free (display_name);
- g_object_unref (file);
- valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (chooser_entry->completion_store), &iter);
- }
-
- /* Second pass: see if the prefix we found is a complete match */
-
- if (*common_prefix_ret != NULL)
- {
- valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (chooser_entry->completion_store), &iter);
-
- while (valid)
- {
- gchar *display_name;
- int len;
-
- gtk_tree_model_get (GTK_TREE_MODEL (chooser_entry->completion_store),
- &iter,
- DISPLAY_NAME_COLUMN, &display_name,
- -1);
- len = strlen (display_name);
- g_assert (len > 0);
-
- if (G_IS_DIR_SEPARATOR (display_name[len - 1]))
- len--;
-
- if (*unique_file_ret == NULL && strncmp (*common_prefix_ret, display_name, len) == 0)
- *is_complete_not_unique_ret = TRUE;
-
- g_free (display_name);
- valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (chooser_entry->completion_store), &iter);
- }
-
- /* Finally: Did we generate a new completion, or was the user's input already completed as far as it could go? */
-
- *prefix_expands_the_file_part_ret = g_utf8_strlen (*common_prefix_ret, -1) > g_utf8_strlen (parsed_file_part, -1);
- }
-
- g_free (parsed_file_part);
-
- return TRUE;
-}
-
-typedef enum {
- INVALID_INPUT, /* what the user typed is bogus */
- NO_MATCH, /* no matches based on what the user typed */
- NOTHING_INSERTED_COMPLETE, /* what the user typed is already completed as far as it will go */
- NOTHING_INSERTED_UNIQUE, /* what the user typed is already completed, and is a unique match */
- COMPLETED, /* completion inserted (ambiguous suffix) */
- COMPLETED_UNIQUE, /* completion inserted, and it is a complete name and a unique match */
- COMPLETE_BUT_NOT_UNIQUE /* completion inserted, it is a complete name but not unique */
-} CommonPrefixResult;
-
-/* Finds a common prefix based on the contents of the entry
- * and mandatorily appends it
- */
-static CommonPrefixResult
-append_common_prefix (GtkFileChooserEntry *chooser_entry,
- gboolean highlight,
- gboolean show_errors)
-{
- gchar *common_prefix;
- GFile *unique_file;
- gboolean is_complete_not_unique;
- gboolean prefix_expands_the_file_part;
- GError *error;
- CommonPrefixResult result = NO_MATCH;
- gboolean have_result;
-
- clear_completions (chooser_entry);
-
- if (chooser_entry->completion_store == NULL)
- return NO_MATCH;
-
- error = NULL;
- if (!find_common_prefix (chooser_entry, &common_prefix, &unique_file, &is_complete_not_unique, &prefix_expands_the_file_part, &error))
- {
- /* If the user types an incomplete hostname ("http://foo" without
- * a slash after that), it's not an error. We just don't want to
- * pop up a meaningless completion window in that state.
- */
- if (!g_error_matches (error, GTK_FILE_CHOOSER_ERROR, GTK_FILE_CHOOSER_ERROR_INCOMPLETE_HOSTNAME)
- && show_errors)
- {
- beep (chooser_entry);
- pop_up_completion_feedback (chooser_entry, _("Invalid path"));
- }
-
- g_error_free (error);
-
- return INVALID_INPUT;
- }
-
- have_result = FALSE;
-
- if (unique_file)
- {
- g_object_unref (unique_file);
-
- if (prefix_expands_the_file_part)
- result = COMPLETED_UNIQUE;
- else
- result = NOTHING_INSERTED_UNIQUE;
-
- have_result = TRUE;
- }
- else
- {
- if (is_complete_not_unique)
- {
- result = COMPLETE_BUT_NOT_UNIQUE;
- have_result = TRUE;
- }
- }
-
- if (common_prefix)
- {
- gint cursor_pos;
- gint pos;
-
- cursor_pos = gtk_editable_get_position (GTK_EDITABLE (chooser_entry));
-
- pos = chooser_entry->file_part_pos;
-
- if (prefix_expands_the_file_part)
- {
- chooser_entry->in_change = TRUE;
- gtk_editable_delete_text (GTK_EDITABLE (chooser_entry),
- pos, cursor_pos);
- gtk_editable_insert_text (GTK_EDITABLE (chooser_entry),
- common_prefix, -1,
- &pos);
- chooser_entry->in_change = FALSE;
-
- if (highlight)
- {
- /* equivalent to cursor_pos + common_prefix_len); */
- gtk_editable_select_region (GTK_EDITABLE (chooser_entry),
- cursor_pos,
- pos);
- chooser_entry->has_completion = TRUE;
- }
- else
- gtk_editable_set_position (GTK_EDITABLE (chooser_entry), pos);
- }
- else if (!have_result)
- {
- result = NOTHING_INSERTED_COMPLETE;
- have_result = TRUE;
- }
-
- g_free (common_prefix);
-
- if (have_result)
- return result;
- else
- return COMPLETED;
- }
- else
- {
- if (have_result)
- return result;
- else
- return NO_MATCH;
- }
-}
-
-static void
-gtk_file_chooser_entry_do_insert_text (GtkEditable *editable,
- const gchar *new_text,
- gint new_text_length,
- gint *position)
-{
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (editable);
- gint old_text_len;
- gint insert_pos;
-
- old_text_len = gtk_entry_get_text_length (GTK_ENTRY (chooser_entry));
- insert_pos = *position;
-
- parent_editable_iface->do_insert_text (editable, new_text, new_text_length, position);
-
- if (chooser_entry->in_change)
- return;
-
- remove_completion_feedback (chooser_entry);
-
- if ((chooser_entry->action == GTK_FILE_CHOOSER_ACTION_OPEN
- || chooser_entry->action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER)
- && insert_pos == old_text_len)
- install_start_autocompletion_idle (chooser_entry);
-}
-
-static void
-clear_completions_if_not_in_change (GtkFileChooserEntry *chooser_entry)
-{
- if (chooser_entry->in_change)
- return;
-
- clear_completions (chooser_entry);
-}
-
-static void
-gtk_file_chooser_entry_do_delete_text (GtkEditable *editable,
- gint start_pos,
- gint end_pos)
-{
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (editable);
-
- parent_editable_iface->do_delete_text (editable, start_pos, end_pos);
-
- clear_completions_if_not_in_change (chooser_entry);
-}
-
-static void
-gtk_file_chooser_entry_set_position (GtkEditable *editable,
- gint position)
-{
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (editable);
-
- parent_editable_iface->set_position (editable, position);
-
- clear_completions_if_not_in_change (chooser_entry);
-}
-
-static void
-gtk_file_chooser_entry_set_selection_bounds (GtkEditable *editable,
- gint start_pos,
- gint end_pos)
-{
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (editable);
-
- parent_editable_iface->set_selection_bounds (editable, start_pos, end_pos);
-
- clear_completions_if_not_in_change (chooser_entry);
-}
-
-static void
-gtk_file_chooser_entry_grab_focus (GtkWidget *widget)
-{
- GTK_WIDGET_CLASS (_gtk_file_chooser_entry_parent_class)->grab_focus (widget);
- _gtk_file_chooser_entry_select_filename (GTK_FILE_CHOOSER_ENTRY (widget));
-}
-
-static void
-gtk_file_chooser_entry_unmap (GtkWidget *widget)
-{
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (widget);
-
- remove_completion_feedback (chooser_entry);
-
- GTK_WIDGET_CLASS (_gtk_file_chooser_entry_parent_class)->unmap (widget);
-}
-
-static gboolean
-completion_feedback_window_draw_cb (GtkWidget *widget,
- cairo_t *cr,
- gpointer data)
-{
- /* Stolen from gtk_tooltip_paint_window() */
-
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (data);
- GtkStyleContext *context;
-
- context = gtk_widget_get_style_context (chooser_entry->completion_feedback_window);
-
- gtk_render_background (context, cr, 0, 0,
- gtk_widget_get_allocated_width (widget),
- gtk_widget_get_allocated_height (widget));
- gtk_render_frame (context, cr, 0, 0,
- gtk_widget_get_allocated_width (widget),
- gtk_widget_get_allocated_height (widget));
-
- return FALSE;
-}
-
-static void
-set_invisible_mouse_cursor (GdkWindow *window)
-{
- GdkDisplay *display;
- GdkCursor *cursor;
-
- display = gdk_window_get_display (window);
- cursor = gdk_cursor_new_for_display (display, GDK_BLANK_CURSOR);
-
- gdk_window_set_cursor (window, cursor);
-
- g_object_unref (cursor);
-}
-
-static void
-completion_feedback_window_realize_cb (GtkWidget *widget,
- gpointer data)
-{
- /* We hide the mouse cursor inside the completion feedback window, since
- * GtkEntry hides the cursor when the user types. We don't want the cursor to
- * come back if the completion feedback ends up where the mouse is.
- */
- set_invisible_mouse_cursor (gtk_widget_get_window (widget));
-}
-
-static void
-create_completion_feedback_window (GtkFileChooserEntry *chooser_entry)
-{
- /* Stolen from gtk_tooltip_init() */
- GtkWidget *window, *label;
- GtkStyleContext *context;
-
- window = gtk_window_new (GTK_WINDOW_POPUP);
- gtk_window_set_type_hint (GTK_WINDOW (window), GDK_WINDOW_TYPE_HINT_TOOLTIP);
- gtk_widget_set_app_paintable (window, TRUE);
- gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
- gtk_widget_set_name (window, "gtk-tooltip");
-
- context = gtk_widget_get_style_context (window);
- gtk_style_context_add_class (context, GTK_STYLE_CLASS_TOOLTIP);
-
- g_signal_connect (window, "draw",
- G_CALLBACK (completion_feedback_window_draw_cb), chooser_entry);
- g_signal_connect (window, "realize",
- G_CALLBACK (completion_feedback_window_realize_cb), chooser_entry);
- /* FIXME: connect to motion-notify-event, and *show* the cursor when the mouse moves */
-
- label = gtk_label_new (NULL);
- gtk_widget_set_halign (label, GTK_ALIGN_CENTER);
- gtk_widget_set_valign (label, GTK_ALIGN_CENTER);
- /* FIXME: don't hardcode this */
- gtk_widget_set_margin_left (label, 6);
- gtk_widget_set_margin_right (label, 6);
- gtk_widget_set_margin_top (label, 6);
- gtk_widget_set_margin_bottom (label, 6);
- gtk_container_add (GTK_CONTAINER (window), label);
- gtk_widget_show (label);
-
- chooser_entry->completion_feedback_window = window;
- chooser_entry->completion_feedback_label = label;
-}
-
-static gboolean
-completion_feedback_timeout_cb (gpointer data)
-{
- GtkFileChooserEntry *chooser_entry = GTK_FILE_CHOOSER_ENTRY (data);
-
- chooser_entry->completion_feedback_timeout_id = 0;
-
- remove_completion_feedback (chooser_entry);
- return FALSE;
-}
-
-static void
-install_completion_feedback_timer (GtkFileChooserEntry *chooser_entry)
-{
- if (chooser_entry->completion_feedback_timeout_id != 0)
- g_source_remove (chooser_entry->completion_feedback_timeout_id);
-
- chooser_entry->completion_feedback_timeout_id = gdk_threads_add_timeout (COMPLETION_FEEDBACK_TIMEOUT_MS,
- completion_feedback_timeout_cb,
- chooser_entry);
-}
-
-/* Gets the x position of the text cursor in the entry, in widget coordinates */
-static void
-get_entry_cursor_x (GtkFileChooserEntry *chooser_entry,
- gint *x_ret)
-{
- /* FIXME: see the docs for gtk_entry_get_layout_offsets(). As an exercise for
- * the reader, you have to implement support for the entry's scroll offset and
- * RTL layouts and all the fancy Pango stuff.
- */
-
- PangoLayout *layout;
- gint layout_x, layout_y;
- gint layout_index;
- PangoRectangle strong_pos;
- gint start_pos, end_pos;
-
- layout = gtk_entry_get_layout (GTK_ENTRY (chooser_entry));
-
- gtk_entry_get_layout_offsets (GTK_ENTRY (chooser_entry), &layout_x, &layout_y);
-
- gtk_editable_get_selection_bounds (GTK_EDITABLE (chooser_entry), &start_pos, &end_pos);
- layout_index = gtk_entry_text_index_to_layout_index (GTK_ENTRY (chooser_entry),
- end_pos);
-
-
- pango_layout_get_cursor_pos (layout, layout_index, &strong_pos, NULL);
-
- *x_ret = layout_x + strong_pos.x / PANGO_SCALE;
-}
-
-static void
-show_completion_feedback_window (GtkFileChooserEntry *chooser_entry)
-{
- /* More or less stolen from gtk_tooltip_position() */
-
- GtkRequisition feedback_req;
- GtkWidget *widget = GTK_WIDGET (chooser_entry);
- gint entry_x, entry_y;
- gint cursor_x;
- GtkAllocation entry_allocation;
- int feedback_x, feedback_y;
-
- gtk_widget_get_preferred_size (chooser_entry->completion_feedback_window,
- &feedback_req, NULL);
-
- gdk_window_get_origin (gtk_widget_get_window (widget), &entry_x, &entry_y);
- gtk_widget_get_allocation (widget, &entry_allocation);
-
- get_entry_cursor_x (chooser_entry, &cursor_x);
-
- /* FIXME: fit to the screen if we bump on the screen's edge */
- /* cheap "half M-width", use height as approximation of character em-size */
- feedback_x = entry_x + cursor_x + entry_allocation.height / 2;
- feedback_y = entry_y + (entry_allocation.height - feedback_req.height) / 2;
-
- gtk_window_move (GTK_WINDOW (chooser_entry->completion_feedback_window), feedback_x, feedback_y);
- gtk_widget_show (chooser_entry->completion_feedback_window);
-
- install_completion_feedback_timer (chooser_entry);
-}
-
-static void
-pop_up_completion_feedback (GtkFileChooserEntry *chooser_entry,
- const gchar *feedback)
-{
- if (chooser_entry->completion_feedback_window == NULL)
- create_completion_feedback_window (chooser_entry);
-
- gtk_label_set_text (GTK_LABEL (chooser_entry->completion_feedback_label), feedback);
-
- show_completion_feedback_window (chooser_entry);
-}
-
-static void
-remove_completion_feedback (GtkFileChooserEntry *chooser_entry)
-{
- if (chooser_entry->completion_feedback_window)
- gtk_widget_destroy (chooser_entry->completion_feedback_window);
-
- chooser_entry->completion_feedback_window = NULL;
- chooser_entry->completion_feedback_label = NULL;
-
- if (chooser_entry->completion_feedback_timeout_id != 0)
- {
- g_source_remove (chooser_entry->completion_feedback_timeout_id);
- chooser_entry->completion_feedback_timeout_id = 0;
- }
-}
-
-static void
-explicitly_complete (GtkFileChooserEntry *chooser_entry)
-{
- CommonPrefixResult result;
-
- g_assert (chooser_entry->current_folder != NULL);
- g_assert (chooser_entry->current_folder_loaded);
-
- /* FIXME: see what Emacs does in case there is no common prefix, or there is more than one match:
- *
- * - If there is a common prefix, insert it (done)
- * - If there is no common prefix, pop up the suggestion window
- * - If there are no matches at all, beep and bring up a tooltip (done)
- * - If the suggestion window is already up, scroll it
- */
- result = append_common_prefix (chooser_entry, FALSE, TRUE);
-
- switch (result)
- {
- case INVALID_INPUT:
- /* We already beeped in append_common_prefix(); do nothing here */
- break;
-
- case NO_MATCH:
- beep (chooser_entry);
- /* translators: this text is shown when there are no completions
- * for something the user typed in a file chooser entry
- */
- pop_up_completion_feedback (chooser_entry, _("No match"));
- break;
-
- case NOTHING_INSERTED_COMPLETE:
- /* FIXME: pop up the suggestion window or scroll it */
- break;
-
- case NOTHING_INSERTED_UNIQUE:
- /* translators: this text is shown when there is exactly one completion
- * for something the user typed in a file chooser entry
- */
- pop_up_completion_feedback (chooser_entry, _("Sole completion"));
- break;