#include /* For GDK_PARENT_RELATIVE_BG */ #include #include #include "scrollarea.h" #include "foo-marshal.h" #include "backingstore.h" G_DEFINE_TYPE (FooScrollArea, foo_scroll_area, GTK_TYPE_CONTAINER); static GtkWidgetClass *parent_class; typedef struct AutoScrollInfo AutoScrollInfo; struct AutoScrollInfo { int dx; int dy; int timeout_id; int begin_x; int begin_y; double res_x; double res_y; GTimer *timer; }; struct FooScrollAreaPrivate { int width; int height; GtkAdjustment *hadj; GtkAdjustment *vadj; int x_offset; int y_offset; int min_width; int min_height; GPtrArray *input_regions; AutoScrollInfo *auto_scroll_info; /* During expose, this region is set to the region * being exposed. At other times, it is NULL * * It is used for clipping of input areas */ gboolean implicit_grab; gboolean grabbed; FooScrollAreaEventFunc grab_func; gpointer grab_data; BackingStore *store; gboolean in_size_allocate; gboolean paint_updates; /* Last emitted event went here - used to generate enters and leaves */ FooScrollAreaEventFunc last_func; gpointer last_data; int last_x; int last_y; gboolean last_was_motion; int current_x; int current_y; gboolean need_flush; }; enum { VIEWPORT_SIZE_CHANGED, VIEWPORT_CHANGED, PAINT, INPUT, LAST_SIGNAL, }; static guint signals [LAST_SIGNAL] = { 0 }; static void foo_scroll_area_size_request (GtkWidget *widget, GtkRequisition *requisition); static gboolean foo_scroll_area_expose (GtkWidget *widget, GdkEventExpose *expose); static void foo_scroll_area_size_allocate (GtkWidget *widget, GtkAllocation *allocation); static void foo_scroll_area_set_scroll_adjustments (FooScrollArea *area, GtkAdjustment *hadj, GtkAdjustment *vadj); static void foo_scroll_area_realize (GtkWidget *widget); static void foo_scroll_area_unrealize (GtkWidget *widget); static void foo_scroll_area_map (GtkWidget *widget); static void foo_scroll_area_unmap (GtkWidget *widget); static gboolean foo_scroll_area_button_press (GtkWidget *widget, GdkEventButton *event); static gboolean foo_scroll_area_button_release (GtkWidget *widget, GdkEventButton *event); static gboolean foo_scroll_area_motion (GtkWidget *widget, GdkEventMotion *event); static gboolean foo_scroll_area_crossing (GtkWidget *widget, GdkEventCrossing *event); static void foo_scroll_area_map (GtkWidget *widget) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; GTK_WIDGET_CLASS (parent_class)->map (widget); gdk_window_get_pointer (widget->window, &priv->current_x, &priv->current_y, NULL); } static void foo_scroll_area_unmap (GtkWidget *widget) { GTK_WIDGET_CLASS (parent_class)->unmap (widget); } static void foo_scroll_area_finalize (GObject *object) { FooScrollArea *area = FOO_SCROLL_AREA (object); FooScrollAreaPrivate *priv = area->priv; g_object_unref (priv->hadj); g_object_unref (priv->vadj); g_ptr_array_free (priv->input_regions, TRUE); g_free (priv); G_OBJECT_CLASS (foo_scroll_area_parent_class)->finalize (object); } static void foo_scroll_area_class_init (FooScrollAreaClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); object_class->finalize = foo_scroll_area_finalize; widget_class->size_request = foo_scroll_area_size_request; widget_class->expose_event = foo_scroll_area_expose; widget_class->size_allocate = foo_scroll_area_size_allocate; widget_class->realize = foo_scroll_area_realize; widget_class->unrealize = foo_scroll_area_unrealize; widget_class->button_press_event = foo_scroll_area_button_press; widget_class->button_release_event = foo_scroll_area_button_release; widget_class->motion_notify_event = foo_scroll_area_motion; widget_class->enter_notify_event = foo_scroll_area_crossing; widget_class->leave_notify_event = foo_scroll_area_crossing; widget_class->map = foo_scroll_area_map; widget_class->unmap = foo_scroll_area_unmap; class->set_scroll_adjustments = foo_scroll_area_set_scroll_adjustments; parent_class = g_type_class_peek_parent (class); /* Emitted when the viewport changes size, but not when it only changes position */ signals[VIEWPORT_SIZE_CHANGED] = g_signal_new ("viewport_size_changed", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (FooScrollAreaClass, viewport_size_changed), NULL, NULL, foo_marshal_VOID__BOXED_BOXED, G_TYPE_NONE, 2, GDK_TYPE_RECTANGLE, GDK_TYPE_RECTANGLE); /* Emitted when the viewport changes position or size or both */ signals[VIEWPORT_CHANGED] = g_signal_new ("viewport_changed", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (FooScrollAreaClass, viewport_changed), NULL, NULL, foo_marshal_VOID__BOXED_BOXED, G_TYPE_NONE, 2, GDK_TYPE_RECTANGLE, GDK_TYPE_RECTANGLE); signals[PAINT] = g_signal_new ("paint", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (FooScrollAreaClass, paint), NULL, NULL, foo_marshal_VOID__POINTER_BOXED_POINTER, G_TYPE_NONE, 3, G_TYPE_POINTER, GDK_TYPE_RECTANGLE, G_TYPE_POINTER); widget_class->set_scroll_adjustments_signal = g_signal_new ("set_scroll_adjustments", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (FooScrollAreaClass, set_scroll_adjustments), NULL, NULL, foo_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT); } static GtkAdjustment * new_adjustment (void) { return GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); } static void foo_scroll_area_init (FooScrollArea *area) { FooScrollAreaPrivate *priv; gtk_widget_set_redraw_on_allocate (GTK_WIDGET (area), FALSE); priv = g_new0 (FooScrollAreaPrivate, 1); priv->width = 0; priv->height = 0; priv->hadj = g_object_ref_sink (new_adjustment()); priv->vadj = g_object_ref_sink (new_adjustment()); priv->x_offset = 0.0; priv->y_offset = 0.0; priv->min_width = -1; priv->min_height = -1; priv->auto_scroll_info = NULL; priv->input_regions = g_ptr_array_new (); area->priv = priv; gtk_widget_set_double_buffered (GTK_WIDGET (area), FALSE); } typedef void (* PathForeachFunc) (double *x, double *y, gpointer data); static void path_foreach_point (cairo_path_t *path, PathForeachFunc func, gpointer user_data) { int i; for (i = 0; i < path->num_data; i += path->data[i].header.length) { cairo_path_data_t *data = &(path->data[i]); switch (data->header.type) { case CAIRO_PATH_MOVE_TO: case CAIRO_PATH_LINE_TO: func (&(data[1].point.x), &(data[1].point.y), user_data); break; case CAIRO_PATH_CURVE_TO: func (&(data[1].point.x), &(data[1].point.y), user_data); func (&(data[2].point.x), &(data[2].point.y), user_data); func (&(data[3].point.x), &(data[3].point.y), user_data); break; case CAIRO_PATH_CLOSE_PATH: break; } } } typedef struct { double x1, y1, x2, y2; } Box; #if 0 static void update_box (double *x, double *y, gpointer data) { Box *box = data; if (*x < box->x1) box->x1 = *x; if (*y < box->y1) box->y1 = *y; if (*y > box->y2) box->y2 = *y; if (*x > box->x2) box->x2 = *x; } #endif #if 0 static void path_compute_extents (cairo_path_t *path, GdkRectangle *rect) { if (rect) { Box box = { G_MAXDOUBLE, G_MAXDOUBLE, G_MINDOUBLE, G_MINDOUBLE }; path_foreach_point (path, update_box, &box); rect->x = box.x1; rect->y = box.y1; rect->width = box.x2 - box.x1; rect->height = box.y2 - box.y1; } } #endif static void get_viewport (FooScrollArea *area, GdkRectangle *viewport) { FooScrollAreaPrivate *priv = area->priv; GtkWidget *widget = GTK_WIDGET (area); viewport->x = priv->x_offset; viewport->y = priv->y_offset; viewport->width = widget->allocation.width; viewport->height = widget->allocation.height; } static void setup_background_cr (GdkWindow *window, cairo_t *cr, int x_offset, int y_offset) { GdkWindowObject *private = (GdkWindowObject *)window; if (private->bg_pixmap == GDK_PARENT_RELATIVE_BG && private->parent) { x_offset += private->x; y_offset += private->y; setup_background_cr (GDK_WINDOW (private->parent), cr, x_offset, y_offset); } else if (private->bg_pixmap && private->bg_pixmap != GDK_PARENT_RELATIVE_BG && private->bg_pixmap != GDK_NO_BG) { gdk_cairo_set_source_pixmap (cr, private->bg_pixmap, -x_offset, -y_offset); } else { gdk_cairo_set_source_color (cr, &private->bg_color); } } static void allocation_to_canvas (FooScrollArea *area, int *x, int *y) { FooScrollAreaPrivate *priv = area->priv; *x += priv->x_offset; *y += priv->y_offset; } static void canvas_to_window (FooScrollArea *area, GdkRegion *region) { FooScrollAreaPrivate *priv = area->priv; gdk_region_offset (region, - priv->x_offset, - priv->y_offset); } static void window_to_canvas (FooScrollArea *area, GdkRegion *region) { FooScrollAreaPrivate *priv = area->priv; gdk_region_offset (region, priv->x_offset, priv->y_offset); } static void canvas_to_store (FooScrollArea *area, GdkRegion *region) { FooScrollAreaPrivate *priv = area->priv; gdk_region_offset (region, - priv->x_offset, - priv->y_offset); } static void store_to_canvas (FooScrollArea *area, GdkRegion *region) { FooScrollAreaPrivate *priv = area->priv; gdk_region_offset (region, priv->x_offset, priv->y_offset); } static void paint_region (FooScrollArea *area, GdkRegion *region, double r, double g, double b, double a) { cairo_t *cr = gdk_cairo_create (GTK_WIDGET (area)->window); gdk_cairo_region (cr, region); cairo_clip (cr); cairo_set_source_rgba (cr, r, g, b, a); cairo_paint (cr); cairo_destroy (cr); } static void clip_to_32bit_rect (cairo_t *cr, GdkRectangle *rect) { /* There is a bug in cairo where clipping will overflow when * the rectangle is larger than fits in 15 bits. */ if (rect->width > 32000 || rect->height > 32000) { GdkRectangle r1, r2; r1 = *rect; r2 = *rect; if (rect->width > 32000) { r1.width = 32000; r2.x += 32000; r2.width -= 32000; } else if (rect->height > 32000) { r1.height = 32000; r2.y += 32000; r2.height -= 32000; } clip_to_32bit_rect (cr, &r1); clip_to_32bit_rect (cr, &r2); } else { /* Translating here is a hack to prevent cairo from making * regions with > 16 bit x/y coordinates */ cairo_save (cr); cairo_translate (cr, rect->x, rect->y); cairo_rectangle (cr, 0, 0, rect->width, rect->height); cairo_clip (cr); cairo_restore (cr); } #if 0 /* This is the code we'll use when cairo is fixed */ /* It is fixed in 1.8.0, so we can turn it on when 1.8.0 is more * common */ cairo_rectangle (cr, rect->x, rect->y, rect->width, rect->height); cairo_clip (cr); #endif } static void do_exposes (cairo_t *cr, GdkRegion *store_region, gpointer data) { FooScrollArea *area = data; FooScrollAreaPrivate *priv = area->priv; GdkRegion *region; GdkRectangle rect; GdkRegion *r; /* Paint background */ setup_background_cr (GTK_WIDGET (area)->window, cr, 0, 0); cairo_paint (cr); if (priv->paint_updates) paint_region (area, store_region, 1.0, 0.0, 0.0, 0.8); /* Note that this function can be called at a time * where the adj->value is different from x_offset. * Ie., the GtkScrolledWindow changed the adj->value * without emitting the value_changed signal. * * Hence we must always use the value we got * the last time the signal was emitted, ie., * priv->{x,y}_offset. */ region = gdk_region_copy (store_region); store_to_canvas (area, region); /* Set up cairo context and emit the signal */ #if 0 g_print ("translating %d %d\n", -priv->x_offset, -priv->y_offset); #endif cairo_translate (cr, -priv->x_offset, -priv->y_offset); /* Restrict to the actual canvas */ rect.x = 0; rect.y = 0; rect.width = priv->width; rect.height = priv->height; r = gdk_region_rectangle (&rect); gdk_region_intersect (region, r); gdk_region_destroy (r); clip_to_32bit_rect (cr, &rect); #if 0 g_print ("clipping to %d %d %d %d\n", 0, 0, priv->width, area->priv->height); #endif #if 0 cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0); cairo_paint (cr); #endif if (!gdk_region_empty (region)) { gdk_region_get_clipbox (region, &rect); g_signal_emit (area, signals[PAINT], 0, cr, &rect, region); } gdk_region_destroy (region); } static void print_region (const char *header, GdkRegion *region) { GdkRectangle *rects; int n_rects; int i; g_print ("%s\n", header); gdk_region_get_rectangles (region, &rects, &n_rects); for (i = 0; i < n_rects; ++i) { GdkRectangle *rect = &(rects[i]); g_print (" %d %d %d %d\n", rect->x, rect->y, rect->width, rect->height); } g_free (rects); } static gboolean foo_scroll_area_expose (GtkWidget *widget, GdkEventExpose *expose) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; backing_store_process_updates (priv->store, do_exposes, area); if (priv->paint_updates) { paint_region (area, expose->region, 0, 0, 1, 0.3); gdk_flush (); g_usleep (100000); } if (!gdk_region_empty (expose->region)) { backing_store_draw (area->priv->store, widget->window, expose->region, 0, 0); XImage *image; image = XGetImage (GDK_WINDOW_XDISPLAY (widget->window), GDK_WINDOW_XID (gdk_get_default_root_window()), 0, 0, 1, 1, AllPlanes, ZPixmap); } return TRUE; } void foo_scroll_area_get_viewport (FooScrollArea *area, GdkRectangle *viewport) { g_return_if_fail (FOO_IS_SCROLL_AREA (area)); if (!viewport) return; get_viewport (area, viewport); } static void emit_viewport_signals (FooScrollArea *area, GdkRectangle *new_viewport, GdkRectangle *old_viewport) { gboolean position_changed; gboolean size_changed; size_changed = (new_viewport->width != old_viewport->width || new_viewport->height != old_viewport->height); position_changed = (new_viewport->x != old_viewport->x || new_viewport->y != old_viewport->y); if (size_changed || position_changed) { g_signal_emit (area, signals[VIEWPORT_CHANGED], 0, new_viewport, old_viewport); if (size_changed) { g_signal_emit (area, signals[VIEWPORT_SIZE_CHANGED], 0, new_viewport, old_viewport); } } } static void clamp_adjustment (GtkAdjustment *adj) { double old_value = adj->value; if (adj->upper >= adj->page_size) adj->value = CLAMP (adj->value, 0.0, adj->upper - adj->page_size); else adj->value = 0.0; if (old_value != adj->value) gtk_adjustment_value_changed (adj); gtk_adjustment_changed (adj); } static gboolean set_adjustment_values (FooScrollArea *area) { FooScrollAreaPrivate *priv = area->priv; GtkAllocation *allocation = >K_WIDGET (area)->allocation; GtkAdjustment *hadj = priv->hadj; GtkAdjustment *vadj = priv->vadj; /* Horizontal */ hadj->page_size = allocation->width; hadj->step_increment = 0.1 * allocation->width; hadj->page_increment = 0.9 * allocation->width; hadj->lower = 0.0; hadj->upper = area->priv->width; /* Vertical */ vadj->page_size = allocation->height; vadj->step_increment = 0.1 * allocation->height; vadj->page_increment = 0.9 * allocation->height; vadj->lower = 0.0; vadj->upper = priv->height; clamp_adjustment (hadj); clamp_adjustment (vadj); return TRUE; } static void foo_scroll_area_realize (GtkWidget *widget) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; GdkWindowAttr attributes; gint attributes_mask; GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED); attributes.window_type = GDK_WINDOW_CHILD; attributes.x = widget->allocation.x; attributes.y = widget->allocation.y; attributes.width = widget->allocation.width; attributes.height = widget->allocation.height; attributes.wclass = GDK_INPUT_OUTPUT; attributes.event_mask = gtk_widget_get_events (widget); attributes.event_mask |= (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON2_MOTION_MASK | GDK_BUTTON3_MOTION_MASK | GDK_POINTER_MOTION_MASK | GDK_EXPOSURE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); attributes.visual = gtk_widget_get_visual (widget); attributes.colormap = gtk_widget_get_colormap (widget); attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP; widget->window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask); gdk_window_set_user_data (widget->window, area); priv->store = backing_store_new (widget->window, widget->allocation.width, widget->allocation.height); widget->style = gtk_style_attach (widget->style, widget->window); gtk_style_set_background (widget->style, widget->window, GTK_STATE_NORMAL); } static void foo_scroll_area_unrealize (GtkWidget *widget) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; backing_store_free (priv->store); GTK_WIDGET_CLASS (parent_class)->unrealize (widget); } #if 0 static void allocation_to_canvas_region (FooScrollArea *area, GdkRegion *region) { FooScrollAreaPrivate *priv = area->priv; gdk_region_offset (region, priv->x_offset, priv->y_offset); } #endif static void foo_scroll_area_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; GdkRectangle new_viewport; GdkRectangle old_viewport; priv->in_size_allocate = TRUE; get_viewport (area, &old_viewport); widget->allocation = *allocation; if (GTK_WIDGET_REALIZED (widget)) { gdk_window_move_resize (widget->window, allocation->x, allocation->y, allocation->width, allocation->height); /* FIXME: We should possibly restrict the size of the * backing store to be not more than the size of * the area itself. The only problem with that is that * we still need to paint the background of the exposed * areas outside the area. */ backing_store_resize (priv->store, widget->allocation.width, widget->allocation.height); } set_adjustment_values (area); get_viewport (area, &new_viewport); emit_viewport_signals (area, &new_viewport, &old_viewport); priv->in_size_allocate = FALSE; } gboolean foo_scroll_area_is_resizing (FooScrollArea *area) { FooScrollAreaPrivate *priv = area->priv; return priv->in_size_allocate; } static void begin_grab_internal (FooScrollArea *area, gboolean implicit, FooScrollAreaEventFunc func, gpointer data) { FooScrollAreaPrivate *priv = area->priv; #if 0 g_print ("begin%s grab\n", implicit? " implicit" : ""); #endif /* FIXME: we should probably take a server grab */ /* Also, there should be support for setting the grab cursor */ /* Grabs can be overridden if desired - FIXME: make sure this right */ /* We may need a grab counter here if there are both implicit and * explicit grabs in play at the same time */ priv->implicit_grab = implicit; priv->grabbed = TRUE; priv->grab_func = func; priv->grab_data = data; } static void end_grab_internal (FooScrollArea *area, gboolean only_implicit) { FooScrollAreaPrivate *priv = area->priv; if (only_implicit && !priv->implicit_grab) return; #if 0 g_print ("end grab\n"); #endif priv->implicit_grab = FALSE; priv->grabbed = FALSE; priv->grab_func = NULL; priv->grab_data = NULL; } #if 0 static void print_path (const char *header, cairo_path_t *path) { int i; g_print ("%s\n", header); for (i=0; i < path->num_data; i += path->data[i].header.length) { cairo_path_data_t *data = &(path->data[i]); switch (data->header.type) { case CAIRO_PATH_MOVE_TO: g_print ("move to: %f, %f\n", data[1].point.x, data[1].point.y); break; case CAIRO_PATH_LINE_TO: g_print ("line to: %f, %f\n", data[1].point.x, data[1].point.y); break; case CAIRO_PATH_CURVE_TO: g_print ("curve to: %f, %f\n", data[1].point.x, data[1].point.y); g_print (" %f, %f\n", data[1].point.x, data[1].point.y); g_print (" %f, %f\n", data[1].point.x, data[1].point.y); break; case CAIRO_PATH_CLOSE_PATH: break; } } } #endif static gboolean foo_scroll_area_button_press (GtkWidget *widget, GdkEventButton *event) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; backing_store_process_event (priv->store, (GdkEvent *)event); return TRUE; } static gboolean foo_scroll_area_button_release (GtkWidget *widget, GdkEventButton *event) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; backing_store_process_event (priv->store, (GdkEvent *)event); return FALSE; } static gboolean foo_scroll_area_motion (GtkWidget *widget, GdkEventMotion *event) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; backing_store_process_event (priv->store, (GdkEvent *)event); return TRUE; } static gboolean foo_scroll_area_crossing (GtkWidget *widget, GdkEventCrossing *event) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; backing_store_process_event (priv->store, (GdkEvent *)event); return TRUE; } void foo_scroll_area_set_size_fixed_y (FooScrollArea *area, int width, int height, int old_y, int new_y) { FooScrollAreaPrivate *priv = area->priv; int dy = new_y - old_y; priv->width = width; priv->height = height; priv->vadj->value += dy; set_adjustment_values (area); if (dy != 0) gtk_adjustment_value_changed (priv->vadj); } void foo_scroll_area_set_size (FooScrollArea *area, int width, int height) { FooScrollAreaPrivate *priv = area->priv; g_return_if_fail (FOO_IS_SCROLL_AREA (area)); /* FIXME: Default scroll algorithm should probably be to * keep the same *area* outside the screen as before. * * For wrapper widgets that will do something roughly * right. For widgets that don't change size, it * will do the right thing. Except for idle-layouting * widgets. * * Maybe there should be some generic support for those * widgets. Can that even be done? * * Should we have a version of this function using * fixed points? */ priv->width = width; priv->height = height; set_adjustment_values (area); } static void foo_scroll_area_size_request (GtkWidget *widget, GtkRequisition *requisition) { FooScrollArea *area = FOO_SCROLL_AREA (widget); FooScrollAreaPrivate *priv = area->priv; requisition->width = priv->min_width; requisition->height = priv->min_height; #if 0 g_print ("request %d %d\n", requisition->width, requisition->height); #endif } static void foo_scrollbar_adjustment_changed (GtkAdjustment *adj, FooScrollArea *area) { FooScrollAreaPrivate *priv = area->priv; GtkWidget *widget = GTK_WIDGET (area); gint dx = 0; gint dy = 0; GdkRectangle old_viewport, new_viewport; /* FIXME: When dragging diagonally, this function will be called * twice, causing two separate scrolls in the backing store. * * On the other hand with the constant time scrolling, it shouldn't * really matter. */ get_viewport (area, &old_viewport); if (adj == priv->hadj) { /* FIXME: do we treat the offset as int or double, and, * if int, how do we round? */ dx = (int)adj->value - priv->x_offset; priv->x_offset = adj->value; } else if (adj == priv->vadj) { dy = (int)adj->value - priv->y_offset; priv->y_offset = adj->value; } else { g_assert_not_reached (); } if (GTK_WIDGET_REALIZED (widget) && (dx || dy)) { backing_store_scroll (priv->store, -dx, -dy); gtk_widget_queue_draw (GTK_WIDGET (area)); } get_viewport (area, &new_viewport); emit_viewport_signals (area, &new_viewport, &old_viewport); gdk_window_process_all_updates(); } static void set_one_adjustment (FooScrollArea *area, GtkAdjustment *adjustment, GtkAdjustment **location) { g_return_if_fail (location != NULL); if (adjustment == *location) return; if (!adjustment) adjustment = new_adjustment (); g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment)); if (*location) { g_signal_handlers_disconnect_by_func ( *location, foo_scrollbar_adjustment_changed, area); g_object_unref (*location); } *location = adjustment; g_object_ref_sink (*location); g_signal_connect (*location, "value_changed", G_CALLBACK (foo_scrollbar_adjustment_changed), area); } static void foo_scroll_area_set_scroll_adjustments (FooScrollArea *area, GtkAdjustment *hadjustment, GtkAdjustment *vadjustment) { FooScrollAreaPrivate *priv = area->priv; set_one_adjustment (area, hadjustment, &priv->hadj); set_one_adjustment (area, vadjustment, &priv->vadj); set_adjustment_values (area); } FooScrollArea * foo_scroll_area_new (void) { return g_object_new (FOO_TYPE_SCROLL_AREA, NULL); } void foo_scroll_area_set_min_size (FooScrollArea *area, int min_width, int min_height) { FooScrollAreaPrivate *priv = area->priv; priv->min_width = min_width; priv->min_height = min_height; /* FIXME: think through invalidation. * * Goals: - no repainting everything on size_allocate(), * - make sure input boxes are invalidated when * needed */ gtk_widget_queue_resize (GTK_WIDGET (area)); } #if 0 static void warn_about_adding_input_outside_expose (const char *func) { static gboolean warned = FALSE; if (!warned) { g_warning ("%s() can only be called " "from the paint handler for the FooScrollArea\n", func); warned = TRUE; } } #endif static void user_to_device (double *x, double *y, gpointer data) { cairo_t *cr = data; cairo_user_to_device (cr, x, y); } typedef struct { FooScrollArea *area; FooScrollAreaEventFunc func; gpointer data; } Info; static void backing_store_event (BackingStore *store, BackingStoreEvent *event, gpointer data) { Info *info = data; BackingStoreEvent e = *event; const char *type; e.x = event->x + info->area->priv->x_offset; e.y = event->y + info->area->priv->y_offset; if (info->func) info->func (info->area, &e, info->data); #if 0 if (event->type == BACKING_STORE_BUTTON_PRESS) type = "press"; else if (event->type == BACKING_STORE_BUTTON_RELEASE) type = "reelase"; else if (event->type == BACKING_STORE_LEAVE) type = "leave"; else if (event->type == BACKING_STORE_ENTER) type = "enter"; else if (event->type == BACKING_STORE_MOTION) type = "motion"; else type = "unknown"; g_print ("backing: %s %d %d %p %p\n", type, e.x, e.y, info->func, info->data); #endif } static guint hash_info (gpointer a) { Info *info = a; return (gsize)info->func ^ (gsize)info->data; } static gboolean equal_info (gconstpointer a, gconstpointer b) { const Info *ia = a, *ib = b; return ia->func == ib->func && ia->data == ib->data; } static void add_path (FooScrollArea *area, cairo_t *cr, gboolean is_stroke, FooScrollAreaEventFunc func, gpointer data) { FooScrollAreaPrivate *priv = area->priv; #if 0 double x1, y1, x2, y2; GdkRectangle r; static GHashTable *hash; /* FIXME: This doesn't work because the rectangle we * get from _extents() is in user coordinates, so * if we just transform it back to device coordinates * we may get the wrong bounding box * * Ironically, the extent that cairo produces is not * actually tight - because cairo first converted * from device coordinates ... */ if (is_stroke) cairo_stroke_extents (cr, &x1, &y1, &x2, &y2); else cairo_fill_extents (cr, &x1, &y1, &x2, &y2); cairo_user_to_device (cr, &x1, &y1); cairo_user_to_device (cr, &x2, &y2); r.x = MIN (x1, x2) - 1; r.y = MIN (y1, y2) - 1; r.width = MAX (x1, x2) - r.x + 2; r.width = MAX (y1, y2) - r.y + 2; path->extents = r; #endif static GHashTable *hash; if (!hash) hash = g_hash_table_new (hash_info, equal_info); Info *info = g_new0 (Info, 1); info->area = area; info->func = func; info->data = data; if (g_hash_table_lookup (hash, info)) { Info *old = info; info = g_hash_table_lookup (hash, info); g_free (old); } else g_hash_table_insert (hash, info, info); backing_store_add_input (priv->store, cr, is_stroke, backing_store_event, info); #if 0 if (gdk_region_rect_in (priv->current_input->region, &r) != GDK_OVERLAP_RECTANGLE_OUT) { #endif #if 0 } #endif } /* FIXME: we probably really want a * * foo_scroll_area_add_input_from_fill (area, cr, ...); * and * foo_scroll_area_add_input_from_stroke (area, cr, ...); * as well. */ void foo_scroll_area_add_input_from_fill (FooScrollArea *area, cairo_t *cr, FooScrollAreaEventFunc func, gpointer data) { FooScrollAreaPrivate *priv = area->priv; g_return_if_fail (FOO_IS_SCROLL_AREA (area)); g_return_if_fail (cr != NULL); add_path (area, cr, FALSE, func, data); } void foo_scroll_area_add_input_from_stroke (FooScrollArea *area, cairo_t *cr, FooScrollAreaEventFunc func, gpointer data) { FooScrollAreaPrivate *priv = area->priv; g_return_if_fail (FOO_IS_SCROLL_AREA (area)); g_return_if_fail (cr != NULL); add_path (area, cr, TRUE, func, data); } void foo_scroll_area_invalidate_region (FooScrollArea *area, GdkRegion *region) { FooScrollAreaPrivate *priv = area->priv; g_return_if_fail (FOO_IS_SCROLL_AREA (area)); if (GTK_WIDGET_REALIZED (area)) { canvas_to_store (area, region); backing_store_invalidate_region (priv->store, region); store_to_canvas (area, region); canvas_to_window (area, region); gdk_window_invalidate_region (GTK_WIDGET (area)->window, region, TRUE); window_to_canvas (area, region); } } void foo_scroll_area_invalidate_rect (FooScrollArea *area, int x, int y, int width, int height) { GdkRectangle rect = { x, y, width, height }; GdkRegion *region; g_return_if_fail (FOO_IS_SCROLL_AREA (area)); region = gdk_region_rectangle (&rect); foo_scroll_area_invalidate_region (area, region); gdk_region_destroy (region); } void foo_scroll_area_invalidate (FooScrollArea *area) { FooScrollAreaPrivate *priv = area->priv; GtkWidget *widget = GTK_WIDGET (area); foo_scroll_area_invalidate_rect (area, priv->x_offset, priv->y_offset, widget->allocation.width, widget->allocation.height); } void foo_scroll_area_begin_grab (FooScrollArea *area, FooScrollAreaEventFunc func, gpointer input_data) { g_return_if_fail (FOO_IS_SCROLL_AREA (area)); begin_grab_internal (area, FALSE, func, input_data); } void foo_scroll_area_end_grab (FooScrollArea *area) { g_return_if_fail (FOO_IS_SCROLL_AREA (area)); end_grab_internal (area, FALSE); } gboolean foo_scroll_area_is_grabbed (FooScrollArea *area) { FooScrollAreaPrivate *priv = area->priv; return priv->grabbed; } void foo_scroll_area_set_viewport_pos (FooScrollArea *area, int x, int y) { FooScrollAreaPrivate *priv = area->priv; int x_changed; int y_changed; GtkAdjustment *hadj, *vadj; hadj = priv->hadj; vadj = priv->vadj; x_changed = (int)hadj->value != x; y_changed = (int)vadj->value != y; hadj->value = x; vadj->value = y; set_adjustment_values (area); if (x_changed && hadj->upper >= hadj->page_size) { #if 0 g_print ("changed x\n"); #endif gtk_adjustment_value_changed (hadj); } if (y_changed && vadj->upper >= vadj->page_size) { #if 0 g_print ("changed y\n"); #endif gtk_adjustment_value_changed (vadj); } } static gboolean rect_contains (const GdkRectangle *rect, int x, int y) { return (x >= rect->x && y >= rect->y && x < rect->x + rect->width && y < rect->y + rect->height); } static void stop_scrolling (FooScrollArea *area) { FooScrollAreaPrivate *priv = area->priv; #if 0 g_print ("stop scrolling\n"); #endif if (priv->auto_scroll_info) { g_source_remove (priv->auto_scroll_info->timeout_id); #if 0 g_print ("removed idle\n"); #endif g_timer_destroy (priv->auto_scroll_info->timer); g_free (priv->auto_scroll_info); priv->auto_scroll_info = NULL; } } static gboolean scroll_idle (gpointer data) { GdkRectangle viewport, new_viewport; FooScrollArea *area = data; FooScrollAreaPrivate *priv = area->priv; AutoScrollInfo *info = priv->auto_scroll_info; #if 0 int dx, dy; #endif int new_x, new_y; double elapsed; #if 0 g_print ("idle\n"); #endif get_viewport (area, &viewport); #if 0 g_print ("old info: %d %d\n", info->dx, info->dy); g_print ("timeout (%d %d)\n", dx, dy); #endif #if 0 viewport.x += info->dx; viewport.y += info->dy; #endif #if 0 g_print ("new info %d %d\n", info->dx, info->dy); #endif if (priv->need_flush) { gdk_flush (); priv->need_flush = FALSE; } elapsed = g_timer_elapsed (info->timer, NULL); info->res_x = elapsed * info->dx / 0.2; info->res_y = elapsed * info->dy / 0.2; #if 0 g_print ("%f: %f %f\n", elapsed, info->res_x, info->res_y); #endif new_x = viewport.x + info->res_x; new_y = viewport.y + info->res_y; #if 0 g_print ("%f\n", elapsed * (info->dx / 0.2)); #endif #if 0 g_print ("new_x, new_y\n: %d %d\n", new_x, new_y); #endif foo_scroll_area_set_viewport_pos (area, new_x, new_y); get_viewport (area, &new_viewport); if (viewport.x == new_viewport.x && viewport.y == new_viewport.y && (info->res_x > 1.0 || info->res_y > 1.0 || info->res_x < -1.0 || info->res_y < -1.0)) { stop_scrolling (area); /* stop scrolling if it didn't have an effect */ return FALSE; } return TRUE; } static void ensure_scrolling (FooScrollArea *area, int dx, int dy) { FooScrollAreaPrivate *priv = area->priv; if (!priv->auto_scroll_info) { #if 0 g_print ("start scrolling\n"); #endif priv->auto_scroll_info = g_new0 (AutoScrollInfo, 1); priv->auto_scroll_info->timeout_id = g_idle_add (scroll_idle, area); #if 0 g_print ("added idle\n"); #endif priv->auto_scroll_info->timer = g_timer_new (); } #if 0 g_print ("setting scrolling to %d %d\n", dx, dy); #endif #if 0 g_print ("dx, dy: %d %d\n", dx, dy); #endif priv->auto_scroll_info->dx = dx; priv->auto_scroll_info->dy = dy; } void foo_scroll_area_auto_scroll (FooScrollArea *area, FooScrollAreaEvent *event) { #define SCROLL_MARGIN 4 GdkRectangle viewport; get_viewport (area, &viewport); viewport.x += SCROLL_MARGIN; viewport.y += SCROLL_MARGIN; viewport.width -= 2 * SCROLL_MARGIN; viewport.height -= 2 * SCROLL_MARGIN; if (rect_contains (&viewport, event->x, event->y)) { #if 0 g_print ("in viewport\n"); #endif stop_scrolling (area); } else { int dx, dy; dx = dy = 0; if (event->y < viewport.y) { dy = event->y - viewport.y; dy = MIN (dy + 2, 0); } else if (event->y >= viewport.y + viewport.height) { #if 0 g_print ("event->y: %d, y + h: %d\n", event->y, viewport.y + viewport.height); #endif dy = event->y - (viewport.y + viewport.height - 1); dy = MAX (dy - 2, 0); } if (event->x < viewport.x) { dx = event->x - viewport.x; dx = MIN (dx + 2, 0); } else if (event->x >= viewport.x + viewport.width) { dx = event->x - (viewport.x + viewport.width - 1); dx = MAX (dx - 2, 0); } #if 0 g_print ("dx, dy: %d %d\n", dx, dy); #endif ensure_scrolling (area, dx, dy); } } void foo_scroll_area_begin_auto_scroll (FooScrollArea *area) { /* noop for now */ } void foo_scroll_area_end_auto_scroll (FooScrollArea *area) { stop_scrolling (area); } void foo_scroll_area_set_paint_updates (FooScrollArea *area, gboolean paint_upd) { FooScrollAreaPrivate *priv = area->priv; priv->paint_updates = paint_upd; }