/* 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);
}
