/* tidy-finger-scroll.c: Finger scrolling container actor
 *
 * Copyright (C) 2008 OpenedHand
 *
 * This library 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 of the License, or (at your option) any later version.
 *
 * This library 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 library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Written by: Chris Lord <chris@openedhand.com>
 */

#include "tidy-finger-scroll.h"
#include "tidy-enum-types.h"
#include "tidy-marshal.h"
#include "tidy-scroll-bar.h"
#include "tidy-scrollable.h"
#include "tidy-scroll-view.h"
#include <clutter/clutter.h>
#include <math.h>

G_DEFINE_TYPE (TidyFingerScroll, tidy_finger_scroll, TIDY_TYPE_SCROLL_VIEW)

#define FINGER_SCROLL_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), \
                                  TIDY_TYPE_FINGER_SCROLL, \
                                  TidyFingerScrollPrivate))

typedef struct {
  /* Units to store the origin of a click when scrolling */
  ClutterUnit x;
  ClutterUnit y;
  GTimeVal    time;
} TidyFingerScrollMotion;

struct _TidyFingerScrollPrivate
{
  /* Scroll mode */
  TidyFingerScrollMode mode;
  
  GArray                *motion_buffer;
  guint                  last_motion;
  
  /* Variables for storing acceleration information for kinetic mode */
  ClutterTimeline       *deceleration_timeline;
  ClutterUnit            dx;
  ClutterUnit            dy;
  ClutterFixed           decel_rate;
  
  /* Variables to fade in/out scroll-bars */
  ClutterEffectTemplate *template;
  ClutterTimeline       *hscroll_timeline;
  ClutterTimeline       *vscroll_timeline;
};

enum {
  PROP_MODE = 1,
  PROP_DECEL_RATE,
  PROP_BUFFER,
};

static void
tidy_finger_scroll_get_property (GObject *object, guint property_id,
                                 GValue *value, GParamSpec *pspec)
{
  TidyFingerScrollPrivate *priv = TIDY_FINGER_SCROLL (object)->priv;
  
  switch (property_id)
    {
    case PROP_MODE :
      g_value_set_enum (value, priv->mode);
      break;
    case PROP_DECEL_RATE :
      g_value_set_double (value, CLUTTER_FIXED_TO_FLOAT (priv->decel_rate));
      break;
    case PROP_BUFFER :
      g_value_set_uint (value, priv->motion_buffer->len);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

static void
tidy_finger_scroll_set_property (GObject *object, guint property_id,
                                 const GValue *value, GParamSpec *pspec)
{
  TidyFingerScrollPrivate *priv = TIDY_FINGER_SCROLL (object)->priv;
  
  switch (property_id)
    {
    case PROP_MODE :
      priv->mode = g_value_get_enum (value);
      g_object_notify (object, "mode");
      break;
    case PROP_DECEL_RATE :
      priv->decel_rate = CLUTTER_FLOAT_TO_FIXED (g_value_get_double (value));
      g_object_notify (object, "decel-rate");
      break;
    case PROP_BUFFER :
      g_array_set_size (priv->motion_buffer, g_value_get_uint (value));
      g_object_notify (object, "motion-buffer");
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

static void
tidy_finger_scroll_dispose (GObject *object)
{
  TidyFingerScrollPrivate *priv = TIDY_FINGER_SCROLL (object)->priv;

  if (priv->deceleration_timeline)
    {
      clutter_timeline_stop (priv->deceleration_timeline);
      g_object_unref (priv->deceleration_timeline);
      priv->deceleration_timeline = NULL;
    }
  
  if (priv->hscroll_timeline)
    {
      clutter_timeline_stop (priv->hscroll_timeline);
      g_object_unref (priv->hscroll_timeline);
      priv->hscroll_timeline = NULL;
    }
  
  if (priv->vscroll_timeline)
    {
      clutter_timeline_stop (priv->vscroll_timeline);
      g_object_unref (priv->vscroll_timeline);
      priv->vscroll_timeline = NULL;
    }
  
  if (priv->template)
    {
      g_object_unref (priv->template);
      priv->template = NULL;
    }

  G_OBJECT_CLASS (tidy_finger_scroll_parent_class)->dispose (object);
}

static void
tidy_finger_scroll_finalize (GObject *object)
{
  TidyFingerScrollPrivate *priv = TIDY_FINGER_SCROLL (object)->priv;

  g_array_free (priv->motion_buffer, TRUE);
  
  G_OBJECT_CLASS (tidy_finger_scroll_parent_class)->finalize (object);
}


static void
tidy_finger_scroll_class_init (TidyFingerScrollClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  g_type_class_add_private (klass, sizeof (TidyFingerScrollPrivate));

  object_class->get_property = tidy_finger_scroll_get_property;
  object_class->set_property = tidy_finger_scroll_set_property;
  object_class->dispose = tidy_finger_scroll_dispose;
  object_class->finalize = tidy_finger_scroll_finalize;

  g_object_class_install_property (object_class,
                                   PROP_MODE,
                                   g_param_spec_enum ("mode",
                                                      "TidyFingerScrollMode",
                                                      "Scrolling mode",
                                                      TIDY_TYPE_FINGER_SCROLL_MODE,
                                                      TIDY_FINGER_SCROLL_MODE_PUSH,
                                                      G_PARAM_READWRITE));

  g_object_class_install_property (object_class,
                                   PROP_DECEL_RATE,
                                   g_param_spec_double ("decel-rate",
                                                        "Deceleration rate",
                                                        "Rate at which the view "
                                                        "will decelerate in "
                                                        "kinetic mode.",
                                                        CLUTTER_FIXED_TO_FLOAT (CFX_ONE + CFX_MIN),
                                                        CLUTTER_FIXED_TO_FLOAT (CFX_MAX),
                                                        1.1,
                                                        G_PARAM_READWRITE));

  g_object_class_install_property (object_class,
                                   PROP_BUFFER,
                                   g_param_spec_uint ("motion-buffer",
                                                      "Motion buffer",
                                                      "Amount of motion "
                                                      "events to buffer",
                                                      1, G_MAXUINT, 3,
                                                      G_PARAM_READWRITE));
}

static gboolean
motion_event_cb (ClutterActor *actor,
                 ClutterMotionEvent *event,
                 TidyFingerScroll *scroll)
{
  ClutterUnit x, y;
  
  TidyFingerScrollPrivate *priv = scroll->priv;

  if (clutter_actor_transform_stage_point (actor,
                                           CLUTTER_UNITS_FROM_DEVICE(event->x),
                                           CLUTTER_UNITS_FROM_DEVICE(event->y),
                                           &x, &y))
    {
      TidyFingerScrollMotion *motion;
      ClutterActor *child =
        tidy_scroll_view_get_child (TIDY_SCROLL_VIEW(scroll));
      
      if (child)
        {
          ClutterFixed dx, dy;
          TidyAdjustment *hadjust, *vadjust;
          
          tidy_scrollable_get_adjustments (TIDY_SCROLLABLE (child),
                                           &hadjust,
                                           &vadjust);

          motion = &g_array_index (priv->motion_buffer,
                                   TidyFingerScrollMotion, priv->last_motion);
          dx = CLUTTER_UNITS_TO_FIXED(motion->x - x) +
               tidy_adjustment_get_valuex (hadjust);
          dy = CLUTTER_UNITS_TO_FIXED(motion->y - y) +
               tidy_adjustment_get_valuex (vadjust);
          
          tidy_adjustment_set_valuex (hadjust, dx);
          tidy_adjustment_set_valuex (vadjust, dy);
        }
      
      priv->last_motion ++;
      if (priv->last_motion == priv->motion_buffer->len)
        {
          priv->motion_buffer = g_array_remove_index (priv->motion_buffer, 0);
          g_array_set_size (priv->motion_buffer, priv->last_motion);
          priv->last_motion --;
        }
      
      motion = &g_array_index (priv->motion_buffer,
                               TidyFingerScrollMotion, priv->last_motion);
      motion->x = x;
      motion->y = y;
      g_get_current_time (&motion->time);
    }

  return TRUE;
}

static void
hfade_complete_cb (ClutterActor *scrollbar, TidyFingerScroll *scroll)
{
  scroll->priv->hscroll_timeline = NULL;
}

static void
vfade_complete_cb (ClutterActor *scrollbar, TidyFingerScroll *scroll)
{
  scroll->priv->vscroll_timeline = NULL;
}

static void
show_scrollbars (TidyFingerScroll *scroll, gboolean show)
{
  ClutterActor *hscroll, *vscroll;
  TidyFingerScrollPrivate *priv = scroll->priv;
  
  /* Stop current timelines */
  if (priv->hscroll_timeline)
    {
      clutter_timeline_stop (priv->hscroll_timeline);
      g_object_unref (priv->hscroll_timeline);
    }
  
  if (priv->vscroll_timeline)
    {
      clutter_timeline_stop (priv->vscroll_timeline);
      g_object_unref (priv->vscroll_timeline);
    }

  hscroll = tidy_scroll_view_get_hscroll_bar (TIDY_SCROLL_VIEW (scroll));
  vscroll = tidy_scroll_view_get_vscroll_bar (TIDY_SCROLL_VIEW (scroll));
  
  /* Create new ones */
  if (!CLUTTER_ACTOR_IS_REACTIVE (hscroll))
    priv->hscroll_timeline = clutter_effect_fade (
                               priv->template,
                               hscroll,
                               show ? 0xFF : 0x00,
                               (ClutterEffectCompleteFunc)hfade_complete_cb,
                               scroll);

  if (!CLUTTER_ACTOR_IS_REACTIVE (vscroll))
    priv->vscroll_timeline = clutter_effect_fade (
                               priv->template,
                               vscroll,
                               show ? 0xFF : 0x00,
                               (ClutterEffectCompleteFunc)vfade_complete_cb,
                               scroll);
}

static void
clamp_adjustments (TidyFingerScroll *scroll)
{
  ClutterActor *child = tidy_scroll_view_get_child (TIDY_SCROLL_VIEW (scroll));
  
  if (child)
    {
      guint fps, n_frames;
      TidyAdjustment *hadj, *vadj;
      gboolean snap;
      
      tidy_scrollable_get_adjustments (TIDY_SCROLLABLE (child),
                                       &hadj, &vadj);
      
      /* FIXME: Hard-coded value here */
      fps = clutter_get_default_frame_rate ();
      n_frames = fps / 6;
      
      snap = TRUE;
      if (tidy_adjustment_get_elastic (hadj))
        snap = !tidy_adjustment_clamp (hadj, TRUE, n_frames, fps);
      
      /* Snap to the nearest step increment on hadjustment */
      if (snap)
        {
          gdouble d, value, lower, step_increment;
          
          tidy_adjustment_get_values (hadj, &value, &lower, NULL,
                                      &step_increment, NULL, NULL);
          d = (rint ((value - lower) / step_increment) *
              step_increment) + lower;
          tidy_adjustment_set_value (hadj, d);
        }
      
      snap = TRUE;
      if (tidy_adjustment_get_elastic (vadj))
        snap = !tidy_adjustment_clamp (vadj, TRUE, n_frames, fps);

      /* Snap to the nearest step increment on vadjustment */
      if (snap)
        {
          gdouble d, value, lower, step_increment;
          
          tidy_adjustment_get_values (vadj, &value, &lower, NULL,
                                      &step_increment, NULL, NULL);
          d = (rint ((value - lower) / step_increment) *
              step_increment) + lower;
          tidy_adjustment_set_value (vadj, d);
        }
    }
}

static void
deceleration_completed_cb (ClutterTimeline *timeline,
                           TidyFingerScroll *scroll)
{
  show_scrollbars (scroll, FALSE);
  clamp_adjustments (scroll);
  g_object_unref (timeline);
  scroll->priv->deceleration_timeline = NULL;
}

static void
deceleration_new_frame_cb (ClutterTimeline *timeline,
                           gint frame_num,
                           TidyFingerScroll *scroll)
{
  TidyFingerScrollPrivate *priv = scroll->priv;
  ClutterActor *child = tidy_scroll_view_get_child (TIDY_SCROLL_VIEW(scroll));
  
  if (child)
    {
      ClutterFixed value, lower, upper, page_size;
      TidyAdjustment *hadjust, *vadjust;
      gint i;
      gboolean stop = TRUE;
      
      tidy_scrollable_get_adjustments (TIDY_SCROLLABLE (child),
                                       &hadjust,
                                       &vadjust);
      
      for (i = 0; i < clutter_timeline_get_delta (timeline, NULL); i++)
        {
          tidy_adjustment_set_valuex (hadjust,
                                      priv->dx +
                                        tidy_adjustment_get_valuex (hadjust));
          tidy_adjustment_set_valuex (vadjust,
                                      priv->dy +
                                        tidy_adjustment_get_valuex (vadjust));
          priv->dx = clutter_qdivx (priv->dx, priv->decel_rate);
          priv->dy = clutter_qdivx (priv->dy, priv->decel_rate);
        }
      
      /* Check if we've hit the upper or lower bounds and stop the timeline */
      tidy_adjustment_get_valuesx (hadjust, &value, &lower, &upper,
                                   NULL, NULL, &page_size);
      if (((priv->dx > 0) && (value < upper - page_size)) ||
          ((priv->dx < 0) && (value > lower)))
        stop = FALSE;
      
      if (stop)
        {
          tidy_adjustment_get_valuesx (vadjust, &value, &lower, &upper,
                                       NULL, NULL, &page_size);
          if (((priv->dy > 0) && (value < upper - page_size)) ||
              ((priv->dy < 0) && (value > lower)))
            stop = FALSE;
        }
      
      if (stop)
        {
          clutter_timeline_stop (timeline);
          deceleration_completed_cb (timeline, scroll);
        }
    }
}

static gboolean
button_release_event_cb (ClutterActor *actor,
                         ClutterButtonEvent *event,
                         TidyFingerScroll *scroll)
{
  TidyFingerScrollPrivate *priv = scroll->priv;
  ClutterActor *child = tidy_scroll_view_get_child (TIDY_SCROLL_VIEW(scroll));
  gboolean decelerating = FALSE;

  if (event->button != 1)
    return FALSE;
  
  g_signal_handlers_disconnect_by_func (actor,
                                        motion_event_cb,
                                        scroll);
  g_signal_handlers_disconnect_by_func (actor,
                                        button_release_event_cb,
                                        scroll);
  
  clutter_ungrab_pointer ();

  if ((priv->mode == TIDY_FINGER_SCROLL_MODE_KINETIC) && (child))
    {
      ClutterUnit x, y;
      
      if (clutter_actor_transform_stage_point (actor,
                                               CLUTTER_UNITS_FROM_DEVICE(event->x),
                                               CLUTTER_UNITS_FROM_DEVICE(event->y),
                                               &x, &y))
        {
          ClutterUnit frac, x_origin, y_origin;
          GTimeVal release_time, motion_time;
          TidyAdjustment *hadjust, *vadjust;
          glong time_diff;
          gint i;
          
          /* Get time delta */
          g_get_current_time (&release_time);
          
          /* Get average position/time of last x mouse events */
          priv->last_motion ++;
          x_origin = y_origin = 0;
          motion_time = (GTimeVal){ 0, 0 };
          for (i = 0; i < priv->last_motion; i++)
            {
              TidyFingerScrollMotion *motion =
                &g_array_index (priv->motion_buffer, TidyFingerScrollMotion, i);
              
              /* FIXME: This doesn't guard against overflows - Should
               *        either fix that, or calculate the correct maximum
               *        value for the buffer size
               */
              x_origin += motion->x;
              y_origin += motion->y;
              motion_time.tv_sec += motion->time.tv_sec;
              motion_time.tv_usec += motion->time.tv_usec;
            }
          x_origin = CLUTTER_UNITS_FROM_FIXED (
            clutter_qdivx (CLUTTER_UNITS_TO_FIXED (x_origin),
                           CLUTTER_INT_TO_FIXED (priv->last_motion)));
          y_origin = CLUTTER_UNITS_FROM_FIXED (
            clutter_qdivx (CLUTTER_UNITS_TO_FIXED (y_origin),
                           CLUTTER_INT_TO_FIXED (priv->last_motion)));
          motion_time.tv_sec /= priv->last_motion;
          motion_time.tv_usec /= priv->last_motion;
          
          if (motion_time.tv_sec == release_time.tv_sec)
            time_diff = release_time.tv_usec - motion_time.tv_usec;
          else
            time_diff = release_time.tv_usec +
                        (G_USEC_PER_SEC - motion_time.tv_usec);
          
          /* Work out the fraction of 1/60th of a second that has elapsed */
          frac = clutter_qdivx (CLUTTER_FLOAT_TO_FIXED (time_diff/1000.0),
                                CLUTTER_FLOAT_TO_FIXED (1000.0/60.0));
          
          /* See how many units to move in 1/60th of a second */
          priv->dx = CLUTTER_UNITS_FROM_FIXED(clutter_qdivx (
                     CLUTTER_UNITS_TO_FIXED(x_origin - x), frac));
          priv->dy = CLUTTER_UNITS_FROM_FIXED(clutter_qdivx (
                     CLUTTER_UNITS_TO_FIXED(y_origin - y), frac));
          
          /* Get adjustments to do step-increment snapping */
          tidy_scrollable_get_adjustments (TIDY_SCROLLABLE (child),
                                           &hadjust,
                                           &vadjust);

          if (ABS(CLUTTER_UNITS_TO_INT(priv->dx)) > 1 ||
              ABS(CLUTTER_UNITS_TO_INT(priv->dy)) > 1)
            {
              gdouble value, lower, step_increment, d, a, x, y, n;
              
              /* TODO: Convert this all to fixed point? */
              
              /* We want n, where x / y^n < z,
               * x = Distance to move per frame
               * y = Deceleration rate
               * z = maximum distance from target
               *
               * Rearrange to n = log (x / z) / log (y)
               * To simplify, z = 1, so n = log (x) / log (y)
               *
               * As z = 1, this will cause stops to be slightly abrupt - 
               * add a constant 15 frames to compensate.
               */
              x = CLUTTER_FIXED_TO_FLOAT (MAX(ABS(priv->dx), ABS(priv->dy)));
              y = CLUTTER_FIXED_TO_FLOAT (priv->decel_rate);
              n = logf (x) / logf (y) + 15.0;

              /* Now we have n, adjust dx/dy so that we finish on a step
               * boundary.
               *
               * Distance moved, using the above variable names:
               *
               * d = x + x/y + x/y^2 + ... + x/y^n
               *
               * Using geometric series,
               *
               * d = (1 - 1/y^(n+1))/(1 - 1/y)*x
               * 
               * Let a = (1 - 1/y^(n+1))/(1 - 1/y),
               *
               * d = a * x
               *
               * Find d and find its nearest page boundary, then solve for x
               *
               * x = d / a
               */
              
              /* Get adjustments, work out y^n */
              a = (1.0 - 1.0 / pow (y, n + 1)) / (1.0 - 1.0 / y);

              /* Solving for dx */
              d = a * CLUTTER_UNITS_TO_FLOAT (priv->dx);
              tidy_adjustment_get_values (hadjust, &value, &lower, NULL,
                                          &step_increment, NULL, NULL);
              d = ((rint (((value + d) - lower) / step_increment) *
                    step_increment) + lower) - value;
              priv->dx = CLUTTER_UNITS_FROM_FLOAT (d / a);

              /* Solving for dy */
              d = a * CLUTTER_UNITS_TO_FLOAT (priv->dy);
              tidy_adjustment_get_values (vadjust, &value, &lower, NULL,
                                          &step_increment, NULL, NULL);
              d = ((rint (((value + d) - lower) / step_increment) *
                    step_increment) + lower) - value;
              priv->dy = CLUTTER_UNITS_FROM_FLOAT (d / a);
              
              priv->deceleration_timeline = clutter_timeline_new ((gint)n, 60);
            }
          else
            {
              gdouble value, lower, step_increment, d, a, y;
              
              /* Start a short effects timeline to snap to the nearest step 
               * boundary (see equations above)
               */
              y = CLUTTER_FIXED_TO_FLOAT (priv->decel_rate);
              a = (1.0 - 1.0 / pow (y, 4 + 1)) / (1.0 - 1.0 / y);
              
              tidy_adjustment_get_values (hadjust, &value, &lower, NULL,
                                          &step_increment, NULL, NULL);
              d = ((rint ((value - lower) / step_increment) *
                    step_increment) + lower) - value;
              priv->dx = CLUTTER_UNITS_FROM_FLOAT (d / a);
              
              tidy_adjustment_get_values (vadjust, &value, &lower, NULL,
                                          &step_increment, NULL, NULL);
              d = ((rint ((value - lower) / step_increment) *
                    step_increment) + lower) - value;
              priv->dy = CLUTTER_UNITS_FROM_FLOAT (d / a);
              
              priv->deceleration_timeline = clutter_timeline_new (4, 60);
            }

          g_signal_connect (priv->deceleration_timeline, "new_frame",
                            G_CALLBACK (deceleration_new_frame_cb), scroll);
          g_signal_connect (priv->deceleration_timeline, "completed",
                            G_CALLBACK (deceleration_completed_cb), scroll);
          clutter_timeline_start (priv->deceleration_timeline);
          decelerating = TRUE;
        }
    }

  /* Reset motion event buffer */
  priv->last_motion = 0;
  
  if (!decelerating)
    {
      show_scrollbars (scroll, FALSE);
      clamp_adjustments (scroll);
    }
  
  /* Pass through events to children.
   * FIXME: this probably breaks click-count.
   */
  clutter_event_put ((ClutterEvent *)event);
  
  return TRUE;
}

static gboolean
after_event_cb (TidyFingerScroll *scroll)
{
  /* Check the pointer grab - if something else has grabbed it - for example,
   * a scroll-bar or some such, don't do our funky stuff.
   */
  if (clutter_get_pointer_grab () != CLUTTER_ACTOR (scroll))
    {
      g_signal_handlers_disconnect_by_func (scroll,
                                            motion_event_cb,
                                            scroll);
      g_signal_handlers_disconnect_by_func (scroll,
                                            button_release_event_cb,
                                            scroll);
    }
  
  return FALSE;
}

static gboolean
captured_event_cb (ClutterActor     *actor,
                   ClutterEvent     *event,
                   TidyFingerScroll *scroll)
{
  TidyFingerScrollPrivate *priv = scroll->priv;
  
  if (event->type == CLUTTER_BUTTON_PRESS)
    {
      TidyFingerScrollMotion *motion;
      ClutterButtonEvent *bevent = (ClutterButtonEvent *)event;
      
      /* Reset motion buffer */
      priv->last_motion = 0;
      motion = &g_array_index (priv->motion_buffer, TidyFingerScrollMotion, 0);
      
      if ((bevent->button == 1) &&
          (clutter_actor_transform_stage_point (actor,
                                           CLUTTER_UNITS_FROM_DEVICE(bevent->x),
                                           CLUTTER_UNITS_FROM_DEVICE(bevent->y),
                                           &motion->x, &motion->y)))
        {
          g_get_current_time (&motion->time);
          
          if (priv->deceleration_timeline)
            {
              clutter_timeline_stop (priv->deceleration_timeline);
              g_object_unref (priv->deceleration_timeline);
              priv->deceleration_timeline = NULL;
            }
          
          /* Fade in scroll-bars */
          show_scrollbars (scroll, TRUE);
          
          clutter_grab_pointer (actor);
          
          /* Add a high priority idle to check the grab after the event
           * emission is finished.
           */
          g_idle_add_full (G_PRIORITY_HIGH_IDLE,
                           (GSourceFunc)after_event_cb,
                           scroll,
                           NULL);
          
          g_signal_connect (actor,
                            "motion-event",
                            G_CALLBACK (motion_event_cb),
                            scroll);
          g_signal_connect (actor,
                            "button-release-event",
                            G_CALLBACK (button_release_event_cb),
                            scroll);
        }
    }
  
  return FALSE;
}

static void
hscroll_notify_reactive_cb (ClutterActor     *bar,
                            GParamSpec       *pspec,
                            TidyFingerScroll *scroll)
{
  TidyFingerScrollPrivate *priv;
  
  priv = scroll->priv;
  if (CLUTTER_ACTOR_IS_REACTIVE (bar))
    {
      if (priv->hscroll_timeline)
        {
          clutter_timeline_stop (priv->hscroll_timeline);
          g_object_unref (priv->hscroll_timeline);
          priv->hscroll_timeline = NULL;
        }
      clutter_actor_set_opacity (bar, 0xFF);
    }
}

static void
vscroll_notify_reactive_cb (ClutterActor     *bar,
                            GParamSpec       *pspec,
                            TidyFingerScroll *scroll)
{
  TidyFingerScrollPrivate *priv;
  
  priv = scroll->priv;
  if (CLUTTER_ACTOR_IS_REACTIVE (bar))
    {
      if (priv->vscroll_timeline)
        {
          clutter_timeline_stop (priv->vscroll_timeline);
          g_object_unref (priv->vscroll_timeline);
          priv->vscroll_timeline = NULL;
        }
      clutter_actor_set_opacity (bar, 0xFF);
    }
}

static void
tidy_finger_scroll_init (TidyFingerScroll *self)
{
  ClutterActor *scrollbar;
  TidyFingerScrollPrivate *priv = self->priv = FINGER_SCROLL_PRIVATE (self);
  
  priv->motion_buffer = g_array_sized_new (FALSE, TRUE,
                                           sizeof (TidyFingerScrollMotion), 3);
  g_array_set_size (priv->motion_buffer, 3);
  priv->decel_rate = CLUTTER_FLOAT_TO_FIXED (1.1f);
  
  clutter_actor_set_reactive (CLUTTER_ACTOR (self), TRUE);
  g_signal_connect (CLUTTER_ACTOR (self),
                    "captured-event",
                    G_CALLBACK (captured_event_cb),
                    self);
  
  /* Make the scroll-bars unreactive and set their opacity - we'll fade them 
   * in/out when we scroll.
   * Also, hook onto notify::reactive and don't fade in/out when the bars are 
   * set reactive (which you might want to do if you want finger-scrolling 
   * *and* a scroll bar.
   */
  scrollbar = tidy_scroll_view_get_hscroll_bar (TIDY_SCROLL_VIEW (self));
  clutter_actor_set_reactive (scrollbar, FALSE);
  clutter_actor_set_opacity (scrollbar, 0x00);
  g_signal_connect (scrollbar, "notify::reactive",
                    G_CALLBACK (hscroll_notify_reactive_cb), self);

  scrollbar = tidy_scroll_view_get_vscroll_bar (TIDY_SCROLL_VIEW (self));
  clutter_actor_set_reactive (scrollbar, FALSE);
  clutter_actor_set_opacity (scrollbar, 0x00);
  g_signal_connect (scrollbar, "notify::reactive",
                    G_CALLBACK (vscroll_notify_reactive_cb), self);
  
  priv->template = clutter_effect_template_new_for_duration (250,
                                                CLUTTER_ALPHA_RAMP_INC);
}

ClutterActor *
tidy_finger_scroll_new (TidyFingerScrollMode mode)
{
  return CLUTTER_ACTOR (g_object_new (TIDY_TYPE_FINGER_SCROLL,
                                      "mode", mode, NULL));
}

void
tidy_finger_scroll_stop (TidyFingerScroll *scroll)
{
  TidyFingerScrollPrivate *priv;
  
  g_return_if_fail (TIDY_IS_FINGER_SCROLL (scroll));
  
  priv = scroll->priv;

  if (priv->deceleration_timeline)
    {
      clutter_timeline_stop (priv->deceleration_timeline);
      g_object_unref (priv->deceleration_timeline);
      priv->deceleration_timeline = NULL;
    }
}
