/* dzl-dock-revealer.c
 *
 * Copyright (C) 2016 Christian Hergert <christian@hergert.me>
 *
 * 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/>.
 */

#define G_LOG_DOMAIN "dzl-dock-revealer"

#include "config.h"

#include "animation/dzl-animation.h"
#include "panel/dzl-dock-revealer.h"
#include "util/dzl-util-private.h"

/**
 * SECTION:dzldockrevealer
 * @title: DzlDockRevealer
 * @short_description: A panel revealer
 *
 * This widget is a bit like #GtkRevealer with a couple of important
 * differences. First, it only supports a couple transition types
 * (the direction to slide reveal). Additionally, the size of the
 * child allocation will not change during the animation. This is not
 * as generally useful as an upstream GTK+ widget, but is extremely
 * important for the panel case to avoid things looking strange while
 * animating into and out of view.
 */

#define IS_HORIZONTAL(type) \
  (((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) || \
  ((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT))

#define IS_VERTICAL(type) \
  (((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP) || \
  ((type) == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN))

typedef struct
{
  DzlAnimation                  *animation;
  GtkAdjustment                 *adjustment;
  GdkWindow                     *window;
  gint                           position;
  gint                           position_tmp;
  guint                          transition_duration;
  DzlDockRevealerTransitionType  transition_type : 3;
  guint                          position_set : 1;
  guint                          reveal_child : 1;
  guint                          child_revealed : 1;
  GtkRequisition                 nat_req;
} DzlDockRevealerPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (DzlDockRevealer, dzl_dock_revealer, DZL_TYPE_BIN)

enum {
  PROP_0,
  PROP_CHILD_REVEALED,
  PROP_POSITION,
  PROP_POSITION_SET,
  PROP_REVEAL_CHILD,
  PROP_TRANSITION_DURATION,
  PROP_TRANSITION_TYPE,
  N_PROPS
};

static GParamSpec *properties [N_PROPS];

GtkWidget *
dzl_dock_revealer_new (void)
{
  return g_object_new (DZL_TYPE_DOCK_REVEALER, NULL);
}

guint
dzl_dock_revealer_get_transition_duration (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), 0);

  return priv->transition_duration;
}

void
dzl_dock_revealer_set_transition_duration (DzlDockRevealer *self,
                                           guint            transition_duration)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_if_fail (DZL_IS_DOCK_REVEALER (self));

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

DzlDockRevealerTransitionType
dzl_dock_revealer_get_transition_type (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), 0);

  return priv->transition_type;
}

void
dzl_dock_revealer_set_transition_type (DzlDockRevealer               *self,
                                       DzlDockRevealerTransitionType  transition_type)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_if_fail (DZL_IS_DOCK_REVEALER (self));
  g_return_if_fail (transition_type >= DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE);
  g_return_if_fail (transition_type <= DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);

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

gboolean
dzl_dock_revealer_get_child_revealed (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE);

  return priv->child_revealed;
}

gboolean
dzl_dock_revealer_get_reveal_child (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE);

  return priv->reveal_child;
}

static void
dzl_dock_revealer_animation_done (gpointer user_data)
{
  g_autoptr(DzlDockRevealer) self = user_data;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkWidget *child;
  gboolean child_revealed = FALSE;
  gboolean child_visible = FALSE;

  g_assert (DZL_DOCK_REVEALER (self));

  child = gtk_bin_get_child (GTK_BIN (self));

  if (priv->adjustment != NULL)
    {
      child_revealed = gtk_adjustment_get_value (priv->adjustment) >= 1.0;
      child_visible = gtk_adjustment_get_value (priv->adjustment) != 0.0;
    }

  if (child != NULL)
    gtk_widget_set_child_visible (GTK_WIDGET (child), child_visible);

  priv->child_revealed = child_revealed;

  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD_REVEALED]);
  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static guint
size_to_duration (GdkMonitor *monitor,
                  gint        size)
{
  g_assert (!monitor || GDK_IS_MONITOR (monitor));

  if (monitor != NULL)
    return dzl_animation_calculate_duration (monitor, 0, size);

  return MAX (150, size * 1.2);
}

static guint
dzl_dock_revealer_calculate_duration (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkWidget *child;
  GdkWindow *window;
  GdkMonitor *monitor = NULL;
  GdkDisplay *display;
  GtkRequisition min_size;
  GtkRequisition nat_size;

  g_assert (DZL_IS_DOCK_REVEALER (self));

  if (!gtk_widget_get_realized (GTK_WIDGET (self)))
    return 0;

  child = gtk_bin_get_child (GTK_BIN (self));

  if (child == NULL)
    return 0;

  if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE)
    return 0;

  if (priv->transition_duration != 0)
    return priv->transition_duration;

  gtk_widget_get_preferred_size (child, &min_size, &nat_size);

  display = gtk_widget_get_display (GTK_WIDGET (self));
  window = gtk_widget_get_window (GTK_WIDGET (self));
  if (window != NULL)
    monitor = gdk_display_get_monitor_at_window (display, window);

  if (IS_HORIZONTAL (priv->transition_type))
    {
      if (priv->position_set)
        {
          if (priv->position_set && priv->position > min_size.width)
            return size_to_duration (monitor, priv->position);
          return size_to_duration (monitor, min_size.width);
        }

      return size_to_duration (monitor, nat_size.width);
    }
  else
    {
      if (priv->position_set)
        {
          if (priv->position_set && priv->position > min_size.height)
            return size_to_duration (monitor, priv->position);
          return size_to_duration (monitor, min_size.height);
        }

      return size_to_duration (monitor, nat_size.height);
    }
}

void
dzl_dock_revealer_set_reveal_child (DzlDockRevealer *self,
                                    gboolean         reveal_child)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_if_fail (DZL_IS_DOCK_REVEALER (self));

  reveal_child = !!reveal_child;

  if (reveal_child != priv->reveal_child)
    {
      GtkWidget *child = gtk_bin_get_child (GTK_BIN (self));

      priv->reveal_child = reveal_child;

      dzl_animation_stop (priv->animation);
      dzl_clear_weak_pointer (&priv->animation);

      if (child != NULL)
        {
          guint duration;

          gtk_widget_set_child_visible (child, TRUE);

          duration = dzl_dock_revealer_calculate_duration (self);

          if (duration == 0)
            {
              gtk_adjustment_set_value (priv->adjustment, reveal_child ? 1.0 : 0.0);
              priv->child_revealed = reveal_child;
              gtk_widget_set_child_visible (child, reveal_child);
              g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVEAL_CHILD]);
              g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD_REVEALED]);
            }
          else
            {
              DzlAnimation *animation;

              animation = dzl_object_animate_full (priv->adjustment,
                                                   DZL_ANIMATION_EASE_IN_OUT_CUBIC,
                                                   duration,
                                                   gtk_widget_get_frame_clock (GTK_WIDGET (self)),
                                                   dzl_dock_revealer_animation_done,
                                                   g_object_ref (self),
                                                   "value", reveal_child ? 1.0 : 0.0,
                                                   NULL);
              dzl_set_weak_pointer (&priv->animation, animation);
              g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVEAL_CHILD]);
            }

          gtk_widget_queue_resize (GTK_WIDGET (self));
        }
    }
}

gint
dzl_dock_revealer_get_position (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), 0);

  return priv->position;
}

void
dzl_dock_revealer_set_position (DzlDockRevealer *self,
                                gint             position)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_if_fail (DZL_IS_DOCK_REVEALER (self));
  g_return_if_fail (position >= 0);

  if (priv->position != position)
    {
      priv->position = position;

      if (!priv->position_set)
        {
          priv->position_set = TRUE;
          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION_SET]);
        }

      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION]);
      gtk_widget_queue_resize (GTK_WIDGET (self));
    }
}

gboolean
dzl_dock_revealer_get_position_set (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE);

  return priv->position_set;
}

void
dzl_dock_revealer_set_position_set (DzlDockRevealer *self,
                                    gboolean         position_set)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_if_fail (DZL_IS_DOCK_REVEALER (self));

  position_set = !!position_set;

  if (priv->position_set != position_set)
    {
      priv->position_set = position_set;
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION_SET]);
      gtk_widget_queue_resize (GTK_WIDGET (self));
    }
}

static void
dzl_dock_revealer_get_child_preferred_width (DzlDockRevealer *self,
                                             gint            *min_width,
                                             gint            *nat_width)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkWidget *child;

  g_assert (DZL_IS_DOCK_REVEALER (self));
  g_assert (min_width != NULL);
  g_assert (nat_width != NULL);

  *min_width = 0;
  *nat_width = 0;

  if (NULL == (child = gtk_bin_get_child (GTK_BIN (self))))
    return;

  if (!gtk_widget_get_child_visible (child) || !gtk_widget_get_visible (child))
    return;

  gtk_widget_get_preferred_width (child, min_width, nat_width);

  if (IS_HORIZONTAL (priv->transition_type) && priv->position_set)
    {
      if (priv->position > *min_width)
        *nat_width = priv->position;
      else
        *nat_width = *min_width;
    }
}

static void
dzl_dock_revealer_get_preferred_width (GtkWidget *widget,
                                       gint      *min_width,
                                       gint      *nat_width)
{
  DzlDockRevealer *self = (DzlDockRevealer *)widget;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkWidget *child;
  GtkBorder borders;

  g_assert (DZL_IS_DOCK_REVEALER (self));
  g_assert (min_width != NULL);
  g_assert (nat_width != NULL);

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);

  child = gtk_bin_get_child (GTK_BIN (self));

  dzl_dock_revealer_get_child_preferred_width (self, min_width, nat_width);

  *min_width += borders.left + borders.right;
  *nat_width += borders.left + borders.right;

  priv->nat_req.width = *nat_width;

  if (IS_HORIZONTAL (priv->transition_type))
    {
      if (priv->animation != NULL)
        {
          /*
           * We allow going smaller than the minimum size during animations
           * and rely on clipping to hide the child.
           */
          *min_width = 0;

          /*
           * Our natural width is adjusted for the in-progress animation.
           */
          *nat_width *= gtk_adjustment_get_value (priv->adjustment);
        }
      else if (child != NULL && !gtk_widget_get_child_visible (child))
        {
          /*
           * Make sure we are completely hidden if the child is not currently
           * visible.
           */
          *min_width = 0;
          *nat_width = 0;
        }
    }
}

static void
dzl_dock_revealer_get_child_preferred_height (DzlDockRevealer *self,
                                              gint            *min_height,
                                              gint            *nat_height)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkWidget *child;

  g_assert (DZL_IS_DOCK_REVEALER (self));
  g_assert (min_height != NULL);
  g_assert (nat_height != NULL);

  *min_height = 0;
  *nat_height = 0;

  if (NULL == (child = gtk_bin_get_child (GTK_BIN (self))))
    return;

  if (!gtk_widget_get_child_visible (child) || !gtk_widget_get_visible (child))
    return;

  gtk_widget_get_preferred_height (child, min_height, nat_height);

  if (IS_VERTICAL (priv->transition_type) && priv->position_set)
    {
      if (priv->position > *min_height)
        *nat_height = priv->position;
      else
        *nat_height = *min_height;
    }
}

static void
dzl_dock_revealer_get_preferred_height (GtkWidget *widget,
                                        gint      *min_height,
                                        gint      *nat_height)
{
  DzlDockRevealer *self = (DzlDockRevealer *)widget;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkWidget *child;
  GtkBorder borders;

  g_assert (DZL_IS_DOCK_REVEALER (self));
  g_assert (min_height != NULL);
  g_assert (nat_height != NULL);

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);

  child = gtk_bin_get_child (GTK_BIN (self));

  dzl_dock_revealer_get_child_preferred_height (self, min_height, nat_height);

  *min_height += borders.top + borders.bottom;
  *nat_height += borders.top + borders.bottom;

  priv->nat_req.height = *nat_height;

  if (IS_VERTICAL (priv->transition_type))
    {
      if (priv->animation != NULL)
        {
          /*
           * We allow going smaller than the minimum size during animations
           * and rely on clipping to hide the child.
           */
          *min_height = 0;

          /*
           * Our natural height is adjusted for the in-progress animation.
           */
          *nat_height *= gtk_adjustment_get_value (priv->adjustment);
        }
      else if (child != NULL && !gtk_widget_get_child_visible (child))
        {
          /*
           * Make sure we are completely hidden if the child is not currently
           * visible.
           */
          *min_height = 0;
          *nat_height = 0;
        }
    }
}

static void
dzl_dock_revealer_size_allocate (GtkWidget     *widget,
                                 GtkAllocation *allocation)
{
  DzlDockRevealer *self = (DzlDockRevealer *)widget;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkAllocation child_allocation;
  GtkRequisition min_req;
  GtkRequisition nat_req;
  GtkStyleContext *style_context;
  GtkWidget *child;
  GtkBorder borders;

  g_assert (DZL_IS_DOCK_REVEALER (self));

  gtk_widget_set_allocation (widget, allocation);

  if (gtk_widget_get_realized (GTK_WIDGET (self)))
    gdk_window_move_resize (priv->window,
                            allocation->x,
                            allocation->y,
                            allocation->width,
                            allocation->height);

  if (NULL == (child = gtk_bin_get_child (GTK_BIN (self))))
    return;

  if (!gtk_widget_get_child_visible (child))
    return;

  child_allocation = *allocation;
  child_allocation.x = 0;
  child_allocation.y = 0;

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);
  dzl_gtk_allocation_subtract_border (&child_allocation, &borders);

  if (IS_HORIZONTAL (priv->transition_type))
    {
      dzl_dock_revealer_get_child_preferred_width (self, &min_req.width, &nat_req.width);
      child_allocation.width = nat_req.width;

      if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
        child_allocation.x = allocation->width - borders.right - child_allocation.width;
    }
  else if (IS_VERTICAL (priv->transition_type))
    {
      dzl_dock_revealer_get_child_preferred_height (self, &min_req.height, &nat_req.height);
      child_allocation.height = nat_req.height;

      if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN)
        child_allocation.y = allocation->height - borders.bottom - child_allocation.height;
    }

  gtk_widget_size_allocate (child, &child_allocation);
}

static void
dzl_dock_revealer_add (GtkContainer *container,
                       GtkWidget    *widget)
{
  DzlDockRevealer *self = (DzlDockRevealer *)container;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_assert (DZL_IS_DOCK_REVEALER (self));
  g_assert (GTK_IS_WIDGET (widget));

  GTK_CONTAINER_CLASS (dzl_dock_revealer_parent_class)->add (container, widget);

  gtk_widget_set_child_visible (widget, priv->reveal_child);
  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static gboolean
dzl_dock_revealer_draw (GtkWidget *widget,
                        cairo_t   *cr)
{
  DzlDockRevealer *self = (DzlDockRevealer *)widget;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GtkAllocation alloc;
  GtkStyleContext *style_context;
  GtkWidget *child;
  GtkBorder margin;
  GtkStateFlags state;

  g_assert (DZL_IS_DOCK_REVEALER (self));

  gtk_widget_get_allocation (widget, &alloc);

  if (gtk_widget_get_has_window (widget))
    {
      alloc.x = 0;
      alloc.y = 0;
    }

  if (priv->animation != NULL)
    {
      /*
       * If we are currently animating, we want to ensure that our background
       * is drawn at the size of the natural allocation. Otherwise borders
       * will be shown in improper locations.
       */
      if (IS_HORIZONTAL (priv->transition_type))
        {
          if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
            alloc.x -= (priv->nat_req.width - alloc.width);
          alloc.width = priv->nat_req.width;
        }
      else
        {
          if (priv->transition_type == DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN)
            alloc.y -= (priv->nat_req.height - alloc.height);
          alloc.height = priv->nat_req.height;
        }
    }

  style_context = gtk_widget_get_style_context (widget);
  state = gtk_style_context_get_state (style_context);

  gtk_style_context_get_margin (style_context, state, &margin);
  dzl_gtk_allocation_subtract_border (&alloc, &margin);

  gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);

  child = gtk_bin_get_child (GTK_BIN (widget));
  if (child != NULL)
    gtk_container_propagate_draw (GTK_CONTAINER (widget), child, cr);

  gtk_render_frame (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);

  return FALSE;
}

static void
dzl_dock_revealer_realize (GtkWidget *widget)
{
  DzlDockRevealer *self = (DzlDockRevealer *)widget;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  GdkWindowAttr attributes = { 0 };
  GdkWindow *parent;
  GtkAllocation alloc;
  gint attributes_mask = 0;

  g_assert (DZL_IS_DOCK_REVEALER (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.visual = gtk_widget_get_visual (GTK_WIDGET (self));
  attributes.x = alloc.x;
  attributes.y = alloc.y;
  attributes.width = alloc.width;
  attributes.height = alloc.height;
  attributes.event_mask = 0;

  attributes_mask = (GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);

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

static void
dzl_dock_revealer_destroy (GtkWidget *widget)
{
  DzlDockRevealer *self = (DzlDockRevealer *)widget;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_clear_object (&priv->adjustment);
  dzl_clear_weak_pointer (&priv->animation);

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

static void
dzl_dock_revealer_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  DzlDockRevealer *self = DZL_DOCK_REVEALER (object);

  switch (prop_id)
    {
    case PROP_CHILD_REVEALED:
      g_value_set_boolean (value, dzl_dock_revealer_get_child_revealed (self));
      break;

    case PROP_POSITION:
      g_value_set_int (value, dzl_dock_revealer_get_position (self));
      break;

    case PROP_POSITION_SET:
      g_value_set_boolean (value, dzl_dock_revealer_get_position_set (self));
      break;

    case PROP_REVEAL_CHILD:
      g_value_set_boolean (value, dzl_dock_revealer_get_reveal_child (self));
      break;

    case PROP_TRANSITION_DURATION:
      g_value_set_uint (value, dzl_dock_revealer_get_transition_duration (self));
      break;

    case PROP_TRANSITION_TYPE:
      g_value_set_enum (value, dzl_dock_revealer_get_transition_type (self));
      break;

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

static void
dzl_dock_revealer_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  DzlDockRevealer *self = DZL_DOCK_REVEALER (object);

  switch (prop_id)
    {
    case PROP_REVEAL_CHILD:
      dzl_dock_revealer_set_reveal_child (self, g_value_get_boolean (value));
      break;

    case PROP_POSITION:
      dzl_dock_revealer_set_position (self, g_value_get_int (value));
      break;

    case PROP_POSITION_SET:
      dzl_dock_revealer_set_position_set (self, g_value_get_boolean (value));
      break;

    case PROP_TRANSITION_DURATION:
      dzl_dock_revealer_set_transition_duration (self, g_value_get_uint (value));
      break;

    case PROP_TRANSITION_TYPE:
      dzl_dock_revealer_set_transition_type (self, g_value_get_enum (value));
      break;

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

static void
dzl_dock_revealer_class_init (DzlDockRevealerClass *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 = dzl_dock_revealer_get_property;
  object_class->set_property = dzl_dock_revealer_set_property;

  widget_class->destroy = dzl_dock_revealer_destroy;
  widget_class->get_preferred_width = dzl_dock_revealer_get_preferred_width;
  widget_class->get_preferred_height = dzl_dock_revealer_get_preferred_height;
  widget_class->realize = dzl_dock_revealer_realize;
  widget_class->size_allocate = dzl_dock_revealer_size_allocate;
  widget_class->draw = dzl_dock_revealer_draw;

  container_class->add = dzl_dock_revealer_add;

  properties [PROP_CHILD_REVEALED] =
    g_param_spec_boolean ("child-revealed",
                          "Child Revealed",
                          "If the child is fully revealed",
                          TRUE,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  properties [PROP_POSITION] =
    g_param_spec_int ("position",
                      "Position",
                      "Position",
                      0,
                      G_MAXINT,
                      0,
                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_POSITION_SET] =
    g_param_spec_boolean ("position-set",
                          "Position Set",
                          "If the position has been set",
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_REVEAL_CHILD] =
    g_param_spec_boolean ("reveal-child",
                          "Reveal Child",
                          "If the child should be revealed",
                          FALSE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_TRANSITION_DURATION] =
    g_param_spec_uint ("transition-duration",
                       "Transition Duration",
                       "Length of duration in milliseconds",
                       0,
                       G_MAXUINT,
                       0,
                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_TRANSITION_TYPE] =
    g_param_spec_enum ("transition-type",
                       "Transition Type",
                       "Transition Type",
                       DZL_TYPE_DOCK_REVEALER_TRANSITION_TYPE,
                       DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE,
                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
dzl_dock_revealer_init (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  gtk_widget_set_has_window (GTK_WIDGET (self), TRUE);

  priv->reveal_child = FALSE;
  priv->child_revealed = FALSE;

  priv->transition_duration = 0;

  priv->adjustment = g_object_new (GTK_TYPE_ADJUSTMENT,
                                   "lower", 0.0,
                                   "upper", 1.0,
                                   "value", 0.0,
                                   NULL);

  g_signal_connect_object (priv->adjustment,
                           "value-changed",
                           G_CALLBACK (gtk_widget_queue_resize),
                           self,
                           G_CONNECT_SWAPPED);
}

GType
dzl_dock_revealer_transition_type_get_type (void)
{
  static GType type_id;

  if (g_once_init_enter (&type_id))
    {
      GType _type_id;
      static const GEnumValue values[] = {
        { DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE,
          "DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE",
          "none" },
        { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT,
          "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT",
          "slide-right" },
        { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT,
          "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT",
          "slide-left" },
        { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP,
          "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_UP",
          "slide-up" },
        { DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN,
          "DZL_DOCK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN",
          "slide-down" },
        { 0 }
      };

      _type_id = g_enum_register_static ("DzlDockRevealerTransitionType", values);

      g_once_init_leave (&type_id, _type_id);
    }

  return type_id;
}

static void
dzl_dock_revealer_animate_to_position_done (gpointer user_data)
{
  g_autoptr(DzlDockRevealer) self = user_data;
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_assert (DZL_DOCK_REVEALER (self));

  if (priv->adjustment != NULL)
    {
      gboolean child_revealed;

      child_revealed = (priv->position_tmp > 0);
      if (priv->child_revealed != child_revealed)
        {
          GtkWidget *child = gtk_bin_get_child (GTK_BIN (self));

          priv->child_revealed = child_revealed;
          gtk_widget_set_child_visible (GTK_WIDGET (child), child_revealed);
        }

      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD_REVEALED]);

      gtk_adjustment_set_value (priv->adjustment, child_revealed ? 1.0 : 0.0);
      priv->position = priv->position_tmp;
      gtk_widget_queue_resize (GTK_WIDGET (self));
    }
}

void
dzl_dock_revealer_animate_to_position (DzlDockRevealer *self,
                                       gint             position,
                                       guint            transition_duration)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);
  gdouble current_position;
  gdouble value;

  g_return_if_fail (DZL_IS_DOCK_REVEALER (self));

  if (transition_duration == 0)
    transition_duration = dzl_dock_revealer_calculate_duration (self);

  current_position = priv->position;
  if (current_position != position)
    {
      DzlAnimation *animation;
      GtkWidget *child;

      priv->reveal_child = (position > 0);
      priv->position_tmp = position;
      if (!priv->position_set)
        {
          priv->position_set = TRUE;
          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION_SET]);
        }

      if (current_position < position)
        {
          value = 1.0;
          if (current_position > 0)
            {
              priv->position = position;
              gtk_adjustment_set_value (priv->adjustment, current_position / position);
            }
        }
      else
        value = position / current_position;

      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSITION]);

      child = gtk_bin_get_child (GTK_BIN (self));
      if (child != NULL)
        {
          if (priv->animation != NULL)
            {
              dzl_animation_stop (priv->animation);
              dzl_clear_weak_pointer (&priv->animation);
            }

          gtk_widget_set_child_visible (child, TRUE);
          animation = dzl_object_animate_full (priv->adjustment,
                                               DZL_ANIMATION_EASE_IN_OUT_CUBIC,
                                               transition_duration,
                                               gtk_widget_get_frame_clock (GTK_WIDGET (self)),
                                               dzl_dock_revealer_animate_to_position_done,
                                               g_object_ref (self),
                                               "value", value,
                                               NULL);

          dzl_set_weak_pointer (&priv->animation, animation);
        }

      if ((priv->reveal_child && position == 0) || (!priv->reveal_child && position != 0))
        g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REVEAL_CHILD]);
    }
}

/**
 * dzl_dock_revealer_is_animating:
 * @self: a #DzlDockRevealer
 *
 * This is a helper to check if the revealer is animating. You probably don't
 * want to poll this function. Connect to notify::child-revealed or
 * notify::reveal-child instead.
 *
 * Returns: %TRUE if an animation is in progress.
 */
gboolean
dzl_dock_revealer_is_animating (DzlDockRevealer *self)
{
  DzlDockRevealerPrivate *priv = dzl_dock_revealer_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_DOCK_REVEALER (self), FALSE);

  return (priv->animation != NULL);
}
