/* ide-frame.c * * Copyright 2017-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-frame" #include "config.h" #include #include #include #include #include #include "ide-frame.h" #include "ide-frame-addin.h" #include "ide-frame-header.h" #include "ide-frame-wrapper.h" #include "ide-gui-private.h" #define TRANSITION_DURATION 300 #define DISTANCE_THRESHOLD(alloc) (MIN(250, (gint)((alloc)->width * .333))) /** * SECTION:ide-frame * @title: IdeFrame * @short_description: A stack of #IdePage * * This widget is used to represent a stack of #IdePage widgets. it * includes an #IdeFrameHeader at the top, and then a stack of pages * below. * * If there are no #IdePage visibile, then an empty state widget is * displayed with some common information for the user. * * To simplify integration with other systems, #IdeFrame implements * the #GListModel interface for each of the #IdePage. * * Since: 3.32 */ typedef struct { DzlBindingGroup *bindings; DzlSignalGroup *signals; GPtrArray *pages; GPtrArray *in_transition; PeasExtensionSet *addins; /* * Our gestures are used to do interactive moves when the user * does a three finger swipe. We create the dummy gesture to * ensure things work, because it for some reason does not without * the dummy gesture set. * * https://bugzilla.gnome.org/show_bug.cgi?id=788914 */ GtkGesture *dummy; GtkGesture *pan; DzlBoxTheatric *pan_theatric; IdePage *pan_page; /* Template references */ GtkBox *empty_placeholder; DzlEmptyState *failed_state; IdeFrameHeader *header; GtkStack *stack; GtkStack *top_stack; GtkEventBox *event_box; } IdeFramePrivate; typedef struct { IdeFrame *source; IdeFrame *dest; IdePage *page; DzlBoxTheatric *theatric; } AnimationState; enum { PROP_0, PROP_HAS_VIEW, PROP_VISIBLE_CHILD, N_PROPS }; enum { CHANGE_CURRENT_PAGE, N_SIGNALS }; static void buildable_iface_init (GtkBuildableIface *iface); static void list_model_iface_init (GListModelInterface *iface); static void animation_state_complete (gpointer data); G_DEFINE_TYPE_WITH_CODE (IdeFrame, ide_frame, GTK_TYPE_BOX, G_ADD_PRIVATE (IdeFrame) G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init) G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) static GParamSpec *properties [N_PROPS]; static guint signals [N_SIGNALS]; static inline gboolean is_uninitialized (GtkAllocation *alloc) { return (alloc->x == -1 && alloc->y == -1 && alloc->width == 1 && alloc->height == 1); } static void ide_frame_set_cursor (IdeFrame *self, const gchar *name) { GdkWindow *window; GdkDisplay *display; GdkCursor *cursor; g_assert (IDE_IS_FRAME (self)); g_assert (name != NULL); window = gtk_widget_get_window (GTK_WIDGET (self)); display = gtk_widget_get_display (GTK_WIDGET (self)); cursor = gdk_cursor_new_from_name (display, name); gdk_window_set_cursor (window, cursor); g_clear_object (&cursor); } static void ide_frame_page_failed (IdeFrame *self, GParamSpec *pspec, IdePage *page) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_assert (IDE_IS_FRAME (self)); g_assert (IDE_IS_PAGE (page)); if (ide_page_get_failed (page)) gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state)); else gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack)); } static void ide_frame_bindings_notify_source (IdeFrame *self, GParamSpec *pspec, DzlBindingGroup *bindings) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); GObject *source; g_assert (DZL_IS_BINDING_GROUP (bindings)); g_assert (pspec != NULL); g_assert (IDE_IS_FRAME (self)); source = dzl_binding_group_get_source (bindings); if (source == NULL) { _ide_frame_header_set_title (priv->header, _("No Open Pages")); _ide_frame_header_set_modified (priv->header, FALSE); _ide_frame_header_set_background_rgba (priv->header, NULL); _ide_frame_header_set_foreground_rgba (priv->header, NULL); } } static void ide_frame_notify_addin_of_page (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeFrameAddin *addin = (IdeFrameAddin *)exten; IdePage *page = user_data; g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_FRAME_ADDIN (addin)); g_assert (!page || IDE_IS_PAGE (page)); ide_frame_addin_set_page (addin, page); } static void ide_frame_notify_visible_child (IdeFrame *self, GParamSpec *pspec, GtkStack *stack) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); GtkWidget *visible_child; g_assert (IDE_IS_FRAME (self)); g_assert (GTK_IS_STACK (stack)); if (gtk_widget_in_destruction (GTK_WIDGET (self))) return; if ((visible_child = gtk_stack_get_visible_child (priv->stack))) { if (gtk_widget_in_destruction (visible_child)) visible_child = NULL; } /* * Mux/Proxy actions to our level so that they also be activated * from the header bar without any weirdness by the View. */ dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self), visible_child, "IDE_FRAME_MUXED_ACTION"); /* Update our bindings targets */ dzl_binding_group_set_source (priv->bindings, visible_child); dzl_signal_group_set_target (priv->signals, visible_child); /* Show either the empty state, failed state, or actual page */ if (visible_child != NULL && ide_page_get_failed (IDE_PAGE (visible_child))) gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state)); else if (visible_child != NULL) gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack)); else gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->empty_placeholder)); /* Allow the header to update settings */ _ide_frame_header_update (priv->header, IDE_PAGE (visible_child)); /* Ensure action state is up to date */ _ide_frame_update_actions (self); if (priv->addins != NULL) peas_extension_set_foreach (priv->addins, ide_frame_notify_addin_of_page, visible_child); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_CHILD]); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_VIEW]); } static void collect_widgets (GtkWidget *widget, gpointer user_data) { g_ptr_array_add (user_data, widget); } static void ide_frame_change_current_page (IdeFrame *self, gint direction) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_autoptr(GPtrArray) ar = NULL; GtkWidget *visible_child; gint position = 0; g_assert (IDE_IS_FRAME (self)); if (direction < -1) direction = -1; else if (direction > 1) direction = 1; visible_child = gtk_stack_get_visible_child (priv->stack); if (visible_child == NULL) return; gtk_container_child_get (GTK_CONTAINER (priv->stack), visible_child, "position", &position, NULL); ar = g_ptr_array_new (); gtk_container_foreach (GTK_CONTAINER (priv->stack), collect_widgets, ar); if (ar->len == 0) g_return_if_reached (); position = (position + (int)ar->len - direction) % (int)ar->len; visible_child = g_ptr_array_index (ar, position); gtk_stack_set_visible_child (priv->stack, visible_child); } static void ide_frame_add (GtkContainer *container, GtkWidget *widget) { IdeFrame *self = (IdeFrame *)container; IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (GTK_IS_WIDGET (widget)); if (IDE_IS_PAGE (widget)) gtk_container_add (GTK_CONTAINER (priv->stack), widget); else GTK_CONTAINER_CLASS (ide_frame_parent_class)->add (container, widget); gtk_widget_queue_resize (GTK_WIDGET (self)); } static void ide_frame_page_added (IdeFrame *self, IdePage *page) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); guint position; g_assert (IDE_IS_FRAME (self)); g_assert (IDE_IS_PAGE (page)); /* * Make sure that the header has dismissed all of the popovers immediately. * We don't want them lingering while we do other UI work which might want to * grab focus, etc. */ _ide_frame_header_popdown (priv->header); /* Notify GListModel consumers of the new page and it's position within * our stack of page widgets. */ position = priv->pages->len; g_ptr_array_add (priv->pages, page); g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1); /* * Now ensure that the page is displayed and focus the widget so the * user can immediately start typing. */ ide_frame_set_visible_child (self, page); gtk_widget_grab_focus (GTK_WIDGET (page)); } static void ide_frame_page_removed (IdeFrame *self, IdePage *page) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_assert (IDE_IS_FRAME (self)); g_assert (IDE_IS_PAGE (page)); if (priv->pages != NULL) { guint position = 0; /* If this is the last page, hide the popdown now. We use our hide * variant instead of popdown so that we don't have jittery animations. */ if (priv->pages->len == 1) _ide_frame_header_hide (priv->header); /* * Only remove the page if it is not in transition. We hold onto the * page during the transition so that we keep the list stable. */ if (!g_ptr_array_find_with_equal_func (priv->in_transition, page, NULL, &position)) { for (guint i = 0; i < priv->pages->len; i++) { if ((gpointer)page == g_ptr_array_index (priv->pages, i)) { g_ptr_array_remove_index (priv->pages, i); g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0); } } } } } static void ide_frame_real_agree_to_close_async (IdeFrame *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; g_assert (IDE_IS_FRAME (self)); g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_frame_real_agree_to_close_async); ide_task_set_priority (task, G_PRIORITY_LOW); ide_task_return_boolean (task, TRUE); } static gboolean ide_frame_real_agree_to_close_finish (IdeFrame *self, GAsyncResult *result, GError **error) { g_assert (IDE_IS_FRAME (self)); g_assert (IDE_IS_TASK (result)); return ide_task_propagate_boolean (IDE_TASK (result), error); } static void ide_frame_addin_added (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeFrameAddin *addin = (IdeFrameAddin *)exten; IdeFrame *self = user_data; IdePage *visible_child; g_assert (IDE_IS_FRAME (self)); g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_FRAME_ADDIN (addin)); ide_frame_addin_load (addin, self); visible_child = ide_frame_get_visible_child (self); if (visible_child != NULL) ide_frame_addin_set_page (addin, visible_child); } static void ide_frame_addin_removed (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeFrameAddin *addin = (IdeFrameAddin *)exten; IdeFrame *self = user_data; g_assert (IDE_IS_FRAME (self)); g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_FRAME_ADDIN (addin)); ide_frame_addin_set_page (addin, NULL); ide_frame_addin_unload (addin, self); } static gboolean ide_frame_pan_begin (IdeFrame *self, GdkEventSequence *sequence, GtkGesturePan *gesture) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); GtkAllocation alloc; cairo_surface_t *surface = NULL; GdkDevice *device; IdePage *page; GdkWindow *window; GtkWidget *grid; cairo_t *cr; gdouble x, y; gboolean enable_animations; GdkModifierType state = 0; IDE_ENTRY; g_assert (IDE_IS_FRAME (self)); g_assert (GTK_IS_GESTURE_PAN (gesture)); g_assert (priv->pan_theatric == NULL); if (!(page = ide_frame_get_visible_child (self)) || !(device = gtk_gesture_get_device (GTK_GESTURE (gesture))) || !(window = gtk_widget_get_window (GTK_WIDGET (page)))) goto failure; gtk_widget_get_allocation (GTK_WIDGET (page), &alloc); gdk_device_get_state (device, window, NULL, &state); g_object_get (gtk_settings_get_default (), "gtk-enable-animations", &enable_animations, NULL); if (sequence != NULL || !enable_animations || (state & GDK_SHIFT_MASK) == 0 || is_uninitialized (&alloc) || NULL == (surface = gdk_window_create_similar_surface (window, CAIRO_CONTENT_COLOR, alloc.width, alloc.height))) goto failure; gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y); cr = cairo_create (surface); gtk_widget_draw (GTK_WIDGET (page), cr); cairo_destroy (cr); grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID); gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0, &alloc.x, &alloc.y); priv->pan_page = g_object_ref (page); priv->pan_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC, "surface", surface, "target", grid, "x", alloc.x + (gint)x, "y", alloc.y, "width", alloc.width, "height", alloc.height, NULL); g_clear_pointer (&surface, cairo_surface_destroy); /* Hide the page while we begin the possible transition to another * layout stack. */ gtk_widget_hide (GTK_WIDGET (priv->pan_page)); /* * Hide the mouse cursor until ide_frame_pan_end() is called. * It can be distracting otherwise (and we want to warp it to the new * grid column too). */ ide_frame_set_cursor (self, "none"); IDE_RETURN (TRUE); failure: if (sequence != NULL) gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); IDE_RETURN (FALSE); } static void ide_frame_pan_update (IdeFrame *self, GdkEventSequence *sequence, GtkGestureSwipe *gesture) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); GtkAllocation alloc; GtkWidget *grid; gdouble x, y; IDE_ENTRY; g_assert (IDE_IS_FRAME (self)); g_assert (GTK_IS_GESTURE_PAN (gesture)); g_assert (!priv->pan_theatric || DZL_IS_BOX_THEATRIC (priv->pan_theatric)); if (priv->pan_theatric == NULL) { if (sequence != NULL) gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED); IDE_EXIT; } gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y); gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID); gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0, &alloc.x, &alloc.y); g_object_set (priv->pan_theatric, "x", alloc.x + (gint)x, NULL); IDE_EXIT; } static void ide_frame_pan_end (IdeFrame *self, GdkEventSequence *sequence, GtkGesturePan *gesture) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); IdeFramePrivate *dest_priv; IdeFrame *dest; GtkAllocation alloc; GtkWidget *grid; GtkWidget *column; gdouble x, y; gint direction; gint index = 0; IDE_ENTRY; g_assert (IDE_IS_FRAME (self)); g_assert (GTK_IS_GESTURE_PAN (gesture)); if (priv->pan_theatric == NULL || priv->pan_page == NULL) IDE_GOTO (cleanup); gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y); if (x > DISTANCE_THRESHOLD (&alloc)) direction = 1; else if (x < -DISTANCE_THRESHOLD (&alloc)) direction = -1; else direction = 0; grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID); g_assert (grid != NULL); g_assert (IDE_IS_GRID (grid)); column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN); g_assert (column != NULL); g_assert (IDE_IS_GRID_COLUMN (column)); gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column), "index", &index, NULL); dest = _ide_grid_get_nth_stack (IDE_GRID (grid), index + direction); dest_priv = ide_frame_get_instance_private (dest); g_assert (dest != NULL); g_assert (IDE_IS_FRAME (dest)); gtk_widget_get_allocation (GTK_WIDGET (dest), &alloc); if (!is_uninitialized (&alloc)) { AnimationState *state; state = g_slice_new0 (AnimationState); state->source = g_object_ref (self); state->dest = g_object_ref (dest); state->page = g_object_ref (priv->pan_page); state->theatric = g_object_ref (priv->pan_theatric); gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->top_stack), grid, 0, 0, &alloc.x, &alloc.y); /* * Use EASE_OUT_CUBIC, because user initiated the beginning of the * acceleration curve just by swiping. No need to duplicate. */ dzl_object_animate_full (state->theatric, DZL_ANIMATION_EASE_OUT_CUBIC, TRANSITION_DURATION, gtk_widget_get_frame_clock (GTK_WIDGET (self)), animation_state_complete, state, "x", alloc.x, "width", alloc.width, NULL); if (dest != self) { g_ptr_array_add (priv->in_transition, g_object_ref (priv->pan_page)); gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (priv->pan_page)); } IDE_TRACE_MSG ("Animating transition to %s column", dest != self ? "another" : "same"); } else { g_autoptr(IdePage) page = g_object_ref (priv->pan_page); IDE_TRACE_MSG ("Moving page to a previously non-existant column"); gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page)); gtk_widget_show (GTK_WIDGET (page)); gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page)); } cleanup: g_clear_object (&priv->pan_theatric); g_clear_object (&priv->pan_page); gtk_widget_queue_draw (gtk_widget_get_toplevel (GTK_WIDGET (self))); ide_frame_set_cursor (self, "arrow"); IDE_EXIT; } static void ide_frame_constructed (GObject *object) { IdeFrame *self = (IdeFrame *)object; IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_assert (IDE_IS_FRAME (self)); G_OBJECT_CLASS (ide_frame_parent_class)->constructed (object); priv->addins = peas_extension_set_new (peas_engine_get_default (), IDE_TYPE_FRAME_ADDIN, NULL); g_signal_connect (priv->addins, "extension-added", G_CALLBACK (ide_frame_addin_added), self); g_signal_connect (priv->addins, "extension-removed", G_CALLBACK (ide_frame_addin_removed), self); peas_extension_set_foreach (priv->addins, ide_frame_addin_added, self); gtk_widget_add_events (GTK_WIDGET (priv->event_box), GDK_TOUCH_MASK); priv->pan = g_object_new (GTK_TYPE_GESTURE_PAN, "widget", priv->event_box, "orientation", GTK_ORIENTATION_HORIZONTAL, "n-points", 3, NULL); g_signal_connect_swapped (priv->pan, "begin", G_CALLBACK (ide_frame_pan_begin), self); g_signal_connect_swapped (priv->pan, "update", G_CALLBACK (ide_frame_pan_update), self); g_signal_connect_swapped (priv->pan, "end", G_CALLBACK (ide_frame_pan_end), self); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->pan), GTK_PHASE_BUBBLE); /* * FIXME: Our priv->pan gesture does not activate unless we add another * dummy gesture. I currently have no idea why that is. * * https://bugzilla.gnome.org/show_bug.cgi?id=788914 */ priv->dummy = gtk_gesture_rotate_new (GTK_WIDGET (priv->event_box)); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->dummy), GTK_PHASE_BUBBLE); } static void ide_frame_grab_focus (GtkWidget *widget) { IdeFrame *self = (IdeFrame *)widget; IdePage *child; g_assert (IDE_IS_FRAME (self)); child = ide_frame_get_visible_child (self); if (child != NULL) gtk_widget_grab_focus (GTK_WIDGET (child)); else GTK_WIDGET_CLASS (ide_frame_parent_class)->grab_focus (widget); } static void ide_frame_destroy (GtkWidget *widget) { IdeFrame *self = (IdeFrame *)widget; IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_assert (IDE_IS_FRAME (self)); g_clear_object (&priv->addins); g_clear_pointer (&priv->in_transition, g_ptr_array_unref); if (priv->pages != NULL) { g_list_model_items_changed (G_LIST_MODEL (self), 0, priv->pages->len, 0); g_clear_pointer (&priv->pages, g_ptr_array_unref); } if (priv->bindings != NULL) { dzl_binding_group_set_source (priv->bindings, NULL); g_clear_object (&priv->bindings); } if (priv->signals != NULL) { dzl_signal_group_set_target (priv->signals, NULL); g_clear_object (&priv->signals); } g_clear_object (&priv->pan); GTK_WIDGET_CLASS (ide_frame_parent_class)->destroy (widget); } static void ide_frame_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeFrame *self = IDE_FRAME (object); switch (prop_id) { case PROP_HAS_VIEW: g_value_set_boolean (value, ide_frame_get_has_page (self)); break; case PROP_VISIBLE_CHILD: g_value_set_object (value, ide_frame_get_visible_child (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_frame_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { IdeFrame *self = IDE_FRAME (object); switch (prop_id) { case PROP_VISIBLE_CHILD: ide_frame_set_visible_child (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_frame_class_init (IdeFrameClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); object_class->constructed = ide_frame_constructed; object_class->get_property = ide_frame_get_property; object_class->set_property = ide_frame_set_property; widget_class->destroy = ide_frame_destroy; widget_class->grab_focus = ide_frame_grab_focus; container_class->add = ide_frame_add; klass->agree_to_close_async = ide_frame_real_agree_to_close_async; klass->agree_to_close_finish = ide_frame_real_agree_to_close_finish; properties [PROP_HAS_VIEW] = g_param_spec_boolean ("has-page", NULL, NULL, FALSE, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); properties [PROP_VISIBLE_CHILD] = g_param_spec_object ("visible-child", "Visible Child", "The current page to be displayed", IDE_TYPE_PAGE, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); signals [CHANGE_CURRENT_PAGE] = g_signal_new_class_handler ("change-current-page", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (ide_frame_change_current_page), NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT); gtk_widget_class_set_css_name (widget_class, "ideframe"); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame.ui"); gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, empty_placeholder); gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, failed_state); gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, header); gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, stack); gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, top_stack); gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, event_box); g_type_ensure (IDE_TYPE_FRAME_HEADER); g_type_ensure (IDE_TYPE_FRAME_WRAPPER); g_type_ensure (IDE_TYPE_SHORTCUT_LABEL); } static void ide_frame_init (IdeFrame *self) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); gtk_widget_init_template (GTK_WIDGET (self)); _ide_frame_init_actions (self); _ide_frame_init_shortcuts (self); priv->pages = g_ptr_array_new (); priv->in_transition = g_ptr_array_new_with_free_func (g_object_unref); priv->signals = dzl_signal_group_new (IDE_TYPE_PAGE); dzl_signal_group_connect_swapped (priv->signals, "notify::failed", G_CALLBACK (ide_frame_page_failed), self); priv->bindings = dzl_binding_group_new (); g_signal_connect_object (priv->bindings, "notify::source", G_CALLBACK (ide_frame_bindings_notify_source), self, G_CONNECT_SWAPPED); dzl_binding_group_bind (priv->bindings, "title", priv->header, "title", G_BINDING_SYNC_CREATE); dzl_binding_group_bind (priv->bindings, "modified", priv->header, "modified", G_BINDING_SYNC_CREATE); dzl_binding_group_bind (priv->bindings, "primary-color-bg", priv->header, "background-rgba", G_BINDING_SYNC_CREATE); dzl_binding_group_bind (priv->bindings, "primary-color-fg", priv->header, "foreground-rgba", G_BINDING_SYNC_CREATE); g_signal_connect_object (priv->stack, "notify::visible-child", G_CALLBACK (ide_frame_notify_visible_child), self, G_CONNECT_SWAPPED); g_signal_connect_object (priv->stack, "add", G_CALLBACK (ide_frame_page_added), self, G_CONNECT_SWAPPED | G_CONNECT_AFTER); g_signal_connect_object (priv->stack, "remove", G_CALLBACK (ide_frame_page_removed), self, G_CONNECT_SWAPPED); _ide_frame_header_set_pages (priv->header, G_LIST_MODEL (self)); _ide_frame_header_update (priv->header, NULL); } GtkWidget * ide_frame_new (void) { return g_object_new (IDE_TYPE_FRAME, NULL); } /** * ide_frame_set_visible_child: * @self: a #IdeFrame * * Sets the current page for the stack. * * Since: 3.32 */ void ide_frame_set_visible_child (IdeFrame *self, IdePage *page) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (IDE_IS_PAGE (page)); g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (page)) == (GtkWidget *)priv->stack); gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (page)); } /** * ide_frame_get_visible_child: * @self: a #IdeFrame * * Gets the visible #IdePage if there is one; otherwise %NULL. * * Returns: (nullable) (transfer none): An #IdePage or %NULL * * Since: 3.32 */ IdePage * ide_frame_get_visible_child (IdeFrame *self) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_val_if_fail (IDE_IS_FRAME (self), NULL); return IDE_PAGE (gtk_stack_get_visible_child (priv->stack)); } /** * ide_frame_get_titlebar: * @self: a #IdeFrame * * Gets the #IdeFrameHeader header that is at the top of the stack. * * Returns: (transfer none) (type IdeFrameHeader): The layout stack header. * * Since: 3.32 */ GtkWidget * ide_frame_get_titlebar (IdeFrame *self) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_val_if_fail (IDE_IS_FRAME (self), NULL); return GTK_WIDGET (priv->header); } /** * ide_frame_get_has_page: * @self: an #IdeFrame * * Gets the "has-page" property. * * This property is a convenience to allow widgets to easily bind * properties based on whether or not a page is visible in the stack. * * Returns: %TRUE if the stack has a page * * Since: 3.32 */ gboolean ide_frame_get_has_page (IdeFrame *self) { IdePage *visible_child; g_return_val_if_fail (IDE_IS_FRAME (self), FALSE); visible_child = ide_frame_get_visible_child (self); return visible_child != NULL; } static void ide_frame_close_page_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdePage *page = (IdePage *)object; g_autoptr(IdeFrame) self = user_data; g_autoptr(GError) error = NULL; GtkWidget *toplevel; GtkWidget *focus; gboolean had_focus = FALSE; g_assert (IDE_IS_PAGE (page)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_FRAME (self)); if (!ide_page_agree_to_close_finish (page, result, &error)) { g_message ("%s does not agree to close: %s", G_OBJECT_TYPE_NAME (page), error ? error->message : "No reason"); return; } /* Keep track of whether or not the widget had focus (which * would happen if we were activated from a keybinding. */ toplevel = gtk_widget_get_toplevel (GTK_WIDGET (page)); if (GTK_IS_WINDOW (toplevel) && NULL != (focus = gtk_window_get_focus (GTK_WINDOW (toplevel))) && (focus == GTK_WIDGET (page) || gtk_widget_is_ancestor (focus, GTK_WIDGET (page)))) had_focus = TRUE; /* Now we can destroy the child */ gtk_widget_destroy (GTK_WIDGET (page)); /* We don't want to leave the widget focus in an indeterminate * state so we immediately focus the next child in the stack. * But only do so if we had focus previously. */ if (had_focus) { IdePage *visible_child = ide_frame_get_visible_child (self); if (visible_child != NULL) gtk_widget_grab_focus (GTK_WIDGET (visible_child)); } } void _ide_frame_request_close (IdeFrame *self, IdePage *page) { g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (IDE_IS_PAGE (page)); ide_page_agree_to_close_async (page, NULL, ide_frame_close_page_cb, g_object_ref (self)); } static GType ide_frame_get_item_type (GListModel *model) { return IDE_TYPE_PAGE; } static guint ide_frame_get_n_items (GListModel *model) { IdeFrame *self = (IdeFrame *)model; IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_assert (IDE_IS_FRAME (self)); return priv->pages ? priv->pages->len : 0; } static gpointer ide_frame_get_item (GListModel *model, guint position) { IdeFrame *self = (IdeFrame *)model; IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_assert (IDE_IS_FRAME (self)); g_assert (position < priv->pages->len); return g_object_ref (g_ptr_array_index (priv->pages, position)); } static void list_model_iface_init (GListModelInterface *iface) { iface->get_n_items = ide_frame_get_n_items; iface->get_item = ide_frame_get_item; iface->get_item_type = ide_frame_get_item_type; } void ide_frame_agree_to_close_async (IdeFrame *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); IDE_FRAME_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data); } gboolean ide_frame_agree_to_close_finish (IdeFrame *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (IDE_IS_FRAME (self), FALSE); g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); return IDE_FRAME_GET_CLASS (self)->agree_to_close_finish (self, result, error); } static void animation_state_complete (gpointer data) { IdeFramePrivate *priv; AnimationState *state = data; g_assert (state != NULL); g_assert (IDE_IS_FRAME (state->source)); g_assert (IDE_IS_FRAME (state->dest)); g_assert (IDE_IS_PAGE (state->page)); /* Add the widget to the new stack */ if (state->dest != state->source) { gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->page)); /* Now remove it from our temporary transition. Be careful in case we were * destroyed in the mean time. */ priv = ide_frame_get_instance_private (state->source); if (priv->in_transition != NULL) { guint position = 0; if (g_ptr_array_find_with_equal_func (priv->pages, state->page, NULL, &position)) { g_ptr_array_remove (priv->in_transition, state->page); g_ptr_array_remove_index (priv->pages, position); g_list_model_items_changed (G_LIST_MODEL (state->source), position, 1, 0); } } } /* * We might need to reshow the widget in cases where we are in a * three-finger-swipe of the page. There is also a chance that we * aren't the proper visible child and that needs to be restored now. */ gtk_widget_show (GTK_WIDGET (state->page)); ide_frame_set_visible_child (state->dest, state->page); g_clear_object (&state->source); g_clear_object (&state->dest); g_clear_object (&state->page); g_clear_object (&state->theatric); g_slice_free (AnimationState, state); } void _ide_frame_transfer (IdeFrame *self, IdeFrame *dest, IdePage *page) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); IdeFramePrivate *dest_priv = ide_frame_get_instance_private (dest); const GdkRGBA *fg; const GdkRGBA *bg; g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (IDE_IS_FRAME (dest)); g_return_if_fail (IDE_IS_PAGE (page)); g_return_if_fail (GTK_WIDGET (priv->stack) == gtk_widget_get_parent (GTK_WIDGET (page))); /* * Inform the destination stack about our new primary colors so that it can * begin a transition to the new colors. We also want to do this upfront so * that we can reduce the amount of style invalidation caused during the * transitions. */ fg = ide_page_get_primary_color_fg (page); bg = ide_page_get_primary_color_bg (page); _ide_frame_header_set_foreground_rgba (dest_priv->header, fg); _ide_frame_header_set_background_rgba (dest_priv->header, bg); /* * If both the old and the new stacks are mapped, we can animate * between them using a snapshot of the page. Well, we also need * to be sure they have a valid allocation, but that check is done * slightly after this because it makes things easier. */ if (gtk_widget_get_mapped (GTK_WIDGET (self)) && gtk_widget_get_mapped (GTK_WIDGET (dest)) && gtk_widget_get_mapped (GTK_WIDGET (page))) { GtkAllocation alloc, dest_alloc; cairo_surface_t *surface = NULL; GdkWindow *window; GtkWidget *grid; gboolean enable_animations; grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID); gtk_widget_get_allocation (GTK_WIDGET (page), &alloc); gtk_widget_get_allocation (GTK_WIDGET (dest), &dest_alloc); g_object_get (gtk_settings_get_default (), "gtk-enable-animations", &enable_animations, NULL); if (enable_animations && grid != NULL && !is_uninitialized (&alloc) && !is_uninitialized (&dest_alloc) && dest_alloc.width > 0 && dest_alloc.height > 0 && NULL != (window = gtk_widget_get_window (GTK_WIDGET (page))) && NULL != (surface = gdk_window_create_similar_surface (window, CAIRO_CONTENT_COLOR, alloc.width, alloc.height))) { DzlBoxTheatric *theatric = NULL; AnimationState *state; cairo_t *cr; cr = cairo_create (surface); gtk_widget_draw (GTK_WIDGET (page), cr); cairo_destroy (cr); gtk_widget_translate_coordinates (GTK_WIDGET (priv->stack), grid, 0, 0, &alloc.x, &alloc.y); gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->stack), grid, 0, 0, &dest_alloc.x, &dest_alloc.y); theatric = g_object_new (DZL_TYPE_BOX_THEATRIC, "surface", surface, "height", alloc.height, "target", grid, "width", alloc.width, "x", alloc.x, "y", alloc.y, NULL); state = g_slice_new0 (AnimationState); state->source = g_object_ref (self); state->dest = g_object_ref (dest); state->page = g_object_ref (page); state->theatric = theatric; dzl_object_animate_full (theatric, DZL_ANIMATION_EASE_IN_OUT_CUBIC, TRANSITION_DURATION, gtk_widget_get_frame_clock (GTK_WIDGET (self)), animation_state_complete, state, "x", dest_alloc.x, "width", dest_alloc.width, "y", dest_alloc.y, "height", dest_alloc.height, NULL); /* * Mark the page as in-transition so that when we remove it * we can ignore the items-changed until the animation completes. */ g_ptr_array_add (priv->in_transition, g_object_ref (page)); gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page)); cairo_surface_destroy (surface); return; } } g_object_ref (page); gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page)); gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page)); g_object_unref (page); } /** * ide_frame_foreach_page: * @self: a #IdeFrame * @callback: (scope call) (closure user_data): A callback for each page * @user_data: user data for @callback * * This function will call @callback for every page found in @self. * * Since: 3.32 */ void ide_frame_foreach_page (IdeFrame *self, GtkCallback callback, gpointer user_data) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (callback != NULL); gtk_container_foreach (GTK_CONTAINER (priv->stack), callback, user_data); } /** * ide_frame_addin_find_by_module_name: * @frame: An #IdeFrame * @module_name: the module name which provides the addin * * This function will locate the #IdeFrameAddin that was registered by * the plugin named @module_name (which should match the "Module" field * provided in the .plugin file). * * If no module was found or that module does not implement the * #IdeFrameAddinInterface, then %NULL is returned. * * Returns: (transfer none) (nullable): An #IdeFrameAddin or %NULL * * Since: 3.32 */ IdeFrameAddin * ide_frame_addin_find_by_module_name (IdeFrame *frame, const gchar *module_name) { IdeFramePrivate *priv = ide_frame_get_instance_private (frame); PeasExtension *ret = NULL; PeasPluginInfo *plugin_info; g_return_val_if_fail (IDE_IS_FRAME (frame), NULL); g_return_val_if_fail (priv->addins != NULL, NULL); g_return_val_if_fail (module_name != NULL, NULL); plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name); if (plugin_info != NULL) ret = peas_extension_set_get_extension (priv->addins, plugin_info); else g_warning ("No addin could be found matching module \"%s\"", module_name); return ret ? IDE_FRAME_ADDIN (ret) : NULL; } void ide_frame_add_with_depth (IdeFrame *self, GtkWidget *widget, guint position) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (GTK_IS_WIDGET (widget)); gtk_container_add_with_properties (GTK_CONTAINER (priv->stack), widget, "position", position, NULL); } static void ide_frame_add_child (GtkBuildable *buildable, GtkBuilder *builder, GObject *object, const gchar *type) { IdeFrame *self = (IdeFrame *)buildable; GtkBuildableIface *parent = g_type_interface_peek_parent (GTK_BUILDABLE_GET_IFACE (buildable)); if (g_strcmp0 (type, "placeholder") == 0 && GTK_IS_WIDGET (object)) ide_frame_set_placeholder (self, GTK_WIDGET (object)); else parent->add_child (buildable, builder, object, type); } static void buildable_iface_init (GtkBuildableIface *iface) { iface->add_child = ide_frame_add_child; } void ide_frame_set_placeholder (IdeFrame *self, GtkWidget *placeholder) { IdeFramePrivate *priv = ide_frame_get_instance_private (self); g_return_if_fail (IDE_IS_FRAME (self)); g_return_if_fail (!placeholder || GTK_IS_WIDGET (placeholder)); gtk_container_foreach (GTK_CONTAINER (priv->empty_placeholder), (GtkCallback) gtk_widget_destroy, NULL); if (placeholder != NULL) gtk_container_add (GTK_CONTAINER (priv->empty_placeholder), placeholder); }