From: Christian Dywan Date: Sun, 27 Sep 2009 21:48:08 +0000 (+0200) Subject: Implement drag (finger) and kinetic scrolling with KatzeScrolled X-Git-Url: https://spindle.queued.net/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=96093d906754a986ef9b6dae5eae0ba8411653d4;p=midori Implement drag (finger) and kinetic scrolling with KatzeScrolled The feature is primarily interesting for mobile devices and by default enabled with gtk-touchscreen-mode. --- diff --git a/katze/katze-scrolled.c b/katze/katze-scrolled.c new file mode 100644 index 00000000..b7596f5a --- /dev/null +++ b/katze/katze-scrolled.c @@ -0,0 +1,1086 @@ +/* + Copyright (C) 2007 Henrik Hedberg + Copyright (C) 2009 Nadav Wiener + Copyright (C) 2009 Christian Dywan + + 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.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "katze-scrolled.h" + +#define DEFAULT_INTERVAL 50 +#define DEFAULT_DECELERATION 0.7 +#define DEFAULT_DRAGGING_STOPPED_DELAY 100 + +/** + * SECTION:katze-scrolled + * @short_description: Implements drag scrolling and kinetic scrolling + * @see_also: #GtkScrolledWindow + * + * A scrolled window derived from #GtkScrolledWindow that implements + * drag scrolling and kinetic scrolling. Can be used as a drop-in replacement + * for the existing #GtkScrolledWindow. + * + * If a direct child of the #KatzeScrolled has its own window + * (InputOnly is enough for events), it is automatically activated when added + * as a child. All motion events in that area will be used to scroll. + * + * If some descendant widgets capture button press, button release and/ or + * motion nofity events, the user can not scroll the area by pressing those + * widgets (unless the widget is activated). #GtkButton is a typical example + * of that. Usually that is the desired behaviour. + * + * Any widget can be registered to provide pointer events for the + * #KatzeScrolled by using the + * #katze_scrolled_activate_scrolling function. + * + **/ + +G_DEFINE_TYPE (KatzeScrolled, katze_scrolled, GTK_TYPE_SCROLLED_WINDOW); + +enum +{ + PROP_0, + + PROP_DRAG_SCROLLING, + PROP_KINETIC_SCROLLING +}; + +static void +katze_scrolled_set_property (GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec); + +static void +katze_scrolled_get_property (GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec); + +static void +katze_scrolled_dispose (GObject* object); + +static void +katze_scrolled_activate_scrolling (KatzeScrolled* scrolled, + GtkWidget* widget); + +static void +katze_scrolled_set_drag_scrolling (KatzeScrolled* scrolled, + gboolean drag_scrolling); + +struct _KatzeScrolledPrivate +{ + /* Settings */ + guint interval; + gdouble deceleration; + gboolean drag_scrolling; + gboolean kinetic_scrolling; + guint32 dragging_stopped_delay; + gboolean scrolling_hints; + + /* Temporary variables */ + gboolean dragged; + gboolean press_received; + GdkWindow* synthetic_crossing_event_window; + + /* Disabling twice happening scrolling adjustment */ + GtkAdjustment* hadjustment; + GtkWidget* viewport; + + /* Motion scrolling */ + gint start_x; + gint start_y; + gint previous_x; + gint previous_y; + gint farest_x; + gint farest_y; + guint32 start_time; + guint32 previous_time; + guint32 farest_time; + gboolean going_right; + gboolean going_down; + + /* Kinetic scrolling */ + guint scrolling_timeout_id; + gdouble horizontal_speed; + gdouble vertical_speed; + gdouble horizontal_deceleration; + gdouble vertical_deceleration; + + /* Internal scrollbars */ + GdkWindow* vertical_scrollbar_window; + GdkWindow* horizontal_scrollbar_window; + gint vertical_scrollbar_size; + gint horizontal_scrollbar_size; + guint hide_scrollbars_timeout_id; + GdkGC* hilight_gc; + GdkGC* shadow_gc; +}; + +typedef struct _KatzeScrolledState KatzeScrolledState; +typedef gboolean (*KatzeScrolledEventHandler)(GdkEvent* event, + KatzeScrolledState* state, + gpointer user_data); + +typedef struct +{ + KatzeScrolledEventHandler event_handler; + gpointer user_data; +} EventHandlerData; + +struct _KatzeScrolledState +{ + GList* current_event_handler; +}; + +static GList* event_handlers = NULL; + +static void +katze_scrolled_event_handler_func (GdkEvent* event, + gpointer data); + +static void +katze_scrolled_event_handler_append (KatzeScrolledEventHandler event_handler, + gpointer user_data) +{ + EventHandlerData* data; + + data = g_new0 (EventHandlerData, 1); + data->event_handler = event_handler; + data->user_data = user_data; + event_handlers = g_list_append (event_handlers, data); + + gdk_event_handler_set ((GdkEventFunc)katze_scrolled_event_handler_func, NULL, NULL); +} + +static void +katze_scrolled_event_handler_next (GdkEvent* event, + KatzeScrolledState* state) +{ + EventHandlerData* data; + gboolean stop_propagating; + + state->current_event_handler = g_list_next (state->current_event_handler); + if (state->current_event_handler) + { + data = (EventHandlerData*)state->current_event_handler->data; + stop_propagating = data->event_handler (event, state, data->user_data); + if (!stop_propagating && state->current_event_handler) + g_critical ("%s: handler returned FALSE without calling %s first", + G_STRFUNC, G_STRFUNC); + } + else + gtk_main_do_event (event); +} + +static void +katze_scrolled_event_handler_func (GdkEvent* event, + gpointer user_data) +{ + KatzeScrolledState* state; + EventHandlerData* data; + gboolean stop_propagating; + + state = g_new0 (KatzeScrolledState, 1); + state->current_event_handler = g_list_first (event_handlers); + if (state->current_event_handler) + { + data = (EventHandlerData*)state->current_event_handler->data; + stop_propagating = data->event_handler (event, state, data->user_data); + if (!stop_propagating && state->current_event_handler) + g_critical ("%s: handler returned FALSE without calling %s first", + G_STRFUNC, "katze_scrolled_event_handler_next"); + } + else + gtk_main_do_event (event); + + g_free (state); +} + +static GdkWindow* current_gdk_window; +static KatzeScrolled* current_scrolled_window; +static GtkWidget* current_widget; +static gboolean synthetized_crossing_event; + +static GTree* activated_widgets; + +static gint +compare_pointers (gconstpointer a, + gconstpointer b) +{ + return a - b; +} + +static void +disable_hadjustment (KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + GtkAdjustment* hadjustment; + GtkWidget* viewport; + + if ((hadjustment = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (scrolled))) + && priv->hadjustment != hadjustment) + { + priv->hadjustment = hadjustment; + priv->viewport = NULL; + viewport = GTK_WIDGET (scrolled); + while (GTK_IS_BIN (viewport)) + { + viewport = gtk_bin_get_child (GTK_BIN (viewport)); + if (GTK_IS_VIEWPORT (viewport)) + { + priv->viewport = viewport; + break; + } + } + } + g_signal_handlers_block_matched (priv->hadjustment, G_SIGNAL_MATCH_DATA, + 0, 0, 0, 0, priv->viewport); +} + +static void +enable_hadjustment (KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + + g_signal_handlers_unblock_matched (priv->hadjustment, G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, priv->viewport); +} + +static gboolean +on_expose_event (GtkWidget* widget, + GdkEventExpose* event, + KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + gboolean ret = FALSE; + + if (GTK_WIDGET_DRAWABLE (widget)) + { + if (event->window == priv->horizontal_scrollbar_window) + { + if (priv->horizontal_scrollbar_size) + { + gdk_draw_line (event->window, priv->hilight_gc, 0, 0, priv->horizontal_scrollbar_size - 1, 0); + gdk_draw_line (event->window, priv->hilight_gc, 0, 1, 0, 9); + gdk_draw_line (event->window, priv->shadow_gc, priv->horizontal_scrollbar_size - 1, 1, priv->horizontal_scrollbar_size - 1, 9); + gdk_draw_line (event->window, priv->shadow_gc, 0, 9, priv->horizontal_scrollbar_size - 1, 9); + } + + ret = TRUE; + } + else if (event->window == priv->vertical_scrollbar_window) + { + if (priv->vertical_scrollbar_size) + { + gdk_draw_line (event->window, priv->hilight_gc, 0, 0, 9, 0); + gdk_draw_line (event->window, priv->hilight_gc, 0, 1, 0, priv->vertical_scrollbar_size - 1); + gdk_draw_line (event->window, priv->shadow_gc, 9, 1, 9, priv->vertical_scrollbar_size - 1); + gdk_draw_line (event->window, priv->shadow_gc, 0, priv->vertical_scrollbar_size - 1, 9, priv->vertical_scrollbar_size - 1); + } + + ret = TRUE; + } + } + + return ret; +} + +static gboolean +adjust_scrollbar (KatzeScrolled* scrolled, + GdkWindow* scrollbar_window, + GtkAdjustment* adjustment, + gint* previous_size, + gboolean horizontal) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + GtkWidget* widget = GTK_WIDGET (scrolled); + gint x, y; + gint size; + double position; + GtkWidget* window; + + if (adjustment->page_size >= adjustment->upper - adjustment->lower) + { + *previous_size = 0; + return FALSE; + } + + size = ((double)adjustment->page_size) / (adjustment->upper - adjustment->lower) * (horizontal + ? widget->allocation.height : widget->allocation.width); + if (size != *previous_size) + { + *previous_size = size; + if (horizontal) + { + gdk_window_resize (scrollbar_window, 10, size); + gdk_window_clear (scrollbar_window); + gdk_draw_line (scrollbar_window, priv->hilight_gc, 0, 0, 9, 0); + gdk_draw_line (scrollbar_window, priv->hilight_gc, 0, 1, 0, size - 1); + gdk_draw_line (scrollbar_window, priv->shadow_gc, 9, 1, 9, size - 1); + gdk_draw_line (scrollbar_window, priv->shadow_gc, 0, size - 1, 9, size - 1); + } + else + { + gdk_window_resize (scrollbar_window, size, 10); + gdk_window_clear (scrollbar_window); + gdk_draw_line (scrollbar_window, priv->hilight_gc, 0, 0, size - 1, 0); + gdk_draw_line (scrollbar_window, priv->hilight_gc, 0, 1, 0, 9); + gdk_draw_line (scrollbar_window, priv->shadow_gc, size - 1, 1, size - 1, 9); + gdk_draw_line (scrollbar_window, priv->shadow_gc, 0, 9, size - 1, 9); + } + } + + position = (adjustment->value - adjustment->lower) / (adjustment->upper - adjustment->lower); + window = gtk_widget_get_toplevel (widget); + if (horizontal) + { + gtk_widget_translate_coordinates (widget, window, + widget->allocation.width - 20, position * widget->allocation.height, &x, &y); + gdk_window_move (scrollbar_window, x, y); + } + else + { + gtk_widget_translate_coordinates (widget, window, + position * widget->allocation.width, widget->allocation.height - 20, &x, &y); + gdk_window_move (scrollbar_window, x, y); + } + + return TRUE; +} + +static gboolean +hide_scrollbars_timeout (gpointer data) +{ + KatzeScrolled* scrolled = KATZE_SCROLLED (data); + KatzeScrolledPrivate* priv = scrolled->priv; + + gdk_threads_enter (); + gdk_window_hide (priv->vertical_scrollbar_window); + gdk_window_hide (priv->horizontal_scrollbar_window); + + priv->hide_scrollbars_timeout_id = 0; + gdk_threads_leave (); + + return FALSE; +} + +static gdouble +calculate_timeout_scroll_values (gdouble old_value, + gdouble upper_limit, + gdouble* scrolling_speed_pointer, + gdouble deceleration, + gdouble* other_deceleration, + gdouble normal_deceleration) +{ + gdouble new_value = old_value; + + if (*scrolling_speed_pointer > deceleration || + *scrolling_speed_pointer < -deceleration) + { + if (old_value + *scrolling_speed_pointer <= 0.0) + { + new_value = -1.0; + *scrolling_speed_pointer = 0.0; + *other_deceleration = normal_deceleration; + } + else if (old_value + *scrolling_speed_pointer >= upper_limit) + { + new_value = upper_limit; + *scrolling_speed_pointer = 0.0; + *other_deceleration = normal_deceleration; + } + else + new_value = old_value + *scrolling_speed_pointer; + if (*scrolling_speed_pointer > deceleration) + *scrolling_speed_pointer -= deceleration; + else if (*scrolling_speed_pointer < -deceleration) + *scrolling_speed_pointer += deceleration; + } + + return new_value; +} + +static void +do_timeout_scroll (KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + GtkScrolledWindow* gtk_scrolled = GTK_SCROLLED_WINDOW (scrolled); + GtkAdjustment* hadjustment; + GtkAdjustment* vadjustment; + gdouble hvalue; + gdouble vvalue; + + hadjustment = gtk_scrolled_window_get_hadjustment (gtk_scrolled); + vadjustment = gtk_scrolled_window_get_vadjustment (gtk_scrolled); + hvalue = calculate_timeout_scroll_values (hadjustment->value, + hadjustment->upper - hadjustment->page_size, + &priv->horizontal_speed, + priv->horizontal_deceleration, + &priv->vertical_deceleration, + priv->deceleration); + vvalue = calculate_timeout_scroll_values (vadjustment->value, + vadjustment->upper - vadjustment->page_size, + &priv->vertical_speed, + priv->vertical_deceleration, + &priv->horizontal_deceleration, + priv->deceleration); + if (vvalue != vadjustment->value) + { + if (hvalue != hadjustment->value) + { + disable_hadjustment (scrolled); + gtk_adjustment_set_value (hadjustment, hvalue); + enable_hadjustment (scrolled); + } + gtk_adjustment_set_value (vadjustment, vvalue); + } + else if (hvalue != hadjustment->value) + gtk_adjustment_set_value (hadjustment, hvalue); + + adjust_scrollbar (scrolled, priv->horizontal_scrollbar_window, + gtk_scrolled_window_get_hadjustment (gtk_scrolled), + &priv->horizontal_scrollbar_size, FALSE); + adjust_scrollbar (scrolled, priv->vertical_scrollbar_window, + gtk_scrolled_window_get_vadjustment (gtk_scrolled), + &priv->vertical_scrollbar_size, TRUE); +} + +static gboolean +timeout_scroll (gpointer data) +{ + gboolean ret = TRUE; + KatzeScrolled* scrolled = KATZE_SCROLLED (data); + KatzeScrolledPrivate* priv = scrolled->priv; + + gdk_threads_enter (); + do_timeout_scroll (scrolled); + + if (priv->vertical_speed < priv->deceleration && + priv->vertical_speed > -priv->deceleration && + priv->horizontal_speed < priv->deceleration && + priv->horizontal_speed > -priv->deceleration) + { + priv->scrolling_timeout_id = 0; + if (!priv->hide_scrollbars_timeout_id) + priv->hide_scrollbars_timeout_id = g_timeout_add (500, + hide_scrollbars_timeout, scrolled); + + ret = FALSE; + } + gdk_threads_leave (); + + return ret; +} + +static gdouble +calculate_motion_scroll_values (gdouble old_value, + gdouble upper_limit, + gint current_coordinate, + gint previous_coordinate) +{ + gdouble new_value = old_value; + gint movement; + + movement = current_coordinate - previous_coordinate; + + if (old_value - movement < upper_limit) + new_value = old_value - movement; + else + new_value = upper_limit; + + return new_value; +} + +static void +do_motion_scroll (KatzeScrolled* scrolled, + GtkWidget* widget, + gint x, + gint y, + guint32 timestamp) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + GtkAdjustment* hadjustment; + GtkAdjustment* vadjustment; + gdouble hvalue; + gdouble vvalue; + + if (priv->dragged || gtk_drag_check_threshold (widget, priv->start_x, priv->start_y, x, y)) + { + if (timestamp - priv->previous_time > priv->dragging_stopped_delay || !priv->dragged) + { + priv->dragged = TRUE; + priv->going_right = priv->start_x < x; + priv->going_down = priv->start_y < y; + priv->start_x = priv->farest_x = x; + priv->start_y = priv->farest_y = y; + priv->start_time = priv->farest_time = timestamp; + } + else + { + if ((priv->going_right && x > priv->farest_x) + || (!priv->going_right && x < priv->farest_x)) + { + priv->farest_x = x; + priv->farest_time = timestamp; + } + if ((priv->going_down && y > priv->farest_y) + || (!priv->going_down && y < priv->farest_y)) + { + priv->farest_y = y; + priv->farest_time = timestamp; + } + if (gtk_drag_check_threshold (widget, priv->farest_x, priv->farest_y, x, y)) + { + priv->start_x = priv->farest_x; + priv->farest_x = x; + priv->start_y = priv->farest_y; + priv->farest_y = y; + priv->start_time = priv->farest_time; + priv->farest_time = timestamp; + priv->going_right = priv->start_x < x; + priv->going_down = priv->start_y < y; + } + } + + hadjustment = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (scrolled)); + vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled)); + hvalue = calculate_motion_scroll_values (hadjustment->value, + hadjustment->upper - hadjustment->page_size, x, priv->previous_x); + vvalue = calculate_motion_scroll_values (vadjustment->value, + vadjustment->upper - vadjustment->page_size, y, priv->previous_y); + if (vvalue != vadjustment->value) + { + if (hvalue != hadjustment->value) + { + disable_hadjustment (scrolled); + gtk_adjustment_set_value (hadjustment, hvalue); + enable_hadjustment (scrolled); + } + gtk_adjustment_set_value (vadjustment, vvalue); + } + else if (hvalue != hadjustment->value) + gtk_adjustment_set_value (hadjustment, hvalue); + } + + priv->previous_y = y; + priv->previous_x = x; + priv->previous_time = timestamp; + + adjust_scrollbar (scrolled, priv->horizontal_scrollbar_window, + gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (scrolled)), + &priv->horizontal_scrollbar_size, FALSE); + adjust_scrollbar (scrolled, priv->vertical_scrollbar_window, + gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled)), + &priv->vertical_scrollbar_size, TRUE); +} + +static gboolean +button_press_event (GtkWidget* widget, + GdkEventButton* event, + KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + gint x; + gint y; + GdkModifierType mask; + + if (!priv->drag_scrolling) + return FALSE; + + if (event->button != 1) + return FALSE; + + priv->press_received = TRUE; + + if (event->time - priv->previous_time < priv->dragging_stopped_delay && + gtk_drag_check_threshold (widget, priv->previous_x, priv->previous_y, x, y)) + { + if (priv->scrolling_timeout_id) + { + g_source_remove (priv->scrolling_timeout_id); + priv->scrolling_timeout_id = 0; + } + gdk_window_get_pointer (GTK_WIDGET (scrolled)->window, &x, &y, &mask); + /* do_motion_scroll (scrolled, widget, x, y, event->time); */ + } + else + { + if (priv->scrolling_timeout_id) + { + g_source_remove (priv->scrolling_timeout_id); + priv->scrolling_timeout_id = 0; + priv->previous_time = 0; + } + else + { + priv->dragged = FALSE; + priv->previous_time = event->time; + } + gdk_window_get_pointer (GTK_WIDGET (scrolled)->window, &x, &y, &mask); + priv->start_x = priv->previous_x = priv->farest_x = x; + priv->start_y = priv->previous_y = priv->farest_y = y; + priv->start_time = event->time; + } + + if (priv->scrolling_hints && !GTK_SCROLLED_WINDOW (scrolled)->hscrollbar_visible && + adjust_scrollbar (scrolled, priv->horizontal_scrollbar_window, + gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (scrolled)), + &priv->horizontal_scrollbar_size, FALSE)) + { + gdk_window_raise (priv->horizontal_scrollbar_window); + gdk_window_show (priv->horizontal_scrollbar_window); + } + if (priv->scrolling_hints && !GTK_SCROLLED_WINDOW (scrolled)->vscrollbar_visible && + adjust_scrollbar (scrolled, priv->vertical_scrollbar_window, + gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled)), + &priv->vertical_scrollbar_size, TRUE)) + { + gdk_window_raise (priv->vertical_scrollbar_window); + gdk_window_show (priv->vertical_scrollbar_window); + } + if (priv->hide_scrollbars_timeout_id) + { + g_source_remove (priv->hide_scrollbars_timeout_id); + priv->hide_scrollbars_timeout_id = 0; + } + + return FALSE; +} + +static gboolean +button_release_event (GtkWidget* widget, + GdkEventButton* event, + KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + gint x; + gint y; + GdkModifierType mask; + + gdk_window_get_pointer (GTK_WIDGET (scrolled)->window, &x, &y, &mask); + if (priv->press_received && + gtk_drag_check_threshold (widget, priv->start_x, priv->start_y, x, y)) { + priv->dragged = TRUE; + } + + if (priv->press_received && priv->kinetic_scrolling && + event->time - priv->previous_time < priv->dragging_stopped_delay) { + priv->vertical_speed = (gdouble)(priv->start_y - y) / (event->time - priv->start_time) * priv->interval; + priv->horizontal_speed = (gdouble)(priv->start_x - x) / (event->time - priv->start_time) * priv->interval; + if (ABS (priv->vertical_speed) > ABS (priv->horizontal_speed)) { + priv->vertical_deceleration = priv->deceleration; + priv->horizontal_deceleration = priv->deceleration * ABS (priv->horizontal_speed / priv->vertical_speed); + } else { + priv->horizontal_deceleration = priv->deceleration; + priv->vertical_deceleration = priv->deceleration * ABS (priv->vertical_speed / priv->horizontal_speed); + } + priv->scrolling_timeout_id = g_timeout_add (priv->interval, timeout_scroll, scrolled); + + do_timeout_scroll (scrolled); + } + else if (!priv->hide_scrollbars_timeout_id) { + priv->hide_scrollbars_timeout_id = g_timeout_add (500, hide_scrollbars_timeout, scrolled); + } + priv->previous_x = x; + priv->previous_y = y; + priv->previous_time = event->time; + + priv->press_received = FALSE; + + return FALSE; +} + +static gboolean +motion_notify_event (GtkWidget* widget, + GdkEventMotion* event, + KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + gint x; + gint y; + GdkModifierType mask; + + if (priv->press_received) + { + gdk_window_get_pointer (GTK_WIDGET (scrolled)->window, &x, &y, &mask); + do_motion_scroll (scrolled, widget, x, y, event->time); + } + + return FALSE; +} + +static gboolean +katze_scrolled_event_handler (GdkEvent* event, + KatzeScrolledState* state, + gpointer user_data) +{ + gboolean stop_propagating; + GdkEventCrossing crossing; + + stop_propagating = FALSE; + + if (event->type == GDK_BUTTON_PRESS) + { + gdk_window_get_user_data (event->button.window, (gpointer)¤t_widget); + + if ((current_scrolled_window = g_tree_lookup (activated_widgets, current_widget))) + { + current_gdk_window = event->button.window; + stop_propagating = button_press_event (current_widget, &event->button, current_scrolled_window); + } + else + current_gdk_window = NULL; + } + else if (event->any.window == current_gdk_window) + { + if (event->type == GDK_MOTION_NOTIFY) + { + if (current_scrolled_window->priv->dragged) + stop_propagating = motion_notify_event (current_widget, &event->motion, current_scrolled_window); + else + { + stop_propagating = motion_notify_event (current_widget, &event->motion, current_scrolled_window); + if (current_scrolled_window->priv->dragged) + { + crossing.type = GDK_LEAVE_NOTIFY; + crossing.window = event->motion.window; + crossing.send_event = event->motion.send_event; + crossing.subwindow = GTK_WIDGET (current_scrolled_window)->window; + crossing.time = event->motion.time; + crossing.x = event->motion.x; + crossing.y = event->motion.y; + crossing.x_root = event->motion.x_root; + crossing.y_root = event->motion.y_root; + crossing.mode = GDK_CROSSING_GRAB; + crossing.detail = GDK_NOTIFY_ANCESTOR; + crossing.focus = TRUE; + crossing.state = event->motion.state; + + gtk_main_do_event ((GdkEvent*)&crossing); + synthetized_crossing_event = TRUE; + } + } + } + else if ((event->type == GDK_ENTER_NOTIFY || event->type == GDK_LEAVE_NOTIFY) && + synthetized_crossing_event) + stop_propagating = TRUE; + else if (event->type == GDK_BUTTON_RELEASE) + stop_propagating = button_release_event (current_widget, &event->button, current_scrolled_window); + } + + + if (!stop_propagating) + katze_scrolled_event_handler_next (event, state); + + if (event->type == GDK_BUTTON_RELEASE && event->button.window == current_gdk_window) + { + crossing.type = GDK_ENTER_NOTIFY; + crossing.window = event->button.window; + crossing.send_event = event->button.send_event; + crossing.subwindow = GTK_WIDGET (current_scrolled_window)->window; + crossing.time = event->button.time; + crossing.x = event->button.x; + crossing.y = event->button.y; + crossing.x_root = event->button.x_root; + crossing.y_root = event->button.y_root; + crossing.mode = GDK_CROSSING_UNGRAB; + crossing.detail = GDK_NOTIFY_ANCESTOR; + crossing.focus = TRUE; + crossing.state = event->button.state; + + gtk_main_do_event ((GdkEvent*)&crossing); + synthetized_crossing_event = FALSE; + } + + return stop_propagating; +} + +static void +katze_scrolled_add (GtkContainer* container, + GtkWidget* widget) +{ + katze_scrolled_activate_scrolling (KATZE_SCROLLED (container), widget); + + (*GTK_CONTAINER_CLASS (katze_scrolled_parent_class)->add) (container, widget); +} + +static void +katze_scrolled_realize (GtkWidget* widget) +{ + KatzeScrolled* scrolled = KATZE_SCROLLED (widget); + KatzeScrolledPrivate* priv = scrolled->priv; + gboolean drag_scrolling; + GtkSettings* settings = gtk_widget_get_settings (widget); + GtkPolicyType policy; + GdkWindowAttr attr; + GdkColor color; + + (*GTK_WIDGET_CLASS (katze_scrolled_parent_class)->realize) (widget); + + g_object_get (settings, "gtk-touchscreen-mode", &drag_scrolling, NULL); + policy = drag_scrolling ? GTK_POLICY_NEVER : GTK_POLICY_AUTOMATIC; + g_object_set (scrolled, "drag-scrolling", drag_scrolling, + "hscrollbar-policy", policy, "vscrollbar-policy", policy, NULL); + + widget->window = g_object_ref (gtk_widget_get_parent_window (widget)); + + attr.height = attr.width = 10; + attr.event_mask = GDK_EXPOSURE_MASK; + attr.wclass = GDK_INPUT_OUTPUT; + attr.window_type = GDK_WINDOW_CHILD; + attr.override_redirect = TRUE; + priv->vertical_scrollbar_window = gdk_window_new (widget->window, &attr, 0); + priv->horizontal_scrollbar_window = gdk_window_new (widget->window, &attr, 0); + + gdk_window_set_user_data (priv->vertical_scrollbar_window, widget); + gdk_window_set_user_data (priv->horizontal_scrollbar_window, widget); + g_signal_connect (widget, "expose-event", + G_CALLBACK (on_expose_event), scrolled); + + color.red = color.green = color.blue = 0x9999; + gdk_rgb_find_color (gdk_colormap_get_system (), &color); + gdk_window_set_background (priv->vertical_scrollbar_window, &color); + gdk_window_set_background (priv->horizontal_scrollbar_window, &color); + + priv->hilight_gc = gdk_gc_new (widget->window); + color.red = color.green = color.blue = 0xcccc; + gdk_gc_set_rgb_fg_color (priv->hilight_gc, &color); + priv->shadow_gc = gdk_gc_new (widget->window); + color.red = color.green = color.blue = 0x6666; + gdk_gc_set_rgb_fg_color (priv->shadow_gc, &color); + + GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED); +} + +static void +katze_scrolled_dispose (GObject* object) +{ + KatzeScrolled* scrolled = KATZE_SCROLLED (object); + KatzeScrolledPrivate* priv = scrolled->priv; + + if (priv->scrolling_timeout_id) + { + g_source_remove (priv->scrolling_timeout_id); + priv->scrolling_timeout_id = 0; + } + if (priv->hide_scrollbars_timeout_id) + { + g_source_remove (priv->hide_scrollbars_timeout_id); + priv->hide_scrollbars_timeout_id = 0; + } + + (*G_OBJECT_CLASS (katze_scrolled_parent_class)->dispose) (object); +} + +static void +katze_scrolled_class_init (KatzeScrolledClass* class) +{ + GObjectClass* gobject_class = G_OBJECT_CLASS (class); + GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (class); + GtkContainerClass* container_class = GTK_CONTAINER_CLASS (class); + GParamFlags flags = G_PARAM_READWRITE | G_PARAM_CONSTRUCT; + + gobject_class->set_property = katze_scrolled_set_property; + gobject_class->get_property = katze_scrolled_get_property; + gobject_class->dispose = katze_scrolled_dispose; + + widget_class->realize = katze_scrolled_realize; + + container_class->add = katze_scrolled_add; + + /** + * KatzeScrolled:drag-scrolling: + * + * Whether the widget can be scrolled by dragging its contents. + * + * If "gtk-touchscreen-mode" is enabled, drag scrolling is + * automatically enabled. + * + * Since: 0.2.0 + */ + g_object_class_install_property (gobject_class, + PROP_DRAG_SCROLLING, + g_param_spec_boolean ( + "drag-scrolling", + "Drag Scrolling", + "Whether the widget can be scrolled by dragging its contents", + FALSE, + flags)); + + /** + * KatzeScrolled:kinetic-scrolling: + * + * Whether drag scrolling is kinetic, that is releasing the + * pointer keeps the contents scrolling further relative to + * the speed with which they were dragged. + * + * Since: 0.2.0 + */ + g_object_class_install_property (gobject_class, + PROP_KINETIC_SCROLLING, + g_param_spec_boolean ( + "kinetic-scrolling", + "Kinetic Scrolling", + "Whether drag scrolling is kinetic", + TRUE, + flags)); + + activated_widgets = g_tree_new ((GCompareFunc)compare_pointers); + current_gdk_window = NULL; + + katze_scrolled_event_handler_append (katze_scrolled_event_handler, NULL); + + g_type_class_add_private (class, sizeof (KatzeScrolledPrivate)); +} + +static void +katze_scrolled_init (KatzeScrolled* scrolled) +{ + KatzeScrolledPrivate* priv; + + scrolled->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE ((scrolled), + KATZE_TYPE_SCROLLED, KatzeScrolledPrivate); + + priv->interval = DEFAULT_INTERVAL; + priv->deceleration = DEFAULT_DECELERATION; + priv->drag_scrolling = FALSE; + priv->kinetic_scrolling = TRUE; + priv->dragging_stopped_delay = DEFAULT_DRAGGING_STOPPED_DELAY; +} + +static void +katze_scrolled_set_property (GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* pspec) +{ + KatzeScrolled* scrolled = KATZE_SCROLLED (object); + + switch (prop_id) + { + case PROP_DRAG_SCROLLING: + katze_scrolled_set_drag_scrolling (scrolled, g_value_get_boolean (value)); + break; + case PROP_KINETIC_SCROLLING: + scrolled->priv->kinetic_scrolling = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +katze_scrolled_get_property (GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec) +{ + KatzeScrolled* scrolled = KATZE_SCROLLED (object); + + switch (prop_id) + { + case PROP_DRAG_SCROLLING: + g_value_set_boolean (value, scrolled->priv->drag_scrolling); + break; + case PROP_KINETIC_SCROLLING: + g_value_set_boolean (value, scrolled->priv->kinetic_scrolling); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * katze_scrolled_new: + * @hadjustment: a horizontal #GtkAdjustment, or %NULL + * @vadjustment: a vertical #GtkAdjustment, or %NULL + * + * Creates a new #KatzeScrolled. + * + * Since: 0.2.0 + **/ + +GtkWidget* +katze_scrolled_new (GtkAdjustment* hadjustment, + GtkAdjustment* vadjustment) +{ + if (hadjustment) + g_return_val_if_fail (GTK_IS_ADJUSTMENT (hadjustment), NULL); + if (vadjustment) + g_return_val_if_fail (GTK_IS_ADJUSTMENT (vadjustment), NULL); + + return gtk_widget_new (KATZE_TYPE_SCROLLED, + "hadjustment", hadjustment, + "vadjustment", vadjustment, NULL); +} + +/** + * katze_scrolled_activate_scrolling: + * @scrolled: a #KatzeScrolled + * @widget: a #GtkWidget of which area is made active event source for + * drag and kinetic scrolling. + * + * Activates the widget so that pointer motion events inside the widget are + * used to scroll the #KatzeScrolled. The widget can be a child of the + * #KatzeScrolled or even a separate widget ("touchpad" style). + * + * The direct child of the #KatzeScrolled (typically #GtkViewport) is + * activated automatically when added. This function has to be used if indirect + * descendant widgets are stopping propagation of the button press and release + * as well as motion events (for example GtkButton is doing so) but scrolling + * should be possible inside their area too. + * + * This function adds #GDK_BUTTON_PRESS_MASK, #GDK_BUTTON_RELEASE_MASK, + * #GDK_POINTER_MOTION_MASK, and #GDK_MOTION_HINT_MAKS into the widgets event mask. + */ + +static void +katze_scrolled_activate_scrolling (KatzeScrolled* scrolled, + GtkWidget* widget) +{ + gtk_widget_add_events (widget, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); + g_tree_insert (activated_widgets, widget, scrolled); +} + +static void +katze_scrolled_set_drag_scrolling (KatzeScrolled* scrolled, + gboolean drag_scrolling) +{ + KatzeScrolledPrivate* priv = scrolled->priv; + + if (priv->drag_scrolling && !drag_scrolling) + { + if (priv->scrolling_timeout_id) + { + g_source_remove (priv->scrolling_timeout_id); + priv->scrolling_timeout_id = 0; + priv->previous_time = 0; + } + + gdk_window_hide (priv->vertical_scrollbar_window); + gdk_window_hide (priv->horizontal_scrollbar_window); + if (priv->hide_scrollbars_timeout_id) + { + g_source_remove (priv->hide_scrollbars_timeout_id); + priv->hide_scrollbars_timeout_id = 0; + } + + priv->press_received = FALSE; + } + + priv->drag_scrolling = drag_scrolling; +} diff --git a/katze/katze-scrolled.h b/katze/katze-scrolled.h new file mode 100644 index 00000000..e8787f17 --- /dev/null +++ b/katze/katze-scrolled.h @@ -0,0 +1,59 @@ +/* + Copyright (C) 2007 Henrik Hedberg + Copyright (C) 2009 Nadav Wiener + Copyright (C) 2009 Christian Dywan + + 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.1 of the License, or (at your option) any later version. + + See the file COPYING for the full license text. + */ + +#ifndef KATZE_SCROLLED_H +#define KATZE_SCROLLED_H + +#include + +G_BEGIN_DECLS + +#define KATZE_TYPE_SCROLLED (katze_scrolled_get_type()) +#define KATZE_SCROLLED(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KATZE_TYPE_SCROLLED, KatzeScrolled)) +#define KATZE_SCROLLED_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), KATZE_TYPE_SCROLLED, KatzeScrolledClass)) +#define KATZE_IS_SCROLLED(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KATZE_TYPE_SCROLLED)) +#define KATZE_IS_SCROLLED_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), KATZE_TYPE_SCROLLED)) +#define KATZE_SCROLLED_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), KATZE_TYPE_SCROLLED, KatzeScrolledClass)) + +typedef struct _KatzeScrolled KatzeScrolled; +typedef struct _KatzeScrolledClass KatzeScrolledClass; +typedef struct _KatzeScrolledPrivate KatzeScrolledPrivate; + +struct _KatzeScrolled +{ + GtkScrolledWindow parent; + + KatzeScrolledPrivate* priv; +}; + +struct _KatzeScrolledClass +{ + GtkScrolledWindowClass parent; + + /* Padding for future expansion */ + void (*_katze_reserved1) (void); + void (*_katze_reserved2) (void); + void (*_katze_reserved3) (void); + void (*_katze_reserved4) (void); +}; + +GType +katze_scrolled_get_type (void); + +GtkWidget* +katze_scrolled_new (GtkAdjustment* hadjustment, + GtkAdjustment* vadjustment); + +G_END_DECLS + +#endif /* __KATZE_SCROLLED_H__ */ diff --git a/katze/katze.h b/katze/katze.h index 8dd286bf..ab85d27b 100644 --- a/katze/katze.h +++ b/katze/katze.h @@ -21,5 +21,6 @@ #include "katze-arrayaction.h" #include "katze-separatoraction.h" #include "katze-net.h" +#include "katze-scrolled.h" #endif /* __KATZE_H__ */ diff --git a/midori/midori-browser.c b/midori/midori-browser.c index 1b7bc767..4e7b6b8c 100644 --- a/midori/midori-browser.c +++ b/midori/midori-browser.c @@ -1554,9 +1554,6 @@ _midori_browser_add_tab (MidoriBrowser* browser, KatzeItem* item; guint n; - gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (view), - GTK_POLICY_AUTOMATIC, - GTK_POLICY_AUTOMATIC); GTK_WIDGET_SET_FLAGS (view, GTK_CAN_FOCUS); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (view), GTK_SHADOW_ETCHED_IN); diff --git a/midori/midori-preferences.c b/midori/midori-preferences.c index 34bfa793..82c19c45 100644 --- a/midori/midori-preferences.c +++ b/midori/midori-preferences.c @@ -315,6 +315,9 @@ midori_preferences_set_settings (MidoriPreferences* preferences, GtkWidget* header; GtkWindow* parent; const gchar* icon_name; + #if WEBKIT_CHECK_VERSION (1, 1, 15) + GtkSettings* gtk_settings; + #endif GtkSizeGroup* sizegroup; GtkWidget* toolbar; GtkWidget* toolbutton; @@ -341,6 +344,9 @@ midori_preferences_set_settings (MidoriPreferences* preferences, gtk_window_get_title (GTK_WINDOW (preferences))))) gtk_box_pack_start (GTK_BOX (GTK_DIALOG (preferences)->vbox), header, FALSE, FALSE, 0); + #if WEBKIT_CHECK_VERSION (1, 1, 15) + gtk_settings = parent ? gtk_widget_get_settings (GTK_WIDGET (parent)) : NULL; + #endif preferences->notebook = gtk_notebook_new (); gtk_container_set_border_width (GTK_CONTAINER (preferences->notebook), 6); @@ -487,9 +493,14 @@ midori_preferences_set_settings (MidoriPreferences* preferences, gtk_widget_set_tooltip_text (button, _("Load and display images automatically")); INDENTED_ADD (button, 0, 1, 0, 1); #if WEBKIT_CHECK_VERSION (1, 1, 15) - button = katze_property_proxy (settings, "auto-shrink-images", NULL); - gtk_button_set_label (GTK_BUTTON (button), _("Shrink images automatically")); - gtk_widget_set_tooltip_text (button, _("Automatically shrink standalone images to fit")); + if (katze_object_get_boolean (gtk_settings, "gtk-touchscreen-mode")) + button = katze_property_proxy (settings, "kinetic-scrolling", NULL); + else + { + button = katze_property_proxy (settings, "auto-shrink-images", NULL); + gtk_button_set_label (GTK_BUTTON (button), _("Shrink images automatically")); + gtk_widget_set_tooltip_text (button, _("Automatically shrink standalone images to fit")); + } #else button = katze_property_proxy (settings, "middle-click-opens-selection", NULL); #endif diff --git a/midori/midori-view.c b/midori/midori-view.c index 942f9d78..f3c17ba0 100644 --- a/midori/midori-view.c +++ b/midori/midori-view.c @@ -59,7 +59,7 @@ midori_view_item_meta_data_changed (KatzeItem* item, struct _MidoriView { - GtkScrolledWindow parent_instance; + KatzeScrolled parent_instance; gchar* uri; gchar* title; @@ -104,10 +104,10 @@ struct _MidoriView struct _MidoriViewClass { - GtkScrolledWindowClass parent_class; + KatzeScrolledClass parent_class; }; -G_DEFINE_TYPE (MidoriView, midori_view, GTK_TYPE_SCROLLED_WINDOW) +G_DEFINE_TYPE (MidoriView, midori_view, KATZE_TYPE_SCROLLED); GType midori_load_status_get_type (void) @@ -1525,6 +1525,9 @@ webkit_web_view_populate_popup_cb (WebKitWebView* web_view, katze_object_assign (view->hit_test, webkit_web_view_get_hit_test_result (web_view, &event)); context = katze_object_get_int (view->hit_test, "context"); + /* Ensure view->link_uri is correct. */ + katze_assign (view->link_uri, + katze_object_get_string (view->hit_test, "link-uri")); has_selection = context & WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION; /* Ensure view->selected_text */ midori_view_has_selection (view); @@ -1533,6 +1536,8 @@ webkit_web_view_populate_popup_cb (WebKitWebView* web_view, is_media = context & WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA; is_document = !view->link_uri && !has_selection && !is_image && !is_media; #else + /* There is no guarantee view->link_uri is correct in case + gtk-touchscreen-mode is enabled, nothing we can do. */ has_selection = midori_view_has_selection (view); is_document = !view->link_uri && !has_selection; @@ -2329,13 +2334,14 @@ midori_view_new (KatzeNet* net) static void _midori_view_update_settings (MidoriView* view) { - gboolean zoom_text_and_images; + gboolean zoom_text_and_images, kinetic_scrolling; g_object_get (view->settings, "speed-dial-in-new-tabs", &view->speed_dial_in_new_tabs, "download-manager", &view->download_manager, "news-aggregator", &view->news_aggregator, "zoom-text-and-images", &zoom_text_and_images, + "kinetic-scrolling", &kinetic_scrolling, "close-buttons-on-tabs", &view->close_buttons_on_tabs, "open-new-pages-in", &view->open_new_pages_in, "ask-for-destination-folder", &view->ask_for_destination_folder, @@ -2345,8 +2351,9 @@ _midori_view_update_settings (MidoriView* view) NULL); if (view->web_view) - g_object_set (view->web_view, "full-content-zoom", - zoom_text_and_images, NULL); + g_object_set (view->web_view, + "full-content-zoom", zoom_text_and_images, NULL); + g_object_set (view, "kinetic-scrolling", kinetic_scrolling, NULL); } static void @@ -2379,6 +2386,11 @@ midori_view_settings_notify_cb (MidoriWebSettings* settings, g_object_set (view->web_view, "full-content-zoom", g_value_get_boolean (&value), NULL); } + else if (name == g_intern_string ("kinetic-scrolling")) + { + g_object_set (view, "kinetic-scrolling", + g_value_get_boolean (&value), NULL); + } else if (name == g_intern_string ("close-buttons-on-tabs")) { view->close_buttons_on_tabs = g_value_get_boolean (&value); diff --git a/midori/midori-websettings.c b/midori/midori-websettings.c index 9ef8b402..336a14fb 100644 --- a/midori/midori-websettings.c +++ b/midori/midori-websettings.c @@ -75,6 +75,7 @@ struct _MidoriWebSettings gboolean zoom_text_and_images; gboolean find_while_typing; + gboolean kinetic_scrolling; MidoriAcceptCookies accept_cookies; gboolean original_cookies_only; gint maximum_cookie_age; @@ -151,6 +152,7 @@ enum PROP_ZOOM_TEXT_AND_IMAGES, PROP_FIND_WHILE_TYPING, + PROP_KINETIC_SCROLLING, PROP_ACCEPT_COOKIES, PROP_ORIGINAL_COOKIES_ONLY, PROP_MAXIMUM_COOKIE_AGE, @@ -833,6 +835,22 @@ midori_web_settings_class_init (MidoriWebSettingsClass* class) FALSE, flags)); + /** + * MidoriWebSettings:kinetic-scrolling: + * + * Whether scrolling should kinetically move according to speed. + * + * Since: 0.2.0 + */ + g_object_class_install_property (gobject_class, + PROP_KINETIC_SCROLLING, + g_param_spec_boolean ( + "kinetic-scrolling", + _("Kinetic scrolling"), + _("Whether scrolling should kinetically move according to speed"), + TRUE, + flags)); + g_object_class_install_property (gobject_class, PROP_ACCEPT_COOKIES, g_param_spec_enum ( @@ -1025,6 +1043,7 @@ midori_web_settings_init (MidoriWebSettings* web_settings) web_settings->open_popups_in_tabs = TRUE; web_settings->remember_last_form_inputs = TRUE; web_settings->remember_last_downloaded_files = TRUE; + web_settings->kinetic_scrolling = TRUE; web_settings->auto_detect_proxy = TRUE; g_signal_connect (web_settings, "notify::default-encoding", @@ -1303,6 +1322,9 @@ midori_web_settings_set_property (GObject* object, case PROP_FIND_WHILE_TYPING: web_settings->find_while_typing = g_value_get_boolean (value); break; + case PROP_KINETIC_SCROLLING: + web_settings->kinetic_scrolling = g_value_get_boolean (value); + break; case PROP_ACCEPT_COOKIES: web_settings->accept_cookies = g_value_get_enum (value); break; @@ -1506,6 +1528,9 @@ midori_web_settings_get_property (GObject* object, case PROP_FIND_WHILE_TYPING: g_value_set_boolean (value, web_settings->find_while_typing); break; + case PROP_KINETIC_SCROLLING: + g_value_set_boolean (value, web_settings->kinetic_scrolling); + break; case PROP_ACCEPT_COOKIES: g_value_set_enum (value, web_settings->accept_cookies); break;