/* ide-omni-bar.c * * Copyright 2018-2019 Christian Hergert * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-omni-bar" #include "config.h" #include #include #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); }