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