gem-graph-client/libide/gui/ide-omni-bar.c

690 lines
22 KiB
C
Raw Normal View History

/* ide-omni-bar.c
*
* Copyright 2018-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-omni-bar"
#include "config.h"
#include <libpeas/peas.h>
#include <dazzle.h>
#include "ide-gui-global.h"
#include "ide-gui-private.h"
#include "ide-notification-list-box-row-private.h"
#include "ide-notification-stack-private.h"
#include "ide-omni-bar-addin.h"
#include "ide-omni-bar.h"
struct _IdeOmniBar
{
GtkBin parent_instance;
PeasExtensionSet *addins;
GtkGesture *gesture;
GtkEventController *motion;
GtkEventBox *entry_event_box;
GtkStack *top_stack;
GtkPopover *popover;
DzlEntryBox *entry_box;
IdeNotificationStack *notification_stack;
GtkListBox *notifications_list_box;
DzlPriorityBox *inner_box;
DzlPriorityBox *outer_box;
GtkProgressBar *progress;
GtkWidget *placeholder;
DzlPriorityBox *sections_box;
guint in_button : 1;
};
static void ide_omni_bar_move_next (IdeOmniBar *self,
GVariant *param);
static void ide_omni_bar_move_previous (IdeOmniBar *self,
GVariant *param);
static void buildable_iface_init (GtkBuildableIface *iface);
DZL_DEFINE_ACTION_GROUP (IdeOmniBar, ide_omni_bar, {
{ "move-next", ide_omni_bar_move_next },
{ "move-previous", ide_omni_bar_move_previous },
})
G_DEFINE_FINAL_TYPE_WITH_CODE (IdeOmniBar, ide_omni_bar, GTK_TYPE_BIN,
G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_omni_bar_init_action_group)
G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
static GtkBuildableIface *parent_buildable_iface;
static void
ide_omni_bar_popover_closed_cb (IdeOmniBar *self,
GtkPopover *popover)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_POPOVER (popover));
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
state_flags = gtk_style_context_get_state (style_context);
state_flags &= ~GTK_STATE_FLAG_ACTIVE;
state_flags &= ~GTK_STATE_FLAG_PRELIGHT;
gtk_style_context_set_state (style_context, state_flags);
}
static void
multipress_pressed_cb (IdeOmniBar *self,
guint n_press,
gdouble x,
gdouble y,
GtkGestureMultiPress *gesture)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
if (gtk_widget_get_focus_on_click (GTK_WIDGET (self)) &&
!gtk_widget_has_focus (GTK_WIDGET (self)))
gtk_widget_grab_focus (GTK_WIDGET (self));
self->in_button = TRUE;
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
state_flags = gtk_style_context_get_state (style_context);
gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_ACTIVE);
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}
static void
multipress_released_cb (IdeOmniBar *self,
guint n_press,
gdouble x,
gdouble y,
GtkGestureMultiPress *gesture)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
gboolean show;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
show = self->in_button;
self->in_button = FALSE;
if (show)
{
gtk_popover_popup (self->popover);
return;
}
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
state_flags = gtk_style_context_get_state (style_context);
gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_ACTIVE);
}
static void
multipress_cancel_cb (IdeOmniBar *self,
GdkEventSequence *sequence,
GtkGestureMultiPress *gesture)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
self->in_button = FALSE;
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
state_flags = gtk_style_context_get_state (style_context);
gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_ACTIVE);
}
static void
ide_omni_bar_notification_stack_changed_cb (IdeOmniBar *self,
IdeNotificationStack *stack)
{
IdeNotification *notif;
gboolean enabled;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (IDE_IS_NOTIFICATION_STACK (stack));
enabled = ide_notification_stack_get_can_move (stack);
ide_omni_bar_set_action_enabled (self, "move-previous", enabled);
ide_omni_bar_set_action_enabled (self, "move-next", enabled);
_ide_gtk_progress_bar_stop_pulsing (self->progress);
gtk_widget_hide (GTK_WIDGET (self->progress));
if ((notif = ide_notification_stack_get_visible (stack)))
{
if (ide_notification_get_has_progress (notif))
{
if (ide_notification_get_progress_is_imprecise (notif))
_ide_gtk_progress_bar_start_pulsing (self->progress);
gtk_widget_show (GTK_WIDGET (self->progress));
}
}
if (ide_notification_stack_is_empty (stack))
gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
else
gtk_stack_set_visible_child_name (self->top_stack, "notifications");
}
static void
ide_omni_bar_extension_added_cb (PeasExtensionSet *set,
PeasPluginInfo *plugin_info,
PeasExtension *exten,
gpointer user_data)
{
IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
IdeOmniBar *self = user_data;
g_assert (PEAS_IS_EXTENSION_SET (set));
g_assert (plugin_info != NULL);
g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
g_assert (IDE_IS_OMNI_BAR (self));
ide_omni_bar_addin_load (addin, self);
}
static void
ide_omni_bar_extension_removed_cb (PeasExtensionSet *set,
PeasPluginInfo *plugin_info,
PeasExtension *exten,
gpointer user_data)
{
IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
IdeOmniBar *self = user_data;
g_assert (PEAS_IS_EXTENSION_SET (set));
g_assert (plugin_info != NULL);
g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
g_assert (IDE_IS_OMNI_BAR (self));
ide_omni_bar_addin_unload (addin, self);
}
static GtkWidget *
create_notification_row (gpointer item,
gpointer user_data)
{
IdeNotification *notif = item;
gboolean has_default;
g_assert (IDE_IS_NOTIFICATION (notif));
has_default = ide_notification_get_default_action (notif, NULL, NULL);
return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
"activatable", has_default,
"notification", notif,
"visible", TRUE,
NULL);
}
static gboolean
filter_for_popover (GObject *object,
gpointer user_data)
{
IdeNotification *notif = (IdeNotification *)object;
g_assert (IDE_IS_NOTIFICATION (notif));
g_assert (user_data == NULL);
return !ide_notification_get_has_progress (notif) &&
ide_notification_get_urgent (notif);
}
static void
ide_omni_bar_context_set_cb (GtkWidget *widget,
IdeContext *context)
{
IdeOmniBar *self = (IdeOmniBar *)widget;
g_autoptr(IdeObject) notifications = NULL;
g_autoptr(DzlListModelFilter) filter = NULL;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (IDE_IS_CONTEXT (context));
g_assert (self->addins == NULL);
notifications = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
ide_notification_stack_bind_model (self->notification_stack, G_LIST_MODEL (notifications));
filter = dzl_list_model_filter_new (G_LIST_MODEL (notifications));
dzl_list_model_filter_set_filter_func (filter, filter_for_popover, NULL, NULL);
gtk_list_box_bind_model (self->notifications_list_box,
G_LIST_MODEL (filter),
create_notification_row,
NULL, NULL);
self->addins = peas_extension_set_new (peas_engine_get_default (),
IDE_TYPE_OMNI_BAR_ADDIN,
NULL);
g_signal_connect (self->addins,
"extension-added",
G_CALLBACK (ide_omni_bar_extension_added_cb),
self);
g_signal_connect (self->addins,
"extension-removed",
G_CALLBACK (ide_omni_bar_extension_removed_cb),
self);
peas_extension_set_foreach (self->addins,
ide_omni_bar_extension_added_cb,
self);
}
static void
ide_omni_bar_motion_enter_cb (IdeOmniBar *self,
gdouble x,
gdouble y,
GtkEventControllerMotion *motion)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
state_flags = gtk_style_context_get_state (style_context);
if ((state_flags & GTK_STATE_FLAG_PRELIGHT) == 0)
gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_PRELIGHT);
}
static void
ide_omni_bar_motion_leave_cb (IdeOmniBar *self,
GtkEventControllerMotion *motion)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
state_flags = gtk_style_context_get_state (style_context);
if (state_flags & GTK_STATE_FLAG_PRELIGHT)
gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_PRELIGHT);
}
static void
ide_omni_bar_motion_cb (IdeOmniBar *self,
gdouble x,
gdouble y,
GtkEventControllerMotion *motion)
{
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
/*
* Because of how crossing-events work with Gtk 3, we don't get reliable
* crossing events for the motion controller. So every motion (which we do
* seem to get semi-reliably), just re-run the enter-notify path to ensure
* we get proper state set.
*/
ide_omni_bar_motion_enter_cb (self, x, y, motion);
}
static gboolean
ide_omni_bar_query_tooltip (GtkWidget *widget,
gint x,
gint y,
gboolean keyboard_mode,
GtkTooltip *tooltip)
{
IdeOmniBar *self = (IdeOmniBar *)widget;
IdeNotification *notif;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_TOOLTIP (tooltip));
if ((notif = ide_notification_stack_get_visible (self->notification_stack)))
{
g_autofree gchar *body = ide_notification_dup_body (notif);
if (body != NULL)
{
gtk_tooltip_set_text (tooltip, body);
return TRUE;
}
}
return FALSE;
}
static void
ide_omni_bar_notification_row_activated (IdeOmniBar *self,
IdeNotificationListBoxRow *row,
GtkListBox *list_box)
{
g_autofree gchar *default_action = NULL;
g_autoptr(GVariant) default_target = NULL;
IdeNotification *notif;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (row));
g_assert (GTK_IS_LIST_BOX (list_box));
notif = ide_notification_list_box_row_get_notification (row);
if (ide_notification_get_default_action (notif, &default_action, &default_target))
{
gchar *name = strchr (default_action, '.');
gchar *group = default_action;
if (name != NULL)
{
*name = '\0';
name++;
}
else
{
group = NULL;
name = default_action;
}
dzl_gtk_widget_action (GTK_WIDGET (list_box), group, name, default_target);
}
}
static void
ide_omni_bar_destroy (GtkWidget *widget)
{
IdeOmniBar *self = (IdeOmniBar *)widget;
g_assert (IDE_IS_OMNI_BAR (self));
if (self->progress != NULL)
_ide_gtk_progress_bar_stop_pulsing (self->progress);
g_clear_object (&self->addins);
g_clear_object (&self->gesture);
g_clear_object (&self->motion);
GTK_WIDGET_CLASS (ide_omni_bar_parent_class)->destroy (widget);
}
static void
ide_omni_bar_class_init (IdeOmniBarClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
widget_class->destroy = ide_omni_bar_destroy;
widget_class->query_tooltip = ide_omni_bar_query_tooltip;
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-omni-bar.ui");
gtk_widget_class_set_css_name (widget_class, "omnibar");
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, entry_box);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, entry_event_box);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, inner_box);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notification_stack);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notifications_list_box);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, outer_box);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, popover);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, progress);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, sections_box);
gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, top_stack);
gtk_widget_class_bind_template_callback (widget_class, ide_omni_bar_notification_row_activated);
g_type_ensure (DZL_TYPE_ENTRY_BOX);
g_type_ensure (IDE_TYPE_NOTIFICATION_STACK);
}
static void
ide_omni_bar_init (IdeOmniBar *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE);
gtk_widget_add_events (GTK_WIDGET (self),
(GDK_POINTER_MOTION_MASK |
GDK_ENTER_NOTIFY_MASK |
GDK_LEAVE_NOTIFY_MASK));
self->motion = gtk_event_controller_motion_new (GTK_WIDGET (self));
gtk_event_controller_set_propagation_phase (self->motion, GTK_PHASE_CAPTURE);
g_signal_connect_swapped (self->motion,
"enter",
G_CALLBACK (ide_omni_bar_motion_enter_cb),
self);
g_signal_connect_swapped (self->motion,
"motion",
G_CALLBACK (ide_omni_bar_motion_cb),
self);
g_signal_connect_swapped (self->motion,
"leave",
G_CALLBACK (ide_omni_bar_motion_leave_cb),
self);
self->gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self->entry_event_box));
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->gesture), FALSE);
gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (self->gesture), TRUE);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->gesture), GDK_BUTTON_PRIMARY);
g_signal_connect_object (self->gesture,
"pressed",
G_CALLBACK (multipress_pressed_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->gesture,
"released",
G_CALLBACK (multipress_released_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->gesture,
"cancel",
G_CALLBACK (multipress_cancel_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->notification_stack,
"changed",
G_CALLBACK (ide_omni_bar_notification_stack_changed_cb),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->popover,
"closed",
G_CALLBACK (ide_omni_bar_popover_closed_cb),
self,
G_CONNECT_SWAPPED);
gtk_widget_insert_action_group (GTK_WIDGET (self), "omnibar", G_ACTION_GROUP (self));
ide_omni_bar_set_action_enabled (self, "move-previous", FALSE);
ide_omni_bar_set_action_enabled (self, "move-next", FALSE);
ide_widget_set_context_handler (GTK_WIDGET (self), ide_omni_bar_context_set_cb);
}
GtkWidget *
ide_omni_bar_new (void)
{
return g_object_new (IDE_TYPE_OMNI_BAR, NULL);
}
static void
ide_omni_bar_move_next (IdeOmniBar *self,
GVariant *param)
{
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (param == NULL);
ide_notification_stack_move_next (self->notification_stack);
}
static void
ide_omni_bar_move_previous (IdeOmniBar *self,
GVariant *param)
{
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (param == NULL);
ide_notification_stack_move_previous (self->notification_stack);
}
/**
* ide_omni_bar_add_status_icon:
* @self: a #IdeOmniBar
* @widget: the #GtkWidget to add
* @priority: the sort priority for @widget
*
* Adds a status-icon style widget to the end of the omnibar. Generally,
* you'll want this to be either a GtkButton, GtkLabel, or something simple.
*
* Since: 3.32
*/
void
ide_omni_bar_add_status_icon (IdeOmniBar *self,
GtkWidget *widget,
gint priority)
{
g_return_if_fail (IDE_IS_OMNI_BAR (self));
g_return_if_fail (GTK_IS_WIDGET (widget));
gtk_container_add_with_properties (GTK_CONTAINER (self->inner_box), widget,
"pack-type", GTK_PACK_END,
"priority", priority,
NULL);
}
void
ide_omni_bar_add_button (IdeOmniBar *self,
GtkWidget *widget,
GtkPackType pack_type,
gint priority)
{
g_return_if_fail (IDE_IS_OMNI_BAR (self));
g_return_if_fail (GTK_IS_WIDGET (widget));
g_return_if_fail (pack_type == GTK_PACK_START ||
pack_type == GTK_PACK_END);
gtk_container_add_with_properties (GTK_CONTAINER (self->outer_box), widget,
"pack-type", pack_type,
"priority", priority,
NULL);
}
void
ide_omni_bar_set_placeholder (IdeOmniBar *self,
GtkWidget *widget)
{
g_return_if_fail (IDE_IS_OMNI_BAR (self));
g_return_if_fail (!widget || GTK_IS_WIDGET (widget));
if (self->placeholder == widget)
return;
if (self->placeholder)
gtk_widget_destroy (self->placeholder);
self->placeholder = widget;
if (self->placeholder)
{
g_signal_connect (self->placeholder,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
self->placeholder);
gtk_container_add_with_properties (GTK_CONTAINER (self->top_stack), self->placeholder,
"name", "placeholder",
NULL);
if (self->notification_stack == NULL ||
ide_notification_stack_is_empty (self->notification_stack))
gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
}
}
static void
ide_omni_bar_add_child (GtkBuildable *buildable,
GtkBuilder *builder,
GObject *child,
const gchar *type)
{
IdeOmniBar *self = (IdeOmniBar *)buildable;
g_assert (IDE_IS_OMNI_BAR (self));
g_assert (GTK_IS_BUILDER (builder));
g_assert (G_IS_OBJECT (child));
if (ide_str_equal0 (type, "start") && GTK_IS_WIDGET (child))
ide_omni_bar_add_button (IDE_OMNI_BAR (self),
GTK_WIDGET (child),
GTK_PACK_START,
0);
else if (ide_str_equal0 (type, "end") && GTK_IS_WIDGET (child))
ide_omni_bar_add_button (IDE_OMNI_BAR (self),
GTK_WIDGET (child),
GTK_PACK_END,
0);
else if (ide_str_equal0 (type, "placeholder") && GTK_IS_WIDGET (child))
ide_omni_bar_set_placeholder (IDE_OMNI_BAR (self), GTK_WIDGET (child));
else
parent_buildable_iface->add_child (buildable, builder, child, type);
}
static void
buildable_iface_init (GtkBuildableIface *iface)
{
parent_buildable_iface = g_type_interface_peek_parent (iface);
iface->add_child = ide_omni_bar_add_child;
}
/**
* ide_omni_bar_add_popover_section:
* @self: an #IdeOmniBar
* @widget: a #GtkWidget
* @priority: sort priority for the section
*
* Adds @widget to the omnibar popover, sorted by @priority
*
* Since: 3.32
*/
void
ide_omni_bar_add_popover_section (IdeOmniBar *self,
GtkWidget *widget,
gint priority)
{
g_return_if_fail (IDE_IS_OMNI_BAR (self));
g_return_if_fail (GTK_IS_WIDGET (widget));
gtk_container_add_with_properties (GTK_CONTAINER (self->sections_box), widget,
"priority", priority,
NULL);
}