gem-graph-client/libide/gui/ide-frame.c

1468 lines
45 KiB
C

/* ide-frame.c
*
* Copyright 2017-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-frame"
#include "config.h"
#include <dazzle.h>
#include <glib/gi18n.h>
#include <libide-core.h>
#include <libide-threading.h>
#include <libpeas/peas.h>
#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);
}