/* This file is part of Ganv. * Copyright 2007-2013 David Robillard * * Ganv is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. * * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. * * You should have received a copy of the GNU General Public License along * with Ganv. If not, see . */ /* Based on GnomeCanvas, by Federico Mena * and Raph Levien * Copyright 1997-2000 Free Software Foundation */ #include #include #include #include #include #include "ganv/canvas-base.h" #include "ganv/group.h" #include "./ganv-marshal.h" #include "./ganv-private.h" #include "./gettext.h" /* We must run our idle update handler *before* GDK wants to redraw. */ #define CANVAS_IDLE_PRIORITY (GDK_PRIORITY_REDRAW - 5) static void add_idle(GanvCanvasBase* canvas); typedef struct { int x; int y; int width; int height; } IRect; /*** GanvCanvasBase ***/ static void ganv_canvas_base_destroy(GtkObject* object); static void ganv_canvas_base_map(GtkWidget* widget); static void ganv_canvas_base_unmap(GtkWidget* widget); static void ganv_canvas_base_realize(GtkWidget* widget); static void ganv_canvas_base_unrealize(GtkWidget* widget); static void ganv_canvas_base_size_allocate(GtkWidget* widget, GtkAllocation* allocation); static gint ganv_canvas_base_button(GtkWidget* widget, GdkEventButton* event); static gint ganv_canvas_base_motion(GtkWidget* widget, GdkEventMotion* event); static gint ganv_canvas_base_expose(GtkWidget* widget, GdkEventExpose* event); static gboolean ganv_canvas_base_key(GtkWidget* widget, GdkEventKey* event); static gboolean ganv_canvas_base_scroll(GtkWidget* widget, GdkEventScroll* event); static gint ganv_canvas_base_crossing(GtkWidget* widget, GdkEventCrossing* event); static gint ganv_canvas_base_focus_in(GtkWidget* widget, GdkEventFocus* event); static gint ganv_canvas_base_focus_out(GtkWidget* widget, GdkEventFocus* event); static void ganv_canvas_base_request_update_real(GanvCanvasBase* canvas); static GtkLayoutClass* canvas_parent_class; enum { PROP_FOCUSED_ITEM = 1 }; G_DEFINE_TYPE(GanvCanvasBase, ganv_canvas_base, GTK_TYPE_LAYOUT) static void ganv_canvas_base_get_property(GObject* object, guint prop_id, GValue* value, GParamSpec* pspec) { switch (prop_id) { case PROP_FOCUSED_ITEM: g_value_set_object(value, GANV_CANVAS_BASE(object)->focused_item); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void ganv_canvas_base_set_property(GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec) { switch (prop_id) { case PROP_FOCUSED_ITEM: GANV_CANVAS_BASE(object)->focused_item = g_value_get_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } /* Class initialization function for GanvCanvasBaseClass */ static void ganv_canvas_base_class_init(GanvCanvasBaseClass* klass) { GObjectClass* gobject_class; GtkObjectClass* object_class; GtkWidgetClass* widget_class; gobject_class = (GObjectClass*)klass; object_class = (GtkObjectClass*)klass; widget_class = (GtkWidgetClass*)klass; canvas_parent_class = g_type_class_peek_parent(klass); gobject_class->set_property = ganv_canvas_base_set_property; gobject_class->get_property = ganv_canvas_base_get_property; object_class->destroy = ganv_canvas_base_destroy; widget_class->map = ganv_canvas_base_map; widget_class->unmap = ganv_canvas_base_unmap; widget_class->realize = ganv_canvas_base_realize; widget_class->unrealize = ganv_canvas_base_unrealize; widget_class->size_allocate = ganv_canvas_base_size_allocate; widget_class->button_press_event = ganv_canvas_base_button; widget_class->button_release_event = ganv_canvas_base_button; widget_class->motion_notify_event = ganv_canvas_base_motion; widget_class->expose_event = ganv_canvas_base_expose; widget_class->key_press_event = ganv_canvas_base_key; widget_class->key_release_event = ganv_canvas_base_key; widget_class->enter_notify_event = ganv_canvas_base_crossing; widget_class->leave_notify_event = ganv_canvas_base_crossing; widget_class->focus_in_event = ganv_canvas_base_focus_in; widget_class->focus_out_event = ganv_canvas_base_focus_out; widget_class->scroll_event = ganv_canvas_base_scroll; klass->request_update = ganv_canvas_base_request_update_real; g_object_class_install_property(gobject_class, PROP_FOCUSED_ITEM, g_param_spec_object("focused_item", NULL, NULL, GANV_TYPE_ITEM, (G_PARAM_READABLE | G_PARAM_WRITABLE))); } /* Callback used when the root item of a canvas is destroyed. The user should * never ever do this, so we panic if this happens. */ static void panic_root_destroyed(GtkObject* object, gpointer data) { g_error("Eeeek, root item %p of canvas %p was destroyed!", (void*)object, data); } /* Object initialization function for GanvCanvasBase */ static void ganv_canvas_base_init(GanvCanvasBase* canvas) { GTK_WIDGET_SET_FLAGS(canvas, GTK_CAN_FOCUS); canvas->need_update = FALSE; canvas->need_redraw = FALSE; canvas->redraw_region = NULL; canvas->idle_id = 0; canvas->scroll_x1 = 0.0; canvas->scroll_y1 = 0.0; canvas->scroll_x2 = canvas->layout.width; canvas->scroll_y2 = canvas->layout.height; canvas->pixels_per_unit = 1.0; canvas->pick_event.type = GDK_LEAVE_NOTIFY; canvas->pick_event.crossing.x = 0; canvas->pick_event.crossing.y = 0; canvas->center_scroll_region = TRUE; gtk_layout_set_hadjustment(GTK_LAYOUT(canvas), NULL); gtk_layout_set_vadjustment(GTK_LAYOUT(canvas), NULL); /* Create the root item as a special case */ canvas->root = GANV_ITEM(g_object_new(ganv_group_get_type(), NULL)); canvas->root->canvas = canvas; g_object_ref_sink(canvas->root); canvas->root_destroy_id = g_signal_connect(canvas->root, "destroy", G_CALLBACK(panic_root_destroyed), canvas); canvas->need_repick = TRUE; } /* Convenience function to remove the idle handler of a canvas */ static void remove_idle(GanvCanvasBase* canvas) { if (canvas->idle_id == 0) { return; } g_source_remove(canvas->idle_id); canvas->idle_id = 0; } /* Removes the transient state of the canvas (idle handler, grabs). */ static void shutdown_transients(GanvCanvasBase* canvas) { /* We turn off the need_redraw flag, since if the canvas is mapped again * it will request a redraw anyways. We do not turn off the need_update * flag, though, because updates are not queued when the canvas remaps * itself. */ if (canvas->need_redraw) { canvas->need_redraw = FALSE; g_slist_foreach(canvas->redraw_region, (GFunc)g_free, NULL); g_slist_free(canvas->redraw_region); canvas->redraw_region = NULL; canvas->redraw_x1 = 0; canvas->redraw_y1 = 0; canvas->redraw_x2 = 0; canvas->redraw_y2 = 0; } if (canvas->grabbed_item) { canvas->grabbed_item = NULL; gdk_pointer_ungrab(GDK_CURRENT_TIME); } remove_idle(canvas); } /* Destroy handler for GanvCanvasBase */ static void ganv_canvas_base_destroy(GtkObject* object) { GanvCanvasBase* canvas; g_return_if_fail(GANV_IS_CANVAS_BASE(object)); /* remember, destroy can be run multiple times! */ canvas = GANV_CANVAS_BASE(object); if (canvas->root_destroy_id) { g_signal_handler_disconnect(canvas->root, canvas->root_destroy_id); canvas->root_destroy_id = 0; } if (canvas->root) { gtk_object_destroy(GTK_OBJECT(canvas->root)); g_object_unref(G_OBJECT(canvas->root)); canvas->root = NULL; } shutdown_transients(canvas); if (GTK_OBJECT_CLASS(canvas_parent_class)->destroy) { (*GTK_OBJECT_CLASS(canvas_parent_class)->destroy)(object); } } /** * ganv_canvas_base_new: * * Creates a new empty canvas in non-antialiased mode. * * Return value: A newly-created canvas. **/ GtkWidget* ganv_canvas_base_new(void) { return GTK_WIDGET(g_object_new(ganv_canvas_base_get_type(), NULL)); } /* Map handler for the canvas */ static void ganv_canvas_base_map(GtkWidget* widget) { GanvCanvasBase* canvas; g_return_if_fail(GANV_IS_CANVAS_BASE(widget)); /* Normal widget mapping stuff */ if (GTK_WIDGET_CLASS(canvas_parent_class)->map) { (*GTK_WIDGET_CLASS(canvas_parent_class)->map)(widget); } canvas = GANV_CANVAS_BASE(widget); if (canvas->need_update) { add_idle(canvas); } /* Map items */ if (GANV_ITEM_GET_CLASS(canvas->root)->map) { (*GANV_ITEM_GET_CLASS(canvas->root)->map)(canvas->root); } } /* Unmap handler for the canvas */ static void ganv_canvas_base_unmap(GtkWidget* widget) { GanvCanvasBase* canvas; g_return_if_fail(GANV_IS_CANVAS_BASE(widget)); canvas = GANV_CANVAS_BASE(widget); shutdown_transients(canvas); /* Unmap items */ if (GANV_ITEM_GET_CLASS(canvas->root)->unmap) { (*GANV_ITEM_GET_CLASS(canvas->root)->unmap)(canvas->root); } /* Normal widget unmapping stuff */ if (GTK_WIDGET_CLASS(canvas_parent_class)->unmap) { (*GTK_WIDGET_CLASS(canvas_parent_class)->unmap)(widget); } } /* Realize handler for the canvas */ static void ganv_canvas_base_realize(GtkWidget* widget) { GanvCanvasBase* canvas; g_return_if_fail(GANV_IS_CANVAS_BASE(widget)); /* Normal widget realization stuff */ if (GTK_WIDGET_CLASS(canvas_parent_class)->realize) { (*GTK_WIDGET_CLASS(canvas_parent_class)->realize)(widget); } canvas = GANV_CANVAS_BASE(widget); gdk_window_set_events(canvas->layout.bin_window, (gdk_window_get_events(canvas->layout.bin_window) | GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_FOCUS_CHANGE_MASK)); /* Create our own temporary pixmap gc and realize all the items */ canvas->pixmap_gc = gdk_gc_new(canvas->layout.bin_window); (*GANV_ITEM_GET_CLASS(canvas->root)->realize)(canvas->root); } /* Unrealize handler for the canvas */ static void ganv_canvas_base_unrealize(GtkWidget* widget) { GanvCanvasBase* canvas; g_return_if_fail(GANV_IS_CANVAS_BASE(widget)); canvas = GANV_CANVAS_BASE(widget); shutdown_transients(canvas); /* Unrealize items and parent widget */ (*GANV_ITEM_GET_CLASS(canvas->root)->unrealize)(canvas->root); g_object_unref(canvas->pixmap_gc); canvas->pixmap_gc = NULL; if (GTK_WIDGET_CLASS(canvas_parent_class)->unrealize) { (*GTK_WIDGET_CLASS(canvas_parent_class)->unrealize)(widget); } } /* Handles scrolling of the canvas. Adjusts the scrolling and zooming offset to * keep as much as possible of the canvas scrolling region in view. */ static void scroll_to(GanvCanvasBase* canvas, int cx, int cy) { int scroll_width, scroll_height; int right_limit, bottom_limit; int old_zoom_xofs, old_zoom_yofs; int changed_x = FALSE, changed_y = FALSE; int canvas_width, canvas_height; canvas_width = GTK_WIDGET(canvas)->allocation.width; canvas_height = GTK_WIDGET(canvas)->allocation.height; scroll_width = floor((canvas->scroll_x2 - canvas->scroll_x1) * canvas->pixels_per_unit + 0.5); scroll_height = floor((canvas->scroll_y2 - canvas->scroll_y1) * canvas->pixels_per_unit + 0.5); right_limit = scroll_width - canvas_width; bottom_limit = scroll_height - canvas_height; old_zoom_xofs = canvas->zoom_xofs; old_zoom_yofs = canvas->zoom_yofs; if (right_limit < 0) { cx = 0; if (canvas->center_scroll_region) { canvas->zoom_xofs = (canvas_width - scroll_width) / 2; scroll_width = canvas_width; } else { canvas->zoom_xofs = 0; } } else if (cx < 0) { cx = 0; canvas->zoom_xofs = 0; } else if (cx > right_limit) { cx = right_limit; canvas->zoom_xofs = 0; } else { canvas->zoom_xofs = 0; } if (bottom_limit < 0) { cy = 0; if (canvas->center_scroll_region) { canvas->zoom_yofs = (canvas_height - scroll_height) / 2; scroll_height = canvas_height; } else { canvas->zoom_yofs = 0; } } else if (cy < 0) { cy = 0; canvas->zoom_yofs = 0; } else if (cy > bottom_limit) { cy = bottom_limit; canvas->zoom_yofs = 0; } else { canvas->zoom_yofs = 0; } if ((canvas->zoom_xofs != old_zoom_xofs) || (canvas->zoom_yofs != old_zoom_yofs)) { ganv_canvas_base_request_update(canvas); gtk_widget_queue_draw(GTK_WIDGET(canvas)); } if (canvas->layout.hadjustment && ( ((int)canvas->layout.hadjustment->value) != cx) ) { canvas->layout.hadjustment->value = cx; changed_x = TRUE; } if (canvas->layout.vadjustment && ( ((int)canvas->layout.vadjustment->value) != cy) ) { canvas->layout.vadjustment->value = cy; changed_y = TRUE; } if ((scroll_width != (int)canvas->layout.width) || (scroll_height != (int)canvas->layout.height)) { gtk_layout_set_size(GTK_LAYOUT(canvas), scroll_width, scroll_height); } /* Signal GtkLayout that it should do a redraw. */ if (changed_x) { g_signal_emit_by_name(canvas->layout.hadjustment, "value_changed"); } if (changed_y) { g_signal_emit_by_name(canvas->layout.vadjustment, "value_changed"); } } /* Size allocation handler for the canvas */ static void ganv_canvas_base_size_allocate(GtkWidget* widget, GtkAllocation* allocation) { GanvCanvasBase* canvas; g_return_if_fail(GANV_IS_CANVAS_BASE(widget)); g_return_if_fail(allocation != NULL); if (GTK_WIDGET_CLASS(canvas_parent_class)->size_allocate) { (*GTK_WIDGET_CLASS(canvas_parent_class)->size_allocate)(widget, allocation); } canvas = GANV_CANVAS_BASE(widget); /* Recenter the view, if appropriate */ canvas->layout.hadjustment->page_size = allocation->width; canvas->layout.hadjustment->page_increment = allocation->width / 2; canvas->layout.vadjustment->page_size = allocation->height; canvas->layout.vadjustment->page_increment = allocation->height / 2; scroll_to(canvas, canvas->layout.hadjustment->value, canvas->layout.vadjustment->value); g_signal_emit_by_name(canvas->layout.hadjustment, "changed"); g_signal_emit_by_name(canvas->layout.vadjustment, "changed"); } /* Returns whether the item is an inferior of or is equal to the parent. */ static gboolean is_descendant(GanvItem* item, GanvItem* parent) { for (; item; item = item->parent) { if (item == parent) { return TRUE; } } return FALSE; } /* Emits an event for an item in the canvas, be it the current item, grabbed * item, or focused item, as appropriate. */ int ganv_canvas_base_emit_event(GanvCanvasBase* canvas, GdkEvent* event) { GdkEvent* ev; gint finished; GanvItem* item; GanvItem* parent; guint mask; /* Perform checks for grabbed items */ if (canvas->grabbed_item && !is_descendant(canvas->current_item, canvas->grabbed_item)) { /* I think this warning is annoying and I don't know what it's for * so I'll disable it for now. */ /* g_warning ("emit_event() returning FALSE!\n");*/ return FALSE; } if (canvas->grabbed_item) { switch (event->type) { case GDK_ENTER_NOTIFY: mask = GDK_ENTER_NOTIFY_MASK; break; case GDK_LEAVE_NOTIFY: mask = GDK_LEAVE_NOTIFY_MASK; break; case GDK_MOTION_NOTIFY: mask = GDK_POINTER_MOTION_MASK; break; case GDK_BUTTON_PRESS: case GDK_2BUTTON_PRESS: case GDK_3BUTTON_PRESS: mask = GDK_BUTTON_PRESS_MASK; break; case GDK_BUTTON_RELEASE: mask = GDK_BUTTON_RELEASE_MASK; break; case GDK_KEY_PRESS: mask = GDK_KEY_PRESS_MASK; break; case GDK_KEY_RELEASE: mask = GDK_KEY_RELEASE_MASK; break; case GDK_SCROLL: mask = GDK_SCROLL_MASK; break; default: mask = 0; break; } if (!(mask & canvas->grabbed_event_mask)) { return FALSE; } } /* Convert to world coordinates -- we have two cases because of diferent * offsets of the fields in the event structures. */ ev = gdk_event_copy(event); switch (ev->type) { case GDK_ENTER_NOTIFY: case GDK_LEAVE_NOTIFY: ganv_canvas_base_window_to_world(canvas, ev->crossing.x, ev->crossing.y, &ev->crossing.x, &ev->crossing.y); break; case GDK_MOTION_NOTIFY: case GDK_BUTTON_PRESS: case GDK_2BUTTON_PRESS: case GDK_3BUTTON_PRESS: case GDK_BUTTON_RELEASE: ganv_canvas_base_window_to_world(canvas, ev->motion.x, ev->motion.y, &ev->motion.x, &ev->motion.y); break; default: break; } /* Choose where we send the event */ item = canvas->current_item; if (canvas->focused_item && ((event->type == GDK_KEY_PRESS) || (event->type == GDK_KEY_RELEASE) || (event->type == GDK_FOCUS_CHANGE))) { item = canvas->focused_item; } /* The event is propagated up the hierarchy (for if someone connected to * a group instead of a leaf event), and emission is stopped if a * handler returns TRUE, just like for GtkWidget events. */ finished = FALSE; while (item && !finished) { g_object_ref(G_OBJECT(item)); ganv_item_emit_event(item, ev, &finished); parent = item->parent; g_object_unref(G_OBJECT(item)); item = parent; } gdk_event_free(ev); return finished; } /* Re-picks the current item in the canvas, based on the event's coordinates. * Also emits enter/leave events for items as appropriate. */ static int pick_current_item(GanvCanvasBase* canvas, GdkEvent* event) { int button_down; double x, y; int cx, cy; int retval; retval = FALSE; /* If a button is down, we'll perform enter and leave events on the * current item, but not enter on any other item. This is more or less * like X pointer grabbing for canvas items. */ button_down = canvas->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK | GDK_BUTTON4_MASK | GDK_BUTTON5_MASK); if (!button_down) { canvas->left_grabbed_item = FALSE; } /* Save the event in the canvas. This is used to synthesize enter and * leave events in case the current item changes. It is also used to * re-pick the current item if the current one gets deleted. Also, * synthesize an enter event. */ if (event != &canvas->pick_event) { if ((event->type == GDK_MOTION_NOTIFY) || (event->type == GDK_BUTTON_RELEASE)) { /* these fields have the same offsets in both types of events */ canvas->pick_event.crossing.type = GDK_ENTER_NOTIFY; canvas->pick_event.crossing.window = event->motion.window; canvas->pick_event.crossing.send_event = event->motion.send_event; canvas->pick_event.crossing.subwindow = NULL; canvas->pick_event.crossing.x = event->motion.x; canvas->pick_event.crossing.y = event->motion.y; canvas->pick_event.crossing.mode = GDK_CROSSING_NORMAL; canvas->pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; canvas->pick_event.crossing.focus = FALSE; canvas->pick_event.crossing.state = event->motion.state; /* these fields don't have the same offsets in both types of events */ if (event->type == GDK_MOTION_NOTIFY) { canvas->pick_event.crossing.x_root = event->motion.x_root; canvas->pick_event.crossing.y_root = event->motion.y_root; } else { canvas->pick_event.crossing.x_root = event->button.x_root; canvas->pick_event.crossing.y_root = event->button.y_root; } } else { canvas->pick_event = *event; } } /* Don't do anything else if this is a recursive call */ if (canvas->in_repick) { return retval; } /* LeaveNotify means that there is no current item, so we don't look for one */ if (canvas->pick_event.type != GDK_LEAVE_NOTIFY) { /* these fields don't have the same offsets in both types of events */ if (canvas->pick_event.type == GDK_ENTER_NOTIFY) { x = canvas->pick_event.crossing.x - canvas->zoom_xofs; y = canvas->pick_event.crossing.y - canvas->zoom_yofs; } else { x = canvas->pick_event.motion.x - canvas->zoom_xofs; y = canvas->pick_event.motion.y - canvas->zoom_yofs; } /* canvas pixel coords */ cx = (int)(x + 0.5); cy = (int)(y + 0.5); /* world coords */ x = canvas->scroll_x1 + x / canvas->pixels_per_unit; y = canvas->scroll_y1 + y / canvas->pixels_per_unit; /* find the closest item */ if (canvas->root->object.flags & GANV_ITEM_VISIBLE) { GANV_ITEM_GET_CLASS(canvas->root)->point( canvas->root, x - canvas->root->x, y - canvas->root->y, cx, cy, &canvas->new_current_item); } else { canvas->new_current_item = NULL; } } else { canvas->new_current_item = NULL; } if ((canvas->new_current_item == canvas->current_item) && !canvas->left_grabbed_item) { return retval; /* current item did not change */ } /* Synthesize events for old and new current items */ if ((canvas->new_current_item != canvas->current_item) && (canvas->current_item != NULL) && !canvas->left_grabbed_item) { GdkEvent new_event; new_event = canvas->pick_event; new_event.type = GDK_LEAVE_NOTIFY; new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; new_event.crossing.subwindow = NULL; canvas->in_repick = TRUE; retval = ganv_canvas_base_emit_event(canvas, &new_event); canvas->in_repick = FALSE; } /* new_current_item may have been set to NULL during the call to ganv_canvas_base_emit_event() above */ if ((canvas->new_current_item != canvas->current_item) && button_down) { canvas->left_grabbed_item = TRUE; return retval; } /* Handle the rest of cases */ canvas->left_grabbed_item = FALSE; canvas->current_item = canvas->new_current_item; if (canvas->current_item != NULL) { GdkEvent new_event; new_event = canvas->pick_event; new_event.type = GDK_ENTER_NOTIFY; new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; new_event.crossing.subwindow = NULL; retval = ganv_canvas_base_emit_event(canvas, &new_event); } return retval; } /* Button event handler for the canvas */ static gint ganv_canvas_base_button(GtkWidget* widget, GdkEventButton* event) { GanvCanvasBase* canvas; int mask; int retval; g_return_val_if_fail(GANV_IS_CANVAS_BASE(widget), FALSE); g_return_val_if_fail(event != NULL, FALSE); retval = FALSE; canvas = GANV_CANVAS_BASE(widget); /* * dispatch normally regardless of the event's window if an item has * has a pointer grab in effect */ if (!canvas->grabbed_item && ( event->window != canvas->layout.bin_window) ) { return retval; } switch (event->button) { case 1: mask = GDK_BUTTON1_MASK; break; case 2: mask = GDK_BUTTON2_MASK; break; case 3: mask = GDK_BUTTON3_MASK; break; case 4: mask = GDK_BUTTON4_MASK; break; case 5: mask = GDK_BUTTON5_MASK; break; default: mask = 0; } switch (event->type) { case GDK_BUTTON_PRESS: case GDK_2BUTTON_PRESS: case GDK_3BUTTON_PRESS: /* Pick the current item as if the button were not pressed, and * then process the event. */ canvas->state = event->state; pick_current_item(canvas, (GdkEvent*)event); canvas->state ^= mask; retval = ganv_canvas_base_emit_event(canvas, (GdkEvent*)event); break; case GDK_BUTTON_RELEASE: /* Process the event as if the button were pressed, then repick * after the button has been released */ canvas->state = event->state; retval = ganv_canvas_base_emit_event(canvas, (GdkEvent*)event); event->state ^= mask; canvas->state = event->state; pick_current_item(canvas, (GdkEvent*)event); event->state ^= mask; break; default: g_assert_not_reached(); } return retval; } /* Motion event handler for the canvas */ static gint ganv_canvas_base_motion(GtkWidget* widget, GdkEventMotion* event) { GanvCanvasBase* canvas; g_return_val_if_fail(GANV_IS_CANVAS_BASE(widget), FALSE); g_return_val_if_fail(event != NULL, FALSE); canvas = GANV_CANVAS_BASE(widget); if (event->window != canvas->layout.bin_window) { return FALSE; } canvas->state = event->state; pick_current_item(canvas, (GdkEvent*)event); return ganv_canvas_base_emit_event(canvas, (GdkEvent*)event); } static gboolean ganv_canvas_base_scroll(GtkWidget* widget, GdkEventScroll* event) { GanvCanvasBase* canvas; g_return_val_if_fail(GANV_IS_CANVAS_BASE(widget), FALSE); g_return_val_if_fail(event != NULL, FALSE); canvas = GANV_CANVAS_BASE(widget); if (event->window != canvas->layout.bin_window) { return FALSE; } canvas->state = event->state; pick_current_item(canvas, (GdkEvent*)event); return ganv_canvas_base_emit_event(canvas, (GdkEvent*)event); } /* Key event handler for the canvas */ static gboolean ganv_canvas_base_key(GtkWidget* widget, GdkEventKey* event) { GanvCanvasBase* canvas; g_return_val_if_fail(GANV_IS_CANVAS_BASE(widget), FALSE); g_return_val_if_fail(event != NULL, FALSE); canvas = GANV_CANVAS_BASE(widget); if (!ganv_canvas_base_emit_event(canvas, (GdkEvent*)event)) { GtkWidgetClass* widget_class; widget_class = GTK_WIDGET_CLASS(canvas_parent_class); if (event->type == GDK_KEY_PRESS) { if (widget_class->key_press_event) { return (*widget_class->key_press_event)(widget, event); } } else if (event->type == GDK_KEY_RELEASE) { if (widget_class->key_release_event) { return (*widget_class->key_release_event)(widget, event); } } else { g_assert_not_reached(); } return FALSE; } else { return TRUE; } } /* Crossing event handler for the canvas */ static gint ganv_canvas_base_crossing(GtkWidget* widget, GdkEventCrossing* event) { GanvCanvasBase* canvas; g_return_val_if_fail(GANV_IS_CANVAS_BASE(widget), FALSE); g_return_val_if_fail(event != NULL, FALSE); canvas = GANV_CANVAS_BASE(widget); if (event->window != canvas->layout.bin_window) { return FALSE; } canvas->state = event->state; return pick_current_item(canvas, (GdkEvent*)event); } /* Focus in handler for the canvas */ static gint ganv_canvas_base_focus_in(GtkWidget* widget, GdkEventFocus* event) { GanvCanvasBase* canvas; GTK_WIDGET_SET_FLAGS(widget, GTK_HAS_FOCUS); canvas = GANV_CANVAS_BASE(widget); if (canvas->focused_item) { return ganv_canvas_base_emit_event(canvas, (GdkEvent*)event); } else { return FALSE; } } /* Focus out handler for the canvas */ static gint ganv_canvas_base_focus_out(GtkWidget* widget, GdkEventFocus* event) { GanvCanvasBase* canvas; GTK_WIDGET_UNSET_FLAGS(widget, GTK_HAS_FOCUS); canvas = GANV_CANVAS_BASE(widget); if (canvas->focused_item) { return ganv_canvas_base_emit_event(canvas, (GdkEvent*)event); } else { return FALSE; } } #define REDRAW_QUANTUM_SIZE 512 static void ganv_canvas_base_paint_rect(GanvCanvasBase* canvas, gint x0, gint y0, gint x1, gint y1) { gint draw_x1, draw_y1; gint draw_x2, draw_y2; gint draw_width, draw_height; g_return_if_fail(!canvas->need_update); draw_x1 = MAX(x0, canvas->layout.hadjustment->value - canvas->zoom_xofs); draw_y1 = MAX(y0, canvas->layout.vadjustment->value - canvas->zoom_yofs); draw_x2 = MIN(draw_x1 + GTK_WIDGET(canvas)->allocation.width, x1); draw_y2 = MIN(draw_y1 + GTK_WIDGET(canvas)->allocation.height, y1); draw_width = draw_x2 - draw_x1; draw_height = draw_y2 - draw_y1; if ((draw_width < 1) || (draw_height < 1)) { return; } canvas->redraw_x1 = draw_x1; canvas->redraw_y1 = draw_y1; canvas->redraw_x2 = draw_x2; canvas->redraw_y2 = draw_y2; canvas->draw_xofs = draw_x1; canvas->draw_yofs = draw_y1; cairo_t* cr = gdk_cairo_create(canvas->layout.bin_window); double wx, wy; ganv_canvas_base_window_to_world(canvas, 0, 0, &wx, &wy); cairo_translate(cr, -wx, -wy); if (canvas->root->object.flags & GANV_ITEM_VISIBLE) { (*GANV_ITEM_GET_CLASS(canvas->root)->draw)( canvas->root, cr, draw_x1, draw_y1, draw_width, draw_height); } cairo_destroy(cr); } /* Expose handler for the canvas */ static gint ganv_canvas_base_expose(GtkWidget* widget, GdkEventExpose* event) { GanvCanvasBase* canvas = GANV_CANVAS_BASE(widget); if (!GTK_WIDGET_DRAWABLE(widget) || (event->window != canvas->layout.bin_window)) { return FALSE; } /* Find a single bounding rectangle for all rectangles in the region. Since drawing the root group is O(n) and thus very expensive for large canvases, it's much faster to do a single paint than many, even though more area may be painted that way. With a better group implementation, it would likely be better to paint each changed rectangle separately. */ GdkRectangle clip; gdk_region_get_clipbox(event->region, &clip); const int x2 = clip.x + clip.width; const int y2 = clip.y + clip.height; if (canvas->need_update || canvas->need_redraw) { /* Update or drawing is scheduled, so just mark exposed area as dirty */ ganv_canvas_base_request_redraw(canvas, clip.x, clip.y, x2, y2); } else { /* No pending updates, draw exposed area immediately */ ganv_canvas_base_paint_rect(canvas, clip.x, clip.y, x2, y2); /* And call expose on parent container class */ if (GTK_WIDGET_CLASS(canvas_parent_class)->expose_event) { (*GTK_WIDGET_CLASS(canvas_parent_class)->expose_event)( widget, event); } } return FALSE; } /* Repaints the areas in the canvas that need it */ static void paint(GanvCanvasBase* canvas) { for (GSList* l = canvas->redraw_region; l; l = l->next) { IRect* rect = (IRect*)l->data; const GdkRectangle gdkrect = { rect->x + canvas->zoom_xofs, rect->y + canvas->zoom_yofs, rect->width, rect->height }; gdk_window_invalidate_rect(canvas->layout.bin_window, &gdkrect, FALSE); g_free(rect); } g_slist_free(canvas->redraw_region); canvas->redraw_region = NULL; canvas->need_redraw = FALSE; canvas->redraw_x1 = 0; canvas->redraw_y1 = 0; canvas->redraw_x2 = 0; canvas->redraw_y2 = 0; } static void do_update(GanvCanvasBase* canvas) { /* Cause the update if necessary */ update_again: if (canvas->need_update) { ganv_item_invoke_update(canvas->root, 0); canvas->need_update = FALSE; } /* Pick new current item */ while (canvas->need_repick) { canvas->need_repick = FALSE; pick_current_item(canvas, &canvas->pick_event); } /* it is possible that during picking we emitted an event in which the user then called some function which then requested update of something. Without this we'd be left in a state where need_update would have been left TRUE and the canvas would have been left unpainted. */ if (canvas->need_update) { goto update_again; } /* Paint if able to */ if (GTK_WIDGET_DRAWABLE(canvas) && canvas->need_redraw) { paint(canvas); } } /* Idle handler for the canvas. It deals with pending updates and redraws. */ static gboolean idle_handler(gpointer data) { GanvCanvasBase* canvas; GDK_THREADS_ENTER(); canvas = GANV_CANVAS_BASE(data); do_update(canvas); /* Reset idle id */ canvas->idle_id = 0; GDK_THREADS_LEAVE(); return FALSE; } /* Convenience function to add an idle handler to a canvas */ static void add_idle(GanvCanvasBase* canvas) { g_assert(canvas->need_update || canvas->need_redraw); if (!canvas->idle_id) { canvas->idle_id = g_idle_add_full(CANVAS_IDLE_PRIORITY, idle_handler, canvas, NULL); } /* canvas->idle_id = gtk_idle_add (idle_handler, canvas); */ } /** * ganv_canvas_base_root: * @canvas: A canvas. * * Queries the root group of a canvas. * * Return value: (transfer none): The root group of the specified canvas. **/ GanvItem* ganv_canvas_base_root(GanvCanvasBase* canvas) { g_return_val_if_fail(GANV_IS_CANVAS_BASE(canvas), NULL); return canvas->root; } /** * ganv_canvas_base_set_scroll_region: * @canvas: A canvas. * @x1: Leftmost limit of the scrolling region. * @y1: Upper limit of the scrolling region. * @x2: Rightmost limit of the scrolling region. * @y2: Lower limit of the scrolling region. * * Sets the scrolling region of a canvas to the specified rectangle. The canvas * will then be able to scroll only within this region. The view of the canvas * is adjusted as appropriate to display as much of the new region as possible. **/ void ganv_canvas_base_set_scroll_region(GanvCanvasBase* canvas, double x1, double y1, double x2, double y2) { double wxofs, wyofs; int xofs, yofs; g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); /* * Set the new scrolling region. If possible, do not move the visible contents of the * canvas. */ ganv_canvas_base_c2w(canvas, GTK_LAYOUT(canvas)->hadjustment->value + canvas->zoom_xofs, GTK_LAYOUT(canvas)->vadjustment->value + canvas->zoom_yofs, /*canvas->zoom_xofs, canvas->zoom_yofs,*/ &wxofs, &wyofs); canvas->scroll_x1 = x1; canvas->scroll_y1 = y1; canvas->scroll_x2 = x2; canvas->scroll_y2 = y2; ganv_canvas_base_w2c(canvas, wxofs, wyofs, &xofs, &yofs); scroll_to(canvas, xofs, yofs); canvas->need_repick = TRUE; #if 0 /* todo: should be requesting update */ (*GANV_ITEM_CLASS(canvas->root->object.klass)->update)( canvas->root, NULL, NULL, 0); #endif } /** * ganv_canvas_base_get_scroll_region: * @canvas: A canvas. * @x1: Leftmost limit of the scrolling region (return value). * @y1: Upper limit of the scrolling region (return value). * @x2: Rightmost limit of the scrolling region (return value). * @y2: Lower limit of the scrolling region (return value). * * Queries the scrolling region of a canvas. **/ void ganv_canvas_base_get_scroll_region(GanvCanvasBase* canvas, double* x1, double* y1, double* x2, double* y2) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); if (x1) { *x1 = canvas->scroll_x1; } if (y1) { *y1 = canvas->scroll_y1; } if (x2) { *x2 = canvas->scroll_x2; } if (y2) { *y2 = canvas->scroll_y2; } } /** * ganv_canvas_base_set_center_scroll_region: * @canvas: A canvas. * @center_scroll_region: Whether to center the scrolling region in the canvas * window when it is smaller than the canvas' allocation. * * When the scrolling region of the canvas is smaller than the canvas window, * e.g. the allocation of the canvas, it can be either centered on the window * or simply made to be on the upper-left corner on the window. This function * lets you configure this property. **/ void ganv_canvas_base_set_center_scroll_region(GanvCanvasBase* canvas, gboolean center_scroll_region) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); canvas->center_scroll_region = center_scroll_region != 0; scroll_to(canvas, canvas->layout.hadjustment->value, canvas->layout.vadjustment->value); } /** * ganv_canvas_base_get_center_scroll_region: * @canvas: A canvas. * * Returns whether the canvas is set to center the scrolling region in the window * if the former is smaller than the canvas' allocation. * * Return value: Whether the scroll region is being centered in the canvas window. **/ gboolean ganv_canvas_base_get_center_scroll_region(GanvCanvasBase* canvas) { g_return_val_if_fail(GANV_IS_CANVAS_BASE(canvas), FALSE); return canvas->center_scroll_region ? TRUE : FALSE; } /** * ganv_canvas_base_set_pixels_per_unit: * @canvas: A canvas. * @n: The number of pixels that correspond to one canvas unit. * * Sets the zooming factor of a canvas by specifying the number of pixels that * correspond to one canvas unit. * * The anchor point for zooming, i.e. the point that stays fixed and all others * zoom inwards or outwards from it, depends on whether the canvas is set to * center the scrolling region or not. You can control this using the * ganv_canvas_base_set_center_scroll_region() function. If the canvas is set to * center the scroll region, then the center of the canvas window is used as the * anchor point for zooming. Otherwise, the upper-left corner of the canvas * window is used as the anchor point. **/ void ganv_canvas_base_set_pixels_per_unit(GanvCanvasBase* canvas, double n) { double ax, ay; int x1, y1; int anchor_x, anchor_y; g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); g_return_if_fail(n > GANV_CANVAS_BASE_EPSILON); if (canvas->center_scroll_region) { anchor_x = GTK_WIDGET(canvas)->allocation.width / 2; anchor_y = GTK_WIDGET(canvas)->allocation.height / 2; } else { anchor_x = anchor_y = 0; } /* Find the coordinates of the anchor point in units. */ if (canvas->layout.hadjustment) { ax = (canvas->layout.hadjustment->value + anchor_x) / canvas->pixels_per_unit + canvas->scroll_x1 + canvas->zoom_xofs; } else { ax = (0.0 + anchor_x) / canvas->pixels_per_unit + canvas->scroll_x1 + canvas->zoom_xofs; } if (canvas->layout.hadjustment) { ay = (canvas->layout.vadjustment->value + anchor_y) / canvas->pixels_per_unit + canvas->scroll_y1 + canvas->zoom_yofs; } else { ay = (0.0 + anchor_y) / canvas->pixels_per_unit + canvas->scroll_y1 + canvas->zoom_yofs; } /* Now calculate the new offset of the upper left corner. */ x1 = ((ax - canvas->scroll_x1) * n) - anchor_x; y1 = ((ay - canvas->scroll_y1) * n) - anchor_y; canvas->pixels_per_unit = n; scroll_to(canvas, x1, y1); ganv_canvas_base_request_update(canvas); canvas->need_repick = TRUE; } /** * ganv_canvas_base_scroll_to: * @canvas: A canvas. * @cx: Horizontal scrolling offset in canvas pixel units. * @cy: Vertical scrolling offset in canvas pixel units. * * Makes a canvas scroll to the specified offsets, given in canvas pixel units. * The canvas will adjust the view so that it is not outside the scrolling * region. This function is typically not used, as it is better to hook * scrollbars to the canvas layout's scrolling adjusments. **/ void ganv_canvas_base_scroll_to(GanvCanvasBase* canvas, int cx, int cy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); scroll_to(canvas, cx, cy); } /** * ganv_canvas_base_get_scroll_offsets: * @canvas: A canvas. * @cx: Horizontal scrolling offset (return value). * @cy: Vertical scrolling offset (return value). * * Queries the scrolling offsets of a canvas. The values are returned in canvas * pixel units. **/ void ganv_canvas_base_get_scroll_offsets(GanvCanvasBase* canvas, int* cx, int* cy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); if (cx) { *cx = canvas->layout.hadjustment->value; } if (cy) { *cy = canvas->layout.vadjustment->value; } } /** * ganv_canvas_base_update_now: * @canvas: A canvas. * * Forces an immediate update and redraw of a canvas. If the canvas does not * have any pending update or redraw requests, then no action is taken. This is * typically only used by applications that need explicit control of when the * display is updated, like games. It is not needed by normal applications. */ void ganv_canvas_base_update_now(GanvCanvasBase* canvas) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); if (!(canvas->need_update || canvas->need_redraw)) { g_assert(canvas->idle_id == 0); g_assert(canvas->redraw_region == NULL); return; } remove_idle(canvas); do_update(canvas); } /** * ganv_canvas_base_get_item_at: * @canvas: A canvas. * @x: X position in world coordinates. * @y: Y position in world coordinates. * * Looks for the item that is under the specified position, which must be * specified in world coordinates. * * Return value: (transfer none): The sought item, or NULL if no item is at the * specified coordinates. **/ GanvItem* ganv_canvas_base_get_item_at(GanvCanvasBase* canvas, double x, double y) { GanvItem* item; double dist; int cx, cy; g_return_val_if_fail(GANV_IS_CANVAS_BASE(canvas), NULL); ganv_canvas_base_w2c(canvas, x, y, &cx, &cy); dist = GANV_ITEM_GET_CLASS(canvas->root)->point( canvas->root, x - canvas->root->x, y - canvas->root->y, cx, cy, &item); if ((int)(dist * canvas->pixels_per_unit + 0.5) <= canvas->close_enough) { return item; } else { return NULL; } } /* Queues an update of the canvas */ void ganv_canvas_base_request_update(GanvCanvasBase* canvas) { GANV_CANVAS_BASE_GET_CLASS(canvas)->request_update(canvas); } static void ganv_canvas_base_request_update_real(GanvCanvasBase* canvas) { if (canvas->need_update) { return; } canvas->need_update = TRUE; if (GTK_WIDGET_MAPPED((GtkWidget*)canvas)) { add_idle(canvas); } } static inline gboolean rect_overlaps(const IRect* a, const IRect* b) { if ((a->x > b->x + b->width) || (a->y > b->y + b->height) || (a->x + a->width < b->x) || (a->y + a->height < b->y)) { return FALSE; } return TRUE; } static inline gboolean rect_is_visible(GanvCanvasBase* canvas, const IRect* r) { const IRect rect = { canvas->layout.hadjustment->value - canvas->zoom_xofs, canvas->layout.vadjustment->value - canvas->zoom_yofs, GTK_WIDGET(canvas)->allocation.width, GTK_WIDGET(canvas)->allocation.height }; return rect_overlaps(&rect, r); } /** * ganv_canvas_base_request_redraw: * @canvas: A canvas. * @x1: Leftmost coordinate of the rectangle to be redrawn. * @y1: Upper coordinate of the rectangle to be redrawn. * @x2: Rightmost coordinate of the rectangle to be redrawn, plus 1. * @y2: Lower coordinate of the rectangle to be redrawn, plus 1. * * Informs a canvas that the specified area needs to be repainted. To be used * only by item implementations. **/ void ganv_canvas_base_request_redraw(GanvCanvasBase* canvas, int x1, int y1, int x2, int y2) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); if (!GTK_WIDGET_DRAWABLE(canvas) || (x1 >= x2) || (y1 >= y2)) { return; } const IRect rect = { x1, y1, x2 - x1, y2 - y1 }; if (!rect_is_visible(canvas, &rect)) { return; } IRect* r = (IRect*)g_malloc(sizeof(IRect)); *r = rect; canvas->redraw_region = g_slist_prepend(canvas->redraw_region, r); canvas->need_redraw = TRUE; if (canvas->idle_id == 0) { add_idle(canvas); } } /** * ganv_canvas_base_w2c_affine: * @canvas: A canvas. * @matrix: An affine transformation matrix (return value). * * Gets the affine transform that converts from world coordinates to canvas * pixel coordinates. **/ void ganv_canvas_base_w2c_affine(GanvCanvasBase* canvas, cairo_matrix_t* matrix) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); g_return_if_fail(matrix != NULL); cairo_matrix_init_translate(matrix, -canvas->scroll_x1, -canvas->scroll_y1); cairo_matrix_scale(matrix, canvas->pixels_per_unit, canvas->pixels_per_unit); } /** * ganv_canvas_base_w2c: * @canvas: A canvas. * @wx: World X coordinate. * @wy: World Y coordinate. * @cx: X pixel coordinate (return value). * @cy: Y pixel coordinate (return value). * * Converts world coordinates into canvas pixel coordinates. **/ void ganv_canvas_base_w2c(GanvCanvasBase* canvas, double wx, double wy, int* cx, int* cy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); cairo_matrix_t matrix; ganv_canvas_base_w2c_affine(canvas, &matrix); cairo_matrix_transform_point(&matrix, &wx, &wy); if (cx) { *cx = floor(wx + 0.5); } if (cy) { *cy = floor(wy + 0.5); } } /** * ganv_canvas_base_w2c_d: * @canvas: A canvas. * @wx: World X coordinate. * @wy: World Y coordinate. * @cx: X pixel coordinate (return value). * @cy: Y pixel coordinate (return value). * * Converts world coordinates into canvas pixel coordinates. This * version returns coordinates in floating point coordinates, for * greater precision. **/ void ganv_canvas_base_w2c_d(GanvCanvasBase* canvas, double wx, double wy, double* cx, double* cy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); cairo_matrix_t matrix; ganv_canvas_base_w2c_affine(canvas, &matrix); cairo_matrix_transform_point(&matrix, &wx, &wy); if (cx) { *cx = wx; } if (cy) { *cy = wy; } } /** * ganv_canvas_base_c2w: * @canvas: A canvas. * @cx: Canvas pixel X coordinate. * @cy: Canvas pixel Y coordinate. * @wx: X world coordinate (return value). * @wy: Y world coordinate (return value). * * Converts canvas pixel coordinates to world coordinates. **/ void ganv_canvas_base_c2w(GanvCanvasBase* canvas, int cx, int cy, double* wx, double* wy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); cairo_matrix_t matrix; ganv_canvas_base_w2c_affine(canvas, &matrix); cairo_matrix_invert(&matrix); double x = cx; double y = cy; cairo_matrix_transform_point(&matrix, &x, &y); if (wx) { *wx = x; } if (wy) { *wy = y; } } /** * ganv_canvas_base_window_to_world: * @canvas: A canvas. * @winx: Window-relative X coordinate. * @winy: Window-relative Y coordinate. * @worldx: X world coordinate (return value). * @worldy: Y world coordinate (return value). * * Converts window-relative coordinates into world coordinates. You can use * this when you need to convert mouse coordinates into world coordinates, for * example. **/ void ganv_canvas_base_window_to_world(GanvCanvasBase* canvas, double winx, double winy, double* worldx, double* worldy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); if (worldx) { *worldx = canvas->scroll_x1 + ((winx - canvas->zoom_xofs) / canvas->pixels_per_unit); } if (worldy) { *worldy = canvas->scroll_y1 + ((winy - canvas->zoom_yofs) / canvas->pixels_per_unit); } } /** * ganv_canvas_base_world_to_window: * @canvas: A canvas. * @worldx: World X coordinate. * @worldy: World Y coordinate. * @winx: X window-relative coordinate. * @winy: Y window-relative coordinate. * * Converts world coordinates into window-relative coordinates. **/ void ganv_canvas_base_world_to_window(GanvCanvasBase* canvas, double worldx, double worldy, double* winx, double* winy) { g_return_if_fail(GANV_IS_CANVAS_BASE(canvas)); if (winx) { *winx = (canvas->pixels_per_unit) * (worldx - canvas->scroll_x1) + canvas->zoom_xofs; } if (winy) { *winy = (canvas->pixels_per_unit) * (worldy - canvas->scroll_y1) + canvas->zoom_yofs; } }