/* * GooCanvas. Copyright (C) 2005 Damon Chaplin. * Released under the GNU LGPL license. See COPYING for details. * * goocanvasview.c - the main canvas widget. */ /** * SECTION: goocanvasview * @Title: GooCanvasView * @Short_Description: the main canvas widget. * * #GooCanvasView is the main widget containing the view of the canvas model. * * * Here is a simple example: * * * #include <goocanvas.h> * * static gboolean on_rect_button_press (GooCanvasItemView *view, * GooCanvasItemView *target, * GdkEventButton *event, * gpointer data); * * int * main (int argc, char *argv[]) * { * GtkWidget *window, *scrolled_win, *canvas; * GooCanvasModelSimple *canvas_model; * GooCanvasItemView *item_view; * GooCanvasItem *root, *rect_item, *text_item; * * /* Initialize GTK+. */ * gtk_set_locale (); * gtk_init (&argc, &argv); * * /* Create the window and widgets. */ * window = gtk_window_new (GTK_WINDOW_TOPLEVEL); * gtk_window_set_default_size (GTK_WINDOW (window), 640, 600); * gtk_widget_show (window); * * scrolled_win = gtk_scrolled_window_new (NULL, NULL); * gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_win), * GTK_SHADOW_IN); * gtk_widget_show (scrolled_win); * gtk_container_add (GTK_CONTAINER (window), scrolled_win); * * canvas = goo_canvas_view_new (); * gtk_widget_set_size_request (canvas, 600, 450); * goo_canvas_view_set_bounds (GOO_CANVAS_VIEW (canvas), 0, 0, 1000, 1000); * gtk_widget_show (canvas); * gtk_container_add (GTK_CONTAINER (scrolled_win), canvas); * * /* Create the canvas model */ * canvas_model = goo_canvas_model_simple_new (); * * root = goo_canvas_model_get_root_item (GOO_CANVAS_MODEL (canvas_model)); * * /* Add a few simple items. */ * rect_item = goo_canvas_rect_new (root, 100, 100, 400, 400, * "line-width", 10.0, * "radius-x", 20.0, * "radius-y", 10.0, * "stroke-color", "yellow", * "fill-color", "red", * NULL); * * text_item = goo_canvas_text_new (root, "Hello World", 300, 300, -1, * GTK_ANCHOR_CENTER, * "font", "Sans 24", * NULL); * goo_canvas_item_rotate (text_item, 45, 300, 300); * * /* Set the model of the canvas view. This will create item views for all the * items in the model. */ * goo_canvas_view_set_model (GOO_CANVAS_VIEW (canvas), * GOO_CANVAS_MODEL (canvas_model)); * g_object_unref (canvas_model); * * /* Connect a signal handler for the item view of the rectangle item. */ * item_view = goo_canvas_view_get_item_view (GOO_CANVAS_VIEW (canvas), * rect_item); * g_signal_connect (item_view, "button_press_event", * (GtkSignalFunc) on_rect_button_press, NULL); * * /* Pass control to the GTK+ main event loop. */ * gtk_main (); * * return 0; * } * * * /* This handles button presses in item views. We simply output a message to * the console. */ * static gboolean * on_rect_button_press (GooCanvasItemView *view, * GooCanvasItemView *target, * GdkEventButton *event, * gpointer data) * { * g_print ("rect item received button press event\n"); * return TRUE; * } * * * */ #include #include #include #include #include "goocanvasatk.h" #include "goocanvasview.h" #include "goocanvasitemview.h" #include "goocanvasgroupview.h" #include "goocanvasmarshal.h" enum { PROP_0, PROP_MODEL, PROP_SCALE, PROP_ANCHOR, PROP_X1, PROP_Y1, PROP_X2, PROP_Y2 }; enum { ITEM_VIEW_CREATED, LAST_SIGNAL }; static guint canvas_view_signals[LAST_SIGNAL] = { 0 }; static void goo_canvas_view_finalize (GObject *object); static void goo_canvas_view_realize (GtkWidget *widget); static void goo_canvas_view_unrealize (GtkWidget *widget); static void goo_canvas_view_map (GtkWidget *widget); static void goo_canvas_view_style_set (GtkWidget *widget, GtkStyle *old_style); static void goo_canvas_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation); static void goo_canvas_view_set_adjustments (GooCanvasView *view, GtkAdjustment *hadj, GtkAdjustment *vadj); static gboolean goo_canvas_view_expose_event (GtkWidget *widget, GdkEventExpose *event); static gboolean goo_canvas_view_button_press (GtkWidget *widget, GdkEventButton *event); static gboolean goo_canvas_view_button_release (GtkWidget *widget, GdkEventButton *event); static gboolean goo_canvas_view_motion (GtkWidget *widget, GdkEventMotion *event); static gboolean goo_canvas_view_scroll (GtkWidget *widget, GdkEventScroll *event); static gboolean goo_canvas_view_focus (GtkWidget *widget, GtkDirectionType direction); static gboolean goo_canvas_view_key_press (GtkWidget *widget, GdkEventKey *event); static gboolean goo_canvas_view_key_release (GtkWidget *widget, GdkEventKey *event); static gboolean goo_canvas_view_crossing (GtkWidget *widget, GdkEventCrossing *event); static gboolean goo_canvas_view_focus_in (GtkWidget *widget, GdkEventFocus *event); static gboolean goo_canvas_view_focus_out (GtkWidget *widget, GdkEventFocus *event); static gboolean goo_canvas_view_grab_broken (GtkWidget *widget, GdkEventGrabBroken *event); static void goo_canvas_view_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static void goo_canvas_view_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void set_item_view_pointer (GooCanvasItemView **view, GooCanvasItemView *new_view); static void update_pointer_item_view (GooCanvasView *view, GdkEvent *event); static void reconfigure_canvas (GooCanvasView *view, gboolean redraw_if_needed); G_DEFINE_TYPE (GooCanvasView, goo_canvas_view, GTK_TYPE_CONTAINER) /* This evaluates to TRUE if an item view is still in the canvas view. */ #define VIEW_IS_VALID(view) (goo_canvas_item_view_get_canvas_view (view)) static void goo_canvas_view_class_init (GooCanvasViewClass *klass) { GObjectClass *gobject_class = (GObjectClass*) klass; GtkWidgetClass *widget_class = (GtkWidgetClass*) klass; gobject_class->finalize = goo_canvas_view_finalize; gobject_class->get_property = goo_canvas_view_get_property; gobject_class->set_property = goo_canvas_view_set_property; widget_class->realize = goo_canvas_view_realize; widget_class->unrealize = goo_canvas_view_unrealize; widget_class->map = goo_canvas_view_map; widget_class->size_allocate = goo_canvas_view_size_allocate; widget_class->style_set = goo_canvas_view_style_set; widget_class->expose_event = goo_canvas_view_expose_event; widget_class->button_press_event = goo_canvas_view_button_press; widget_class->button_release_event = goo_canvas_view_button_release; widget_class->motion_notify_event = goo_canvas_view_motion; widget_class->scroll_event = goo_canvas_view_scroll; widget_class->focus = goo_canvas_view_focus; widget_class->key_press_event = goo_canvas_view_key_press; widget_class->key_release_event = goo_canvas_view_key_release; widget_class->enter_notify_event = goo_canvas_view_crossing; widget_class->leave_notify_event = goo_canvas_view_crossing; widget_class->focus_in_event = goo_canvas_view_focus_in; widget_class->focus_out_event = goo_canvas_view_focus_out; widget_class->grab_broken_event = goo_canvas_view_grab_broken; klass->set_scroll_adjustments = goo_canvas_view_set_adjustments; atk_registry_set_factory_type (atk_get_default_registry (), GOO_TYPE_CANVAS_VIEW, goo_canvas_view_accessible_factory_get_type ()); g_object_class_install_property (gobject_class, PROP_MODEL, g_param_spec_object ("model", _("Model"), _("The model for the canvas view"), GOO_TYPE_CANVAS_MODEL, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_SCALE, g_param_spec_double ("scale", _("Scale"), _("The number of pixels to use for each device unit"), 0.0, G_MAXDOUBLE, 1.0, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_ANCHOR, g_param_spec_enum ("anchor", _("Anchor"), _("Where to place the canvas when it is smaller than the widget's allocated area"), GTK_TYPE_ANCHOR_TYPE, GTK_ANCHOR_NW, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_X1, g_param_spec_double ("x1", _("X1"), _("The x coordinate of the left edge of the canvas bounds, in device units"), -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_Y1, g_param_spec_double ("y1", _("Y1"), _("The y coordinate of the top edge of the canvas bounds, in device units"), -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_X2, g_param_spec_double ("x2", _("X2"), _("The x coordinate of the right edge of the canvas bounds, in device units"), -G_MAXDOUBLE, G_MAXDOUBLE, 1000.0, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_Y2, g_param_spec_double ("y2", _("Y2"), _("The y coordinate of the bottom edge of the canvas bounds, in device units"), -G_MAXDOUBLE, G_MAXDOUBLE, 1000.0, G_PARAM_READWRITE)); /** * GooCanvasView::set-scroll-adjustments * @view: the canvas view. * @hadjustment: the horizontal adjustment. * @vadjustment: the vertical adjustment. * * This is used when the #GooCanvas is placed inside a #GtkScrolledWindow, * to connect up the adjustments so scrolling works properly. * * It isn't useful for applications. */ widget_class->set_scroll_adjustments_signal = g_signal_new ("set_scroll_adjustments", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GooCanvasViewClass, set_scroll_adjustments), NULL, NULL, goo_canvas_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT); /* Signals. */ /** * GooCanvasView::item-view-created * @view: the canvas view. * @item_view: the new item view. * @item: the canvas item. * * This is emitted when a new item view is created. * * Applications can set up signal handlers for the new item views here. */ canvas_view_signals[ITEM_VIEW_CREATED] = g_signal_new ("item-view-created", G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GooCanvasViewClass, item_view_created), NULL, NULL, goo_canvas_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, GOO_TYPE_CANVAS_ITEM_VIEW, GOO_TYPE_CANVAS_ITEM); } static void goo_canvas_view_init (GooCanvasView *canvas_view) { canvas_view->scale = 1.0; canvas_view->need_update = TRUE; canvas_view->crossing_event.type = GDK_LEAVE_NOTIFY; canvas_view->anchor = GTK_ANCHOR_NORTH_WEST; /* Set the default bounds to a reasonable size. */ canvas_view->bounds.x1 = 0.0; canvas_view->bounds.y1 = 0.0; canvas_view->bounds.x2 = 1000.0; canvas_view->bounds.y2 = 1000.0; /* Create our own adjustments, in case we aren't inserted into a scrolled window. The accessibility code needs these. */ canvas_view->hadjustment = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); canvas_view->vadjustment = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); g_object_ref_sink (canvas_view->hadjustment); g_object_ref_sink (canvas_view->vadjustment); canvas_view->item_to_view = g_hash_table_new (g_direct_hash, g_direct_equal); } /** * goo_canvas_view_new: * * Creates a new #GooCanvasView widget. * * Returns: a new #GooCanvasView widget. **/ GtkWidget* goo_canvas_view_new (void) { return GTK_WIDGET (g_object_new (GOO_TYPE_CANVAS_VIEW, NULL)); } static void goo_canvas_view_finalize (GObject *object) { GooCanvasView *view = (GooCanvasView*) object; if (view->root_view) g_object_unref (view->root_view); if (view->model) g_object_unref (view->model); if (view->idle_id) g_source_remove (view->idle_id); /* Release any references we hold to item views. */ set_item_view_pointer (&view->pointer_item_view, NULL); set_item_view_pointer (&view->pointer_grab_item_view, NULL); set_item_view_pointer (&view->pointer_grab_initial_item_view, NULL); set_item_view_pointer (&view->focused_item_view, NULL); set_item_view_pointer (&view->keyboard_grab_item_view, NULL); g_object_unref (view->hadjustment); g_object_unref (view->vadjustment); g_hash_table_destroy (view->item_to_view); G_OBJECT_CLASS (goo_canvas_view_parent_class)->finalize (object); } static cairo_t* goo_canvas_view_create_cairo (GooCanvasView *view) { cairo_t *cr; cr = gdk_cairo_create (view->canvas_window); /*cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE);*/ return cr; } static void goo_canvas_view_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GooCanvasView *view = (GooCanvasView*) object; switch (prop_id) { case PROP_MODEL: g_value_set_object (value, view->model); break; case PROP_SCALE: g_value_set_double (value, view->scale); break; case PROP_ANCHOR: g_value_set_enum (value, view->anchor); break; case PROP_X1: g_value_set_double (value, view->bounds.x1); break; case PROP_Y1: g_value_set_double (value, view->bounds.y1); break; case PROP_X2: g_value_set_double (value, view->bounds.x2); break; case PROP_Y2: g_value_set_double (value, view->bounds.y2); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void goo_canvas_view_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GooCanvasView *view = (GooCanvasView*) object; switch (prop_id) { case PROP_MODEL: goo_canvas_view_set_model (view, g_value_get_object (value)); break; case PROP_SCALE: goo_canvas_view_set_scale (view, g_value_get_double (value)); break; case PROP_ANCHOR: view->anchor = g_value_get_enum (value); reconfigure_canvas (view, TRUE); break; case PROP_X1: view->bounds.x1 = g_value_get_double (value); reconfigure_canvas (view, TRUE); break; case PROP_Y1: view->bounds.y1 = g_value_get_double (value); reconfigure_canvas (view, TRUE); break; case PROP_X2: view->bounds.x2 = g_value_get_double (value); reconfigure_canvas (view, TRUE); break; case PROP_Y2: view->bounds.y2 = g_value_get_double (value); reconfigure_canvas (view, TRUE); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } /** * goo_canvas_view_get_model: * @view: a #GooCanvasView. * * Gets the model being displayed in the canvas. * * Returns: the #GooCanvasModel being displayed. **/ GooCanvasModel* goo_canvas_view_get_model (GooCanvasView *view) { g_return_val_if_fail (GOO_IS_CANVAS_VIEW (view), NULL); return view->model; } /** * goo_canvas_view_set_model: * @view: a #GooCanvasView. * @model: a #GooCanvasModel. * * Sets the model to be displayed in the #GooCanvasView. * * A hierarchy of item views will be created, corresponding to the hierarchy * of items in the model. **/ void goo_canvas_view_set_model (GooCanvasView *view, GooCanvasModel *model) { GooCanvasItem *root_item; g_return_if_fail (GOO_IS_CANVAS_VIEW (view)); if (view->model == model) return; if (view->model) { g_object_unref (view->model); view->model = NULL; g_object_unref (view->root_view); view->root_view = NULL; } if (model) { view->model = g_object_ref (model); root_item = goo_canvas_model_get_root_item (model); /* Create a hierarchy of views for all the items in the model. */ view->root_view = goo_canvas_view_create_item_view (view, root_item, NULL); view->need_update = TRUE; if (GTK_WIDGET_REALIZED (view)) goo_canvas_view_update (view); } gtk_widget_queue_draw (GTK_WIDGET (view)); } /** * goo_canvas_view_get_root_view: * @view: a #GooCanvasView. * * Gets the root item view. * * Returns: the root item view, or %NULL of no canvas model is set. **/ GooCanvasItemView* goo_canvas_view_get_root_view (GooCanvasView *view) { return view->root_view; } /** * goo_canvas_view_get_item_view: * @view: a #GooCanvasView. * @item: a #GooCanvasItem. * * Gets the view for the given #GooCanvasItem. * * For simple applications you can use this function to set up signal handlers * for your item views, e.g. * * * item_view = goo_canvas_view_get_item_view (GOO_CANVAS_VIEW (canvas), * my_item); * g_signal_connect (item_view, "button_press_event", * (GtkSignalFunc) on_my_item_view_button_press, NULL); * * * More complex applications may want to use the * #GooCanvasView::item-view-created signal to hook up their signal handlers. * * Returns: the view for the given #GooCanvasItem, or %NULL if no view has been * created for it yet. **/ GooCanvasItemView * goo_canvas_view_get_item_view (GooCanvasView *view, GooCanvasItem *item) { GooCanvasItemView *item_view; item_view = g_hash_table_lookup (view->item_to_view, item); /* If the item has a view check it is valid. */ g_return_val_if_fail (!item_view || GOO_IS_CANVAS_ITEM_VIEW (item_view), NULL); return item_view; } /** * goo_canvas_view_get_item_view_at: * @view: a #GooCanvasView. * @x: the x coordinate of the point. * @y: the y coordinate of the point * @is_pointer_event: %TRUE if the "pointer-events" properties of items should * be used to determine which parts of the item are tested. * * Gets the item view at the given point. * * Returns: the item view found at the given point, or %NULL if no item view * was found. **/ GooCanvasItemView* goo_canvas_view_get_item_view_at (GooCanvasView *view, gdouble x, gdouble y, gboolean is_pointer_event) { cairo_t *cr; GooCanvasItemView *found_view; /* If no canvas model is set, just return NULL. */ if (!view->root_view) return NULL; cr = goo_canvas_view_create_cairo (view); found_view = goo_canvas_item_view_get_item_view_at (view->root_view, x, y, cr, is_pointer_event, TRUE); cairo_destroy (cr); return found_view; } static void goo_canvas_view_realize (GtkWidget *widget) { GooCanvasView *view; GdkWindowAttr attributes; gint attributes_mask; gint width_pixels, height_pixels; g_return_if_fail (GOO_IS_CANVAS_VIEW (widget)); view = GOO_CANVAS_VIEW (widget); GTK_WIDGET_SET_FLAGS (view, 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.visual = gtk_widget_get_visual (widget); attributes.colormap = gtk_widget_get_colormap (widget); attributes.event_mask = GDK_VISIBILITY_NOTIFY_MASK; 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, widget); /* We want to round the sizes up to the next pixel. */ width_pixels = ((view->bounds.x2 - view->bounds.x1) * view->scale) + 1; height_pixels = ((view->bounds.y2 - view->bounds.y1) * view->scale) + 1; attributes.x = view->hadjustment ? - view->hadjustment->value : 0, attributes.y = view->vadjustment ? - view->vadjustment->value : 0; attributes.width = MAX (width_pixels, widget->allocation.width); attributes.height = MAX (height_pixels, widget->allocation.height); attributes.event_mask = GDK_EXPOSURE_MASK | GDK_SCROLL_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_FOCUS_CHANGE_MASK | gtk_widget_get_events (widget); view->canvas_window = gdk_window_new (widget->window, &attributes, attributes_mask); gdk_window_set_user_data (view->canvas_window, widget); attributes.x = widget->allocation.x; attributes.y = widget->allocation.y; attributes.width = widget->allocation.width; attributes.height = widget->allocation.height; attributes.event_mask = 0; view->tmp_window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask); gdk_window_set_user_data (view->tmp_window, widget); widget->style = gtk_style_attach (widget->style, widget->window); gdk_window_set_background (widget->window, &widget->style->base[widget->state]); gdk_window_set_background (view->canvas_window, &widget->style->base[widget->state]); gdk_window_set_back_pixmap (view->tmp_window, NULL, FALSE); goo_canvas_view_update (GOO_CANVAS_VIEW (widget)); } static void goo_canvas_view_unrealize (GtkWidget *widget) { GooCanvasView *view; g_return_if_fail (GOO_IS_CANVAS_VIEW (widget)); view = GOO_CANVAS_VIEW (widget); gdk_window_set_user_data (view->canvas_window, NULL); gdk_window_destroy (view->canvas_window); view->canvas_window = NULL; gdk_window_set_user_data (view->tmp_window, NULL); gdk_window_destroy (view->tmp_window); view->tmp_window = NULL; if (GTK_WIDGET_CLASS (goo_canvas_view_parent_class)->unrealize) (* GTK_WIDGET_CLASS (goo_canvas_view_parent_class)->unrealize) (widget); } static void goo_canvas_view_map (GtkWidget *widget) { GooCanvasView *view; g_return_if_fail (GOO_IS_CANVAS_VIEW (widget)); view = GOO_CANVAS_VIEW (widget); GTK_WIDGET_SET_FLAGS (widget, GTK_MAPPED); gdk_window_show (view->canvas_window); gdk_window_show (widget->window); } static void goo_canvas_view_style_set (GtkWidget *widget, GtkStyle *old_style) { if (GTK_WIDGET_CLASS (goo_canvas_view_parent_class)->style_set) (* GTK_WIDGET_CLASS (goo_canvas_view_parent_class)->style_set) (widget, old_style); if (GTK_WIDGET_REALIZED (widget)) { gdk_window_set_background (widget->window, &widget->style->base[widget->state]); gdk_window_set_background (GOO_CANVAS_VIEW (widget)->canvas_window, &widget->style->base[widget->state]); } } static void goo_canvas_view_configure_hadjustment (GooCanvasView *view, gint window_width) { GtkWidget *widget = GTK_WIDGET (view); GtkAdjustment *adj = view->hadjustment; gboolean changed = FALSE; gboolean value_changed = FALSE; gdouble max_value; if (adj->upper != window_width) { adj->upper = window_width; changed = TRUE; } if (adj->page_size != widget->allocation.width) { adj->page_size = widget->allocation.width; adj->page_increment = adj->page_size * 0.9; adj->step_increment = adj->page_size * 0.1; changed = TRUE; } max_value = MAX (0.0, adj->upper - adj->page_size); if (adj->value > max_value) { adj->value = max_value; value_changed = TRUE; } if (changed) gtk_adjustment_changed (adj); if (value_changed) gtk_adjustment_value_changed (adj); } static void goo_canvas_view_configure_vadjustment (GooCanvasView *view, gint window_height) { GtkWidget *widget = GTK_WIDGET (view); GtkAdjustment *adj = view->vadjustment; gboolean changed = FALSE; gboolean value_changed = FALSE; gdouble max_value; if (adj->upper != window_height) { adj->upper = window_height; changed = TRUE; } if (adj->page_size != widget->allocation.height) { adj->page_size = widget->allocation.height; adj->page_increment = adj->page_size * 0.9; adj->step_increment = adj->page_size * 0.1; changed = TRUE; } max_value = MAX (0.0, adj->upper - adj->page_size); if (adj->value > max_value) { adj->value = max_value; value_changed = TRUE; } if (changed) gtk_adjustment_changed (adj); if (value_changed) gtk_adjustment_value_changed (adj); } /* This makes sure the canvas is all set up correctly, i.e. the scrollbar adjustments are set, the canvas x & y offsets are calculated, and the canvas window is sized. */ static void reconfigure_canvas (GooCanvasView *view, gboolean redraw_if_needed) { gint width_pixels, height_pixels; gint window_x = 0, window_y = 0, window_width, window_height; gint new_x_offset = 0, new_y_offset = 0; GtkWidget *widget; widget = GTK_WIDGET (view); /* Make sure the bounds are sane. */ if (view->bounds.x2 < view->bounds.x1) view->bounds.x2 = view->bounds.x1; if (view->bounds.y2 < view->bounds.y1) view->bounds.y2 = view->bounds.y1; /* This is the natural size of the canvas window in pixels, rounded up to the next pixel. */ width_pixels = ((view->bounds.x2 - view->bounds.x1) * view->scale) + 1; height_pixels = ((view->bounds.y2 - view->bounds.y1) * view->scale) + 1; /* The actual window size is always at least as big as the widget's window.*/ window_width = MAX (width_pixels, widget->allocation.width); window_height = MAX (height_pixels, widget->allocation.height); /* If the width or height is smaller than the window, we need to calculate the canvas x & y offsets according to the anchor. */ if (width_pixels < widget->allocation.width) { switch (view->anchor) { case GTK_ANCHOR_NORTH_WEST: case GTK_ANCHOR_WEST: case GTK_ANCHOR_SOUTH_WEST: new_x_offset = 0; break; case GTK_ANCHOR_NORTH: case GTK_ANCHOR_CENTER: case GTK_ANCHOR_SOUTH: new_x_offset = (widget->allocation.width - width_pixels) / 2; break; case GTK_ANCHOR_NORTH_EAST: case GTK_ANCHOR_EAST: case GTK_ANCHOR_SOUTH_EAST: new_x_offset = widget->allocation.width - width_pixels; break; } } if (height_pixels < widget->allocation.height) { switch (view->anchor) { case GTK_ANCHOR_NORTH_WEST: case GTK_ANCHOR_NORTH: case GTK_ANCHOR_NORTH_EAST: new_y_offset = 0; break; case GTK_ANCHOR_WEST: case GTK_ANCHOR_CENTER: case GTK_ANCHOR_EAST: new_y_offset = (widget->allocation.height - height_pixels) / 2; break; case GTK_ANCHOR_SOUTH_WEST: case GTK_ANCHOR_SOUTH: case GTK_ANCHOR_SOUTH_EAST: new_y_offset = widget->allocation.height - height_pixels; break; } } view->freeze_count++; if (view->hadjustment) { goo_canvas_view_configure_hadjustment (view, window_width); window_x = - view->hadjustment->value; } if (view->vadjustment) { goo_canvas_view_configure_vadjustment (view, window_height); window_y = - view->vadjustment->value; } view->freeze_count--; if (GTK_WIDGET_REALIZED (view)) { gdk_window_move_resize (view->canvas_window, window_x, window_y, window_width, window_height); } /* If one of the offsets has changed we have to redraw the widget. */ if (view->canvas_x_offset != new_x_offset || view->canvas_y_offset != new_y_offset) { view->canvas_x_offset = new_x_offset; view->canvas_y_offset = new_y_offset; if (redraw_if_needed) gtk_widget_queue_draw (GTK_WIDGET (view)); } } static void goo_canvas_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GooCanvasView *view; g_return_if_fail (GOO_IS_CANVAS_VIEW (widget)); view = GOO_CANVAS_VIEW (widget); widget->allocation = *allocation; if (GTK_WIDGET_REALIZED (widget)) { gdk_window_move_resize (widget->window, allocation->x, allocation->y, allocation->width, allocation->height); gdk_window_move_resize (view->tmp_window, allocation->x, allocation->y, allocation->width, allocation->height); } reconfigure_canvas (view, TRUE); } static void goo_canvas_view_adjustment_value_changed (GtkAdjustment *adjustment, GooCanvasView *view) { AtkObject *accessible; if (!view->freeze_count && GTK_WIDGET_REALIZED (view)) { gdk_window_move (view->canvas_window, - view->hadjustment->value, - view->vadjustment->value); /* If this is callback from a signal for one of the scrollbars, process updates here for smoother scrolling. */ if (adjustment) gdk_window_process_updates (view->canvas_window, TRUE); /* Notify any accessibility modules that the view has changed. */ accessible = gtk_widget_get_accessible (GTK_WIDGET (view)); g_signal_emit_by_name (accessible, "visible_data_changed"); } } /* Sets either or both adjustments, If hadj or vadj is NULL a new adjustment is created. */ static void goo_canvas_view_set_adjustments (GooCanvasView *view, GtkAdjustment *hadj, GtkAdjustment *vadj) { gboolean need_reconfigure = FALSE; g_return_if_fail (GOO_IS_CANVAS_VIEW (view)); if (hadj) g_return_if_fail (GTK_IS_ADJUSTMENT (hadj)); else if (view->hadjustment) hadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); if (vadj) g_return_if_fail (GTK_IS_ADJUSTMENT (vadj)); else if (view->vadjustment) vadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); if (view->hadjustment && (view->hadjustment != hadj)) { g_signal_handlers_disconnect_by_func (view->hadjustment, goo_canvas_view_adjustment_value_changed, view); g_object_unref (view->hadjustment); } if (view->vadjustment && (view->vadjustment != vadj)) { g_signal_handlers_disconnect_by_func (view->vadjustment, goo_canvas_view_adjustment_value_changed, view); g_object_unref (view->vadjustment); } if (view->hadjustment != hadj) { view->hadjustment = hadj; g_object_ref_sink (view->hadjustment); g_signal_connect (view->hadjustment, "value_changed", G_CALLBACK (goo_canvas_view_adjustment_value_changed), view); need_reconfigure = TRUE; } if (view->vadjustment != vadj) { view->vadjustment = vadj; g_object_ref_sink (view->vadjustment); g_signal_connect (view->vadjustment, "value_changed", G_CALLBACK (goo_canvas_view_adjustment_value_changed), view); need_reconfigure = TRUE; } if (need_reconfigure) reconfigure_canvas (view, TRUE); } /* Sets one of our pointers to an item view, adding a reference to it and releasing any reference to the current item view. */ static void set_item_view_pointer (GooCanvasItemView **view, GooCanvasItemView *new_view) { /* If the view hasn't changed, just return. */ if (*view == new_view) return; /* Unref the current item view, if it isn't NULL. */ if (*view) g_object_unref (*view); /* Set the new item view. */ *view = new_view; /* Add a reference to it, if it isn't NULL. */ if (*view) g_object_ref (*view); } /** * goo_canvas_view_get_bounds: * @view: a #GooCanvasView. * @left: a pointer to a #gdouble to return the left edge, or %NULL. * @top: a pointer to a #gdouble to return the top edge, or %NULL. * @right: a pointer to a #gdouble to return the right edge, or %NULL. * @bottom: a pointer to a #gdouble to return the bottom edge, or %NULL. * * Gets the bounds of the canvas. **/ void goo_canvas_view_get_bounds (GooCanvasView *view, gdouble *left, gdouble *top, gdouble *right, gdouble *bottom) { g_return_if_fail (GOO_IS_CANVAS_VIEW (view)); if (left) *left = view->bounds.x1; if (top) *top = view->bounds.y1; if (right) *right = view->bounds.x2; if (bottom) *bottom = view->bounds.y2; } /** * goo_canvas_view_set_bounds: * @view: a #GooCanvasView. * @left: the left edge. * @top: the top edge. * @right: the right edge. * @bottom: the bottom edge. * * Sets the bounds of the #GooCanvasView, in device units. * * By default, device units are the same as pixels, though * goo_canvas_view_set_scale() can be used to specify a different scale. **/ void goo_canvas_view_set_bounds (GooCanvasView *view, gdouble left, gdouble top, gdouble right, gdouble bottom) { g_return_if_fail (GOO_IS_CANVAS_VIEW (view)); view->bounds.x1 = left; view->bounds.y1 = top; view->bounds.x2 = right; view->bounds.y2 = bottom; reconfigure_canvas (view, TRUE); } /** * goo_canvas_view_scroll_to: * @view: a #GooCanvasView. * @left: the x coordinate to scroll to. * @top: the y coordinate to scroll to. * * Scrolls the canvas, placing the given point as close to the top-left of * the view as possible. **/ void goo_canvas_view_scroll_to (GooCanvasView *view, gdouble left, gdouble top) { gdouble x = left, y = top; g_return_if_fail (GOO_IS_CANVAS_VIEW (view)); /* The scrollbar adjustments use pixel values, so convert to pixels. */ goo_canvas_view_convert_to_pixels (view, &x, &y); /* Make sure we stay within the bounds. */ x = CLAMP (x, view->hadjustment->lower, view->hadjustment->upper - view->hadjustment->page_size); y = CLAMP (y, view->vadjustment->lower, view->vadjustment->upper - view->vadjustment->page_size); view->freeze_count++; gtk_adjustment_set_value (view->hadjustment, x); gtk_adjustment_set_value (view->vadjustment, y); view->freeze_count--; goo_canvas_view_adjustment_value_changed (NULL, view); } /* This makes sure the given item view is displayed, scrolling if necessary. */ static void goo_canvas_view_scroll_to_item_view (GooCanvasView *view, GooCanvasItemView *item_view) { GooCanvasBounds bounds; gdouble hvalue, vvalue; goo_canvas_item_view_get_bounds (item_view, &bounds); goo_canvas_view_convert_to_pixels (view, &bounds.x1, &bounds.y1); goo_canvas_view_convert_to_pixels (view, &bounds.x2, &bounds.y2); view->freeze_count++; /* Remember the current adjustment values. */ hvalue = view->hadjustment->value; vvalue = view->vadjustment->value; /* Update the adjustments so the item view is displayed. */ gtk_adjustment_clamp_page (view->hadjustment, bounds.x1, bounds.x2); gtk_adjustment_clamp_page (view->vadjustment, bounds.y1, bounds.y2); view->freeze_count--; /* If the adjustments have changed we need to scroll. */ if (hvalue != view->hadjustment->value || vvalue != view->vadjustment->value) goo_canvas_view_adjustment_value_changed (NULL, view); } /** * goo_canvas_view_get_scale: * @view: a #GooCanvasView. * * Gets the current scale of the canvas, i.e. the number of pixels to use * for each device unit. * * Returns: the current scale setting. **/ gdouble goo_canvas_view_get_scale (GooCanvasView *view) { g_return_val_if_fail (GOO_IS_CANVAS_VIEW (view), 1.0); return view->scale; } /** * goo_canvas_view_set_scale: * @view: a #GooCanvasView. * @pixels_per_unit: the new scale setting. * * Sets the current scale of the canvas, i.e. the number of pixels to use * for each device unit. **/ void goo_canvas_view_set_scale (GooCanvasView *view, gdouble pixels_per_unit) { gdouble x, y; g_return_if_fail (GOO_IS_CANVAS_VIEW (view)); /* Calculate the coords of the current center point in pixels. */ x = view->hadjustment->value + view->hadjustment->page_size / 2; y = view->vadjustment->value + view->vadjustment->page_size / 2; /* Convert from pixel units to device units. */ goo_canvas_view_convert_from_pixels (view, &x, &y); /* Show our temporary window above the canvas window, so that the windowing system doesn't try to scroll the contents when we change the adjustments. Since we are changing the scale we need to redraw everything so the scrolling is unnecessary and really ugly. FIXME: There is a possible issue with keyboard focus/input methods here, since hidden windows can't have the keyboard focus. */ if (GTK_WIDGET_MAPPED (view)) gdk_window_show (view->tmp_window); view->freeze_count++; view->scale = pixels_per_unit; reconfigure_canvas (view, FALSE); /* Convert from the center point to the new desired top-left posision. */ x -= view->hadjustment->page_size / view->scale / 2; y -= view->vadjustment->page_size / view->scale / 2; /* Now try to scroll to it. */ goo_canvas_view_scroll_to (view, x, y); view->freeze_count--; goo_canvas_view_adjustment_value_changed (NULL, view); /* Now hide the temporary window, so the canvas window will get an expose event. */ if (GTK_WIDGET_MAPPED (view)) gdk_window_hide (view->tmp_window); } /** * goo_canvas_view_unregister_item_view: * @view: a #GooCanvasView. * @item: the item whose view is being finalized. * * This function should be called in the finalize method of #GooCanvasItemView * objects, to remove the item view from the #GooCanvasView's hash table. **/ void goo_canvas_view_unregister_item_view (GooCanvasView *view, GooCanvasItem *item) { g_hash_table_remove (view->item_to_view, item); } /** * goo_canvas_view_create_item_view: * @view: a #GooCanvasView. * @item: the item to create a view for. * @parent_view: the parent view of the new item. * * Creates a new item view for the given item. * * It uses the create_item_view() virtual method if it has been set. * Subclasses of #GooCanvasView can define this method if they want to use * custom views for items. * * It emits the "item-view-created" signal after creating the view, so * application code can connect signal handlers to the new view if desired. * * Returns: a new item view. **/ GooCanvasItemView* goo_canvas_view_create_item_view (GooCanvasView *view, GooCanvasItem *item, GooCanvasItemView *parent_view) { GooCanvasItemView *item_view = NULL; /* Use the virtual method if it has been set. */ if (GOO_CANVAS_VIEW_GET_CLASS (view)->create_item_view) item_view = GOO_CANVAS_VIEW_GET_CLASS (view)->create_item_view (view, item, parent_view); /* The virtual method can return NULL to use the default view for an item. */ if (!item_view) item_view = GOO_CANVAS_ITEM_GET_IFACE (item)->create_view (item, view, parent_view); g_hash_table_insert (view->item_to_view, item, item_view); /* Emit a signal so apps can hook up signal handlers if they want. */ g_signal_emit (view, canvas_view_signals[ITEM_VIEW_CREATED], 0, item_view, item); return item_view; } static void goo_canvas_view_update_internal (GooCanvasView *view, cairo_t *cr) { GooCanvasBounds bounds; /* It is possible that processing the first set of updates causes other updates to be scheduled, so we loop round until all are done. Items should ensure that they don't cause this to loop forever. */ while (view->need_update) { view->need_update = FALSE; if (view->root_view) goo_canvas_item_view_update (view->root_view, FALSE, cr, &bounds); } /* Check which item view is under the pointer. */ update_pointer_item_view (view, NULL); } /** * goo_canvas_view_update: * @view: a #GooCanvasView. * * Updates any item views that need updating. * * This is only intended to be used by subclasses of #GooCanvasView or * #GooCanvasItemView implementations. * * If the bounds of items change, they will request a redraw of the old and * new bounds so the display is updated correctly. **/ void goo_canvas_view_update (GooCanvasView *view) { cairo_t *cr = goo_canvas_view_create_cairo (view); goo_canvas_view_update_internal (view, cr); cairo_destroy (cr); } static gint goo_canvas_view_idle_handler (GooCanvasView *view) { GDK_THREADS_ENTER (); goo_canvas_view_update (view); /* Reset idle id. Note that we do this after goo_canvas_view_update(), to make sure we don't schedule another idle handler while that is running. */ view->idle_id = 0; GDK_THREADS_LEAVE (); /* Return FALSE to remove the idle handler. */ return FALSE; } /** * goo_canvas_view_request_update: * @view: a #GooCanvasView. * * Schedules an update of the #GooCanvasView. This will be performed in * the idle loop, after all pending events have been handled, but before * the canvas has been repainted. **/ void goo_canvas_view_request_update (GooCanvasView *view) { view->need_update = TRUE; /* We have to wait until we are realized. We'll do a full update then. */ if (!GTK_WIDGET_REALIZED (view)) return; /* We use a higher priority than the normal GTK+ redraw idle handler so the * canvas state will be updated before redrawing. */ if (!view->idle_id) view->idle_id = g_idle_add_full (GDK_PRIORITY_REDRAW - 20, (GSourceFunc) goo_canvas_view_idle_handler, view, NULL); } /** * goo_canvas_view_request_redraw: * @view: a #GooCanvasView. * @bounds: the bounds to redraw. * * Requests that the given bounds be redrawn. **/ void goo_canvas_view_request_redraw (GooCanvasView *view, GooCanvasBounds *bounds) { GdkRectangle rect; if (!GTK_WIDGET_DRAWABLE (view) || (bounds->x1 == bounds->x2)) return; /* We subtract one from the left & top edges, in case anti-aliasing makes the drawing use an extra pixel. */ rect.x = (double) (bounds->x1 - view->bounds.x1) * view->scale - 1; rect.y = (double) (bounds->y1 - view->bounds.y1) * view->scale - 1; /* We add an extra one here for the same reason. (The other extra one is to round up to the next pixel.) And one for luck! */ rect.width = (double) (bounds->x2 - view->bounds.x1) * view->scale - rect.x + 2 + 1; rect.height = (double) (bounds->y2 - view->bounds.y1) * view->scale - rect.y + 2 + 1; rect.x += view->canvas_x_offset; rect.y += view->canvas_y_offset; gdk_window_invalidate_rect (view->canvas_window, &rect, FALSE); } static gboolean goo_canvas_view_expose_event (GtkWidget *widget, GdkEventExpose *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); GooCanvasBounds bounds; cairo_t *cr; if (!view->root_view) return FALSE; if (event->window != view->canvas_window) return FALSE; cr = goo_canvas_view_create_cairo (view); if (view->need_update) goo_canvas_view_update_internal (view, cr); bounds.x1 = ((event->area.x - view->canvas_x_offset) / view->scale) + view->bounds.x1; bounds.y1 = ((event->area.y - view->canvas_y_offset) / view->scale) + view->bounds.y1; bounds.x2 = (event->area.width / view->scale) + bounds.x1; bounds.y2 = (event->area.height / view->scale) + bounds.y1; /* Translate it to use the canvas pixel offsets. */ cairo_translate (cr, view->canvas_x_offset, view->canvas_y_offset); /* Scale it so we can use canvas coordinates. */ cairo_scale (cr, view->scale, view->scale); /* Translate it so the top-left of the canvas becomes (0,0). */ cairo_translate (cr, -view->bounds.x1, -view->bounds.y1); goo_canvas_item_view_paint (view->root_view, cr, &bounds, view->scale); cairo_destroy (cr); return FALSE; } /** * goo_canvas_view_render: * @view: a #GooCanvasView. * @cr: a cairo context. * @bounds: the area to render, or %NULL to render the entire canvas. * @scale: the scale to compare with each item's visibility * threshold to see if they should be rendered. This only affects items that * have their visibility set to %GOO_CANVAS_ITEM_VISIBLE_ABOVE_THRESHOLD. * * Renders all or part of a canvas to the given cairo context. **/ void goo_canvas_view_render (GooCanvasView *view, cairo_t *cr, GooCanvasBounds *bounds, gdouble scale) { if (bounds) goo_canvas_item_view_paint (view->root_view, cr, bounds, scale); else goo_canvas_item_view_paint (view->root_view, cr, &view->bounds, scale); } /* * Keyboard/Mouse Event & Grab Handling. */ /* Initializes a synthesized crossing event from a given button press/release, motion, or crossing event. */ static void initialize_crossing_event (GooCanvasView *view, GdkEvent *event) { GdkEventCrossing *crossing_event = &view->crossing_event; /* Initialize the crossing event. */ crossing_event->type = event->any.type; crossing_event->window = event->any.window; crossing_event->send_event = event->any.send_event; crossing_event->subwindow = NULL; crossing_event->detail = GDK_NOTIFY_ANCESTOR; crossing_event->focus = FALSE; crossing_event->mode = GDK_CROSSING_NORMAL; switch (event->type) { case GDK_MOTION_NOTIFY: crossing_event->time = event->motion.time; crossing_event->x = event->motion.x; crossing_event->y = event->motion.y; crossing_event->x_root = event->motion.x_root; crossing_event->y_root = event->motion.y_root; crossing_event->state = event->motion.state; break; case GDK_ENTER_NOTIFY: case GDK_LEAVE_NOTIFY: crossing_event->time = event->crossing.time; crossing_event->x = event->crossing.x; crossing_event->y = event->crossing.y; crossing_event->x_root = event->crossing.x_root; crossing_event->y_root = event->crossing.y_root; crossing_event->state = event->crossing.state; break; default: /* It must be a button press/release event. */ crossing_event->time = event->button.time; crossing_event->x = event->button.x; crossing_event->y = event->button.y; crossing_event->x_root = event->button.x_root; crossing_event->y_root = event->button.y_root; crossing_event->state = event->button.state; break; } } /* Emits a signal for an item view and all its parents, until one of them returns TRUE. */ static gboolean propagate_event (GooCanvasView *view, GooCanvasItemView *item_view, gchar *signal_name, GdkEvent *event) { GooCanvasItemView *ancestor; gboolean stop_emission = FALSE, valid; /* Don't emit any events if the canvas is not realized. */ if (!GTK_WIDGET_REALIZED (view)) return FALSE; if (item_view) { /* Check if the item view is still in the canvas view. */ if (!VIEW_IS_VALID (item_view)) return FALSE; ancestor = item_view; } else { /* If there is no target item view, we send the event to the root view, with target_view set to NULL. */ ancestor = view->root_view; } /* Make sure the item_view pointer remains valid throughout the emission. */ if (item_view) g_object_ref (item_view); while (ancestor) { g_object_ref (ancestor); g_signal_emit_by_name (ancestor, signal_name, item_view, event, &stop_emission); /* Check if the ancestor is still in the canvas. */ valid = VIEW_IS_VALID (ancestor) ? TRUE : FALSE; g_object_unref (ancestor); if (stop_emission || !valid) break; ancestor = goo_canvas_item_view_get_parent_view (ancestor); } if (item_view) g_object_unref (item_view); return stop_emission; } /* This is called to emit pointer events - enter/leave notify, motion notify, and button press/release. */ static gboolean emit_pointer_event (GooCanvasView *view, gchar *signal_name, GdkEvent *original_event) { GdkEvent event = *original_event; GooCanvasItemView *target_item_view = view->pointer_item_view; double *x, *y, *x_root, *y_root; /* Check if an item has grabbed the pointer. */ if (view->pointer_grab_item_view) { /* When the pointer is grabbed, it receives all the pointer motion, button press/release events and its own enter/leave notify events. Enter/leave notify events for other items are discarded. */ if ((event.type == GDK_ENTER_NOTIFY || event.type == GDK_LEAVE_NOTIFY) && view->pointer_item_view != view->pointer_grab_item_view) return FALSE; target_item_view = view->pointer_grab_item_view; } /* Check if the target item view is still in the canvas. */ if (target_item_view && !VIEW_IS_VALID (target_item_view)) return FALSE; /* Translate the x & y coordinates to the item's space. */ switch (event.type) { case GDK_MOTION_NOTIFY: x = &event.motion.x; y = &event.motion.y; x_root = &event.motion.x_root; y_root = &event.motion.y_root; break; case GDK_ENTER_NOTIFY: case GDK_LEAVE_NOTIFY: x = &event.crossing.x; y = &event.crossing.y; x_root = &event.crossing.x_root; y_root = &event.crossing.y_root; break; default: /* It must be a button press/release event. */ x = &event.button.x; y = &event.button.y; x_root = &event.button.x_root; y_root = &event.button.y_root; break; } /* Add 0.5 to the pixel coordinates so we use the center of the pixel. */ *x += 0.5; *y += 0.5; /* Convert to the canvas view coordinate space. */ goo_canvas_view_convert_from_pixels (view, x, y); /* Copy to the x_root & y_root fields. */ *x_root = *x; *y_root = *y; /* Convert to the item's coordinate space. */ goo_canvas_view_convert_to_item_space (view, target_item_view, x, y); return propagate_event (view, target_item_view, signal_name, &event); } /* Finds the item view that the mouse is over, using the given event's * coordinates. It emits enter/leave events for item views as appropriate. */ static void update_pointer_item_view (GooCanvasView *view, GdkEvent *event) { GooCanvasItemView *new_item_view = NULL; if (event) initialize_crossing_event (view, event); /* If the event type is GDK_LEAVE_NOTIFY, the mouse has left the canvas, so we leave new_item_view as NULL, otherwise we find which item is underneath the mouse. Note that we initialize the type to GDK_LEAVE_NOTIFY in goo_canvas_view_init() to indicate the mouse isn't in the canvas. */ if (view->crossing_event.type != GDK_LEAVE_NOTIFY && view->root_view) { double x = view->crossing_event.x; double y = view->crossing_event.y; cairo_t *cr; goo_canvas_view_convert_from_pixels (view, &x, &y); cr = goo_canvas_view_create_cairo (view); new_item_view = goo_canvas_item_view_get_item_view_at (view->root_view, x, y, cr, TRUE, TRUE); cairo_destroy (cr); } /* If the current item view hasn't changed, just return. */ if (new_item_view == view->pointer_item_view) return; /* Ref the new item view, in case it is removed below. */ if (new_item_view) g_object_ref (new_item_view); /* Emit a leave-notify event for the current item view. */ if (view->pointer_item_view) { view->crossing_event.type = GDK_LEAVE_NOTIFY; emit_pointer_event (view, "leave_notify_event", (GdkEvent*) &view->crossing_event); } /* If there is no new item view we are done. */ if (!new_item_view) { set_item_view_pointer (&view->pointer_item_view, NULL); return; } /* If the new item view isn't in the canvas any more, don't use it. */ if (!VIEW_IS_VALID (new_item_view)) { set_item_view_pointer (&view->pointer_item_view, NULL); g_object_unref (new_item_view); return; } /* Emit an enter-notify for the new current item view. */ set_item_view_pointer (&view->pointer_item_view, new_item_view); view->crossing_event.type = GDK_ENTER_NOTIFY; emit_pointer_event (view, "enter_notify_event", (GdkEvent*) &view->crossing_event); g_object_unref (new_item_view); } static gboolean goo_canvas_view_crossing (GtkWidget *widget, GdkEventCrossing *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); if (event->window != view->canvas_window) return FALSE; /* This will result in synthesizing focus_in/out events as appropriate. */ update_pointer_item_view (view, (GdkEvent*) event); return FALSE; } static gboolean goo_canvas_view_motion (GtkWidget *widget, GdkEventMotion *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); if (event->window != view->canvas_window) return FALSE; /* For motion notify hint events we need to call gdk_window_get_pointer() to let X know we're ready for another pointer event. */ if (event->is_hint) gdk_window_get_pointer (event->window, NULL, NULL, NULL); update_pointer_item_view (view, (GdkEvent*) event); return emit_pointer_event (view, "motion_notify_event", (GdkEvent*) event); } static gboolean goo_canvas_view_button_press (GtkWidget *widget, GdkEventButton *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); GdkDisplay *display; if (event->window != view->canvas_window) return FALSE; update_pointer_item_view (view, (GdkEvent*) event); /* Check if this is the start of an implicit pointer grab, i.e. if we don't already have a grab and the app has no active grab. */ display = gtk_widget_get_display (widget); if (!view->pointer_grab_item_view && !gdk_display_pointer_is_grabbed (display)) { set_item_view_pointer (&view->pointer_grab_initial_item_view, view->pointer_item_view); set_item_view_pointer (&view->pointer_grab_item_view, view->pointer_item_view); view->pointer_grab_button = event->button; } return emit_pointer_event (view, "button_press_event", (GdkEvent*) event); } static gboolean goo_canvas_view_button_release (GtkWidget *widget, GdkEventButton *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); GdkDisplay *display; gboolean retval; if (event->window != view->canvas_window) return FALSE; update_pointer_item_view (view, (GdkEvent*) event); retval = emit_pointer_event (view, "button_release_event", (GdkEvent*) event); /* Check if an implicit (passive) grab has ended. */ display = gtk_widget_get_display (widget); if (view->pointer_grab_item_view && event->button == view->pointer_grab_button && !gdk_display_pointer_is_grabbed (display)) { /* We set the pointer item view back to the view it was in before the grab, so we'll synthesize enter/leave notify events as appropriate. */ if (view->pointer_grab_initial_item_view && VIEW_IS_VALID (view->pointer_grab_initial_item_view)) set_item_view_pointer (&view->pointer_item_view, view->pointer_grab_initial_item_view); else set_item_view_pointer (&view->pointer_item_view, NULL); set_item_view_pointer (&view->pointer_grab_item_view, NULL); set_item_view_pointer (&view->pointer_grab_initial_item_view, NULL); update_pointer_item_view (view, (GdkEvent*) event); } return retval; } static gint goo_canvas_view_scroll (GtkWidget *widget, GdkEventScroll *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); GtkAdjustment *adj; gdouble delta, new_value; if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_DOWN) adj = view->vadjustment; else adj = view->hadjustment; delta = pow (adj->page_size, 2.0 / 3.0); if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_LEFT) delta = - delta; new_value = CLAMP (adj->value + delta, adj->lower, adj->upper - adj->page_size); gtk_adjustment_set_value (adj, new_value); return TRUE; } static gboolean goo_canvas_view_focus_in (GtkWidget *widget, GdkEventFocus *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); GTK_WIDGET_SET_FLAGS (widget, GTK_HAS_FOCUS); if (view->focused_item_view) return propagate_event (view, view->focused_item_view, "focus_in_event", (GdkEvent*) event); else return FALSE; } static gboolean goo_canvas_view_focus_out (GtkWidget *widget, GdkEventFocus *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); GTK_WIDGET_UNSET_FLAGS (widget, GTK_HAS_FOCUS); if (view->focused_item_view) return propagate_event (view, view->focused_item_view, "focus_out_event", (GdkEvent*) event); else return FALSE; } static gboolean goo_canvas_view_key_press (GtkWidget *widget, GdkEventKey *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); if (view->focused_item_view) if (propagate_event (view, view->focused_item_view, "key_press_event", (GdkEvent*) event)) return TRUE; return GTK_WIDGET_CLASS (goo_canvas_view_parent_class)->key_press_event (widget, event); } static gboolean goo_canvas_view_key_release (GtkWidget *widget, GdkEventKey *event) { GooCanvasView *view = GOO_CANVAS_VIEW (widget); if (view->focused_item_view) if (propagate_event (view, view->focused_item_view, "key_release_event", (GdkEvent*) event)) return TRUE; return GTK_WIDGET_CLASS (goo_canvas_view_parent_class)->key_release_event (widget, event); } static void generate_grab_broken (GooCanvasView *canvas_view, GooCanvasItemView *item_view, gboolean keyboard, gboolean implicit) { GdkEventGrabBroken event; if (!VIEW_IS_VALID (item_view)) return; event.type = GDK_GRAB_BROKEN; event.window = canvas_view->canvas_window; event.send_event = 0; event.keyboard = keyboard; event.implicit = implicit; event.grab_window = event.window; propagate_event (canvas_view, item_view, "grab_broken_event", (GdkEvent*) &event); } static gboolean goo_canvas_view_grab_broken (GtkWidget *widget, GdkEventGrabBroken *event) { GooCanvasView *view; g_return_val_if_fail (GOO_IS_CANVAS_VIEW (widget), FALSE); view = GOO_CANVAS_VIEW (widget); if (event->keyboard) { if (view->keyboard_grab_item_view) { generate_grab_broken (view, view->keyboard_grab_item_view, event->keyboard, event->implicit); set_item_view_pointer (&view->keyboard_grab_item_view, NULL); } } else { if (view->pointer_grab_item_view) { generate_grab_broken (view, view->pointer_grab_item_view, event->keyboard, event->implicit); set_item_view_pointer (&view->pointer_grab_item_view, NULL); } } return TRUE; } /** * goo_canvas_view_grab_focus: * @canvas_view: a #GooCanvasView. * @item_view: the item view to grab the focus. * * Grabs the keyboard focus for the given item. **/ void goo_canvas_view_grab_focus (GooCanvasView *canvas_view, GooCanvasItemView *item_view) { GdkEventFocus event; g_return_if_fail (GOO_IS_CANVAS_VIEW (canvas_view)); g_return_if_fail (GOO_IS_CANVAS_ITEM_VIEW (item_view)); g_return_if_fail (GTK_WIDGET_CAN_FOCUS (canvas_view)); if (canvas_view->focused_item_view) { event.type = GDK_FOCUS_CHANGE; event.window = canvas_view->canvas_window; event.send_event = FALSE; event.in = FALSE; propagate_event (canvas_view, canvas_view->focused_item_view, "focus_out_event", (GdkEvent*) &event); } set_item_view_pointer (&canvas_view->focused_item_view, item_view); gtk_widget_grab_focus (GTK_WIDGET (canvas_view)); if (canvas_view->focused_item_view) { event.type = GDK_FOCUS_CHANGE; event.window = canvas_view->canvas_window; event.send_event = FALSE; event.in = TRUE; propagate_event (canvas_view, canvas_view->focused_item_view, "focus_in_event", (GdkEvent*) &event); } } /* * Pointer/keyboard grabbing & ungrabbing. */ /** * goo_canvas_view_pointer_grab: * @canvas_view: a #GooCanvasView. * @item_view: the item view to grab the pointer for. * @event_mask: the events to receive during the grab. * @cursor: the cursor to display during the grab, or NULL. * @time: the time of the event that lead to the pointer grab. This should * come from the relevant #GdkEvent. * * Attempts to grab the pointer for the given item view. * * Returns: %GDK_GRAB_SUCCESS if the grab succeeded. **/ GdkGrabStatus goo_canvas_view_pointer_grab (GooCanvasView *canvas_view, GooCanvasItemView *item_view, GdkEventMask event_mask, GdkCursor *cursor, guint32 time) { GdkGrabStatus status = GDK_GRAB_SUCCESS; g_return_val_if_fail (GOO_IS_CANVAS_VIEW (canvas_view), GDK_GRAB_NOT_VIEWABLE); g_return_val_if_fail (GOO_IS_CANVAS_ITEM_VIEW (item_view), GDK_GRAB_NOT_VIEWABLE); /* If another item already has the pointer grab, we need to synthesize a grab-broken event for the current grab item view. */ if (canvas_view->pointer_grab_item_view && canvas_view->pointer_grab_item_view != item_view) { generate_grab_broken (canvas_view, canvas_view->pointer_grab_item_view, FALSE, FALSE); set_item_view_pointer (&canvas_view->pointer_grab_item_view, NULL); } /* This overrides any existing grab. */ status = gdk_pointer_grab (canvas_view->canvas_window, FALSE, event_mask, NULL, cursor, time); if (status == GDK_GRAB_SUCCESS) { set_item_view_pointer (&canvas_view->pointer_grab_initial_item_view, canvas_view->pointer_item_view); set_item_view_pointer (&canvas_view->pointer_grab_item_view, item_view); } return status; } /** * goo_canvas_view_pointer_ungrab: * @canvas_view: a #GooCanvasView. * @item_view: the item view that has the grab. * @time: the time of the event that lead to the pointer ungrab. This should * come from the relevant #GdkEvent. * * Ungrabs the pointer, if the given item view has the pointer grab. **/ void goo_canvas_view_pointer_ungrab (GooCanvasView *canvas_view, GooCanvasItemView *item_view, guint32 time) { GdkDisplay *display; g_return_if_fail (GOO_IS_CANVAS_VIEW (canvas_view)); g_return_if_fail (GOO_IS_CANVAS_ITEM_VIEW (item_view)); /* If the item view doesn't actually have the pointer grab, just return. */ if (canvas_view->pointer_grab_item_view != item_view) return; /* If it is an active pointer grab, ungrab it explicitly. */ display = gtk_widget_get_display (GTK_WIDGET (canvas_view)); if (gdk_display_pointer_is_grabbed (display)) gdk_display_pointer_ungrab (display, time); /* We set the pointer item view back to the view it was in before the grab, so we'll synthesize enter/leave notify events as appropriate. */ if (canvas_view->pointer_grab_initial_item_view && VIEW_IS_VALID (canvas_view->pointer_grab_initial_item_view)) set_item_view_pointer (&canvas_view->pointer_item_view, canvas_view->pointer_grab_initial_item_view); else set_item_view_pointer (&canvas_view->pointer_item_view, NULL); set_item_view_pointer (&canvas_view->pointer_grab_item_view, NULL); set_item_view_pointer (&canvas_view->pointer_grab_initial_item_view, NULL); update_pointer_item_view (canvas_view, NULL); } /** * goo_canvas_view_keyboard_grab: * @canvas_view: a #GooCanvasView. * @item_view: the item view to grab the keyboard for. * @owner_events: %TRUE if keyboard events for this application will be * reported normally, or %FALSE if all keyboard events will be reported with * respect to the grab item view. * @time: the time of the event that lead to the keyboard grab. This should * come from the relevant #GdkEvent. * * Attempts to grab the keyboard for the given item view. * * Returns: %GDK_GRAB_SUCCESS if the grab succeeded. **/ GdkGrabStatus goo_canvas_view_keyboard_grab (GooCanvasView *canvas_view, GooCanvasItemView *item_view, gboolean owner_events, guint32 time) { GdkGrabStatus status = GDK_GRAB_SUCCESS; g_return_val_if_fail (GOO_IS_CANVAS_VIEW (canvas_view), GDK_GRAB_NOT_VIEWABLE); g_return_val_if_fail (GOO_IS_CANVAS_ITEM_VIEW (item_view), GDK_GRAB_NOT_VIEWABLE); /* Check if the item already has the keyboard grab. */ if (canvas_view->keyboard_grab_item_view == item_view) return GDK_GRAB_ALREADY_GRABBED; /* If another item already has the keyboard grab, we need to synthesize a grab-broken event for the current grab item view. */ if (canvas_view->keyboard_grab_item_view) { generate_grab_broken (canvas_view, canvas_view->keyboard_grab_item_view, TRUE, FALSE); set_item_view_pointer (&canvas_view->keyboard_grab_item_view, NULL); } /* This overrides any existing grab. */ status = gdk_keyboard_grab (canvas_view->canvas_window, owner_events, time); if (status == GDK_GRAB_SUCCESS) set_item_view_pointer (&canvas_view->keyboard_grab_item_view, item_view); return status; } /** * goo_canvas_view_keyboard_ungrab: * @canvas_view: a #GooCanvasView. * @item_view: the item view that has the keyboard grab. * @time: the time of the event that lead to the keyboard ungrab. This should * come from the relevant #GdkEvent. * * Ungrabs the keyboard, if the given item view has the keyboard grab. **/ void goo_canvas_view_keyboard_ungrab (GooCanvasView *canvas_view, GooCanvasItemView *item_view, guint32 time) { GdkDisplay *display; g_return_if_fail (GOO_IS_CANVAS_VIEW (canvas_view)); g_return_if_fail (GOO_IS_CANVAS_ITEM_VIEW (item_view)); /* If the item view doesn't actually have the keyboard grab, just return. */ if (canvas_view->keyboard_grab_item_view != item_view) return; set_item_view_pointer (&canvas_view->keyboard_grab_item_view, NULL); display = gtk_widget_get_display (GTK_WIDGET (canvas_view)); gdk_display_keyboard_ungrab (display, time); } /* * Coordinate conversion. */ /** * goo_canvas_view_convert_to_pixels: * @canvas_view: a #GooCanvasView. * @x: a pointer to the x coordinate to convert. * @y: a pointer to the y coordinate to convert. * * Converts a coordinate from the canvas view coordinate space to pixels. * * The canvas view coordinate space is specified in the call to * goo_canvas_view_set_bounds(). * * The pixel coordinate space specifies pixels from the top-left of the entire * canvas window, according to the current scale setting. * See goo_canvas_view_set_scale(). **/ void goo_canvas_view_convert_to_pixels (GooCanvasView *canvas_view, gdouble *x, gdouble *y) { *x = ((*x - canvas_view->bounds.x1) * canvas_view->scale) + canvas_view->canvas_x_offset; *y = ((*y - canvas_view->bounds.y1) * canvas_view->scale) + canvas_view->canvas_y_offset; } /** * goo_canvas_view_convert_from_pixels: * @canvas_view: a #GooCanvasView. * @x: a pointer to the x coordinate to convert. * @y: a pointer to the y coordinate to convert. * * Converts a coordinate from pixels to the canvas view coordinate space. * * The pixel coordinate space specifies pixels from the top-left of the entire * canvas window, according to the current scale setting. * See goo_canvas_view_set_scale(). * * The canvas view coordinate space is specified in the call to * goo_canvas_view_set_bounds(). * **/ void goo_canvas_view_convert_from_pixels (GooCanvasView *canvas_view, gdouble *x, gdouble *y) { *x = ((*x - canvas_view->canvas_x_offset) / canvas_view->scale) + canvas_view->bounds.x1; *y = ((*y - canvas_view->canvas_y_offset) / canvas_view->scale) + canvas_view->bounds.y1; } static void goo_canvas_view_convert_from_window_pixels (GooCanvasView *canvas_view, gdouble *x, gdouble *y) { *x += canvas_view->hadjustment->value; *y += canvas_view->vadjustment->value; goo_canvas_view_convert_from_pixels (canvas_view, x, y); } /** * goo_canvas_view_convert_to_item_space: * @canvas_view: a #GooCanvasView. * @item_view: a #GooCanvasItemView. * @x: a pointer to the x coordinate to convert. * @y: a pointer to the y coordinate to convert. * * Converts a coordinate from the canvas view coordinate space to the given * item's coordinate space, applying all transformation matrices including the * item's own transformation matrix, if it has one. **/ void goo_canvas_view_convert_to_item_space (GooCanvasView *canvas_view, GooCanvasItemView *item_view, gdouble *x, gdouble *y) { GooCanvasItemView *tmp_view = item_view; GList *list = NULL, *l; cairo_matrix_t item_view_transform, inverse = { 1, 0, 0, 1, 0, 0 }; /* Step up from the item view to the top, pushing views onto the list. */ while (tmp_view) { list = g_list_prepend (list, tmp_view); tmp_view = goo_canvas_item_view_get_parent_view (tmp_view); } /* Now step down applying the inverse of each view's transformation. */ for (l = list; l; l = l->next) { if (goo_canvas_item_view_get_combined_transform ((GooCanvasItemView*) l->data, &item_view_transform)) { cairo_matrix_invert (&item_view_transform); cairo_matrix_multiply (&inverse, &inverse, &item_view_transform); } } g_list_free (list); /* Now convert the coordinates. */ cairo_matrix_transform_point (&inverse, x, y); } /** * goo_canvas_view_convert_from_item_space: * @canvas_view: a #GooCanvasView. * @item_view: a #GooCanvasItemView. * @x: a pointer to the x coordinate to convert. * @y: a pointer to the y coordinate to convert. * * Converts a coordinate from the given item's coordinate space to the canvas * view coordinate space, applying all transformation matrices including the * item's own transformation matrix, if it has one. **/ void goo_canvas_view_convert_from_item_space (GooCanvasView *canvas_view, GooCanvasItemView *item_view, gdouble *x, gdouble *y) { GooCanvasItemView *tmp_view = item_view; GList *list = NULL, *l; cairo_matrix_t item_view_transform, transform = { 1, 0, 0, 1, 0, 0 }; /* Step up from the item view to the top, pushing views onto the list. */ while (tmp_view) { list = g_list_prepend (list, tmp_view); tmp_view = goo_canvas_item_view_get_parent_view (tmp_view); } /* Now step down applying each view's transformation. */ for (l = list; l; l = l->next) { if (goo_canvas_item_view_get_combined_transform ((GooCanvasItemView*) l->data, &item_view_transform)) { cairo_matrix_multiply (&transform, &transform, &item_view_transform); } } g_list_free (list); /* Now convert the coordinates. */ cairo_matrix_transform_point (&transform, x, y); } /* * Keyboard focus navigation. */ typedef struct _GooCanvasViewFocusData GooCanvasViewFocusData; struct _GooCanvasViewFocusData { /* The current focused item view, or NULL. */ GooCanvasItemView *focused_item_view; /* The bounds of the current focused item view. We try to find the next closest one in the desired direction. */ GooCanvasBounds focused_bounds; gdouble focused_center_x, focused_center_y; /* The direction to move the focus in. */ GtkDirectionType direction; /* The text direction of the widget. */ GtkTextDirection text_direction; /* The best view found so far, and the offsets for it. */ GooCanvasItemView *best_item_view; gdouble best_x_offset, best_y_offset, best_score; /* The offsets for the item being tested. */ GooCanvasBounds current_bounds; gdouble current_x_offset, current_y_offset, current_score; }; /* This tries to figure out the bounds of the currently focused canvas item view or widget. */ static void goo_canvas_view_get_current_focus_bounds (GooCanvasView *view, GooCanvasViewFocusData *data) { GooCanvasBounds *bounds; GtkWidget *toplevel, *focus_widget; GtkAllocation *allocation; gint focus_widget_x, focus_widget_y; /* If an item view is currently focused, we just need its bounds. */ if (data->focused_item_view) { goo_canvas_item_view_get_bounds (data->focused_item_view, &data->focused_bounds); return; } /* Otherwise try to get the currently focused widget in the window. */ toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); bounds = &data->focused_bounds; if (toplevel && GTK_IS_WINDOW (toplevel) && GTK_WINDOW (toplevel)->focus_widget) { focus_widget = GTK_WINDOW (toplevel)->focus_widget; /* Translate the allocation to be relative to the GooCanvasView. Skip ancestor widgets as the coords won't help. */ if (!gtk_widget_is_ancestor (GTK_WIDGET (view), focus_widget) && gtk_widget_translate_coordinates (focus_widget, GTK_WIDGET (view), 0, 0, &focus_widget_x, &focus_widget_y)) { /* Translate into device units. */ bounds->x1 = focus_widget_x; bounds->y1 = focus_widget_y; bounds->x2 = focus_widget_x + focus_widget->allocation.width; bounds->y2 = focus_widget_y + focus_widget->allocation.height; goo_canvas_view_convert_from_window_pixels (view, &bounds->x1, &bounds->y1); goo_canvas_view_convert_from_window_pixels (view, &bounds->x2, &bounds->y2); return; } } /* As a last resort, we guess a starting position based on the direction. */ allocation = >K_WIDGET (view)->allocation; switch (data->direction) { case GTK_DIR_DOWN: case GTK_DIR_RIGHT: /* Start from top-left. */ bounds->x1 = 0.0; bounds->y1 = 0.0; break; case GTK_DIR_UP: /* Start from bottom-left. */ bounds->x1 = 0.0; bounds->y1 = allocation->height; break; case GTK_DIR_LEFT: /* Start from top-right. */ bounds->x1 = allocation->width; bounds->y1 = 0.0; break; case GTK_DIR_TAB_FORWARD: bounds->y1 = 0.0; if (data->text_direction == GTK_TEXT_DIR_RTL) /* Start from top-right. */ bounds->x1 = allocation->width; else /* Start from top-left. */ bounds->x1 = 0.0; break; case GTK_DIR_TAB_BACKWARD: bounds->y1 = allocation->height; if (data->text_direction == GTK_TEXT_DIR_RTL) /* Start from bottom-left. */ bounds->x1 = 0.0; else /* Start from bottom-right. */ bounds->x1 = allocation->width; break; } goo_canvas_view_convert_from_window_pixels (view, &bounds->x1, &bounds->y1); bounds->x2 = bounds->x1; bounds->y2 = bounds->y1; } /* Check if the given item view is a better candidate for the focus than the current best one in the data struct. */ static gboolean goo_canvas_view_focus_check_is_best (GooCanvasView *view, GooCanvasItemView *item_view, GooCanvasViewFocusData *data) { gdouble center_x, center_y; gdouble abs_x_offset, abs_y_offset; data->current_score = 0.0; goo_canvas_item_view_get_bounds (item_view, &data->current_bounds); center_x = (data->current_bounds.x1 + data->current_bounds.x2) / 2.0; center_y = (data->current_bounds.y1 + data->current_bounds.y2) / 2.0; /* Calculate the offsets of the center of this item view from the center of the current focus item view or widget. */ data->current_x_offset = center_x - data->focused_center_x; data->current_y_offset = center_y - data->focused_center_y; abs_x_offset = fabs (data->current_x_offset); abs_y_offset = fabs (data->current_y_offset); switch (data->direction) { case GTK_DIR_UP: /* If the y offset is > 0 we can discard this item view. */ if (data->current_y_offset >= 0 || abs_x_offset > abs_y_offset) return FALSE; /* Compute a score (lower is best) and check if it is the best. */ data->current_score = abs_x_offset * 2 + abs_y_offset; if (!data->best_item_view || data->current_score < data->best_score) return TRUE; break; case GTK_DIR_DOWN: /* If the y offset is < 0 we can discard this item view. */ if (data->current_y_offset <= 0 || abs_x_offset > abs_y_offset) return FALSE; /* Compute a score (lower is best) and check if it is the best. */ data->current_score = abs_x_offset * 2 + abs_y_offset; if (!data->best_item_view || data->current_score < data->best_score) return TRUE; break; case GTK_DIR_LEFT: /* If the x offset is > 0 we can discard this item view. */ if (data->current_x_offset >= 0 || abs_y_offset > abs_x_offset) return FALSE; /* Compute a score (lower is best) and check if it is the best. */ data->current_score = abs_y_offset * 2 + abs_x_offset; if (!data->best_item_view || data->current_score < data->best_score) return TRUE; break; case GTK_DIR_RIGHT: /* If the x offset is < 0 we can discard this item view. */ if (data->current_x_offset <= 0 || abs_y_offset > abs_x_offset) return FALSE; /* Compute a score (lower is best) and check if it is the best. */ data->current_score = abs_y_offset * 2 + abs_x_offset; if (!data->best_item_view || data->current_score < data->best_score) return TRUE; break; case GTK_DIR_TAB_BACKWARD: /* We need to handle this differently depending on text direction. */ if (data->text_direction == GTK_TEXT_DIR_RTL) { /* If the y offset is > 0, or it is 0 and the x offset > 0 we can discard this item view. */ if (data->current_y_offset > 0 || (data->current_y_offset == 0 && data->current_x_offset < 0)) return FALSE; /* If the y offset is > the current best y offset, this is best. */ if (!data->best_item_view || data->current_y_offset > data->best_y_offset) return TRUE; /* If the y offsets are the same, choose the largest x offset. */ if (data->current_y_offset == data->best_y_offset && data->current_x_offset < data->best_x_offset) return TRUE; } else { /* If the y offset is > 0, or it is 0 and the x offset > 0 we can discard this item view. */ if (data->current_y_offset > 0 || (data->current_y_offset == 0 && data->current_x_offset > 0)) return FALSE; /* If the y offset is > the current best y offset, this is best. */ if (!data->best_item_view || data->current_y_offset > data->best_y_offset) return TRUE; /* If the y offsets are the same, choose the largest x offset. */ if (data->current_y_offset == data->best_y_offset && data->current_x_offset > data->best_x_offset) return TRUE; } break; case GTK_DIR_TAB_FORWARD: /* We need to handle this differently depending on text direction. */ if (data->text_direction == GTK_TEXT_DIR_RTL) { /* If the y offset is < 0, or it is 0 and the x offset > 0 we can discard this item view. */ if (data->current_y_offset < 0 || (data->current_y_offset == 0 && data->current_x_offset > 0)) return FALSE; /* If the y offset is < the current best y offset, this is best. */ if (!data->best_item_view || data->current_y_offset < data->best_y_offset) return TRUE; /* If the y offsets are the same, choose the largest x offset. */ if (data->current_y_offset == data->best_y_offset && data->current_x_offset > data->best_x_offset) return TRUE; } else { /* If the y offset is < 0, or it is 0 and the x offset < 0 we can discard this item view. */ if (data->current_y_offset < 0 || (data->current_y_offset == 0 && data->current_x_offset < 0)) return FALSE; /* If the y offset is < the current best y offset, this is best. */ if (!data->best_item_view || data->current_y_offset < data->best_y_offset) return TRUE; /* If the y offsets are the same, choose the smallest x offset. */ if (data->current_y_offset == data->best_y_offset && data->current_x_offset < data->best_x_offset) return TRUE; } break; } return FALSE; } /* Recursively look for the next best item view in the desired direction. */ static void goo_canvas_view_focus_recurse (GooCanvasView *view, GooCanvasItemView *item_view, GooCanvasViewFocusData *data) { GooCanvasItemView *child_view; gboolean can_focus = FALSE, is_best; gint n_children, i; /* If the item is not a possible candidate, just return. */ is_best = goo_canvas_view_focus_check_is_best (view, item_view, data); if (is_best && goo_canvas_item_view_is_visible (item_view)) { /* Check if the item view can take the focus. */ g_object_get (item_view, "can-focus", &can_focus, NULL); /* If the item view can take the focus itself, and it isn't the current focus item, save its score and return. (If it is a container it takes precedence over its children). */ if (can_focus && item_view != data->focused_item_view) { data->best_item_view = item_view; data->best_x_offset = data->current_x_offset; data->best_y_offset = data->current_y_offset; data->best_score = data->current_score; return; } } /* If the item view is a container view, check the children recursively. */ n_children = goo_canvas_item_view_get_n_children (item_view); if (n_children) { /* Check if we can skip the entire group. */ switch (data->direction) { case GTK_DIR_UP: /* If the group is below the bottom of the current focused item view we can skip it. */ if (data->current_bounds.y1 > data->focused_bounds.y2) return; break; case GTK_DIR_DOWN: /* If the group is above the top of the current focused item view we can skip it. */ if (data->current_bounds.y2 < data->focused_bounds.y1) return; break; case GTK_DIR_LEFT: /* If the group is to the right of the current focused item view we can skip it. */ if (data->current_bounds.x1 > data->focused_bounds.x2) return; break; case GTK_DIR_RIGHT: /* If the group is to the left of the current focused item view we can skip it. */ if (data->current_bounds.x2 < data->focused_bounds.x1) return; break; default: break; } for (i = 0; i < n_children; i++) { child_view = goo_canvas_item_view_get_child (item_view, i); goo_canvas_view_focus_recurse (view, child_view, data); } } } static gboolean goo_canvas_view_focus (GtkWidget *widget, GtkDirectionType direction) { GooCanvasView *view; GooCanvasViewFocusData data; g_return_val_if_fail (GOO_IS_CANVAS_VIEW (widget), FALSE); view = GOO_CANVAS_VIEW (widget); /* If keyboard navigation has been turned off for the canvas, return FALSE.*/ if (!GTK_WIDGET_CAN_FOCUS (view)) return FALSE; data.direction = direction; data.text_direction = gtk_widget_get_direction (widget); data.best_item_view = NULL; data.focused_item_view = NULL; if (GTK_WIDGET_HAS_FOCUS (view)) data.focused_item_view = view->focused_item_view; /* Get the bounds of the currently focused item or widget. */ goo_canvas_view_get_current_focus_bounds (view, &data); data.focused_center_x = (data.focused_bounds.x1 + data.focused_bounds.x2) / 2.0; data.focused_center_y = (data.focused_bounds.y1 + data.focused_bounds.y2) / 2.0; /* Recursively look for the next best item view in the desired direction. */ goo_canvas_view_focus_recurse (view, view->root_view, &data); /* If we found an item view to focus, grab the focus and return TRUE. */ if (data.best_item_view) { goo_canvas_view_grab_focus (view, data.best_item_view); goo_canvas_view_scroll_to_item_view (view, data.best_item_view); return TRUE; } return FALSE; }