828 lines
26 KiB
C
828 lines
26 KiB
C
/* ide-frame-header.c
|
|
*
|
|
* Copyright 2016-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-header"
|
|
|
|
#include "config.h"
|
|
|
|
#include <glib/gi18n.h>
|
|
|
|
#include "ide-gui-private.h"
|
|
#include "ide-frame-header.h"
|
|
|
|
#define CSS_PROVIDER_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 100)
|
|
|
|
/**
|
|
* SECTION:ide-frame-header
|
|
* @title: IdeFrameHeader
|
|
* @short_description: The header above document stacks
|
|
*
|
|
* The IdeFrameHeader is the titlebar widget above stacks of documents.
|
|
* It is used to add state when a given document is in view.
|
|
*
|
|
* It can also track the primary color of the content and update it's
|
|
* styling to match.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
|
|
struct _IdeFrameHeader
|
|
{
|
|
DzlPriorityBox parent_instance;
|
|
|
|
GtkCssProvider *css_provider;
|
|
guint update_css_handler;
|
|
|
|
GdkRGBA background_rgba;
|
|
GdkRGBA foreground_rgba;
|
|
|
|
guint background_rgba_set : 1;
|
|
guint foreground_rgba_set : 1;
|
|
|
|
GtkButton *close_button;
|
|
DzlMenuButton *document_button;
|
|
GtkMenuButton *title_button;
|
|
GtkPopover *title_popover;
|
|
GtkListBox *title_list_box;
|
|
DzlPriorityBox *title_box;
|
|
GtkLabel *title_label;
|
|
GtkLabel *title_modified;
|
|
GtkBox *title_views_box;
|
|
|
|
DzlJoinedMenu *menu;
|
|
};
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_BACKGROUND_RGBA,
|
|
PROP_FOREGROUND_RGBA,
|
|
PROP_MODIFIED,
|
|
PROP_SHOW_CLOSE_BUTTON,
|
|
PROP_TITLE,
|
|
N_PROPS
|
|
};
|
|
|
|
G_DEFINE_FINAL_TYPE (IdeFrameHeader, ide_frame_header, DZL_TYPE_PRIORITY_BOX)
|
|
|
|
static GParamSpec *properties [N_PROPS];
|
|
|
|
void
|
|
_ide_frame_header_focus_list (IdeFrameHeader *self)
|
|
{
|
|
g_return_if_fail (IDE_IS_FRAME_HEADER (self));
|
|
|
|
gtk_popover_popup (self->title_popover);
|
|
gtk_widget_grab_focus (GTK_WIDGET (self->title_list_box));
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_hide (IdeFrameHeader *self)
|
|
{
|
|
GtkPopover *popover;
|
|
|
|
g_return_if_fail (IDE_IS_FRAME_HEADER (self));
|
|
|
|
/* This is like _ide_frame_header_popdown() but we hide the
|
|
* popovers immediately without performing the popdown animation.
|
|
*/
|
|
|
|
popover = gtk_menu_button_get_popover (GTK_MENU_BUTTON (self->document_button));
|
|
if (popover != NULL)
|
|
gtk_widget_hide (GTK_WIDGET (popover));
|
|
|
|
gtk_widget_hide (GTK_WIDGET (self->title_popover));
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_popdown (IdeFrameHeader *self)
|
|
{
|
|
GtkPopover *popover;
|
|
|
|
g_return_if_fail (IDE_IS_FRAME_HEADER (self));
|
|
|
|
popover = gtk_menu_button_get_popover (GTK_MENU_BUTTON (self->document_button));
|
|
if (popover != NULL)
|
|
gtk_popover_popdown (popover);
|
|
|
|
gtk_popover_popdown (self->title_popover);
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_update (IdeFrameHeader *self,
|
|
IdePage *view)
|
|
{
|
|
const gchar *action = "frame.close-page";
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
g_assert (!view || IDE_IS_PAGE (view));
|
|
|
|
/*
|
|
* Update our menus for the document to include the menu type needed for the
|
|
* newly focused view. Make sure we keep the Frame section at the end which
|
|
* is always the last section in the joined menus.
|
|
*/
|
|
|
|
while (dzl_joined_menu_get_n_joined (self->menu) > 1)
|
|
dzl_joined_menu_remove_index (self->menu, 0);
|
|
|
|
if (view != NULL)
|
|
{
|
|
const gchar *menu_id = ide_page_get_menu_id (view);
|
|
|
|
if (menu_id != NULL)
|
|
{
|
|
GMenu *menu = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT, menu_id);
|
|
|
|
dzl_joined_menu_prepend_menu (self->menu, G_MENU_MODEL (menu));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Hide the document selectors if there are no views to select (which is
|
|
* indicated by us having a NULL view here.
|
|
*/
|
|
gtk_widget_set_visible (GTK_WIDGET (self->title_views_box), view != NULL);
|
|
|
|
/*
|
|
* The close button acts differently depending on the grid stage.
|
|
*
|
|
* - Last column, single stack => do nothing (action will be disabled)
|
|
* - No more views and more than one stack in column (close just the stack)
|
|
* - No more views and single stack in column and more than one column (close the column)
|
|
*/
|
|
|
|
if (view == NULL)
|
|
{
|
|
GtkWidget *stack;
|
|
GtkWidget *column;
|
|
|
|
action = "gridcolumn.close";
|
|
stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
|
|
column = gtk_widget_get_ancestor (GTK_WIDGET (stack), IDE_TYPE_GRID_COLUMN);
|
|
|
|
if (stack != NULL && column != NULL)
|
|
{
|
|
if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)) > 1)
|
|
action = "frame.close-stack";
|
|
}
|
|
}
|
|
|
|
gtk_actionable_set_action_name (GTK_ACTIONABLE (self->close_button), action);
|
|
|
|
/*
|
|
* Hide any popovers that we know about. If we got here from closing
|
|
* documents, we should hide the popover after the last document is closed
|
|
* (inidicated by NULL view).
|
|
*/
|
|
if (view == NULL)
|
|
_ide_frame_header_popdown (self);
|
|
}
|
|
|
|
static void
|
|
close_view_cb (GtkButton *button,
|
|
IdeFrameHeader *self)
|
|
{
|
|
GtkWidget *stack;
|
|
GtkWidget *row;
|
|
GtkWidget *view;
|
|
|
|
g_assert (GTK_IS_BUTTON (button));
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW);
|
|
if (row == NULL)
|
|
return;
|
|
|
|
view = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
|
|
if (view == NULL)
|
|
return;
|
|
|
|
stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
|
|
if (stack == NULL)
|
|
return;
|
|
|
|
_ide_frame_request_close (IDE_FRAME (stack), IDE_PAGE (view));
|
|
}
|
|
|
|
static gboolean
|
|
modified_to_attrs (GBinding *binding,
|
|
const GValue *src_value,
|
|
GValue *dst_value,
|
|
gpointer user_data)
|
|
{
|
|
PangoAttrList *attrs = NULL;
|
|
|
|
if (g_value_get_boolean (src_value))
|
|
{
|
|
attrs = pango_attr_list_new ();
|
|
pango_attr_list_insert (attrs, pango_attr_style_new (PANGO_STYLE_ITALIC));
|
|
}
|
|
|
|
g_value_take_boxed (dst_value, attrs);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static GtkWidget *
|
|
create_document_row (gpointer item,
|
|
gpointer user_data)
|
|
{
|
|
IdeFrameHeader *self = user_data;
|
|
GtkListBoxRow *row;
|
|
GtkButton *close_button;
|
|
GtkLabel *label;
|
|
GtkImage *image;
|
|
GtkBox *box;
|
|
|
|
g_assert (IDE_IS_PAGE (item));
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
|
|
"visible", TRUE,
|
|
NULL);
|
|
box = g_object_new (GTK_TYPE_BOX,
|
|
"visible", TRUE,
|
|
NULL);
|
|
image = g_object_new (GTK_TYPE_IMAGE,
|
|
"icon-size", GTK_ICON_SIZE_MENU,
|
|
"visible", TRUE,
|
|
NULL);
|
|
label = g_object_new (DZL_TYPE_BOLDING_LABEL,
|
|
"hexpand", TRUE,
|
|
"xalign", 0.0f,
|
|
"visible", TRUE,
|
|
NULL);
|
|
close_button = g_object_new (GTK_TYPE_BUTTON,
|
|
"child", g_object_new (GTK_TYPE_IMAGE,
|
|
"icon-name", "window-close-symbolic",
|
|
"visible", TRUE,
|
|
NULL),
|
|
"valign", GTK_ALIGN_CENTER,
|
|
"visible", TRUE,
|
|
NULL);
|
|
g_signal_connect_object (close_button,
|
|
"clicked",
|
|
G_CALLBACK (close_view_cb),
|
|
self, 0);
|
|
dzl_gtk_widget_add_style_class (GTK_WIDGET (close_button), "image-button");
|
|
|
|
g_object_bind_property (item, "icon", image, "gicon", G_BINDING_SYNC_CREATE);
|
|
g_object_bind_property_full (item, "modified", label, "attributes", G_BINDING_SYNC_CREATE,
|
|
modified_to_attrs, NULL, NULL, NULL);
|
|
g_object_bind_property (item, "title", label, "label", G_BINDING_SYNC_CREATE);
|
|
g_object_set_data (G_OBJECT (row), "IDE_PAGE", item);
|
|
|
|
gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
|
|
gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
|
|
gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
|
|
gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (close_button));
|
|
|
|
return GTK_WIDGET (row);
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_model_changed (IdeFrameHeader *self,
|
|
guint position,
|
|
guint removed,
|
|
guint added,
|
|
GListModel *model)
|
|
{
|
|
guint size;
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
g_assert (G_IS_LIST_MODEL (model));
|
|
|
|
size = g_list_model_get_n_items (model);
|
|
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->title_button), size > 0);
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_set_pages (IdeFrameHeader *self,
|
|
GListModel *model)
|
|
{
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
g_assert (!model || G_IS_LIST_MODEL (model));
|
|
|
|
gtk_list_box_bind_model (self->title_list_box,
|
|
model,
|
|
create_document_row,
|
|
self, NULL);
|
|
|
|
/* We need to watch our model for any new document added or removed */
|
|
g_signal_connect_object (model,
|
|
"items-changed",
|
|
G_CALLBACK (ide_frame_header_model_changed),
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_view_row_activated (GtkListBox *list_box,
|
|
GtkListBoxRow *row,
|
|
IdeFrameHeader *self)
|
|
{
|
|
GtkWidget *stack;
|
|
GtkWidget *page;
|
|
|
|
g_assert (GTK_IS_LIST_BOX (list_box));
|
|
g_assert (GTK_IS_LIST_BOX_ROW (row));
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
|
|
page = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
|
|
|
|
if (stack != NULL && page != NULL)
|
|
{
|
|
ide_frame_set_visible_child (IDE_FRAME (stack), IDE_PAGE (page));
|
|
gtk_widget_grab_focus (page);
|
|
}
|
|
|
|
_ide_frame_header_popdown (self);
|
|
}
|
|
|
|
static gboolean
|
|
ide_frame_header_update_css (IdeFrameHeader *self)
|
|
{
|
|
g_autoptr(GString) str = NULL;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
g_assert (self->css_provider != NULL);
|
|
g_assert (GTK_IS_CSS_PROVIDER (self->css_provider));
|
|
|
|
str = g_string_new (NULL);
|
|
|
|
/*
|
|
* We set various styles on this provider so that we can update multiple
|
|
* widgets using the same CSS style. That includes ourself, various buttons,
|
|
* labels, and some images.
|
|
*/
|
|
|
|
if (self->background_rgba_set)
|
|
{
|
|
g_autofree gchar *bgstr = gdk_rgba_to_string (&self->background_rgba);
|
|
|
|
g_string_append (str, "ideframeheader {\n");
|
|
g_string_append (str, " background: none;\n");
|
|
g_string_append_printf (str, " background-color: %s;\n", bgstr);
|
|
g_string_append (str, " transition: background-color 400ms;\n");
|
|
g_string_append (str, " transition-timing-function: ease;\n");
|
|
g_string_append_printf (str, " border-bottom: 1px solid shade(%s,0.9);\n", bgstr);
|
|
g_string_append (str, " }\n");
|
|
g_string_append (str, "button { background: transparent; }\n");
|
|
g_string_append (str, "button:hover, button:checked {\n");
|
|
g_string_append_printf (str, " background: none; background-color: shade(%s,.85); }\n", bgstr);
|
|
|
|
/* only use foreground when background is set */
|
|
if (self->foreground_rgba_set)
|
|
{
|
|
static const gchar *names[] = { "image", "label" };
|
|
g_autofree gchar *fgstr = gdk_rgba_to_string (&self->foreground_rgba);
|
|
|
|
for (guint i = 0; i < G_N_ELEMENTS (names); i++)
|
|
{
|
|
g_string_append_printf (str, "%s { ", names[i]);
|
|
g_string_append (str, " -gtk-icon-shadow: none;\n");
|
|
g_string_append (str, " text-shadow: none;\n");
|
|
g_string_append_printf (str, " text-shadow: 0 -1px alpha(%s,0.05);\n", fgstr);
|
|
g_string_append_printf (str, " color: %s;\n", fgstr);
|
|
g_string_append (str, "}\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Use -1 for length so CSS provider knows the string is NULL terminated
|
|
* and there-by avoid a string copy.
|
|
*/
|
|
if (!gtk_css_provider_load_from_data (self->css_provider, str->str, -1, &error))
|
|
g_warning ("Failed to load CSS: '%s': %s", str->str, error->message);
|
|
|
|
self->update_css_handler = 0;
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_queue_update_css (IdeFrameHeader *self)
|
|
{
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
if (self->update_css_handler == 0)
|
|
self->update_css_handler =
|
|
gdk_threads_add_idle_full (G_PRIORITY_HIGH,
|
|
(GSourceFunc) ide_frame_header_update_css,
|
|
g_object_ref (self),
|
|
g_object_unref);
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_set_background_rgba (IdeFrameHeader *self,
|
|
const GdkRGBA *background_rgba)
|
|
{
|
|
GdkRGBA old;
|
|
gboolean old_set;
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
old_set = self->background_rgba_set;
|
|
old = self->background_rgba;
|
|
|
|
if (background_rgba != NULL)
|
|
self->background_rgba = *background_rgba;
|
|
|
|
self->background_rgba_set = !!background_rgba;
|
|
|
|
if (self->background_rgba_set != old_set || !gdk_rgba_equal (&self->background_rgba, &old))
|
|
ide_frame_header_queue_update_css (self);
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_set_foreground_rgba (IdeFrameHeader *self,
|
|
const GdkRGBA *foreground_rgba)
|
|
{
|
|
GdkRGBA old;
|
|
gboolean old_set;
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
old_set = self->foreground_rgba_set;
|
|
old = self->foreground_rgba;
|
|
|
|
if (foreground_rgba != NULL)
|
|
self->foreground_rgba = *foreground_rgba;
|
|
|
|
self->foreground_rgba_set = !!foreground_rgba;
|
|
|
|
if (self->background_rgba_set != old_set || !gdk_rgba_equal (&self->foreground_rgba, &old))
|
|
ide_frame_header_queue_update_css (self);
|
|
}
|
|
|
|
static void
|
|
update_widget_providers (GtkWidget *widget,
|
|
IdeFrameHeader *self)
|
|
{
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
g_assert (GTK_IS_WIDGET (widget));
|
|
|
|
/*
|
|
* The goal here is to explore the widget hierarchy a bit to find widget
|
|
* types that we care about styling. This is the second half to our CSS
|
|
* strategy to assign specific CSS providers to widgets instead of a global
|
|
* CSS provider. The goal here is to avoid the giant CSS invalidation that
|
|
* happens when invalidating the global CSS tree.
|
|
*/
|
|
|
|
if (GTK_IS_BUTTON (widget) ||
|
|
GTK_IS_LABEL (widget) ||
|
|
GTK_IS_IMAGE (widget) ||
|
|
DZL_IS_SIMPLE_LABEL (widget))
|
|
{
|
|
GtkStyleContext *style_context;
|
|
|
|
style_context = gtk_widget_get_style_context (widget);
|
|
gtk_style_context_add_provider (style_context,
|
|
GTK_STYLE_PROVIDER (self->css_provider),
|
|
CSS_PROVIDER_PRIORITY);
|
|
}
|
|
|
|
if (GTK_IS_CONTAINER (widget))
|
|
gtk_container_foreach (GTK_CONTAINER (widget),
|
|
(GtkCallback) update_widget_providers,
|
|
self);
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_add (GtkContainer *container,
|
|
GtkWidget *widget)
|
|
{
|
|
IdeFrameHeader *self = (IdeFrameHeader *)container;
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
g_assert (GTK_IS_WIDGET (widget));
|
|
|
|
GTK_CONTAINER_CLASS (ide_frame_header_parent_class)->add (container, widget);
|
|
|
|
update_widget_providers (widget, self);
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_get_preferred_width (GtkWidget *widget,
|
|
gint *min_width,
|
|
gint *nat_width)
|
|
{
|
|
g_assert (IDE_IS_FRAME_HEADER (widget));
|
|
g_assert (min_width != NULL);
|
|
g_assert (nat_width != NULL);
|
|
|
|
GTK_WIDGET_CLASS (ide_frame_header_parent_class)->get_preferred_width (widget, min_width, nat_width);
|
|
|
|
/*
|
|
* We don't want changes to the natural width to influence our positioning of
|
|
* the grid separators (unless necessary). So instead, we always return our
|
|
* minimum position as our natural size and let the grid expand as necessary.
|
|
*/
|
|
*nat_width = *min_width;
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_destroy (GtkWidget *widget)
|
|
{
|
|
IdeFrameHeader *self = (IdeFrameHeader *)widget;
|
|
|
|
g_assert (IDE_IS_FRAME_HEADER (self));
|
|
|
|
dzl_clear_source (&self->update_css_handler);
|
|
g_clear_object (&self->css_provider);
|
|
|
|
if (self->title_list_box != NULL)
|
|
gtk_list_box_bind_model (self->title_list_box, NULL, NULL, NULL, NULL);
|
|
|
|
g_clear_object (&self->menu);
|
|
|
|
GTK_WIDGET_CLASS (ide_frame_header_parent_class)->destroy (widget);
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
IdeFrameHeader *self = IDE_FRAME_HEADER (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_MODIFIED:
|
|
g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->title_modified)));
|
|
break;
|
|
|
|
case PROP_SHOW_CLOSE_BUTTON:
|
|
g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->close_button)));
|
|
break;
|
|
|
|
case PROP_TITLE:
|
|
g_value_set_string (value, gtk_label_get_label (GTK_LABEL (self->title_label)));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
IdeFrameHeader *self = IDE_FRAME_HEADER (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_BACKGROUND_RGBA:
|
|
_ide_frame_header_set_background_rgba (self, g_value_get_boxed (value));
|
|
break;
|
|
|
|
case PROP_FOREGROUND_RGBA:
|
|
_ide_frame_header_set_foreground_rgba (self, g_value_get_boxed (value));
|
|
break;
|
|
|
|
case PROP_MODIFIED:
|
|
_ide_frame_header_set_modified (self, g_value_get_boolean (value));
|
|
break;
|
|
|
|
case PROP_SHOW_CLOSE_BUTTON:
|
|
gtk_widget_set_visible (GTK_WIDGET (self->close_button), g_value_get_boolean (value));
|
|
break;
|
|
|
|
case PROP_TITLE:
|
|
_ide_frame_header_set_title (self, g_value_get_string (value));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_class_init (IdeFrameHeaderClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
|
|
|
|
object_class->get_property = ide_frame_header_get_property;
|
|
object_class->set_property = ide_frame_header_set_property;
|
|
|
|
widget_class->destroy = ide_frame_header_destroy;
|
|
widget_class->get_preferred_width = ide_frame_header_get_preferred_width;
|
|
|
|
container_class->add = ide_frame_header_add;
|
|
|
|
/**
|
|
* IdeFrameHeader:background-rgba:
|
|
*
|
|
* The "background-rgba" property can be used to set the background
|
|
* color of the header. This should be set to the
|
|
* #IdePage:primary-color of the active view.
|
|
*
|
|
* Set to %NULL to unset the primary-color.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
properties [PROP_BACKGROUND_RGBA] =
|
|
g_param_spec_boxed ("background-rgba",
|
|
"Background RGBA",
|
|
"The background color to use for the header",
|
|
GDK_TYPE_RGBA,
|
|
(G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
|
|
|
|
/**
|
|
* IdeFrameHeader:foreground-rgba:
|
|
*
|
|
* Sets the foreground color to use when
|
|
* #IdeFrameHeader:background-rgba is used for the background.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
properties [PROP_FOREGROUND_RGBA] =
|
|
g_param_spec_boxed ("foreground-rgba",
|
|
"Foreground RGBA",
|
|
"The foreground color to use with background-rgba",
|
|
GDK_TYPE_RGBA,
|
|
(G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
|
|
|
|
properties [PROP_SHOW_CLOSE_BUTTON] =
|
|
g_param_spec_boolean ("show-close-button",
|
|
"Show Close Button",
|
|
"If the close button should be displayed",
|
|
FALSE,
|
|
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
properties [PROP_MODIFIED] =
|
|
g_param_spec_boolean ("modified",
|
|
"Modified",
|
|
"If the current document is modified",
|
|
FALSE,
|
|
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
|
|
|
|
properties [PROP_TITLE] =
|
|
g_param_spec_string ("title",
|
|
"Title",
|
|
"The title of the current document or view",
|
|
NULL,
|
|
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_properties (object_class, N_PROPS, properties);
|
|
|
|
gtk_widget_class_set_css_name (widget_class, "ideframeheader");
|
|
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame-header.ui");
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, close_button);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, document_button);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_box);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_button);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_label);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_list_box);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_modified);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_popover);
|
|
gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_views_box);
|
|
}
|
|
|
|
static void
|
|
ide_frame_header_init (IdeFrameHeader *self)
|
|
{
|
|
GtkStyleContext *style_context;
|
|
GMenu *frame_section;
|
|
|
|
/*
|
|
* To keep our foreground/background colors up to date, we use a CSS
|
|
* provider. However, attaching the provider globally causes much CSS
|
|
* style cascading exactly at the moment we want to animate. To avbid
|
|
* that, and keep animations snappy, we add the provider directly to
|
|
* our widget and to the children widgets we care about (buttons, their
|
|
* labels, etc).
|
|
*/
|
|
style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
|
|
self->css_provider = gtk_css_provider_new ();
|
|
gtk_style_context_add_provider (style_context,
|
|
GTK_STYLE_PROVIDER (self->css_provider),
|
|
CSS_PROVIDER_PRIORITY);
|
|
|
|
gtk_widget_init_template (GTK_WIDGET (self));
|
|
|
|
/*
|
|
* Create our menu for the document controls popover. It has two sections.
|
|
* The top section is based on the document and is updated whenever the
|
|
* visible child is changed. The bottom, are the frame controls are and
|
|
* static, but setup by us here.
|
|
*/
|
|
|
|
self->menu = dzl_joined_menu_new ();
|
|
dzl_menu_button_set_model (self->document_button, G_MENU_MODEL (self->menu));
|
|
frame_section = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT,
|
|
"ide-frame-menu");
|
|
dzl_joined_menu_append_menu (self->menu, G_MENU_MODEL (frame_section));
|
|
|
|
/*
|
|
* When a row is selected, we want to change the current view and
|
|
* hide the popover.
|
|
*/
|
|
|
|
g_signal_connect_object (self->title_list_box,
|
|
"row-activated",
|
|
G_CALLBACK (ide_frame_header_view_row_activated),
|
|
self, 0);
|
|
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->title_button), FALSE);
|
|
|
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
|
|
gtk_container_set_reallocate_redraws (GTK_CONTAINER (self), TRUE);
|
|
G_GNUC_END_IGNORE_DEPRECATIONS;
|
|
}
|
|
|
|
GtkWidget *
|
|
ide_frame_header_new (void)
|
|
{
|
|
return g_object_new (IDE_TYPE_FRAME_HEADER, NULL);
|
|
}
|
|
|
|
/**
|
|
* ide_frame_header_add_custom_title:
|
|
* @self: a #IdeFrameHeader
|
|
* @widget: a #GtkWidget
|
|
* @priority: the sort priority
|
|
*
|
|
* This will add @widget to the title area with @priority determining the
|
|
* sort order of the child.
|
|
*
|
|
* All "title" widgets in the #IdeFrameHeader are expanded to the
|
|
* same size. So if you don't need that, you should just use the normal
|
|
* gtk_container_add_with_properties() API to specify your widget with
|
|
* a given priority.
|
|
*
|
|
* Since: 3.32
|
|
*/
|
|
void
|
|
ide_frame_header_add_custom_title (IdeFrameHeader *self,
|
|
GtkWidget *widget,
|
|
gint priority)
|
|
{
|
|
g_return_if_fail (IDE_IS_FRAME_HEADER (self));
|
|
g_return_if_fail (GTK_IS_WIDGET (widget));
|
|
|
|
gtk_container_add_with_properties (GTK_CONTAINER (self->title_box), widget,
|
|
"priority", priority,
|
|
NULL);
|
|
|
|
update_widget_providers (widget, self);
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_set_title (IdeFrameHeader *self,
|
|
const gchar *title)
|
|
{
|
|
g_return_if_fail (IDE_IS_FRAME_HEADER (self));
|
|
|
|
gtk_label_set_label (GTK_LABEL (self->title_label), title);
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
|
|
}
|
|
|
|
void
|
|
_ide_frame_header_set_modified (IdeFrameHeader *self,
|
|
gboolean modified)
|
|
{
|
|
PangoAttrList *attrs = NULL;
|
|
|
|
g_return_if_fail (IDE_IS_FRAME_HEADER (self));
|
|
|
|
if (modified)
|
|
{
|
|
attrs = pango_attr_list_new ();
|
|
pango_attr_list_insert (attrs, pango_attr_style_new (PANGO_STYLE_ITALIC));
|
|
}
|
|
|
|
gtk_label_set_attributes (self->title_label, attrs);
|
|
gtk_widget_set_visible (GTK_WIDGET (self->title_modified), modified);
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
|
|
|
|
g_clear_pointer (&attrs, pango_attr_list_unref);
|
|
}
|