Blob Blame History Raw
/* dzl-tab.c
 *
 * Copyright (C) 2016 Christian Hergert <chergert@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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 Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#define G_LOG_DOMAIN "dzl-tab"

#include "config.h"

#include "panel/dzl-dock-item.h"
#include "panel/dzl-tab.h"
#include "panel/dzl-tab-private.h"
#include "util/dzl-util-private.h"

struct _DzlTab { GtkEventBox parent; };

typedef struct
{
  /*
   * The edge the tab is being displayed upon. We use this to rotate
   * text or alter box orientation.
   */
  GtkPositionType edge : 2;

  /* Can we be closed by the user */
  guint can_close : 1;

  /* Are we the active tab */
  guint active : 1;

  /* If we are currently pressed */
  guint pressed : 1;

  /* If the pointer is over the widget (prelight) */
  guint pointer_in_widget : 1;

  /* If we are currently activating */
  guint in_activate : 1;

  /* Our icon/text style */
  DzlTabStyle style : 2;

  /* The action name to change the current tab */
  gchar *action_name;

  /* The value for the current tab */
  GVariant *action_target_value;

  /*
   * Because we don't have access to GtkActionMuxer or GtkActionHelper
   * from inside of Gtk, we need to manually track the state of the
   * action we are monitoring.
   *
   * We hold a pointer to the group so we can disconnect as necessary.
   */
  GActionGroup *action_group;
  gulong action_state_changed_handler;

  /*
   * These are our control widgets. The box contains the title in the
   * center with the minimize/close buttons to a side, depending on the
   * orientation/edge of the tabs.
   */
  GtkBox *box;
  GtkImage *image;
  GtkLabel *title;
  GtkWidget *close;
  GtkWidget *minimize;

  /*
   * This is a weak pointer to the widget this tab represents.
   * It's used by the stack to translate between the tab/widget
   * as necessary.
   */
  GtkWidget *widget;
} DzlTabPrivate;

static void actionable_iface_init (GtkActionableInterface *iface);

G_DEFINE_TYPE_WITH_CODE (DzlTab, dzl_tab, DZL_TYPE_BIN,
                         G_ADD_PRIVATE (DzlTab)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, actionable_iface_init))

enum {
  PROP_0,
  PROP_ACTIVE,
  PROP_CAN_CLOSE,
  PROP_EDGE,
  PROP_STYLE,
  PROP_TITLE,
  PROP_WIDGET,
  N_PROPS,

  PROP_ACTION_NAME,
  PROP_ACTION_TARGET,
};

enum {
  CLICKED,
  N_SIGNALS
};

static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];

static void
dzl_tab_get_inner_allocation (DzlTab        *self,
                              GtkAllocation *alloc)
{
  GtkBorder margin;
  GtkBorder border;
  GtkStyleContext *style_context;
  GtkStateFlags flags;

  g_assert (DZL_IS_TAB (self));
  g_assert (alloc != NULL);

  gtk_widget_get_allocation (GTK_WIDGET (self), alloc);

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  flags = gtk_widget_get_state_flags (GTK_WIDGET (self));

  gtk_style_context_get_border (style_context, flags, &margin);
  gtk_style_context_get_border (style_context, flags, &border);

  alloc->x += border.left;
  alloc->x += margin.left;

  alloc->y += border.top;
  alloc->y += margin.top;

  alloc->width -= (border.right + border.left);
  alloc->width -= (margin.right + margin.left);

  alloc->height -= (border.bottom + border.top);
  alloc->height -= (margin.bottom + margin.top);
}

static void
dzl_tab_apply_state (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_assert (DZL_IS_TAB (self));

  if (priv->active)
    gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE);
  else
    gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED);
}

static void
dzl_tab_activate (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  g_autoptr(GVariant) value = NULL;

  g_assert (DZL_IS_TAB (self));

  if (priv->in_activate ||
      priv->action_name == NULL ||
      priv->action_target_value == NULL)
    return;

  priv->in_activate = TRUE;

  value = dzl_gtk_widget_get_action_state (GTK_WIDGET (self), priv->action_name);
  if (value != NULL && !g_variant_equal (value, priv->action_target_value))
    dzl_gtk_widget_activate_action (GTK_WIDGET (self), priv->action_name, priv->action_target_value);

  priv->in_activate = FALSE;
}

static void
dzl_tab_destroy (GtkWidget *widget)
{
  DzlTab *self = (DzlTab *)widget;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  if (priv->action_group != NULL)
    {
      g_signal_handler_disconnect (priv->action_group,
                                   priv->action_state_changed_handler);
      priv->action_state_changed_handler = 0;
      dzl_clear_weak_pointer (&priv->action_group);
    }

  dzl_clear_weak_pointer (&priv->widget);
  g_clear_pointer (&priv->action_name, g_free);
  g_clear_pointer (&priv->action_target_value, g_variant_unref);

  GTK_WIDGET_CLASS (dzl_tab_parent_class)->destroy (widget);
}

static void
dzl_tab_update_edge (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_assert (DZL_IS_TAB (self));

  switch (priv->edge)
    {
    case GTK_POS_TOP:
      gtk_label_set_angle (priv->title, 0.0);
      gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_HORIZONTAL);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_END);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_END);
      gtk_widget_set_hexpand (GTK_WIDGET (self), TRUE);
      gtk_widget_set_vexpand (GTK_WIDGET (self), FALSE);
      break;

    case GTK_POS_BOTTOM:
      gtk_label_set_angle (priv->title, 0.0);
      gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_HORIZONTAL);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_END);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_END);
      gtk_widget_set_hexpand (GTK_WIDGET (self), TRUE);
      gtk_widget_set_vexpand (GTK_WIDGET (self), FALSE);
      break;

    case GTK_POS_LEFT:
      gtk_label_set_angle (priv->title, -90.0);
      gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_VERTICAL);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_END);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_END);
      gtk_widget_set_hexpand (GTK_WIDGET (self), FALSE);
      gtk_widget_set_vexpand (GTK_WIDGET (self), TRUE);
      break;

    case GTK_POS_RIGHT:
      gtk_label_set_angle (priv->title, 90.0);
      gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->box), GTK_ORIENTATION_VERTICAL);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0, GTK_PACK_START);
      gtk_box_set_child_packing (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0, GTK_PACK_START);
      gtk_widget_set_hexpand (GTK_WIDGET (self), FALSE);
      gtk_widget_set_vexpand (GTK_WIDGET (self), TRUE);
      break;

    default:
      g_assert_not_reached ();
    }
}

static gboolean
get_widget_coordinates (GtkWidget *widget,
                        GdkEvent  *event,
                        gdouble   *x,
                        gdouble   *y)
{
  GdkWindow *window = ((GdkEventAny *)event)->window;
  GdkWindow *target = gtk_widget_get_window (widget);
  gdouble tx, ty;

  if (!gdk_event_get_coords (event, &tx, &ty))
    return FALSE;

  while (window && window != target)
    {
      gint window_x, window_y;

      gdk_window_get_position (window, &window_x, &window_y);
      tx += window_x;
      ty += window_y;

      window = gdk_window_get_parent (window);
    }

  if (window)
    {
      *x = tx;
      *y = ty;

      return TRUE;
    }
  else
    return FALSE;
}

static void
dzl_tab_update_prelight (DzlTab   *self,
                         GdkEvent *event)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  gdouble x, y;

  g_assert (DZL_IS_TAB (self));
  g_assert (event != NULL);

  if (get_widget_coordinates (GTK_WIDGET (self), (GdkEvent *)event, &x, &y))
    {
      GtkAllocation alloc;

      dzl_tab_get_inner_allocation (self, &alloc);

      /*
       * We've translated our x,y coords to be relative to the widget, so we
       * can discard the x,y of the allocation.
       */
      alloc.x = 0;
      alloc.y = 0;

      if (x >= alloc.x &&
          x <= (alloc.x + alloc.width) &&
          y >= alloc.y &&
          y <= (alloc.y + alloc.height))
        {
          priv->pointer_in_widget = TRUE;
          gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_PRELIGHT, FALSE);
          return;
        }
    }

  priv->pointer_in_widget = FALSE;
  gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_PRELIGHT);
}

static gboolean
dzl_tab_motion_notify_event (GtkWidget      *widget,
                             GdkEventMotion *event)
{
  DzlTab *self = (DzlTab *)widget;

  g_assert (DZL_IS_TAB (self));
  g_assert (event != NULL);

  dzl_tab_update_prelight (self, (GdkEvent *)event);

  return GDK_EVENT_PROPAGATE;
}

static gboolean
dzl_tab_enter_notify_event (GtkWidget        *widget,
                            GdkEventCrossing *event)
{
  DzlTab *self = (DzlTab *)widget;

  g_assert (DZL_IS_TAB (self));
  g_assert (event != NULL);

  dzl_tab_update_prelight (self, (GdkEvent *)event);

  return GDK_EVENT_PROPAGATE;
}

static gboolean
dzl_tab_leave_notify_event (GtkWidget        *widget,
                            GdkEventCrossing *event)
{
  DzlTab *self = (DzlTab *)widget;

  g_return_val_if_fail (DZL_IS_TAB (self), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dzl_tab_update_prelight (self, (GdkEvent *)event);

  return GDK_EVENT_PROPAGATE;
}

static gboolean
dzl_tab_button_press_event (GtkWidget      *widget,
                            GdkEventButton *event)
{
  DzlTab *self = (DzlTab *)widget;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  if (event->button == GDK_BUTTON_PRIMARY)
    {
      priv->pressed = TRUE;
      gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_ACTIVE, FALSE);
      gtk_widget_grab_focus (GTK_WIDGET (self));
      return GDK_EVENT_STOP;
    }

  return GDK_EVENT_PROPAGATE;
}

static gboolean
dzl_tab_button_release_event (GtkWidget      *widget,
                              GdkEventButton *event)
{
  DzlTab *self = (DzlTab *)widget;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  if (event->button == GDK_BUTTON_PRIMARY)
    {
      priv->pressed = FALSE;
      gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_ACTIVE);

      if (priv->pointer_in_widget)
        g_signal_emit (self, signals [CLICKED], 0);

      return GDK_EVENT_STOP;
    }

  return GDK_EVENT_PROPAGATE;
}

static void
dzl_tab_realize (GtkWidget *widget)
{
  DzlTab *self = (DzlTab *)widget;
  GdkWindowAttr attributes = { 0 };
  GdkWindow *parent;
  GdkWindow *window;
  GtkAllocation alloc;
  gint attributes_mask = 0;

  g_assert (DZL_IS_TAB (widget));

  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);

  gtk_widget_set_realized (GTK_WIDGET (self), TRUE);

  parent = gtk_widget_get_parent_window (GTK_WIDGET (self));

  attributes.window_type = GDK_WINDOW_CHILD;
  attributes.wclass = GDK_INPUT_OUTPUT;
  attributes.x = alloc.x;
  attributes.y = alloc.y;
  attributes.width = alloc.width;
  attributes.height = alloc.height;
  attributes.visual = gtk_widget_get_visual (widget);
  attributes.event_mask = gtk_widget_get_events (widget);
  attributes.event_mask |= (GDK_BUTTON_PRESS_MASK |
                            GDK_BUTTON_RELEASE_MASK |
                            GDK_TOUCH_MASK |
                            GDK_EXPOSURE_MASK |
                            GDK_ENTER_NOTIFY_MASK |
                            GDK_LEAVE_NOTIFY_MASK);

  attributes_mask = (GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);

  window = gdk_window_new (parent, &attributes, attributes_mask);
  gtk_widget_register_window (GTK_WIDGET (self), window);
  gtk_widget_set_window (GTK_WIDGET (self), window);
}

static void
dzl_tab_action_state_changed (DzlTab       *self,
                              const gchar  *action_name,
                              GVariant     *value,
                              GActionGroup *group)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  gboolean active;

  g_assert (DZL_IS_TAB (self));
  g_assert (action_name != NULL);
  g_assert (G_IS_ACTION_GROUP (group));

  active = (value != NULL &&
            priv->action_target_value != NULL &&
            g_variant_equal (value, priv->action_target_value));

  if (active != priv->active)
    {
      priv->active = active;
      dzl_tab_apply_state (self);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACTIVE]);
    }
}

static void
dzl_tab_monitor_action_group (DzlTab       *self,
                              GActionGroup *group)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));
  g_return_if_fail (!group || G_IS_ACTION_GROUP (group));

  if (group != priv->action_group)
    {
      if (priv->action_group != NULL)
        {
          g_signal_handler_disconnect (priv->action_group,
                                       priv->action_state_changed_handler);
          priv->action_state_changed_handler = 0;
          dzl_clear_weak_pointer (&priv->action_group);
        }

      if (group != NULL)
        {
          gchar *prefix = NULL;
          gchar *name = NULL;

          dzl_g_action_name_parse (priv->action_name, &prefix, &name);

          if (name != NULL)
            {
              gchar *detailed;

              detailed = g_strdup_printf ("action-state-changed::%s", name);
              dzl_set_weak_pointer (&priv->action_group, group);
              priv->action_state_changed_handler =
                g_signal_connect_object (priv->action_group,
                                         detailed,
                                         G_CALLBACK (dzl_tab_action_state_changed),
                                         self,
                                         G_CONNECT_SWAPPED);

              g_free (detailed);
            }

          g_free (prefix);
          g_free (name);
        }
    }
}

static void
dzl_tab_hierarchy_changed (GtkWidget *widget,
                           GtkWidget *old_toplevel)
{
  DzlTab *self = (DzlTab *)widget;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  GActionGroup *group;

  g_assert (GTK_IS_WIDGET (widget));
  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));

  group = dzl_gtk_widget_find_group_for_action (widget, priv->action_name);
  dzl_tab_monitor_action_group (self, group);
}

static void
dzl_tab_size_allocate (GtkWidget     *widget,
                       GtkAllocation *allocation)
{
  g_assert (DZL_IS_TAB (widget));

  GTK_WIDGET_CLASS (dzl_tab_parent_class)->size_allocate (widget, allocation);

  if (gtk_widget_get_realized (widget))
    gdk_window_move_resize (gtk_widget_get_window (widget),
                            allocation->x,
                            allocation->y,
                            allocation->width,
                            allocation->height);
}

static void
dzl_tab_close_clicked (DzlTab    *self,
                       GtkWidget *button)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_assert (DZL_IS_TAB (self));
  g_assert (GTK_IS_BUTTON (button));

  g_object_ref (self);

  if (DZL_IS_DOCK_ITEM (priv->widget) &&
      dzl_dock_item_get_can_close (DZL_DOCK_ITEM (priv->widget)))
    dzl_dock_item_close (DZL_DOCK_ITEM (priv->widget));

  g_object_unref (self);
}

static void
dzl_tab_minimize_clicked (DzlTab    *self,
                          GtkWidget *button)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  GtkPositionType position = GTK_POS_LEFT;

  g_assert (DZL_IS_TAB (self));
  g_assert (GTK_IS_BUTTON (button));

  g_object_ref (self);

  if (DZL_IS_DOCK_ITEM (priv->widget))
    {
      DzlDockItem *item = DZL_DOCK_ITEM (priv->widget);

      for (DzlDockItem *parent = dzl_dock_item_get_parent (item);
           parent != NULL;
           parent = dzl_dock_item_get_parent (parent))
        {
          if (dzl_dock_item_minimize (parent, item, &position))
            break;
        }
    }

  g_object_unref (self);
}

static gboolean
dzl_tab_query_tooltip (GtkWidget  *widget,
                       gint        x,
                       gint        y,
                       gboolean    keyboard,
                       GtkTooltip *tooltip)
{
  DzlTab *self = (DzlTab *)widget;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  const gchar *title;

  g_assert (DZL_IS_TAB (self));
  g_assert (GTK_IS_TOOLTIP (tooltip));

  title = gtk_label_get_label (priv->title);

  if (title != NULL)
    {
      gtk_tooltip_set_text (tooltip, title);
      return TRUE;
    }

  return FALSE;
}

static void
dzl_tab_get_property (GObject    *object,
                      guint       prop_id,
                      GValue     *value,
                      GParamSpec *pspec)
{
  DzlTab *self = DZL_TAB (object);
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_ACTIVE:
      g_value_set_boolean (value, dzl_tab_get_active (self));
      break;

    case PROP_ACTION_NAME:
      g_value_set_string (value, priv->action_name);
      break;

    case PROP_ACTION_TARGET:
      g_value_set_boxed (value, priv->action_target_value);
      break;

    case PROP_CAN_CLOSE:
      g_value_set_boolean (value, dzl_tab_get_can_close (self));
      break;

    case PROP_EDGE:
      g_value_set_enum (value, dzl_tab_get_edge (self));
      break;

    case PROP_STYLE:
      g_value_set_flags (value, dzl_tab_get_style (self));
      break;

    case PROP_TITLE:
      g_value_set_string (value, dzl_tab_get_title (self));
      break;

    case PROP_WIDGET:
      g_value_set_object (value, dzl_tab_get_widget (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
dzl_tab_set_property (GObject      *object,
                      guint         prop_id,
                      const GValue *value,
                      GParamSpec   *pspec)
{
  DzlTab *self = DZL_TAB (object);
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_ACTIVE:
      dzl_tab_set_active (self, g_value_get_boolean (value));
      break;

    case PROP_ACTION_NAME:
      g_free (priv->action_name);
      priv->action_name = g_value_dup_string (value);
      break;

    case PROP_ACTION_TARGET:
      g_clear_pointer (&priv->action_target_value, g_variant_unref);
      priv->action_target_value = g_value_get_variant (value);
      if (priv->action_target_value != NULL)
        g_variant_ref_sink (priv->action_target_value);
      break;

    case PROP_CAN_CLOSE:
      dzl_tab_set_can_close (self, g_value_get_boolean (value));
      break;

    case PROP_EDGE:
      dzl_tab_set_edge (self, g_value_get_enum (value));
      break;

    case PROP_STYLE:
      dzl_tab_set_style (self, g_value_get_flags (value));
      break;

    case PROP_TITLE:
      dzl_tab_set_title (self, g_value_get_string (value));
      break;

    case PROP_WIDGET:
      dzl_tab_set_widget (self, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
dzl_tab_class_init (DzlTabClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->get_property = dzl_tab_get_property;
  object_class->set_property = dzl_tab_set_property;

  widget_class->destroy = dzl_tab_destroy;
  widget_class->button_press_event = dzl_tab_button_press_event;
  widget_class->button_release_event = dzl_tab_button_release_event;
  widget_class->motion_notify_event = dzl_tab_motion_notify_event;
  widget_class->enter_notify_event = dzl_tab_enter_notify_event;
  widget_class->leave_notify_event = dzl_tab_leave_notify_event;
  widget_class->realize = dzl_tab_realize;
  widget_class->size_allocate = dzl_tab_size_allocate;
  widget_class->hierarchy_changed = dzl_tab_hierarchy_changed;
  widget_class->query_tooltip = dzl_tab_query_tooltip;

  gtk_widget_class_set_css_name (widget_class, "dzltab");

  g_object_class_override_property (object_class, PROP_ACTION_NAME, "action-name");
  g_object_class_override_property (object_class, PROP_ACTION_TARGET, "action-target");

  properties [PROP_ACTIVE] =
    g_param_spec_boolean ("active",
                          "Active",
                          "If the tab is currently active",
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_CAN_CLOSE] =
    g_param_spec_boolean ("can-close",
                          "Can Close",
                          "If the tab widget can be closed",
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_EDGE] =
    g_param_spec_enum ("edge",
                       "Edge",
                       "Edge",
                       GTK_TYPE_POSITION_TYPE,
                       GTK_POS_TOP,
                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_STYLE] =
    g_param_spec_flags ("style",
                        "Style",
                        "The style for the tab",
                        DZL_TYPE_TAB_STYLE,
                        DZL_TAB_BOTH,
                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_TITLE] =
    g_param_spec_string ("title",
                         "Title",
                         "Title",
                         NULL,
                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_WIDGET] =
    g_param_spec_object ("widget",
                         "Widget",
                         "The widget the tab represents",
                         GTK_TYPE_WIDGET,
                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);

  signals [CLICKED] =
    g_signal_new_class_handler ("clicked",
                                G_TYPE_FROM_CLASS (klass),
                                G_SIGNAL_RUN_LAST,
                                G_CALLBACK (dzl_tab_activate),
                                NULL, NULL, NULL, G_TYPE_NONE, 0);
}

static void
dzl_tab_init (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  GtkBox *center;

  priv->style = DZL_TAB_BOTH;
  priv->edge = GTK_POS_TOP;

  gtk_widget_set_has_window (GTK_WIDGET (self), TRUE);

  gtk_widget_add_events (GTK_WIDGET (self),
                         GDK_BUTTON_PRESS_MASK |
                         GDK_BUTTON_RELEASE_MASK |
                         GDK_POINTER_MOTION_MASK |
                         GDK_ENTER_NOTIFY_MASK|
                         GDK_LEAVE_NOTIFY_MASK);
  gtk_widget_set_hexpand (GTK_WIDGET (self), TRUE);
  gtk_widget_set_vexpand (GTK_WIDGET (self), FALSE);

  gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE);

  priv->box = g_object_new (GTK_TYPE_BOX,
                            "orientation", GTK_ORIENTATION_HORIZONTAL,
                            "visible", TRUE,
                            NULL);
  g_signal_connect (priv->box, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->box);
  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->box));

  center = g_object_new (GTK_TYPE_BOX,
                         "spacing", 6,
                         "visible", TRUE,
                         NULL);
  gtk_box_set_center_widget (priv->box, GTK_WIDGET (center));

  priv->image = g_object_new (GTK_TYPE_IMAGE,
                              "visible", TRUE,
                              NULL);
  g_signal_connect (priv->image, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->image);
  gtk_box_pack_start (center, GTK_WIDGET (priv->image), FALSE, FALSE, 0);

  priv->title = g_object_new (GTK_TYPE_LABEL,
                              "ellipsize", PANGO_ELLIPSIZE_END,
                              "use-underline", TRUE,
                              "visible", TRUE,
                              NULL);
  g_signal_connect (priv->title, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->title);
  gtk_box_pack_start (center, GTK_WIDGET (priv->title), FALSE, FALSE, 0);

  priv->close = g_object_new (GTK_TYPE_BUTTON,
                              "halign", GTK_ALIGN_END,
                              "child", g_object_new (GTK_TYPE_IMAGE,
                                                     "icon-name", "window-close-symbolic",
                                                     "visible", TRUE,
                                                     NULL),
                              "visible", TRUE,
                              NULL);
  g_signal_connect_object (priv->close,
                           "clicked",
                           G_CALLBACK (dzl_tab_close_clicked),
                           self,
                           G_CONNECT_SWAPPED);
  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->close)), "close");
  gtk_box_pack_end (priv->box, GTK_WIDGET (priv->close), FALSE, FALSE, 0);
  g_object_bind_property (self, "can-close", priv->close, "visible", G_BINDING_SYNC_CREATE);

  priv->minimize = g_object_new (GTK_TYPE_BUTTON,
                                 "halign", GTK_ALIGN_END,
                                 "child", g_object_new (GTK_TYPE_IMAGE,
                                                        "icon-name", "window-minimize-symbolic",
                                                        "visible", TRUE,
                                                        NULL),
                                 "visible", TRUE,
                                 NULL);
  g_signal_connect_object (priv->minimize,
                           "clicked",
                           G_CALLBACK (dzl_tab_minimize_clicked),
                           self,
                           G_CONNECT_SWAPPED);
  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->minimize)), "minimize");
  gtk_box_pack_end (priv->box, GTK_WIDGET (priv->minimize), FALSE, FALSE, 0);
}

const gchar *
dzl_tab_get_title (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), NULL);

  return gtk_label_get_label (priv->title);
}

void
dzl_tab_set_title (DzlTab      *self,
                   const gchar *title)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  gtk_label_set_label (priv->title, title);
}

const gchar *
dzl_tab_get_icon_name (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  const gchar *ret = NULL;

  g_return_val_if_fail (DZL_IS_TAB (self), NULL);

  gtk_image_get_icon_name (priv->image, &ret, NULL);

  return ret;
}

void
dzl_tab_set_icon_name (DzlTab      *self,
                       const gchar *icon_name)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  g_object_set (priv->image, "icon-name", icon_name, NULL);
}

GtkPositionType
dzl_tab_get_edge (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), 0);

  return priv->edge;
}

void
dzl_tab_set_edge (DzlTab          *self,
                  GtkPositionType  edge)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));
  g_return_if_fail (edge >= 0);
  g_return_if_fail (edge <= 3);

  if (priv->edge != edge)
    {
      priv->edge = edge;
      dzl_tab_update_edge (self);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDGE]);
    }
}

/**
 * dzl_tab_get_widget:
 *
 * Returns: (transfer none) (nullable): A #GtkWidget or %NULL.
 */
GtkWidget *
dzl_tab_get_widget (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), NULL);

  return priv->widget;
}

void
dzl_tab_set_widget (DzlTab    *self,
                    GtkWidget *widget)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  if (dzl_set_weak_pointer (&priv->widget, widget))
    {
      gtk_label_set_mnemonic_widget (priv->title, widget);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]);
    }
}

gboolean
dzl_tab_get_can_close (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), FALSE);

  return priv->can_close;
}

void
dzl_tab_set_can_close (DzlTab   *self,
                       gboolean  can_close)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  can_close = !!can_close;

  if (can_close != priv->can_close)
    {
      priv->can_close = can_close;
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_CLOSE]);
    }
}

gboolean
dzl_tab_get_active (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), FALSE);

  return priv->active;
}

void
dzl_tab_set_active (DzlTab   *self,
                    gboolean  active)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  active = !!active;

  if (priv->active != active)
    {
      priv->active = active;
      dzl_tab_activate (self);
      dzl_tab_apply_state (self);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
    }
}

static const gchar *
dzl_tab_get_action_name (GtkActionable *actionable)
{
  DzlTab *self = (DzlTab *)actionable;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), NULL);

  return priv->action_name;
}

static void
dzl_tab_set_action_name (GtkActionable *actionable,
                         const gchar   *action_name)
{
  DzlTab *self = (DzlTab *)actionable;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  if (g_strcmp0 (priv->action_name, action_name) != 0)
    {
      g_free (priv->action_name);
      priv->action_name = g_strdup (action_name);
      g_object_notify (G_OBJECT (self), "action-name");
    }
}

static GVariant *
dzl_tab_get_action_target_value (GtkActionable *actionable)
{
  DzlTab *self = (DzlTab *)actionable;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), NULL);

  return priv->action_target_value;
}

static void
dzl_tab_set_action_target_value (GtkActionable *actionable,
                                 GVariant      *variant)
{
  DzlTab *self = (DzlTab *)actionable;
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  if (priv->action_target_value != variant)
    {
      g_clear_pointer (&priv->action_target_value, g_variant_unref);
      if (variant != NULL)
        priv->action_target_value = g_variant_ref_sink (variant);
      g_object_notify (G_OBJECT (self), "action-target");
    }
}

static void
actionable_iface_init (GtkActionableInterface *iface)
{
  iface->get_action_name = dzl_tab_get_action_name;
  iface->set_action_name = dzl_tab_set_action_name;
  iface->get_action_target_value = dzl_tab_get_action_target_value;
  iface->set_action_target_value = dzl_tab_set_action_target_value;
}

DzlTabStyle
dzl_tab_get_style (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_TAB (self), 0);

  return priv->style;
}

void
dzl_tab_set_style (DzlTab      *self,
                   DzlTabStyle  style)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);

  g_return_if_fail (DZL_IS_TAB (self));

  if (style != priv->style)
    {
      priv->style = style;
      gtk_widget_set_visible (GTK_WIDGET (priv->image), !!(priv->style & DZL_TAB_ICONS));
      gtk_widget_set_visible (GTK_WIDGET (priv->title), !!(priv->style & DZL_TAB_TEXT));
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STYLE]);
    }
}

void
_dzl_tab_update_controls (DzlTab *self)
{
  DzlTabPrivate *priv = dzl_tab_get_instance_private (self);
  gboolean can_close = FALSE;
  gboolean can_minimize = FALSE;

  g_return_if_fail (DZL_IS_TAB (self));

  if (DZL_IS_DOCK_ITEM (priv->widget))
    {
      can_close = dzl_dock_item_get_can_close (DZL_DOCK_ITEM (priv->widget));
      can_minimize = dzl_dock_item_get_can_minimize (DZL_DOCK_ITEM (priv->widget));
    }

  gtk_widget_set_visible (GTK_WIDGET (priv->close), can_close);
  gtk_widget_set_visible (GTK_WIDGET (priv->minimize), can_minimize);
}

GType
dzl_tab_style_get_type (void)
{
  static GType type_id;

  if (g_once_init_enter (&type_id))
    {
      GType _type_id;
      static const GFlagsValue values[] = {
        { DZL_TAB_ICONS, "DZL_TAB_ICONS", "icons" },
        { DZL_TAB_TEXT, "DZL_TAB_TEXT", "text" },
        { DZL_TAB_BOTH, "DZL_TAB_BOTH", "both" },
        { 0 }
      };

      _type_id = g_flags_register_static ("DzlTabStyle", values);
      g_once_init_leave (&type_id, _type_id);
    }

  return type_id;
}