gem-graph-client/libide/gui/ide-search-button.c

291 lines
9.1 KiB
C
Raw Permalink Normal View History

/* ide-search-button.c
*
* Copyright 2019 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "ide-search-button"
#include "config.h"
#include <glib/gi18n.h>
#include <libide-search.h>
#include "ide-gui-global.h"
#include "ide-search-button.h"
#include "ide-workbench.h"
#define DEFAULT_SEARCH_MAX 25
#define I_ g_intern_string
struct _IdeSearchButton
{
DzlSuggestionButton parent_instance;
// Used to cancel previous search if it was still running when a new character is entered,
// to avoid having the search become sluggish if typing quickly (as the suggestion entry
// reacts quickly but the search can be slow and they tend to pile up in the queue, slowing
// down even more).
GCancellable *cancellable;
};
static const DzlShortcutEntry shortcuts[] = {
{ "org.gnome.builder.workspace.global-search",
0, NULL,
N_("Workspace shortcuts"),
N_("Search"),
N_("Focus to the global search entry") },
};
G_DEFINE_FINAL_TYPE (IdeSearchButton, ide_search_button, DZL_TYPE_SUGGESTION_BUTTON)
static void
search_entry_search_cb (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
IdeSearchEngine *engine = (IdeSearchEngine *)object;
g_autoptr(DzlSuggestionEntry) entry = user_data;
g_autoptr(GListModel) suggestions = NULL;
g_autoptr(GError) error = NULL;
g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
g_assert (IDE_IS_SEARCH_ENGINE (engine));
suggestions = ide_search_engine_search_finish (engine, result, &error);
if (error != NULL)
{
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
IdeWorkspace *workspace = IDE_WORKSPACE (gtk_widget_get_ancestor (GTK_WIDGET (entry), IDE_TYPE_WORKSPACE));
IdeContext *context = ide_workspace_get_context (workspace);
ide_context_warning (context, "%s", error->message);
}
return;
}
g_assert (suggestions != NULL);
g_assert (G_IS_LIST_MODEL (suggestions));
g_assert (g_type_is_a (g_list_model_get_item_type (suggestions), DZL_TYPE_SUGGESTION));
dzl_suggestion_entry_set_model (entry, suggestions);
}
static void
search_entry_changed (DzlSuggestionEntry *entry,
IdeSearchButton *self)
{
IdeSearchEngine *engine;
IdeWorkbench *workbench;
const gchar *typed_text;
g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
g_assert (IDE_IS_SEARCH_BUTTON (self));
workbench = ide_widget_get_workbench (GTK_WIDGET (entry));
engine = ide_workbench_get_search_engine (workbench);
typed_text = dzl_suggestion_entry_get_typed_text (entry);
if (dzl_str_empty0 (typed_text))
dzl_suggestion_entry_set_model (entry, NULL);
else
{
if (ide_search_engine_get_busy (engine))
g_cancellable_cancel (self->cancellable);
// Never reuse a cancellable
g_clear_object (&self->cancellable);
self->cancellable = g_cancellable_new ();
ide_search_engine_search_async (engine,
typed_text,
DEFAULT_SEARCH_MAX,
self->cancellable,
search_entry_search_cb,
g_object_ref (entry));
}
}
static void
search_popover_position_func (DzlSuggestionEntry *entry,
GdkRectangle *area,
gboolean *is_absolute,
gpointer user_data)
{
gint new_width;
g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
g_assert (area != NULL);
g_assert (is_absolute != NULL);
g_assert (user_data == NULL);
#define RIGHT_MARGIN 6
/* We want the search area to be the right 2/5ths of the window, with a bit
* of margin on the popover.
*/
dzl_suggestion_entry_window_position_func (entry, area, is_absolute, NULL);
new_width = (area->width * 2 / 5);
area->x += area->width - new_width;
area->width = new_width - RIGHT_MARGIN;
area->y -= 3;
#undef RIGHT_MARGIN
}
static void
unfocus_action (GSimpleAction *action,
GVariant *param,
gpointer user_data)
{
IdeSearchButton *self = IDE_SEARCH_BUTTON (user_data);
DzlSuggestionEntry *entry;
GtkWidget *toplevel;
g_assert (G_IS_SIMPLE_ACTION (action));
g_assert (IDE_IS_SEARCH_BUTTON (self));
entry = dzl_suggestion_button_get_entry (DZL_SUGGESTION_BUTTON (self));
g_signal_emit_by_name (entry, "hide-suggestions");
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
gtk_widget_grab_focus (toplevel);
gtk_entry_set_text (GTK_ENTRY (entry), "");
}
static void
shortcut_grab_focus (GtkWidget *widget,
gpointer user_data)
{
IdeSearchButton *self = IDE_SEARCH_BUTTON (user_data);
gtk_widget_grab_focus (GTK_WIDGET (self));
}
static void
suggestion_activated (DzlSuggestionEntry *entry,
DzlSuggestion *suggestion)
{
GtkWidget *toplevel;
GtkWidget *focus;
g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
g_assert (IDE_IS_SEARCH_RESULT (suggestion));
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (entry));
focus = GTK_WIDGET (ide_workspace_get_most_recent_page (IDE_WORKSPACE (toplevel)));
if (focus == NULL)
focus = GTK_WIDGET (entry);
ide_search_result_activate (IDE_SEARCH_RESULT (suggestion), focus);
}
static gboolean
search_entry_focus_in (GtkEntry *entry,
GdkEventFocus *focus,
gpointer user_data)
{
IdeWorkbench *workbench;
IdeSearchEngine *engine;
g_assert (GTK_IS_ENTRY (entry));
g_assert (focus != NULL);
/* Load search engine if it is not already */
workbench = ide_widget_get_workbench (GTK_WIDGET (entry));
engine = ide_workbench_get_search_engine (workbench);
(void)engine;
return GDK_EVENT_PROPAGATE;
}
static void
ide_search_button_finalize (GObject *object)
{
IdeSearchButton *self = (IdeSearchButton *)object;
g_assert (IDE_IS_SEARCH_BUTTON (self));
g_clear_object (&self->cancellable);
G_OBJECT_CLASS (ide_search_button_parent_class)->finalize (object);
}
static void
ide_search_button_class_init (IdeSearchButtonClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = ide_search_button_finalize;
}
static void
ide_search_button_init (IdeSearchButton *self)
{
DzlSuggestionEntry *entry = dzl_suggestion_button_get_entry (DZL_SUGGESTION_BUTTON (self));
DzlShortcutController *controller;
g_autoptr(GSimpleActionGroup) group = NULL;
static GActionEntry actions[] = {
{ "unfocus", unfocus_action },
};
group = g_simple_action_group_new ();
g_action_map_add_action_entries (G_ACTION_MAP (group),
actions,
G_N_ELEMENTS (actions),
self);
gtk_widget_insert_action_group (GTK_WIDGET (self),
"search",
G_ACTION_GROUP (group));
dzl_gtk_widget_add_style_class (GTK_WIDGET (entry), "global-search");
g_signal_connect (entry, "changed", G_CALLBACK (search_entry_changed), self);
g_signal_connect (entry, "focus-in-event", G_CALLBACK (search_entry_focus_in), NULL);
g_signal_connect (entry, "suggestion-activated", G_CALLBACK (suggestion_activated), NULL);
dzl_suggestion_entry_set_position_func (entry, search_popover_position_func, NULL, NULL);
controller = dzl_shortcut_controller_find (GTK_WIDGET (entry));
dzl_shortcut_controller_add_command_callback (controller,
I_("org.gnome.builder.workspace.global-search"),
"<Primary>period",
DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL,
shortcut_grab_focus, self, NULL);
dzl_shortcut_controller_add_command_action (controller,
I_("org.gnome.builder.workspace.unfocus"),
"Escape",
DZL_SHORTCUT_PHASE_CAPTURE,
"search.unfocus");
dzl_shortcut_manager_add_shortcut_entries (NULL,
shortcuts,
G_N_ELEMENTS (shortcuts),
GETTEXT_PACKAGE);
}
GtkWidget *
ide_search_button_new (void)
{
return g_object_new (IDE_TYPE_SEARCH_BUTTON, NULL);
}