/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2008 Red Hat, Inc. * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. * * Written by: Ray Strode * * Parts taken from gtkscrolledwindow.c in the GTK+ toolkit. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "gdm-scrollable-widget.h" #include "gdm-timer.h" #define GDM_SCROLLABLE_WIDGET_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GDM_TYPE_SCROLLABLE_WIDGET, GdmScrollableWidgetPrivate)) enum { SCROLL_CHILD, MOVE_FOCUS_OUT, NUMBER_OF_SIGNALS }; typedef struct GdmScrollableWidgetAnimation GdmScrollableWidgetAnimation; struct GdmScrollableWidgetPrivate { GtkWidget *scrollbar; GdmScrollableWidgetAnimation *animation; GtkWidget *invisible_event_sink; guint key_press_signal_id; guint key_release_signal_id; GQueue *key_event_queue; guint child_adjustments_stale : 1; }; struct GdmScrollableWidgetAnimation { GtkWidget *widget; GdmTimer *timer; int start_height; int desired_height; GdmScrollableWidgetSlideStepFunc step_func; gpointer step_func_user_data; GdmScrollableWidgetSlideDoneFunc done_func; gpointer done_func_user_data; }; static void gdm_scrollable_widget_class_init (GdmScrollableWidgetClass *klass); static void gdm_scrollable_widget_init (GdmScrollableWidget *clock_widget); static void gdm_scrollable_widget_finalize (GObject *object); static guint signals[NUMBER_OF_SIGNALS] = { 0 }; G_DEFINE_TYPE (GdmScrollableWidget, gdm_scrollable_widget, GTK_TYPE_BIN) static GdmScrollableWidgetAnimation * gdm_scrollable_widget_animation_new (GtkWidget *widget, int start_height, int desired_height, GdmScrollableWidgetSlideStepFunc step_func, gpointer step_func_user_data, GdmScrollableWidgetSlideDoneFunc done_func, gpointer done_func_user_data) { GdmScrollableWidgetAnimation *animation; animation = g_slice_new (GdmScrollableWidgetAnimation); animation->widget = widget; animation->timer = gdm_timer_new (); animation->start_height = start_height; animation->desired_height = desired_height; animation->step_func = step_func; animation->step_func_user_data = step_func_user_data; animation->done_func = done_func; animation->done_func_user_data = done_func_user_data; return animation; } static void gdm_scrollable_widget_animation_free (GdmScrollableWidgetAnimation *animation) { g_object_unref (animation->timer); animation->timer = NULL; g_slice_free (GdmScrollableWidgetAnimation, animation); } static void on_animation_tick (GdmScrollableWidgetAnimation *animation, double progress) { int progress_in_pixels; int width; int height; progress_in_pixels = progress * (animation->start_height - animation->desired_height); height = animation->start_height - progress_in_pixels; gtk_widget_get_size_request (animation->widget, &width, NULL); gtk_widget_set_size_request (animation->widget, width, height); if (animation->step_func != NULL) { GdmTimer *timer; height = animation->desired_height; height -= animation->widget->style->ythickness * 2; height -= GTK_CONTAINER (animation->widget)->border_width * 2; timer = g_object_ref (animation->timer); animation->step_func (GDM_SCROLLABLE_WIDGET (animation->widget), progress, &height, animation->step_func_user_data); if (gdm_timer_is_started (timer)) { height += animation->widget->style->ythickness * 2; height += GTK_CONTAINER (animation->widget)->border_width * 2; animation->desired_height = height; } g_object_unref (timer); } } static gboolean on_key_event (GdmScrollableWidget *scrollable_widget, GdkEventKey *key_event) { g_queue_push_tail (scrollable_widget->priv->key_event_queue, gdk_event_copy ((GdkEvent *)key_event)); return FALSE; } static gboolean gdm_scrollable_redirect_input_to_event_sink (GdmScrollableWidget *scrollable_widget) { GdkGrabStatus status; status = gdk_pointer_grab (scrollable_widget->priv->invisible_event_sink->window, FALSE, 0, NULL, NULL, GDK_CURRENT_TIME); if (status != GDK_GRAB_SUCCESS) { return FALSE; } status = gdk_keyboard_grab (scrollable_widget->priv->invisible_event_sink->window, FALSE, GDK_CURRENT_TIME); if (status != GDK_GRAB_SUCCESS) { gdk_pointer_ungrab (GDK_CURRENT_TIME); return FALSE; } scrollable_widget->priv->key_press_signal_id = g_signal_connect_swapped (scrollable_widget->priv->invisible_event_sink, "key-press-event", G_CALLBACK (on_key_event), scrollable_widget); scrollable_widget->priv->key_release_signal_id = g_signal_connect_swapped (scrollable_widget->priv->invisible_event_sink, "key-release-event", G_CALLBACK (on_key_event), scrollable_widget); return TRUE; } static void gdm_scrollable_unredirect_input (GdmScrollableWidget *scrollable_widget) { g_signal_handler_disconnect (scrollable_widget->priv->invisible_event_sink, scrollable_widget->priv->key_press_signal_id); scrollable_widget->priv->key_press_signal_id = 0; g_signal_handler_disconnect (scrollable_widget->priv->invisible_event_sink, scrollable_widget->priv->key_release_signal_id); scrollable_widget->priv->key_release_signal_id = 0; gdk_keyboard_ungrab (GDK_CURRENT_TIME); gdk_pointer_ungrab (GDK_CURRENT_TIME); } static void on_animation_stop (GdmScrollableWidgetAnimation *animation) { GdmScrollableWidget *widget; int width; widget = GDM_SCROLLABLE_WIDGET (animation->widget); if (animation->done_func != NULL) { animation->done_func (widget, animation->done_func_user_data); } gdm_scrollable_widget_animation_free (widget->priv->animation); widget->priv->animation = NULL; gtk_widget_get_size_request (GTK_WIDGET (widget), &width, NULL); gtk_widget_set_size_request (GTK_WIDGET (widget), width, -1); gtk_widget_queue_resize (GTK_WIDGET (widget)); gdm_scrollable_unredirect_input (widget); } static void gdm_scrollable_widget_animation_start (GdmScrollableWidgetAnimation *animation) { g_signal_connect_swapped (G_OBJECT (animation->timer), "tick", G_CALLBACK (on_animation_tick), animation); g_signal_connect_swapped (G_OBJECT (animation->timer), "stop", G_CALLBACK (on_animation_stop), animation); gdm_timer_start (animation->timer, .10); } static void gdm_scrollable_widget_animation_stop (GdmScrollableWidgetAnimation *animation) { gdm_timer_stop (animation->timer); } static gboolean gdm_scrollable_widget_needs_scrollbar (GdmScrollableWidget *widget) { gboolean needs_scrollbar; if (widget->priv->scrollbar == NULL) { return FALSE; } if (widget->priv->animation != NULL) { return FALSE; } if (widget->priv->child_adjustments_stale) { return FALSE; } if (GTK_BIN (widget)->child != NULL) { GtkRequisition child_requisition; int available_height; gtk_widget_get_child_requisition (GTK_BIN (widget)->child, &child_requisition); available_height = GTK_WIDGET (widget)->allocation.height; needs_scrollbar = child_requisition.height > available_height; } else { needs_scrollbar = FALSE; } return needs_scrollbar; } static void gdm_scrollable_widget_size_request (GtkWidget *widget, GtkRequisition *requisition) { GdmScrollableWidget *scrollable_widget; GtkRequisition child_requisition; gboolean child_adjustments_stale; scrollable_widget = GDM_SCROLLABLE_WIDGET (widget); requisition->width = 2 * GTK_CONTAINER (widget)->border_width; requisition->height = 2 * GTK_CONTAINER (widget)->border_width; requisition->width += 2 * widget->style->xthickness; requisition->height += 2 * widget->style->ythickness; child_adjustments_stale = FALSE; if (GTK_BIN (widget)->child && GTK_WIDGET_VISIBLE (GTK_BIN (widget)->child)) { int old_child_height; gtk_widget_get_child_requisition (GTK_BIN (widget)->child, &child_requisition); old_child_height = child_requisition.height; gtk_widget_size_request (GTK_BIN (widget)->child, &child_requisition); requisition->width += child_requisition.width; requisition->height += child_requisition.height; child_adjustments_stale = old_child_height != child_requisition.height; } if (gdm_scrollable_widget_needs_scrollbar (scrollable_widget)) { GtkRequisition scrollbar_requisition; gtk_widget_show (scrollable_widget->priv->scrollbar); gtk_widget_size_request (scrollable_widget->priv->scrollbar, &scrollbar_requisition); requisition->height = MAX (requisition->height, scrollbar_requisition.height); requisition->width += scrollbar_requisition.width; } else { gtk_widget_hide (scrollable_widget->priv->scrollbar); } scrollable_widget->priv->child_adjustments_stale = child_adjustments_stale; } static void gdm_scrollable_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GdmScrollableWidget *scrollable_widget; GtkAllocation scrollbar_allocation; GtkAllocation child_allocation; gboolean has_child; gboolean needs_scrollbar; gboolean is_flipped; scrollable_widget = GDM_SCROLLABLE_WIDGET (widget); widget->allocation = *allocation; has_child = GTK_BIN (widget)->child && GTK_WIDGET_VISIBLE (GTK_BIN (widget)->child); needs_scrollbar = gdm_scrollable_widget_needs_scrollbar (scrollable_widget); is_flipped = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL; if (needs_scrollbar) { GtkRequisition scrollbar_requisition; gtk_widget_get_child_requisition (scrollable_widget->priv->scrollbar, &scrollbar_requisition); scrollbar_allocation.width = scrollbar_requisition.width; if (!is_flipped) { scrollbar_allocation.x = allocation->x + allocation->width; scrollbar_allocation.x -= GTK_CONTAINER (widget)->border_width; scrollbar_allocation.x -= scrollbar_allocation.width; } else { scrollbar_allocation.x = allocation->x; scrollbar_allocation.x += GTK_CONTAINER (widget)->border_width; } scrollbar_allocation.height = allocation->height; scrollbar_allocation.height -= 2 * GTK_CONTAINER (widget)->border_width; scrollbar_allocation.y = allocation->y; scrollbar_allocation.y += GTK_CONTAINER (widget)->border_width; gtk_widget_size_allocate (scrollable_widget->priv->scrollbar, &scrollbar_allocation); } if (has_child) { child_allocation.width = allocation->width; child_allocation.width -= 2 * GTK_CONTAINER (widget)->border_width; child_allocation.width -= 2 * widget->style->xthickness; if (needs_scrollbar) { child_allocation.width -= scrollbar_allocation.width; } if (!is_flipped) { child_allocation.x = allocation->x; child_allocation.x += GTK_CONTAINER (widget)->border_width; child_allocation.x += widget->style->xthickness; } else { child_allocation.x = allocation->x + allocation->width; child_allocation.x -= GTK_CONTAINER (widget)->border_width; child_allocation.x -= child_allocation.width; child_allocation.x -= widget->style->xthickness; } child_allocation.height = allocation->height; child_allocation.height -= 2 * GTK_CONTAINER (widget)->border_width; child_allocation.height -= 2 * widget->style->ythickness; child_allocation.y = allocation->y; child_allocation.y += GTK_CONTAINER (widget)->border_width; child_allocation.y += widget->style->ythickness; gtk_widget_size_allocate (GTK_BIN (widget)->child, &child_allocation); scrollable_widget->priv->child_adjustments_stale = FALSE; } } static void gdm_scrollable_widget_add (GtkContainer *container, GtkWidget *child) { GtkAdjustment *adjustment; GTK_CONTAINER_CLASS (gdm_scrollable_widget_parent_class)->add (container, child); adjustment = gtk_range_get_adjustment (GTK_RANGE (GDM_SCROLLABLE_WIDGET (container)->priv->scrollbar)); g_signal_connect_swapped (adjustment, "changed", G_CALLBACK (gtk_widget_queue_resize), container); gtk_widget_set_scroll_adjustments (child, NULL, adjustment); } static void gdm_scrollable_widget_remove (GtkContainer *container, GtkWidget *child) { gtk_widget_set_scroll_adjustments (child, NULL, NULL); GTK_CONTAINER_CLASS (gdm_scrollable_widget_parent_class)->remove (container, child); } static void gdm_scrollable_widget_forall (GtkContainer *container, gboolean include_internals, GtkCallback callback, gpointer callback_data) { GdmScrollableWidget *scrollable_widget; scrollable_widget = GDM_SCROLLABLE_WIDGET (container); GTK_CONTAINER_CLASS (gdm_scrollable_widget_parent_class)->forall (container, include_internals, callback, callback_data); if (!include_internals) { return; } if (scrollable_widget->priv->scrollbar != NULL) { callback (scrollable_widget->priv->scrollbar, callback_data); } } static void gdm_scrollable_widget_destroy (GtkObject *object) { GdmScrollableWidget *scrollable_widget; scrollable_widget = GDM_SCROLLABLE_WIDGET (object); gtk_widget_unparent (scrollable_widget->priv->scrollbar); gtk_widget_destroy (scrollable_widget->priv->scrollbar); GTK_OBJECT_CLASS (gdm_scrollable_widget_parent_class)->destroy (object); } static void gdm_scrollable_widget_finalize (GObject *object) { GdmScrollableWidget *scrollable_widget; scrollable_widget = GDM_SCROLLABLE_WIDGET (object); g_queue_free (scrollable_widget->priv->key_event_queue); G_OBJECT_CLASS (gdm_scrollable_widget_parent_class)->finalize (object); } static gboolean gdm_scrollable_widget_expose_event (GtkWidget *widget, GdkEventExpose *event) { GdmScrollableWidget *scrollable_widget; int x; int y; int width; int height; gboolean is_flipped; scrollable_widget = GDM_SCROLLABLE_WIDGET (widget); if (!GTK_WIDGET_DRAWABLE (widget)) { return FALSE; } is_flipped = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL; x = widget->allocation.x; x += 2 * GTK_CONTAINER (widget)->border_width; width = widget->allocation.width; width -= 2 * GTK_CONTAINER (widget)->border_width; if (gdm_scrollable_widget_needs_scrollbar (scrollable_widget)) { width -= scrollable_widget->priv->scrollbar->allocation.width; if (is_flipped) { x += scrollable_widget->priv->scrollbar->allocation.width; } } y = widget->allocation.y; y += 2 * GTK_CONTAINER (widget)->border_width; height = widget->allocation.height; height -= 2 * GTK_CONTAINER (widget)->border_width; gtk_paint_shadow (widget->style, widget->window, GTK_WIDGET_STATE (widget), GTK_SHADOW_IN, &event->area, widget, "scrolled_window", x, y, width, height); return GTK_WIDGET_CLASS (gdm_scrollable_widget_parent_class)->expose_event (widget, event); } static gboolean gdm_scrollable_widget_scroll_event (GtkWidget *widget, GdkEventScroll *event) { if (event->direction != GDK_SCROLL_UP && event->direction != GDK_SCROLL_DOWN) { return FALSE; } if (!GTK_WIDGET_VISIBLE (GTK_WIDGET (widget))) { return FALSE; } return gtk_widget_event (GDM_SCROLLABLE_WIDGET (widget)->priv->scrollbar, (GdkEvent *) event); } static void add_scroll_binding (GtkBindingSet *binding_set, guint keyval, GdkModifierType mask, GtkScrollType scroll) { guint keypad_keyval = keyval - GDK_Left + GDK_KP_Left; gtk_binding_entry_add_signal (binding_set, keyval, mask, "scroll-child", 1, GTK_TYPE_SCROLL_TYPE, scroll); gtk_binding_entry_add_signal (binding_set, keypad_keyval, mask, "scroll-child", 1, GTK_TYPE_SCROLL_TYPE, scroll); } static void add_tab_bindings (GtkBindingSet *binding_set, GdkModifierType modifiers, GtkDirectionType direction) { gtk_binding_entry_add_signal (binding_set, GDK_Tab, modifiers, "move-focus-out", 1, GTK_TYPE_DIRECTION_TYPE, direction); gtk_binding_entry_add_signal (binding_set, GDK_KP_Tab, modifiers, "move-focus-out", 1, GTK_TYPE_DIRECTION_TYPE, direction); } static void gdm_scrollable_widget_class_install_bindings (GdmScrollableWidgetClass *klass) { GtkBindingSet *binding_set; binding_set = gtk_binding_set_by_class (klass); add_scroll_binding (binding_set, GDK_Up, GDK_CONTROL_MASK, GTK_SCROLL_STEP_BACKWARD); add_scroll_binding (binding_set, GDK_Down, GDK_CONTROL_MASK, GTK_SCROLL_STEP_FORWARD); add_scroll_binding (binding_set, GDK_Page_Up, 0, GTK_SCROLL_PAGE_BACKWARD); add_scroll_binding (binding_set, GDK_Page_Down, 0, GTK_SCROLL_PAGE_FORWARD); add_scroll_binding (binding_set, GDK_Home, 0, GTK_SCROLL_START); add_scroll_binding (binding_set, GDK_End, 0, GTK_SCROLL_END); add_tab_bindings (binding_set, GDK_CONTROL_MASK, GTK_DIR_TAB_FORWARD); add_tab_bindings (binding_set, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_DIR_TAB_BACKWARD); } static void gdm_scrollable_widget_class_init (GdmScrollableWidgetClass *klass) { GObjectClass *object_class; GtkObjectClass *gtk_object_class; GtkWidgetClass *widget_class; GtkContainerClass *container_class; GdmScrollableWidgetClass *scrollable_widget_class; object_class = G_OBJECT_CLASS (klass); gtk_object_class = GTK_OBJECT_CLASS (klass); widget_class = GTK_WIDGET_CLASS (klass); container_class = GTK_CONTAINER_CLASS (klass); scrollable_widget_class = GDM_SCROLLABLE_WIDGET_CLASS (klass); object_class->finalize = gdm_scrollable_widget_finalize; gtk_object_class->destroy = gdm_scrollable_widget_destroy; widget_class->size_request = gdm_scrollable_widget_size_request; widget_class->size_allocate = gdm_scrollable_widget_size_allocate; widget_class->expose_event = gdm_scrollable_widget_expose_event; widget_class->scroll_event = gdm_scrollable_widget_scroll_event; container_class->add = gdm_scrollable_widget_add; container_class->remove = gdm_scrollable_widget_remove; container_class->forall = gdm_scrollable_widget_forall; signals[SCROLL_CHILD] = g_signal_new ("scroll-child", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkScrolledWindowClass, scroll_child), NULL, NULL, g_cclosure_marshal_VOID__ENUM, G_TYPE_BOOLEAN, 1, GTK_TYPE_SCROLL_TYPE); signals[MOVE_FOCUS_OUT] = g_signal_new ("move-focus-out", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkScrolledWindowClass, move_focus_out), NULL, NULL, g_cclosure_marshal_VOID__ENUM, G_TYPE_NONE, 1, GTK_TYPE_DIRECTION_TYPE); gdm_scrollable_widget_class_install_bindings (klass); g_type_class_add_private (klass, sizeof (GdmScrollableWidgetPrivate)); } static void gdm_scrollable_widget_add_scrollbar (GdmScrollableWidget *widget) { gtk_widget_push_composite_child (); widget->priv->scrollbar = gtk_vscrollbar_new (NULL); gtk_widget_set_composite_name (widget->priv->scrollbar, "scrollbar"); gtk_widget_pop_composite_child (); gtk_widget_set_parent (widget->priv->scrollbar, GTK_WIDGET (widget)); g_object_ref (widget->priv->scrollbar); } static void gdm_scrollable_widget_add_invisible_event_sink (GdmScrollableWidget *widget) { widget->priv->invisible_event_sink = gtk_invisible_new_for_screen (gtk_widget_get_screen (GTK_WIDGET (widget))); gtk_widget_show (widget->priv->invisible_event_sink); widget->priv->key_event_queue = g_queue_new (); } static void gdm_scrollable_widget_init (GdmScrollableWidget *widget) { widget->priv = GDM_SCROLLABLE_WIDGET_GET_PRIVATE (widget); gdm_scrollable_widget_add_scrollbar (widget); gdm_scrollable_widget_add_invisible_event_sink (widget); } GtkWidget * gdm_scrollable_widget_new (void) { GObject *object; object = g_object_new (GDM_TYPE_SCROLLABLE_WIDGET, NULL); return GTK_WIDGET (object); } static gboolean gdm_scrollable_widget_animations_are_disabled (GdmScrollableWidget *scrollable_widget) { GtkSettings *settings; gboolean animations_are_enabled; settings = gtk_settings_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (scrollable_widget))); g_object_get (settings, "gtk-enable-animations", &animations_are_enabled, NULL); return animations_are_enabled == FALSE; } void gdm_scrollable_widget_stop_sliding (GdmScrollableWidget *scrollable_widget) { g_return_if_fail (GDM_IS_SCROLLABLE_WIDGET (scrollable_widget)); if (scrollable_widget->priv->animation != NULL) { gdm_scrollable_widget_animation_stop (scrollable_widget->priv->animation); } g_assert (scrollable_widget->priv->animation == NULL); } void gdm_scrollable_widget_slide_to_height (GdmScrollableWidget *scrollable_widget, int height, GdmScrollableWidgetSlideStepFunc step_func, gpointer step_user_data, GdmScrollableWidgetSlideDoneFunc done_func, gpointer done_user_data) { GtkWidget *widget; gboolean input_redirected; g_return_if_fail (GDM_IS_SCROLLABLE_WIDGET (scrollable_widget)); widget = GTK_WIDGET (scrollable_widget); gdm_scrollable_widget_stop_sliding (scrollable_widget); input_redirected = gdm_scrollable_redirect_input_to_event_sink (scrollable_widget); if (!input_redirected || gdm_scrollable_widget_animations_are_disabled (scrollable_widget)) { if (step_func != NULL) { step_func (scrollable_widget, 0.0, &height, step_user_data); } if (done_func != NULL) { done_func (scrollable_widget, done_user_data); } if (input_redirected) { gdm_scrollable_unredirect_input (scrollable_widget); } return; } height += widget->style->ythickness * 2; height += GTK_CONTAINER (widget)->border_width * 2; scrollable_widget->priv->animation = gdm_scrollable_widget_animation_new (widget, widget->allocation.height, height, step_func, step_user_data, done_func, done_user_data); gdm_scrollable_widget_animation_start (scrollable_widget->priv->animation); } gboolean gdm_scrollable_widget_has_queued_key_events (GdmScrollableWidget *widget) { g_return_val_if_fail (GDM_IS_SCROLLABLE_WIDGET (widget), FALSE); return !g_queue_is_empty (widget->priv->key_event_queue); } void gdm_scrollable_widget_replay_queued_key_events (GdmScrollableWidget *widget) { GtkWidget *toplevel; GdkEvent *event; toplevel = gtk_widget_get_toplevel (GTK_WIDGET (widget)); while ((event = g_queue_pop_head (widget->priv->key_event_queue)) != NULL) { gtk_propagate_event (toplevel, event); } }