diff options
author | Søren Sandmann <sandmann@redhat.com> | 2008-06-04 02:14:44 -0400 |
---|---|---|
committer | Søren Sandmann <sandmann@redhat.com> | 2008-06-04 02:14:44 -0400 |
commit | 44ffeb68a9914aba719767ce4c318abceaa87b6a (patch) | |
tree | 38651dee74090d3dae6d38ec3de3d49f43ae6620 |
Various scroll area fixes
-rw-r--r-- | TODO | 4 | ||||
-rw-r--r-- | backingstore.c | 263 | ||||
-rw-r--r-- | backingstore.h | 35 | ||||
-rwxr-xr-x | build.sh | 8 | ||||
-rw-r--r-- | deck.h | 7 | ||||
-rw-r--r-- | main.c | 114 | ||||
-rw-r--r-- | oppress.glade | 429 | ||||
-rw-r--r-- | scrollarea.c | 1705 | ||||
-rw-r--r-- | scrollarea.h | 107 |
9 files changed, 2672 insertions, 0 deletions
@@ -0,0 +1,4 @@ +- Think of a better name than oppress + - Populst? + - Demagogue? + diff --git a/backingstore.c b/backingstore.c new file mode 100644 index 0000000..5e040c4 --- /dev/null +++ b/backingstore.c @@ -0,0 +1,263 @@ +#include "backingstore.h" + +struct BackingStore +{ + GdkPixmap *pixmap; + GdkRegion *update_region; + int width; + int height; +}; + +BackingStore * +backing_store_new (GdkWindow *window, + int width, + int height) +{ + BackingStore *store = g_new0 (BackingStore, 1); + GdkRectangle rect = { 0, 0, width, height }; + + store->pixmap = gdk_pixmap_new (window, width, height, -1); + store->update_region = gdk_region_rectangle (&rect); + store->width = width; + store->height = height; + + return store; +} + +void +backing_store_free (BackingStore *store) +{ + g_object_unref (store->pixmap); + gdk_region_destroy (store->update_region); + g_free (store); +} + +void +backing_store_scroll (BackingStore *store, + int dx, + int dy) +{ + GdkGC *gc = gdk_gc_new (store->pixmap); + GdkRectangle rect; + + gdk_draw_drawable (store->pixmap, gc, store->pixmap, + 0, 0, dx, dy, + store->width, store->height); + + gdk_region_offset (store->update_region, dx, dy); + + /* Invalidate vertically */ + rect.x = 0; + rect.width = store->width; + + if (dy > 0) + { + rect.y = 0; + rect.height = dy; + } + else + { + rect.y = store->height + dy; + rect.height = -dy; + } + + gdk_region_union_with_rect (store->update_region, &rect); + + /* Invalidate horizontally */ + rect.y = 0; + rect.height = store->height; + + if (dx > 0) + { + rect.x = 0; + rect.width = dx; + } + else + { + rect.x = store->width + dx; + rect.width = -dx; + } + + gdk_region_union_with_rect (store->update_region, &rect); +} + +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); +} + +void +backing_store_invalidate_rect (BackingStore *store, + GdkRectangle *rect) +{ + g_return_if_fail (store != NULL); + g_return_if_fail (rect != NULL); + + gdk_region_union_with_rect (store->update_region, rect); +} + +void +backing_store_invalidate_region (BackingStore *store, + GdkRegion *region) +{ + g_return_if_fail (store != NULL); + g_return_if_fail (region != NULL); + + gdk_region_union (store->update_region, region); +} + +void +backing_store_invalidate_all (BackingStore *store) +{ + GdkRectangle rect; + + g_return_if_fail (store != NULL); + + gdk_region_destroy (store->update_region); + + rect.x = 0; + rect.y = 0; + rect.width = store->width; + rect.height = store->height; + + store->update_region = gdk_region_rectangle (&rect); +} + +static void +simple_draw_drawable (GdkDrawable *dst, + GdkDrawable *src, + int src_x, + int src_y, + int dst_x, + int dst_y, + int width, + int height) +{ + GdkGC *gc = gdk_gc_new (dst); + + gdk_draw_drawable (dst, gc, src, src_x, src_y, dst_x, dst_y, width, height); + + g_object_unref (gc); +} + +void +backing_store_resize (BackingStore *store, + int width, + int height) +{ + GdkPixmap *pixmap; + GdkRegion *old, *invalid; + GdkRectangle r; + + width = MAX (width, 1); + height = MAX (height, 1); + + pixmap = gdk_pixmap_new (store->pixmap, width, height, -1); + + /* Unfortunately we don't know in which direction we were resized, + * so we just assume we were dragged from the south-east corner. + * + * Although, maybe we could get the root coordinates of the input-window? + * That might just work, actually. We need to make sure metacity uses + * static gravity for the window before this will be useful. + */ + simple_draw_drawable (pixmap, store->pixmap, 0, 0, 0, 0, -1, -1); + + g_object_unref (store->pixmap); + + store->pixmap = pixmap; + + r.x = r.y = 0; + r.height = store->height; + r.width = store->width; + + old = gdk_region_rectangle (&r); + + store->width = width; + store->height = height; + + r.height = store->height; + r.width = store->width; + + invalid = gdk_region_rectangle (&r); + + gdk_region_subtract (invalid, old); + + backing_store_invalidate_region (store, invalid); + + gdk_region_destroy (old); + gdk_region_destroy (invalid); +} + + +static void +cclip_to_region (cairo_t *cr, GdkRegion *region) +{ + int n_rects; + GdkRectangle *rects; + + gdk_region_get_rectangles (region, &rects, &n_rects); + + cairo_new_path (cr); + while (n_rects--) + { + GdkRectangle *rect = &(rects[n_rects]); + + cairo_rectangle (cr, rect->x, rect->y, rect->width, rect->height); + } + cairo_clip (cr); + + g_free (rects); +} + +void +backing_store_draw (BackingStore *store, + GdkDrawable *dest, + GdkRegion *clip, + int dest_x, + int dest_y) +{ + GdkGC *gc = gdk_gc_new (dest); + + gdk_gc_set_clip_region (gc, clip); + + gdk_draw_drawable (dest, gc, store->pixmap, + 0, 0, dest_x, dest_y, store->width, store->height); + g_object_unref (gc); +} + +void +backing_store_process_updates (BackingStore *store, + BackingPaintFunc func, + gpointer data) +{ + cairo_t *cr = gdk_cairo_create (store->pixmap); + GdkRegion *region = store->update_region; + store->update_region = gdk_region_new (); + + cclip_to_region (cr, region); + + cairo_set_source_rgb (cr, g_random_double(), 0, 0); + + cairo_paint (cr); + + func (cr, region, data); + + gdk_region_destroy (region); + cairo_destroy (cr); +} diff --git a/backingstore.h b/backingstore.h new file mode 100644 index 0000000..2d39214 --- /dev/null +++ b/backingstore.h @@ -0,0 +1,35 @@ +#include <gtk/gtk.h> +#include <gdk/gdk.h> +#include <cairo.h> + +typedef struct BackingStore BackingStore; + +typedef void (* BackingPaintFunc) (cairo_t *cr, + GdkRegion *region, + gpointer data); +typedef gboolean (* BackingInputFunc) (gpointer input_data, + gpointer user_data); + +BackingStore *backing_store_new (GdkWindow *window, + int width, + int height); +void backing_store_free (BackingStore *store); +void backing_store_draw (BackingStore *store, + GdkDrawable *dest, + GdkRegion *clip, + int dest_x, + int dest_y); +void backing_store_scroll (BackingStore *store, + int dx, + int dy); +void backing_store_invalidate_rect (BackingStore *store, + GdkRectangle *rect); +void backing_store_invalidate_region (BackingStore *store, + GdkRegion *region); +void backing_store_invalidate_all (BackingStore *store); +void backing_store_resize (BackingStore *store, + int width, + int height); +void backing_store_process_updates (BackingStore *store, + BackingPaintFunc func, + gpointer data); diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b6662d6 --- /dev/null +++ b/build.sh @@ -0,0 +1,8 @@ +gcc -Wall -g \ + `pkg-config --cflags --libs gtk+-2.0 libglade-2.0` \ + -DGLADE_FILE=\"oppress.glade\" \ + \ + scrollarea.c \ + foo-marshal.c \ + main.c \ + backingstore.c
\ No newline at end of file @@ -0,0 +1,7 @@ +typedef struct Deck Deck; +typedef struct Slide Slide; + +Deck * deck_new (void); +int deck_get_n_slides (Deck *deck); +Slide *deck_get_nth_slide (Deck *deck, + int n); @@ -0,0 +1,114 @@ +#include <gtk/gtk.h> +#include <glade/glade.h> +#include <glib/gprintf.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include "scrollarea.h" + +typedef struct +{ + GladeXML * xml; + GtkWidget * main_area; + GtkWidget * thumbnails; +} App; + +static void * +get_widget (App *app, const char *name) +{ + void *result = glade_xml_get_widget (app->xml, name); + + if (!result) + g_error ("Could not find widget %s\n", name); + + return result; +} + +static void +on_thumbs_paint (FooScrollArea *scroll_area, + cairo_t *cr, + GdkRectangle *extents, + GdkRegion *region, + gpointer data) +{ + g_print ("paint thumbs\n"); +} + + +static void +on_main_paint (FooScrollArea *scroll_area, + cairo_t *cr, + GdkRectangle *extents, + GdkRegion *region, + gpointer data) +{ + g_print ("paint %d %d %d %d\n", extents->x, extents->y, extents->width, extents->height); +} + +static void +set_size (GtkWindow *window) +{ + GdkScreen *screen; + int monitor_num; + GdkRectangle monitor; + int width, height; + GtkWidget *widget = GTK_WIDGET (window); + + screen = gtk_widget_get_screen (widget); + monitor_num = gdk_screen_get_monitor_at_window (screen, widget->window); + + gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor); + + width = monitor.width * 3 / 4; + height = monitor.height * 3 / 4; + + gtk_window_resize (window, width, height); +} + +static void +on_viewport_changed (FooScrollArea *scroll_area, + GdkRectangle *viewport, + GdkRectangle *old_viewport) +{ + foo_scroll_area_set_size (scroll_area, 800, 30); +} + +int +main (int argc, char **argv) +{ + App *app; + + gtk_init (&argc, &argv); + + app = g_new0 (App, 1); + app->xml = glade_xml_new (GLADE_FILE, NULL, NULL); + + app->main_area = foo_scroll_area_new(); + app->thumbnails = foo_scroll_area_new(); + + gtk_container_add ( + GTK_CONTAINER (get_widget (app, "main_area_scrolled_window")), + app->main_area); + + gtk_container_add ( + GTK_CONTAINER (get_widget (app, "thumbnails_scrolled_window")), + app->thumbnails); + + foo_scroll_area_set_size (app->main_area, 80, 30); + foo_scroll_area_set_min_size (app->thumbnails, 200, -1); + + g_signal_connect (app->main_area, "paint", + G_CALLBACK (on_main_paint), app); +#if 0 + g_signal_connect (app->main_area, "viewport_changed", + G_CALLBACK (on_viewport_changed), app); +#endif + + g_signal_connect (app->thumbnails, "paint", + G_CALLBACK (on_thumbs_paint), app); + + set_size (get_widget (app, "main_window")); + gtk_widget_show_all (get_widget (app, "main_window")); + + gtk_main (); +} diff --git a/oppress.glade b/oppress.glade new file mode 100644 index 0000000..43c3412 --- /dev/null +++ b/oppress.glade @@ -0,0 +1,429 @@ +<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*--> +<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd"> + +<glade-interface> + +<widget class="GtkWindow" id="main_window"> + <property name="visible">True</property> + <property name="title" translatable="yes">Oppress</property> + <property name="type">GTK_WINDOW_TOPLEVEL</property> + <property name="window_position">GTK_WIN_POS_NONE</property> + <property name="modal">False</property> + <property name="resizable">True</property> + <property name="destroy_with_parent">False</property> + <property name="decorated">True</property> + <property name="skip_taskbar_hint">False</property> + <property name="skip_pager_hint">False</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property> + <property name="gravity">GDK_GRAVITY_NORTH_WEST</property> + <property name="focus_on_map">True</property> + <property name="urgency_hint">False</property> + + <child> + <widget class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">0</property> + + <child> + <widget class="GtkMenuBar" id="menubar1"> + <property name="visible">True</property> + <property name="pack_direction">GTK_PACK_DIRECTION_LTR</property> + <property name="child_pack_direction">GTK_PACK_DIRECTION_LTR</property> + + <child> + <widget class="GtkMenuItem" id="menuitem1"> + <property name="visible">True</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu1"> + + <child> + <widget class="GtkImageMenuItem" id="new1"> + <property name="visible">True</property> + <property name="label">gtk-new</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_new1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="open1"> + <property name="visible">True</property> + <property name="label">gtk-open</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_open1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="save1"> + <property name="visible">True</property> + <property name="label">gtk-save</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_save1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="save_as1"> + <property name="visible">True</property> + <property name="label">gtk-save-as</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_save_as1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkSeparatorMenuItem" id="separatormenuitem1"> + <property name="visible">True</property> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="quit1"> + <property name="visible">True</property> + <property name="label">gtk-quit</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_quit1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menuitem2"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu2"> + + <child> + <widget class="GtkImageMenuItem" id="cut1"> + <property name="visible">True</property> + <property name="label">gtk-cut</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_cut1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="copy1"> + <property name="visible">True</property> + <property name="label">gtk-copy</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_copy1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="paste1"> + <property name="visible">True</property> + <property name="label">gtk-paste</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_paste1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + + <child> + <widget class="GtkImageMenuItem" id="delete1"> + <property name="visible">True</property> + <property name="label">gtk-delete</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_delete1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menuitem3"> + <property name="visible">True</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu3"> + </widget> + </child> + </widget> + </child> + + <child> + <widget class="GtkMenuItem" id="menuitem4"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + + <child> + <widget class="GtkMenu" id="menu4"> + + <child> + <widget class="GtkMenuItem" id="about1"> + <property name="visible">True</property> + <property name="label" translatable="yes">_About</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_about1_activate" last_modification_time="Wed, 04 Jun 2008 03:11:49 GMT"/> + </widget> + </child> + </widget> + </child> + </widget> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + + <child> + <widget class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="spacing">0</property> + + <child> + <widget class="GtkToolbar" id="toolbar1"> + <property name="visible">True</property> + <property name="orientation">GTK_ORIENTATION_VERTICAL</property> + <property name="toolbar_style">GTK_TOOLBAR_BOTH_HORIZ</property> + <property name="tooltips">True</property> + <property name="show_arrow">False</property> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton2"> + <property name="visible">True</property> + <property name="label" translatable="yes">Text</property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Curve</property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton3"> + <property name="visible">True</property> + <property name="label" translatable="yes">New slide</property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton4"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton5"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton6"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton7"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton8"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton9"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + + <child> + <widget class="GtkRadioToolButton" id="radiotoolbutton10"> + <property name="visible">True</property> + <property name="label" translatable="yes"></property> + <property name="use_underline">True</property> + <property name="visible_horizontal">True</property> + <property name="visible_vertical">True</property> + <property name="is_important">False</property> + <property name="active">False</property> + <property name="group">radiotoolbutton2</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> + + <child> + <widget class="GtkScrolledWindow" id="main_area_scrolled_window"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + + <child> + <widget class="GtkScrolledWindow" id="thumbnails_scrolled_window"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> + <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property> + <property name="shadow_type">GTK_SHADOW_NONE</property> + <property name="window_placement">GTK_CORNER_TOP_LEFT</property> + + <child> + <placeholder/> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + </widget> + </child> +</widget> + +</glade-interface> diff --git a/scrollarea.c b/scrollarea.c new file mode 100644 index 0000000..c4a6954 --- /dev/null +++ b/scrollarea.c @@ -0,0 +1,1705 @@ +#include <gdk/gdkprivate.h> /* For GDK_PARENT_RELATIVE_BG */ +#include <gdk/gdkx.h> +#include <X11/Xlib.h> +#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 InputPath InputPath; +typedef struct InputRegion InputRegion; +typedef struct AutoScrollInfo AutoScrollInfo; + +struct InputPath +{ + gboolean is_stroke; + cairo_fill_rule_t fill_rule; + double line_width; + cairo_path_t *path; /* In canvas coordinates */ + + FooScrollAreaEventFunc func; + gpointer data; + + InputPath *next; +}; + +/* InputRegions are mutually disjoint */ +struct InputRegion +{ + GdkRegion *region; /* the boundary of this area in + * canvas coordinates + */ + InputPath *paths; +}; + +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 + */ + InputRegion *current_input; + + gboolean grabbed; + FooScrollAreaEventFunc grab_func; + gpointer grab_data; + + BackingStore *store; + + guint32 motion_notify_time; +}; + +enum +{ + 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 *scroll_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); + + GTK_WIDGET_CLASS (parent_class)->map (widget); + +#if 0 + if (area->priv->input_window) + gdk_window_show (area->priv->input_window); +#endif +} + +static void +foo_scroll_area_unmap (GtkWidget *widget) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + +#if 0 + if (area->priv->input_window) + gdk_window_hide (area->priv->input_window); +#endif + + GTK_WIDGET_CLASS (parent_class)->unmap (widget); +} + +static void +foo_scroll_area_finalize (GObject *object) +{ + FooScrollArea *scroll_area = FOO_SCROLL_AREA (object); + + g_object_unref (scroll_area->priv->hadj); + g_object_unref (scroll_area->priv->vadj); + + g_ptr_array_free (scroll_area->priv->input_regions, TRUE); + + g_free (scroll_area->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); + + 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 *scroll_area) +{ +#if 0 + GTK_WIDGET_SET_FLAGS (scroll_area, GTK_NO_WINDOW); +#endif + + gtk_widget_set_redraw_on_allocate (GTK_WIDGET (scroll_area), FALSE); + + scroll_area->priv = g_new0 (FooScrollAreaPrivate, 1); + scroll_area->priv->width = 0; + scroll_area->priv->height = 0; + scroll_area->priv->hadj = g_object_ref_sink (new_adjustment()); + scroll_area->priv->vadj = g_object_ref_sink (new_adjustment()); + scroll_area->priv->x_offset = 0.0; + scroll_area->priv->y_offset = 0.0; + scroll_area->priv->min_width = -1; + scroll_area->priv->min_height = -1; + scroll_area->priv->auto_scroll_info = NULL; + scroll_area->priv->input_regions = g_ptr_array_new (); + + gtk_widget_set_double_buffered (GTK_WIDGET (scroll_area), FALSE); +} + +static void +translate_cairo_device (cairo_t *cr, + int x_offset, + int y_offset) +{ + cairo_surface_t *surface = cairo_get_target (cr); + double dev_x; + double dev_y; + + cairo_surface_get_device_offset (surface, &dev_x, &dev_y); + dev_x += x_offset; + dev_y += y_offset; + cairo_surface_set_device_offset (surface, dev_x, dev_y); +} + +#if 0 +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); + } +} +#endif + +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 +input_path_free_list (InputPath *paths) +{ + if (!paths) + return; + + input_path_free_list (paths->next); + cairo_path_destroy (paths->path); + g_free (paths); +} + +static void +input_region_free (InputRegion *region) +{ + input_path_free_list (region->paths); + gdk_region_destroy (region->region); + + g_free (region); +} + +static void +get_viewport (FooScrollArea *scroll_area, + GdkRectangle *viewport) +{ + GtkWidget *widget = GTK_WIDGET (scroll_area); + + viewport->x = scroll_area->priv->x_offset; + viewport->y = scroll_area->priv->y_offset; + viewport->width = widget->allocation.width; + viewport->height = widget->allocation.height; +} + +static void +allocation_to_canvas (FooScrollArea *area, + int *x, + int *y) +{ + *x += area->priv->x_offset; + *y += area->priv->y_offset; +} + +static void +clear_exposed_input_region (FooScrollArea *area, + GdkRegion *exposed) /* in canvas coordinates */ +{ + int i; + GdkRegion *viewport_reg; + GdkRectangle viewport; + + get_viewport (area, &viewport); + + viewport_reg = gdk_region_rectangle (&viewport); + gdk_region_subtract (viewport_reg, exposed); + + for (i = 0; i < area->priv->input_regions->len; ++i) + { + InputRegion *region = area->priv->input_regions->pdata[i]; + + gdk_region_intersect (region->region, viewport_reg); + + if (gdk_region_empty (region->region)) + { + input_region_free (region); + g_ptr_array_remove_index_fast (area->priv->input_regions, i--); + } + } + + gdk_region_destroy (viewport_reg); +} + +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 +canvas_to_window (FooScrollArea *area, + GdkRegion *region) +{ + GtkWidget *widget = GTK_WIDGET (area); + + gdk_region_offset (region, + -area->priv->x_offset, + -area->priv->y_offset); +} + +static void +window_to_canvas (FooScrollArea *area, + GdkRegion *region) +{ + GtkWidget *widget = GTK_WIDGET (area); + + gdk_region_offset (region, + area->priv->x_offset, + area->priv->y_offset); +} + +static void +canvas_to_store (FooScrollArea *area, + GdkRegion *region) +{ + gdk_region_offset (region, -area->priv->x_offset, -area->priv->y_offset); + +} + +static void +store_to_canvas (FooScrollArea *area, + GdkRegion *region) +{ + gdk_region_offset (region, area->priv->x_offset, area->priv->y_offset); +} + +static gboolean paint_updates = FALSE; + +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 +do_exposes (cairo_t *cr, + GdkRegion *store_region, + gpointer data) +{ + FooScrollArea *scroll_area = data; + GdkRegion *region; + GdkRectangle rect; + GdkRegion *r; + + /* Paint background */ + setup_background_cr (GTK_WIDGET (scroll_area)->window, cr, 0, 0); + cairo_paint (cr); + + if (paint_updates) + paint_region (scroll_area, store_region, 0.2, 0.2, 0.8, 0.4); + + /* 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 (scroll_area, region); + + /* Set up input areas */ + clear_exposed_input_region (scroll_area, region); + + scroll_area->priv->current_input = g_new0 (InputRegion, 1); + scroll_area->priv->current_input->region = gdk_region_copy (region); + scroll_area->priv->current_input->paths = NULL; + g_ptr_array_add (scroll_area->priv->input_regions, + scroll_area->priv->current_input); + + /* Set up cairo context and emit the signal */ + translate_cairo_device (cr, + -scroll_area->priv->x_offset, + -scroll_area->priv->y_offset); + + /* Restrict to the actual canvas */ + rect.x = 0; + rect.y = 0; + rect.width = scroll_area->priv->width; + rect.height = scroll_area->priv->height; + + r = gdk_region_rectangle (&rect); + gdk_region_intersect (region, r); + gdk_region_destroy (r); + + if (!gdk_region_empty (region)) + { + gdk_region_get_clipbox (region, &rect); + + g_signal_emit (scroll_area, signals[PAINT], 0, cr, &rect, region); + } + + scroll_area->priv->current_input = NULL; + gdk_region_destroy (region); +} + +static gboolean +foo_scroll_area_expose (GtkWidget *widget, + GdkEventExpose *expose) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + + backing_store_process_updates (area->priv->store, do_exposes, area); + + if (paint_updates) + { + paint_region (area, expose->region, 0.8, 0.2, 0.2, 0.4); + + gdk_flush (); + g_usleep (70000); + } + + backing_store_draw (area->priv->store, widget->window, + expose->region, 0, 0); + + return TRUE; +} + +void +foo_scroll_area_get_viewport (FooScrollArea *scroll_area, + GdkRectangle *viewport) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (scroll_area)); + + if (!viewport) + return; + + get_viewport (scroll_area, viewport); +} + +static void +process_event (FooScrollArea *scroll_area, + FooScrollAreaEventType input_type, + int x, + int y, + int button); + +static void +emit_viewport_changed (FooScrollArea *scroll_area, + GdkRectangle *new_viewport, + GdkRectangle *old_viewport) +{ + int px, py; + + g_signal_emit (scroll_area, signals[VIEWPORT_CHANGED], 0, + new_viewport, old_viewport); + + if (new_viewport->x != old_viewport->x || + new_viewport->y != old_viewport->y || + new_viewport->width != old_viewport->width || + new_viewport->height != old_viewport->height) + { + gdk_window_get_pointer (GTK_WIDGET (scroll_area)->window, &px, &py, NULL); + + process_event (scroll_area, FOO_MOTION, px, py, 0); + } +} + +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 *scroll_area) +{ + GtkAllocation *allocation = >K_WIDGET (scroll_area)->allocation; + + GtkAdjustment *hadj = scroll_area->priv->hadj; + GtkAdjustment *vadj = scroll_area->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 = scroll_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 = scroll_area->priv->height; + + clamp_adjustment (hadj); + clamp_adjustment (vadj); + + return TRUE; +} + +static void +foo_scroll_area_realize (GtkWidget *widget) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + GdkWindowAttr attributes; + gint attributes_mask; + + g_print ("realizing\n"); + + 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); + + 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); + +#if 0 + if (area->priv->input_window) + { + gdk_window_set_user_data (area->priv->input_window, NULL); + gdk_window_destroy (area->priv->input_window); + area->priv->input_window = NULL; + } +#endif + + backing_store_free (area->priv->store); + + GTK_WIDGET_CLASS (parent_class)->unrealize (widget); +} + +static void +allocation_to_canvas_region (FooScrollArea *area, + GdkRegion *region) +{ + gdk_region_offset (region, area->priv->x_offset, area->priv->y_offset); +} + + +static void +foo_scroll_area_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + FooScrollArea *scroll_area = FOO_SCROLL_AREA (widget); + GdkRectangle new_viewport; + GdkRectangle old_viewport; + GdkRegion *old_allocation; + GdkRegion *invalid; + + get_viewport (scroll_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 (scroll_area->priv->store, + widget->allocation.width, + widget->allocation.height); + } + + set_adjustment_values (scroll_area); + + get_viewport (scroll_area, &new_viewport); + + emit_viewport_changed (scroll_area, &new_viewport, &old_viewport); +} + +static void +emit_input (FooScrollArea *scroll_area, + FooScrollAreaEventType type, + int x, + int y, + int button, + FooScrollAreaEventFunc func, + gpointer data) +{ + FooScrollAreaEvent event; + + if (!func) + return; + + if (type != FOO_MOTION) + emit_input (scroll_area, FOO_MOTION, x, y, button, func, data); + +#if 0 + x += scroll_area->priv->x_offset; + y += scroll_area->priv->y_offset; +#endif + + event.type = type; + event.x = x; + event.y = y; + event.button = button; + + func (scroll_area, &event, data); +} + +#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 void +process_event (FooScrollArea *scroll_area, + FooScrollAreaEventType input_type, + int x, + int y, + int button) +{ + GtkWidget *widget = GTK_WIDGET (scroll_area); + int i; + + allocation_to_canvas (scroll_area, &x, &y); + + if (scroll_area->priv->grabbed) + { + emit_input (scroll_area, input_type, x, y, button, + scroll_area->priv->grab_func, + scroll_area->priv->grab_data); + return; + } + + +#if 0 + x += widget->allocation.x; + y += widget->allocation.y; +#endif + +#if 0 + g_print ("number of input regions: %d\n", scroll_area->priv->input_regions->len); +#endif + + for (i = 0; i < scroll_area->priv->input_regions->len; ++i) + { + InputRegion *region = scroll_area->priv->input_regions->pdata[i]; + +#if 0 + g_print ("%d ", i); + print_region ("region:", region->region); +#endif + + if (gdk_region_point_in (region->region, x, y)) + { + InputPath *path; + int n_paths = 0; + + path = region->paths; + while (path) + { + cairo_t *cr; + gboolean inside; + + cr = gdk_cairo_create (widget->window); + cairo_set_fill_rule (cr, path->fill_rule); + cairo_set_line_width (cr, path->line_width); + cairo_append_path (cr, path->path); + + if (path->is_stroke) + inside = cairo_in_stroke (cr, x, y); + else + inside = cairo_in_fill (cr, x, y); + + cairo_destroy (cr); + + if (inside) + { + emit_input (scroll_area, input_type, + x, y, button, + path->func, + path->data); + return; + } + + path = path->next; + n_paths++; + } + + /* Since the regions are all disjoint, no other region + * can match. Of course we could be clever and try and + * sort the regions, but so far I have been unable to + * make this loop show up on a profile. + */ + return; + } + } +} + +static void +process_gdk_event (FooScrollArea *scroll_area, + int x, + int y, + GdkEvent *event) +{ + FooScrollAreaEventType input_type = FOO_BUTTON_PRESS; + int button = 0; + + if (event->type == GDK_BUTTON_PRESS) + { + input_type = FOO_BUTTON_PRESS; + button = event->button.button; + } + else if (event->type == GDK_BUTTON_RELEASE) + { + input_type = FOO_BUTTON_RELEASE; + button = event->button.button; + } + else if (event->type == GDK_MOTION_NOTIFY || + event->type == GDK_LEAVE_NOTIFY || + event->type == GDK_ENTER_NOTIFY) + { + input_type = FOO_MOTION; + button = 0; + } + + process_event (scroll_area, input_type, x, y, button); +} + +static gboolean +foo_scroll_area_button_press (GtkWidget *widget, + GdkEventButton *event) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + + process_gdk_event (area, event->x, event->y, (GdkEvent *)event); + + return TRUE; +} + +static gboolean +foo_scroll_area_button_release (GtkWidget *widget, + GdkEventButton *event) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + + process_gdk_event (area, event->x, event->y, (GdkEvent *)event); + + return FALSE; +} + +typedef struct +{ + Window window; + guint32 last_motion_time; + gboolean stop_compressing; +} EventScannerData; + +static Bool +get_motion_time (Display *dpy, + XEvent *xevent, + XPointer arg) +{ + EventScannerData *esd = (EventScannerData *)arg; + + if (esd->stop_compressing) + return FALSE; + + if (esd->window != xevent->xany.window) + { + esd->stop_compressing = TRUE; + return FALSE; + } + + if (xevent->type == MotionNotify) + { + esd->last_motion_time = xevent->xmotion.time; + return FALSE; + } + + if (xevent->type == EnterNotify || + xevent->type == LeaveNotify) + { + esd->last_motion_time = xevent->xcrossing.time; + return FALSE; + } + + return FALSE; +} + +static gboolean +use_this_motion_notify (GdkWindow *window, + guint32 event_time, + guint32 *motion_notify_time) +{ + if (*motion_notify_time) + { + if (event_time == *motion_notify_time) + { + *motion_notify_time = 0; + + return TRUE; + } + else + { + /* There are more motion events already queued up */ + + return FALSE; + } + } + else + { + Display *dpy = gdk_x11_get_default_xdisplay(); + EventScannerData esd = { GDK_WINDOW_XWINDOW (window), 0, FALSE }; + XEvent dummy; + + XCheckIfEvent (dpy, &dummy, get_motion_time, (XPointer)&esd); + + if (esd.last_motion_time == 0) + { + return TRUE; + } + else + { + *motion_notify_time = esd.last_motion_time; + return FALSE; + } + } +} + +static gboolean +foo_scroll_area_motion (GtkWidget *widget, + GdkEventMotion *event) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + + if (use_this_motion_notify (event->window, + event->time, + &(area->priv->motion_notify_time))) + { + process_gdk_event (area, event->x, event->y, (GdkEvent *)event); + } + + return TRUE; +} + +static gboolean +foo_scroll_area_crossing (GtkWidget *widget, + GdkEventCrossing *event) +{ + FooScrollArea *area = FOO_SCROLL_AREA (widget); + + if (use_this_motion_notify (event->window, + event->time, + &(area->priv->motion_notify_time))) + { + process_gdk_event (area, event->x, event->y, (GdkEvent *)event); + } + + return TRUE; +} + +void +foo_scroll_area_set_size_fixed_y (FooScrollArea *scroll_area, + int width, + int height, + int old_y, + int new_y) +{ + int dy = new_y - old_y; + + scroll_area->priv->width = width; + scroll_area->priv->height = height; + +#if 0 + g_print ("diff: %d\n", new_y - old_y); +#endif + + scroll_area->priv->vadj->value += dy; + + set_adjustment_values (scroll_area); + + if (dy != 0) + gtk_adjustment_value_changed (scroll_area->priv->vadj); +} + +void +foo_scroll_area_set_size (FooScrollArea *scroll_area, + int width, + int height) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (scroll_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? + */ + + scroll_area->priv->width = width; + scroll_area->priv->height = height; + + set_adjustment_values (scroll_area); +} + +static void +foo_scroll_area_size_request (GtkWidget *widget, + GtkRequisition *requisition) +{ + FooScrollArea *scroll_area = FOO_SCROLL_AREA (widget); + + requisition->width = scroll_area->priv->min_width; + requisition->height = scroll_area->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 *scroll_area) +{ + GtkWidget *widget = GTK_WIDGET (scroll_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 (scroll_area, &old_viewport); + + if (adj == scroll_area->priv->hadj) + { + /* FIXME: do we treat the offset as int or double, and, + * if int, how do we round? + */ + dx = (int)adj->value - scroll_area->priv->x_offset; + scroll_area->priv->x_offset = adj->value; + } + else if (adj == scroll_area->priv->vadj) + { + dy = (int)adj->value - scroll_area->priv->y_offset; + scroll_area->priv->y_offset = adj->value; + } + else + { + g_assert_not_reached (); + } + + if (GTK_WIDGET_REALIZED (widget) && (dx || dy)) + { + backing_store_scroll (scroll_area->priv->store, -dx, -dy); + + gtk_widget_queue_draw (GTK_WIDGET (scroll_area)); + } + + get_viewport (scroll_area, &new_viewport); + + emit_viewport_changed (scroll_area, &new_viewport, &old_viewport); +} + +static void +set_one_adjustment (FooScrollArea *scroll_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, scroll_area); + + g_object_unref (*location); + } + + *location = adjustment; + + g_object_ref_sink (*location); + + g_signal_connect (*location, "value_changed", + G_CALLBACK (foo_scrollbar_adjustment_changed), + scroll_area); +} + +static void +foo_scroll_area_set_scroll_adjustments (FooScrollArea *scroll_area, + GtkAdjustment *hadjustment, + GtkAdjustment *vadjustment) +{ + set_one_adjustment (scroll_area, hadjustment, &scroll_area->priv->hadj); + set_one_adjustment (scroll_area, vadjustment, &scroll_area->priv->vadj); + + set_adjustment_values (scroll_area); +} + +FooScrollArea * +foo_scroll_area_new (void) +{ + return g_object_new (FOO_TYPE_SCROLL_AREA, NULL); +} + +void +foo_scroll_area_set_min_size (FooScrollArea *scroll_area, + int min_width, + int min_height) +{ + scroll_area->priv->min_width = min_width; + scroll_area->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 (scroll_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); +} + +static void +add_path (FooScrollArea *area, + cairo_t *cr, + gboolean is_stroke, + FooScrollAreaEventFunc func, + gpointer data) +{ + InputPath *path = g_new0 (InputPath, 1); +#if 0 + double x1, y1, x2, y2; + GdkRectangle r; + + 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 = x1 - 1; + r.y = y1 - 1; + r.width = x2 - x1 + 1; + r.height = y2 - y1 + 1; + + if (gdk_region_rect_in (area->priv->current_input->region, &r) != GDK_OVERLAP_RECTANGLE_OUT) + { +#endif + path->is_stroke = is_stroke; + path->fill_rule = cairo_get_fill_rule (cr); + path->line_width = cairo_get_line_width (cr); + path->path = cairo_copy_path (cr); + path_foreach_point (path->path, user_to_device, cr); + path->func = func; + path->data = data; + path->next = area->priv->current_input->paths; + area->priv->current_input->paths = path; +#if 0 + } +#endif + + return path; +} + +/* 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 *scroll_area, + cairo_t *cr, + FooScrollAreaEventFunc func, + gpointer data) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (scroll_area)); + g_return_if_fail (cr != NULL); + g_return_if_fail (scroll_area->priv->current_input); + + add_path (scroll_area, cr, FALSE, func, data); +} + +void +foo_scroll_area_add_input_from_stroke (FooScrollArea *scroll_area, + cairo_t *cr, + FooScrollAreaEventFunc func, + gpointer data) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (scroll_area)); + g_return_if_fail (cr != NULL); + g_return_if_fail (scroll_area->priv->current_input); + + add_path (scroll_area, cr, TRUE, func, data); +} + +void +foo_scroll_area_invalidate_region (FooScrollArea *area, + GdkRegion *region) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (area)); + + if (GTK_WIDGET_REALIZED (area)) + { + canvas_to_store (area, region); + backing_store_invalidate_region (area->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 *scroll_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 (scroll_area)); + + region = gdk_region_rectangle (&rect); + + foo_scroll_area_invalidate_region (scroll_area, region); + + gdk_region_destroy (region); +} + +void +foo_scroll_area_invalidate (FooScrollArea *scroll_area) +{ + GtkWidget *widget = GTK_WIDGET (scroll_area); + + foo_scroll_area_invalidate_rect (scroll_area, + scroll_area->priv->x_offset, scroll_area->priv->y_offset, + widget->allocation.width, + widget->allocation.height); +} + +void +foo_scroll_area_begin_grab (FooScrollArea *scroll_area, + FooScrollAreaEventFunc func, + gpointer input_data) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (scroll_area)); + g_return_if_fail (!scroll_area->priv->grabbed); + + scroll_area->priv->grabbed = TRUE; + scroll_area->priv->grab_func = func; + scroll_area->priv->grab_data = input_data; + + /* FIXME: we should probably take a server grab */ + /* Also, maybe there should be support for setting the grab cursor */ +} + +void +foo_scroll_area_end_grab (FooScrollArea *scroll_area) +{ + g_return_if_fail (FOO_IS_SCROLL_AREA (scroll_area)); + + scroll_area->priv->grabbed = FALSE; + scroll_area->priv->grab_func = NULL; + scroll_area->priv->grab_data = NULL; +} + +gboolean +foo_scroll_area_is_grabbed (FooScrollArea *scroll_area) +{ + return scroll_area->priv->grabbed; +} + +void +foo_scroll_area_set_viewport_pos (FooScrollArea *scroll_area, + int x, + int y) +{ + int x_changed; + int y_changed; + GtkAdjustment *hadj, *vadj; + + hadj = scroll_area->priv->hadj; + vadj = scroll_area->priv->vadj; + + x_changed = (int)hadj->value != x; + y_changed = (int)vadj->value != y; + + hadj->value = x; + vadj->value = y; + + set_adjustment_values (scroll_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) +{ +#if 0 + g_print ("stop scrolling\n"); +#endif + if (area->priv->auto_scroll_info) + { + g_source_remove (area->priv->auto_scroll_info->timeout_id); + g_timer_destroy (area->priv->auto_scroll_info->timer); + g_free (area->priv->auto_scroll_info); + + area->priv->auto_scroll_info = NULL; + } +} + +static gboolean +scroll_idle (gpointer data) +{ + GdkRectangle viewport, new_viewport; + FooScrollArea *area = data; + AutoScrollInfo *info = area->priv->auto_scroll_info; +#if 0 + int dx, dy; +#endif + int new_x, new_y; + double elapsed; + + 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 + + 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\n", 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); +#if 0 + viewport.x + info->dx, + viewport.y + info->dy); +#endif + + 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) +{ + if (!area->priv->auto_scroll_info) + { +#if 0 + g_print ("start scrolling\n"); +#endif + area->priv->auto_scroll_info = g_new0 (AutoScrollInfo, 1); + area->priv->auto_scroll_info->timeout_id = + g_idle_add (scroll_idle, area); + area->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 + + area->priv->auto_scroll_info->dx = dx; + area->priv->auto_scroll_info->dy = dy; +} + +void +foo_scroll_area_auto_scroll (FooScrollArea *scroll_area, + FooScrollAreaEvent *event) +{ +#define SCROLL_MARGIN 4 + + GdkRectangle viewport; + + get_viewport (scroll_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 (scroll_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 (scroll_area, dx, dy); + } +} + +void +foo_scroll_area_begin_auto_scroll (FooScrollArea *scroll_area) +{ + /* noop for now */ +} + +void +foo_scroll_area_end_auto_scroll (FooScrollArea *scroll_area) +{ + stop_scrolling (scroll_area); +} diff --git a/scrollarea.h b/scrollarea.h new file mode 100644 index 0000000..1c95c9a --- /dev/null +++ b/scrollarea.h @@ -0,0 +1,107 @@ +#include <cairo/cairo.h> +#include <gtk/gtk.h> + +#define FOO_TYPE_SCROLL_AREA (foo_scroll_area_get_type ()) +#define FOO_SCROLL_AREA(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FOO_TYPE_SCROLL_AREA, FooScrollArea)) +#define FOO_SCROLL_AREA_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), FOO_TYPE_SCROLL_AREA, FooScrollAreaClass)) +#define FOO_IS_SCROLL_AREA(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), FOO_TYPE_SCROLL_AREA)) +#define FOO_IS_SCROLL_AREA_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), FOO_TYPE_SCROLL_AREA)) +#define FOO_SCROLL_AREA_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), FOO_TYPE_SCROLL_AREA, FooScrollAreaClass)) + +typedef struct FooScrollArea FooScrollArea; +typedef struct FooScrollAreaClass FooScrollAreaClass; +typedef struct FooScrollAreaPrivate FooScrollAreaPrivate; +typedef struct FooScrollAreaEvent FooScrollAreaEvent; + +typedef enum +{ + FOO_BUTTON_PRESS, + FOO_BUTTON_RELEASE, + FOO_MOTION +} FooScrollAreaEventType; + +struct FooScrollAreaEvent +{ + FooScrollAreaEventType type; + int x; + int y; + int button; +}; + +typedef void (* FooScrollAreaEventFunc) (FooScrollArea *area, + FooScrollAreaEvent *event, + gpointer data); + +struct FooScrollArea +{ + GtkContainer parent_instance; + + FooScrollAreaPrivate *priv; +}; + +struct FooScrollAreaClass +{ + GtkContainerClass parent_class; + + void (*set_scroll_adjustments) (FooScrollArea *scroll_area, + GtkAdjustment *hadjustment, + GtkAdjustment *vadjustment); + + void (*viewport_changed) (FooScrollArea *scroll_area, + GdkRectangle *viewport); + + void (*paint) (FooScrollArea *scroll_area, + cairo_t *cr, + GdkRectangle *extents, + GdkRegion *region); +}; + +GType foo_scroll_area_get_type (void); + +FooScrollArea *foo_scroll_area_new (void); + +/* Set the requisition for the widget. */ +void foo_scroll_area_set_min_size (FooScrollArea *scroll_area, + int min_width, + int min_height); + +/* Set how much of the canvas can be scrolled into view */ +void foo_scroll_area_set_size (FooScrollArea *scroll_area, + int width, + int height); +void foo_scroll_area_set_size_fixed_y (FooScrollArea *scroll_area, + int width, + int height, + int old_y, + int new_y); +void foo_scroll_area_set_viewport_pos (FooScrollArea *scroll_area, + int x, + int y); +void foo_scroll_area_get_viewport (FooScrollArea *scroll_area, + GdkRectangle *viewport); +void foo_scroll_area_add_input_from_stroke (FooScrollArea *scroll_area, + cairo_t *cr, + FooScrollAreaEventFunc func, + gpointer data); +void foo_scroll_area_add_input_from_fill (FooScrollArea *scroll_area, + cairo_t *cr, + FooScrollAreaEventFunc func, + gpointer data); +void foo_scroll_area_invalidate_region (FooScrollArea *area, + GdkRegion *region); +void foo_scroll_area_invalidate (FooScrollArea *scroll_area); +void foo_scroll_area_invalidate_rect (FooScrollArea *scroll_area, + int x, + int y, + int width, + int height); +void foo_scroll_area_begin_grab (FooScrollArea *scroll_area, + FooScrollAreaEventFunc func, + gpointer input_data); +void foo_scroll_area_end_grab (FooScrollArea *scroll_area); +gboolean foo_scroll_area_is_grabbed (FooScrollArea *scroll_area); + +void foo_scroll_area_begin_auto_scroll (FooScrollArea *scroll_area); +void foo_scroll_area_auto_scroll (FooScrollArea *scroll_area, + FooScrollAreaEvent *event); +void foo_scroll_area_end_auto_scroll (FooScrollArea *scroll_area); |