Code

Gradient nodes progress...
[inkscape.git] / src / desktop.cpp
1 #define __SP_DESKTOP_C__
3 /** \file
4  * Editable view implementation
5  *
6  * Authors:
7  *   Lauris Kaplinski <lauris@kaplinski.com>
8  *   MenTaLguY <mental@rydia.net>
9  *   bulia byak <buliabyak@users.sf.net>
10  *   Ralf Stephan <ralf@ark.in-berlin.de>
11  *   John Bintz <jcoswell@coswellproductions.org>
12  *
13  * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
14  * Copyright (C) 2006 John Bintz
15  * Copyright (C) 2004 MenTaLguY
16  * Copyright (C) 1999-2002 Lauris Kaplinski
17  * Copyright (C) 2000-2001 Ximian, Inc.
18  *
19  * Released under GNU GPL, read the file 'COPYING' for more information
20  */
22 /** \class SPDesktop
23  * SPDesktop is a subclass of View, implementing an editable document
24  * canvas.  It is extensively used by many UI controls that need certain
25  * visual representations of their own.
26  *
27  * SPDesktop provides a certain set of SPCanvasItems, serving as GUI
28  * layers of different control objects. The one containing the whole
29  * document is the drawing layer. In addition to it, there are grid,
30  * guide, sketch and control layers. The sketch layer is used for
31  * temporary drawing objects, before the real objects in document are
32  * created. The control layer contains editing knots, rubberband and
33  * similar non-document UI objects.
34  *
35  * Each SPDesktop is associated with a SPNamedView node of the document
36  * tree.  Currently, all desktops are created from a single main named
37  * view, but in the future there may be support for different ones.
38  * SPNamedView serves as an in-document container for desktop-related
39  * data, like grid and guideline placement, snapping options and so on.
40  *
41  * Associated with each SPDesktop are the two most important editing
42  * related objects - SPSelection and SPEventContext.
43  *
44  * Sodipodi keeps track of the active desktop and invokes notification
45  * signals whenever it changes. UI elements can use these to update their
46  * display to the selection of the currently active editing window.
47  * (Lauris Kaplinski)
48  */
50 #ifdef HAVE_CONFIG_H
51 # include "config.h"
52 #endif
54 #include <glibmm/i18n.h>
55 #include <sigc++/functors/mem_fun.h>
57 #include "macros.h"
58 #include "inkscape-private.h"
59 #include "desktop.h"
60 #include "desktop-events.h"
61 #include "desktop-handles.h"
62 #include "document.h"
63 #include "message-stack.h"
64 #include "selection.h"
65 #include "select-context.h"
66 #include "sp-namedview.h"
67 #include "color.h"
68 #include "sp-item-group.h"
69 #include "prefs-utils.h"
70 #include "object-hierarchy.h"
71 #include "helper/units.h"
72 #include "display/canvas-arena.h"
73 #include "display/nr-arena.h"
74 #include "display/gnome-canvas-acetate.h"
75 #include "display/sodipodi-ctrlrect.h"
76 #include "display/sp-canvas-util.h"
77 #include "libnr/nr-matrix-div.h"
78 #include "libnr/nr-rect-ops.h"
79 #include "ui/dialog/dialog-manager.h"
80 #include "xml/repr.h"
81 #include "message-context.h"
82 #include "layer-manager.h"
83 #include "event-log.h"
85 namespace Inkscape { namespace XML { class Node; }}
87 // Callback declarations
88 static void _onSelectionChanged (Inkscape::Selection *selection, SPDesktop *desktop);
89 static gint _arena_handler (SPCanvasArena *arena, NRArenaItem *ai, GdkEvent *event, SPDesktop *desktop);
90 static void _layer_activated(SPObject *layer, SPDesktop *desktop);
91 static void _layer_deactivated(SPObject *layer, SPDesktop *desktop);
92 static void _layer_hierarchy_changed(SPObject *top, SPObject *bottom, SPDesktop *desktop);
93 static void _reconstruction_start(SPDesktop * desktop);
94 static void _reconstruction_finish(SPDesktop * desktop);
95 static void _namedview_modified (SPObject *obj, guint flags, SPDesktop *desktop);
96 static void _update_snap_distances (SPDesktop *desktop);
98 /**
99  * Return new desktop object.
100  * \pre namedview != NULL.
101  * \pre canvas != NULL.
102  */
103 SPDesktop::SPDesktop()
105     _dlg_mgr = NULL;
106     _widget = 0;
107     namedview = NULL;
108     selection = NULL;
109     acetate = NULL;
110     main = NULL;
111     grid = NULL;
112     guides = NULL;
113     drawing = NULL;
114     sketch = NULL;
115     controls = NULL;
116     event_context = 0;
117     layer_manager = 0;
119     _d2w.set_identity();
120     _w2d.set_identity();
121     _doc2dt = NR::Matrix(NR::scale(1, -1));
123     guides_active = false;
125     zooms_past = NULL;
126     zooms_future = NULL;
128     is_fullscreen = false;
130     gr_item = NULL;
131     gr_point_type = 0;
132     gr_point_i = 0;
133     gr_fill_or_stroke = true;
135     _layer_hierarchy = NULL;
136     _active = false;
138     selection = Inkscape::GC::release (new Inkscape::Selection (this));
141 void
142 SPDesktop::init (SPNamedView *nv, SPCanvas *aCanvas)
145     _guides_message_context = new Inkscape::MessageContext(const_cast<Inkscape::MessageStack*>(messageStack()));
147     current = sp_repr_css_attr_inherited (inkscape_get_repr (INKSCAPE, "desktop"), "style");
149     namedview = nv;
150     canvas = aCanvas;
152     SPDocument *document = SP_OBJECT_DOCUMENT (namedview);
153     /* Kill flicker */
154     sp_document_ensure_up_to_date (document);
156     /* Setup Dialog Manager */
157     _dlg_mgr = new Inkscape::UI::Dialog::DialogManager();
159     dkey = sp_item_display_key_new (1);
161     /* Connect document */
162     setDocument (document);
164     number = namedview->getViewCount();
167     /* Setup Canvas */
168     g_object_set_data (G_OBJECT (canvas), "SPDesktop", this);
170     SPCanvasGroup *root = sp_canvas_root (canvas);
172     /* Setup adminstrative layers */
173     acetate = sp_canvas_item_new (root, GNOME_TYPE_CANVAS_ACETATE, NULL);
174     g_signal_connect (G_OBJECT (acetate), "event", G_CALLBACK (sp_desktop_root_handler), this);
175     main = (SPCanvasGroup *) sp_canvas_item_new (root, SP_TYPE_CANVAS_GROUP, NULL);
176     g_signal_connect (G_OBJECT (main), "event", G_CALLBACK (sp_desktop_root_handler), this);
178     table = sp_canvas_item_new (main, SP_TYPE_CTRLRECT, NULL);
179     SP_CTRLRECT(table)->setRectangle(NR::Rect(NR::Point(-80000, -80000), NR::Point(80000, 80000)));
180     SP_CTRLRECT(table)->setColor(0x00000000, true, 0x00000000);
181     sp_canvas_item_move_to_z (table, 0);
183     page = sp_canvas_item_new (main, SP_TYPE_CTRLRECT, NULL);
184     ((CtrlRect *) page)->setColor(0x00000000, FALSE, 0x00000000);
185     page_border = sp_canvas_item_new (main, SP_TYPE_CTRLRECT, NULL);
187     drawing = sp_canvas_item_new (main, SP_TYPE_CANVAS_ARENA, NULL);
188     g_signal_connect (G_OBJECT (drawing), "arena_event", G_CALLBACK (_arena_handler), this);
190     SP_CANVAS_ARENA (drawing)->arena->delta = prefs_get_double_attribute ("options.cursortolerance", "value", 1.0); // default is 1 px
192     if (prefs_get_int_attribute("options.startmode", "outline", 0)) {
193         // Start in outline mode
194         setDisplayModeOutline();
195     } else {
196         // Start in normal mode, default
197         setDisplayModeNormal();
198     }
200     grid = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, NULL);
201     guides = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, NULL);
202     sketch = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, NULL);
203     controls = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, NULL);
205     /* Push select tool to the bottom of stack */
206     /** \todo
207      * FIXME: this is the only call to this.  Everything else seems to just
208      * call "set" instead of "push".  Can we assume that there is only one
209      * context ever?
210      */
211     push_event_context (SP_TYPE_SELECT_CONTEXT, "tools.select", SP_EVENT_CONTEXT_STATIC);
213     // display rect and zoom are now handled in sp_desktop_widget_realize()
215     NR::Rect const d(NR::Point(0.0, 0.0),
216                      NR::Point(sp_document_width(document), sp_document_height(document)));
218     SP_CTRLRECT(page)->setRectangle(d);
219     SP_CTRLRECT(page_border)->setRectangle(d);
221     /* the following sets the page shadow on the canvas
222        It was originally set to 5, which is really cheesy!
223        It now is an attribute in the document's namedview. If a value of
224        0 is used, then the constructor for a shadow is not initialized.
225     */
227     if ( namedview->pageshadow != 0 && namedview->showpageshadow ) {
228         SP_CTRLRECT(page_border)->setShadow(namedview->pageshadow, 0x3f3f3fff);
229     }
232     /* Connect event for page resize */
233     _doc2dt[5] = sp_document_height (document);
234     sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (drawing), _doc2dt);
236     _modified_connection = namedview->connectModified(sigc::bind<2>(sigc::ptr_fun(&_namedview_modified), this));
238     NRArenaItem *ai = sp_item_invoke_show (SP_ITEM (sp_document_root (document)),
239             SP_CANVAS_ARENA (drawing)->arena,
240             dkey,
241             SP_ITEM_SHOW_DISPLAY);
242     if (ai) {
243         nr_arena_item_add_child (SP_CANVAS_ARENA (drawing)->root, ai, NULL);
244         nr_arena_item_unref (ai);
245     }
247     namedview->show(this);
248     /* Ugly hack */
249     activate_guides (true);
250     /* Ugly hack */
251     _namedview_modified (namedview, SP_OBJECT_MODIFIED_FLAG, this);
253 /* Set up notification of rebuilding the document, this allows
254        for saving object related settings in the document. */
255     _reconstruction_start_connection =
256         document->connectReconstructionStart(sigc::bind(sigc::ptr_fun(_reconstruction_start), this));
257     _reconstruction_finish_connection =
258         document->connectReconstructionFinish(sigc::bind(sigc::ptr_fun(_reconstruction_finish), this));
259     _reconstruction_old_layer_id = NULL;
260     
261     _commit_connection = document->connectCommit(sigc::mem_fun(*this, &SPDesktop::updateNow));
262     
263     // ?
264     // sp_active_desktop_set (desktop);
265     _inkscape = INKSCAPE;
267     _activate_connection = _activate_signal.connect(
268         sigc::bind(
269             sigc::ptr_fun(_onActivate),
270             this
271         )
272     );
273      _deactivate_connection = _deactivate_signal.connect(
274         sigc::bind(
275             sigc::ptr_fun(_onDeactivate),
276             this
277         )
278     );
280     _sel_modified_connection = selection->connectModified(
281         sigc::bind(
282             sigc::ptr_fun(&_onSelectionModified),
283             this
284         )
285     );
286     _sel_changed_connection = selection->connectChanged(
287         sigc::bind(
288             sigc::ptr_fun(&_onSelectionChanged),
289             this
290         )
291     );
294     /* setup LayerManager */
295     //   (Setting up after the connections are all in place, as it may use some of them)
296     layer_manager = new Inkscape::LayerManager( this );
300 void SPDesktop::destroy()
302     _activate_connection.disconnect();
303     _deactivate_connection.disconnect();
304     _sel_modified_connection.disconnect();
305     _sel_changed_connection.disconnect();
306     _modified_connection.disconnect();
307     _commit_connection.disconnect();
308     _reconstruction_start_connection.disconnect();
309     _reconstruction_finish_connection.disconnect();
311     g_signal_handlers_disconnect_by_func(G_OBJECT (acetate), (gpointer) G_CALLBACK(sp_desktop_root_handler), this);
312     g_signal_handlers_disconnect_by_func(G_OBJECT (main), (gpointer) G_CALLBACK(sp_desktop_root_handler), this);
313     g_signal_handlers_disconnect_by_func(G_OBJECT (drawing), (gpointer) G_CALLBACK(_arena_handler), this);
315     while (event_context) {
316         SPEventContext *ec = event_context;
317         event_context = ec->next;
318         sp_event_context_finish (ec);
319         g_object_unref (G_OBJECT (ec));
320     }
322     if (_layer_hierarchy) {
323         delete _layer_hierarchy;
324     }
326     if (_inkscape) {
327         _inkscape = NULL;
328     }
330     if (drawing) {
331         sp_item_invoke_hide (SP_ITEM (sp_document_root (doc())), dkey);
332         drawing = NULL;
333     }
335     delete _guides_message_context;
336     _guides_message_context = NULL;
338     g_list_free (zooms_past);
339     g_list_free (zooms_future);
342 SPDesktop::~SPDesktop() {}
344 //--------------------------------------------------------------------
345 /* Public methods */
347 void SPDesktop::setDisplayModeNormal()
349     prefs_set_int_attribute("options.outlinemode", "value", 0);
350     SP_CANVAS_ARENA (drawing)->arena->rendermode = RENDERMODE_NORMAL;
351     canvas->rendermode = RENDERMODE_NORMAL; // canvas needs that for choosing the best buffer size
352     displayMode = RENDERMODE_NORMAL;
353     sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (main), _d2w); // redraw
354     _widget->setTitle(SP_DOCUMENT_NAME(sp_desktop_document(this)));
357 void SPDesktop::setDisplayModeOutline()
359     prefs_set_int_attribute("options.outlinemode", "value", 1);
360     SP_CANVAS_ARENA (drawing)->arena->rendermode = RENDERMODE_OUTLINE;
361     canvas->rendermode = RENDERMODE_OUTLINE; // canvas needs that for choosing the best buffer size
362     displayMode = RENDERMODE_OUTLINE;
363     sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (main), _d2w); // redraw
364     _widget->setTitle(SP_DOCUMENT_NAME(sp_desktop_document(this)));
367 void SPDesktop::displayModeToggle()
369     if (prefs_get_int_attribute("options.outlinemode", "value", prefs_get_int_attribute("options.startmode", "outline", 0)))
370         setDisplayModeNormal();
371     else 
372         setDisplayModeOutline();
375 /**
376  * Returns current root (=bottom) layer.
377  */
378 SPObject *SPDesktop::currentRoot() const
380     return _layer_hierarchy ? _layer_hierarchy->top() : NULL;
383 /**
384  * Returns current top layer.
385  */
386 SPObject *SPDesktop::currentLayer() const
388     return _layer_hierarchy ? _layer_hierarchy->bottom() : NULL;
391 /**
392  * Sets the current layer of the desktop.
393  * 
394  * Make \a object the top layer.
395  */
396 void SPDesktop::setCurrentLayer(SPObject *object) {
397     g_return_if_fail(SP_IS_GROUP(object));
398     g_return_if_fail( currentRoot() == object || (currentRoot() && currentRoot()->isAncestorOf(object)) );
399     // printf("Set Layer to ID: %s\n", SP_OBJECT_ID(object));
400     _layer_hierarchy->setBottom(object);
403 /**
404  * Return layer that contains \a object.
405  */
406 SPObject *SPDesktop::layerForObject(SPObject *object) {
407     g_return_val_if_fail(object != NULL, NULL);
409     SPObject *root=currentRoot();
410     object = SP_OBJECT_PARENT(object);
411     while ( object && object != root && !isLayer(object) ) {
412         object = SP_OBJECT_PARENT(object);
413     }
414     return object;
417 /**
418  * True if object is a layer.
419  */
420 bool SPDesktop::isLayer(SPObject *object) const {
421     return ( SP_IS_GROUP(object)
422              && ( SP_GROUP(object)->effectiveLayerMode(this->dkey)
423                   == SPGroup::LAYER ) );
426 /**
427  * True if desktop viewport fully contains \a item's bbox.
428  */
429 bool SPDesktop::isWithinViewport (SPItem *item) const
431     NR::Rect const viewport = get_display_area();
432     NR::Rect const bbox = sp_item_bbox_desktop(item);
433     return viewport.contains(bbox);
436 ///
437 bool SPDesktop::itemIsHidden(SPItem const *item) const {
438     return item->isHidden(this->dkey);
441 /**
442  * Set activate property of desktop; emit signal if changed.
443  */
444 void
445 SPDesktop::set_active (bool new_active)
447     if (new_active != _active) {
448         _active = new_active;
449         if (new_active) {
450             _activate_signal.emit();
451         } else {
452             _deactivate_signal.emit();
453         }
454     }
457 /**
458  * Set activate status of current desktop's named view.
459  */
460 void
461 SPDesktop::activate_guides(bool activate)
463     guides_active = activate;
464     namedview->activateGuides(this, activate);
467 /**
468  * Make desktop switch documents.
469  */
470 void
471 SPDesktop::change_document (SPDocument *theDocument)
473     g_return_if_fail (theDocument != NULL);
475     /* unselect everything before switching documents */
476     selection->clear();
478     setDocument (theDocument);
479     _namedview_modified (namedview, SP_OBJECT_MODIFIED_FLAG, this);
480     _document_replaced_signal.emit (this, theDocument);
483 /**
484  * Make desktop switch event contexts.
485  */
486 void
487 SPDesktop::set_event_context (GtkType type, const gchar *config)
489     SPEventContext *ec;
490     while (event_context) {
491         ec = event_context;
492         sp_event_context_deactivate (ec);
493         event_context = ec->next;
494         sp_event_context_finish (ec);
495         g_object_unref (G_OBJECT (ec));
496     }
498     Inkscape::XML::Node *repr = (config) ? inkscape_get_repr (_inkscape, config) : NULL;
499     ec = sp_event_context_new (type, this, repr, SP_EVENT_CONTEXT_STATIC);
500     ec->next = event_context;
501     event_context = ec;
502     sp_event_context_activate (ec);
503     _event_context_changed_signal.emit (this, ec);
506 /**
507  * Push event context onto desktop's context stack.
508  */
509 void
510 SPDesktop::push_event_context (GtkType type, const gchar *config, unsigned int key)
512     SPEventContext *ref, *ec;
513     Inkscape::XML::Node *repr;
515     if (event_context && event_context->key == key) return;
516     ref = event_context;
517     while (ref && ref->next && ref->next->key != key) ref = ref->next;
518     if (ref && ref->next) {
519         ec = ref->next;
520         ref->next = ec->next;
521         sp_event_context_finish (ec);
522         g_object_unref (G_OBJECT (ec));
523     }
525     if (event_context) sp_event_context_deactivate (event_context);
526     repr = (config) ? inkscape_get_repr (INKSCAPE, config) : NULL;
527     ec = sp_event_context_new (type, this, repr, key);
528     ec->next = event_context;
529     event_context = ec;
530     sp_event_context_activate (ec);
531     _event_context_changed_signal.emit (this, ec);
534 /**
535  * Sets the coordinate status to a given point
536  */
537 void
538 SPDesktop::set_coordinate_status (NR::Point p) {
539     _widget->setCoordinateStatus(p);
542 /**
543  * \see sp_document_item_from_list_at_point_bottom()
544  */
545 SPItem *
546 SPDesktop::item_from_list_at_point_bottom (const GSList *list, NR::Point const p) const
548     g_return_val_if_fail (doc() != NULL, NULL);
549     return sp_document_item_from_list_at_point_bottom (dkey, SP_GROUP (doc()->root), list, p);
552 /**
553  * \see sp_document_item_at_point()
554  */
555 SPItem *
556 SPDesktop::item_at_point (NR::Point const p, bool into_groups, SPItem *upto) const
558     g_return_val_if_fail (doc() != NULL, NULL);
559     return sp_document_item_at_point (doc(), dkey, p, into_groups, upto);
562 /**
563  * \see sp_document_group_at_point()
564  */
565 SPItem *
566 SPDesktop::group_at_point (NR::Point const p) const
568     g_return_val_if_fail (doc() != NULL, NULL);
569     return sp_document_group_at_point (doc(), dkey, p);
572 /**
573  * \brief  Returns the mouse point in document coordinates; if mouse is
574  * outside the canvas, returns the center of canvas viewpoint
575  */
576 NR::Point
577 SPDesktop::point() const
579     NR::Point p = _widget->getPointer();
580     NR::Point pw = sp_canvas_window_to_world (canvas, p);
581     p = w2d(pw);
583     NR::Rect const r = canvas->getViewbox();
585     NR::Point r0 = w2d(r.min());
586     NR::Point r1 = w2d(r.max());
588     if (p[NR::X] >= r0[NR::X] &&
589         p[NR::X] <= r1[NR::X] &&
590         p[NR::Y] >= r1[NR::Y] &&
591         p[NR::Y] <= r0[NR::Y])
592     {
593         return p;
594     } else {
595         return (r0 + r1) / 2;
596     }
599 /**
600  * Put current zoom data in history list.
601  */
602 void
603 SPDesktop::push_current_zoom (GList **history)
605     NR::Rect const area = get_display_area();
607     NRRect *old_zoom = g_new(NRRect, 1);
608     old_zoom->x0 = area.min()[NR::X];
609     old_zoom->x1 = area.max()[NR::X];
610     old_zoom->y0 = area.min()[NR::Y];
611     old_zoom->y1 = area.max()[NR::Y];
612     if ( *history == NULL
613          || !( ( ((NRRect *) ((*history)->data))->x0 == old_zoom->x0 ) &&
614                ( ((NRRect *) ((*history)->data))->x1 == old_zoom->x1 ) &&
615                ( ((NRRect *) ((*history)->data))->y0 == old_zoom->y0 ) &&
616                ( ((NRRect *) ((*history)->data))->y1 == old_zoom->y1 ) ) )
617     {
618         *history = g_list_prepend (*history, old_zoom);
619     }
622 /**
623  * Set viewbox.
624  */
625 void
626 SPDesktop::set_display_area (double x0, double y0, double x1, double y1, double border, bool log)
628     g_assert(_widget);
630     // save the zoom
631     if (log) {
632         push_current_zoom(&zooms_past);
633         // if we do a logged zoom, our zoom-forward list is invalidated, so delete it
634         g_list_free (zooms_future);
635         zooms_future = NULL;
636     }
638     double const cx = 0.5 * (x0 + x1);
639     double const cy = 0.5 * (y0 + y1);
641     NR::Rect const viewbox = NR::expand(canvas->getViewbox(), border);
643     double scale = expansion(_d2w);
644     double newscale;
645     if (((x1 - x0) * viewbox.dimensions()[NR::Y]) > ((y1 - y0) * viewbox.dimensions()[NR::X])) {
646         newscale = viewbox.dimensions()[NR::X] / (x1 - x0);
647     } else {
648         newscale = viewbox.dimensions()[NR::Y] / (y1 - y0);
649     }
651     newscale = CLAMP(newscale, SP_DESKTOP_ZOOM_MIN, SP_DESKTOP_ZOOM_MAX);
653     int clear = FALSE;
654     if (!NR_DF_TEST_CLOSE (newscale, scale, 1e-4 * scale)) {
655         /* Set zoom factors */
656         _d2w = NR::Matrix(NR::scale(newscale, -newscale));
657         _w2d = NR::Matrix(NR::scale(1/newscale, 1/-newscale));
658         sp_canvas_item_affine_absolute(SP_CANVAS_ITEM(main), _d2w);
659         clear = TRUE;
660     }
662     /* Calculate top left corner */
663     x0 = cx - 0.5 * viewbox.dimensions()[NR::X] / newscale;
664     y1 = cy + 0.5 * viewbox.dimensions()[NR::Y] / newscale;
666     /* Scroll */
667     sp_canvas_scroll_to (canvas, x0 * newscale - border, y1 * -newscale - border, clear);
669     _widget->updateRulers();
670     _widget->updateScrollbars(expansion(_d2w));
671     _widget->updateZoom();
674 void SPDesktop::set_display_area(NR::Rect const &a, NR::Coord b, bool log)
676     set_display_area(a.min()[NR::X], a.min()[NR::Y], a.max()[NR::X], a.max()[NR::Y], b, log);
679 /**
680  * Return viewbox dimensions.
681  */
682 NR::Rect SPDesktop::get_display_area() const
684     NR::Rect const viewbox = canvas->getViewbox();
686     double const scale = _d2w[0];
688     return NR::Rect(NR::Point(viewbox.min()[NR::X] / scale, viewbox.max()[NR::Y] / -scale),
689                     NR::Point(viewbox.max()[NR::X] / scale, viewbox.min()[NR::Y] / -scale));
692 /**
693  * Revert back to previous zoom if possible.
694  */
695 void
696 SPDesktop::prev_zoom()
698     if (zooms_past == NULL) {
699         messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No previous zoom."));
700         return;
701     }
703     // push current zoom into forward zooms list
704     push_current_zoom (&zooms_future);
706     // restore previous zoom
707     set_display_area (((NRRect *) zooms_past->data)->x0,
708             ((NRRect *) zooms_past->data)->y0,
709             ((NRRect *) zooms_past->data)->x1,
710             ((NRRect *) zooms_past->data)->y1,
711             0, false);
713     // remove the just-added zoom from the past zooms list
714     zooms_past = g_list_remove (zooms_past, ((NRRect *) zooms_past->data));
717 /**
718  * Set zoom to next in list.
719  */
720 void
721 SPDesktop::next_zoom()
723     if (zooms_future == NULL) {
724         this->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No next zoom."));
725         return;
726     }
728     // push current zoom into past zooms list
729     push_current_zoom (&zooms_past);
731     // restore next zoom
732     set_display_area (((NRRect *) zooms_future->data)->x0,
733             ((NRRect *) zooms_future->data)->y0,
734             ((NRRect *) zooms_future->data)->x1,
735             ((NRRect *) zooms_future->data)->y1,
736             0, false);
738     // remove the just-used zoom from the zooms_future list
739     zooms_future = g_list_remove (zooms_future, ((NRRect *) zooms_future->data));
742 /**
743  * Zoom to point with absolute zoom factor.
744  */
745 void
746 SPDesktop::zoom_absolute_keep_point (double cx, double cy, double px, double py, double zoom)
748     zoom = CLAMP (zoom, SP_DESKTOP_ZOOM_MIN, SP_DESKTOP_ZOOM_MAX);
750     // maximum or minimum zoom reached, but there's no exact equality because of rounding errors;
751     // this check prevents "sliding" when trying to zoom in at maximum zoom;
752     /// \todo someone please fix calculations properly and remove this hack
753     if (fabs(expansion(_d2w) - zoom) < 0.0001*zoom && (fabs(SP_DESKTOP_ZOOM_MAX - zoom) < 0.01 || fabs(SP_DESKTOP_ZOOM_MIN - zoom) < 0.000001))
754         return;
756     NR::Rect const viewbox = canvas->getViewbox();
758     double const width2 = viewbox.dimensions()[NR::X] / zoom;
759     double const height2 = viewbox.dimensions()[NR::Y] / zoom;
761     set_display_area(cx - px * width2,
762                      cy - py * height2,
763                      cx + (1 - px) * width2,
764                      cy + (1 - py) * height2,
765                      0.0);
768 /**
769  * Zoom to center with absolute zoom factor.
770  */
771 void
772 SPDesktop::zoom_absolute (double cx, double cy, double zoom)
774     zoom_absolute_keep_point (cx, cy, 0.5, 0.5, zoom);
777 /**
778  * Zoom to point with relative zoom factor.
779  */
780 void
781 SPDesktop::zoom_relative_keep_point (double cx, double cy, double zoom)
783     NR::Rect const area = get_display_area();
785     if (cx < area.min()[NR::X]) {
786         cx = area.min()[NR::X];
787     }
788     if (cx > area.max()[NR::X]) {
789         cx = area.max()[NR::X];
790     }
791     if (cy < area.min()[NR::Y]) {
792         cy = area.min()[NR::Y];
793     }
794     if (cy > area.max()[NR::Y]) {
795         cy = area.max()[NR::Y];
796     }
798     gdouble const scale = expansion(_d2w) * zoom;
799     double const px = (cx - area.min()[NR::X]) / area.dimensions()[NR::X];
800     double const py = (cy - area.min()[NR::Y]) / area.dimensions()[NR::Y];
802     zoom_absolute_keep_point(cx, cy, px, py, scale);
805 /**
806  * Zoom to center with relative zoom factor.
807  */
808 void
809 SPDesktop::zoom_relative (double cx, double cy, double zoom)
811     gdouble scale = expansion(_d2w) * zoom;
812     zoom_absolute (cx, cy, scale);
815 /**
816  * Set display area to origin and current document dimensions.
817  */
818 void
819 SPDesktop::zoom_page()
821     NR::Rect d(NR::Point(0, 0),
822                NR::Point(sp_document_width(doc()), sp_document_height(doc())));
824     if (d.dimensions()[NR::X] < 1.0 || d.dimensions()[NR::Y] < 1.0) {
825         return;
826     }
828     set_display_area(d, 10);
831 /**
832  * Set display area to current document width.
833  */
834 void
835 SPDesktop::zoom_page_width()
837     NR::Rect const a = get_display_area();
839     if (sp_document_width(doc()) < 1.0) {
840         return;
841     }
843     NR::Rect d(NR::Point(0, a.midpoint()[NR::Y]),
844                NR::Point(sp_document_width(doc()), a.midpoint()[NR::Y]));
846     set_display_area(d, 10);
849 /**
850  * Zoom to selection.
851  */
852 void
853 SPDesktop::zoom_selection()
855     NR::Rect const d = selection->bounds();
857     if (d.dimensions()[NR::X] < 0.1 || d.dimensions()[NR::Y] < 0.1) {
858         return;
859     }
861     set_display_area(d, 10);
864 /**
865  * Tell widget to let zoom widget grab keyboard focus.
866  */
867 void
868 SPDesktop::zoom_grab_focus()
870     _widget->letZoomGrabFocus();
873 /**
874  * Zoom to whole drawing.
875  */
876 void
877 SPDesktop::zoom_drawing()
879     g_return_if_fail (doc() != NULL);
880     SPItem *docitem = SP_ITEM (sp_document_root (doc()));
881     g_return_if_fail (docitem != NULL);
883     NR::Rect d = sp_item_bbox_desktop(docitem);
885     /* Note that the second condition here indicates that
886     ** there are no items in the drawing.
887     */
888     if ( d.dimensions()[NR::X] < 1.0 || d.dimensions()[NR::Y] < 1.0 ) {
889         return;
890     }
892     set_display_area(d, 10);
895 /**
896  * Scroll canvas by specific coordinate amount.
897  */
898 void
899 SPDesktop::scroll_world (double dx, double dy)
901     g_assert(_widget);
903     NR::Rect const viewbox = canvas->getViewbox();
905     sp_canvas_scroll_to(canvas, viewbox.min()[NR::X] - dx, viewbox.min()[NR::Y] - dy, FALSE);
907     _widget->updateRulers();
908     _widget->updateScrollbars(expansion(_d2w));
911 bool
912 SPDesktop::scroll_to_point (NR::Point const *p, gdouble autoscrollspeed)
914     gdouble autoscrolldistance = (gdouble) prefs_get_int_attribute_limited ("options.autoscrolldistance", "value", 0, -1000, 10000);
916     // autoscrolldistance is in screen pixels, but the display area is in document units
917     autoscrolldistance /= expansion(_d2w);
918     NR::Rect const dbox = NR::expand(get_display_area(), -autoscrolldistance);
920     if (!((*p)[NR::X] > dbox.min()[NR::X] && (*p)[NR::X] < dbox.max()[NR::X]) ||
921         !((*p)[NR::Y] > dbox.min()[NR::Y] && (*p)[NR::Y] < dbox.max()[NR::Y])   ) {
923         NR::Point const s_w( (*p) * _d2w );
925         gdouble x_to;
926         if ((*p)[NR::X] < dbox.min()[NR::X])
927             x_to = dbox.min()[NR::X];
928         else if ((*p)[NR::X] > dbox.max()[NR::X])
929             x_to = dbox.max()[NR::X];
930         else
931             x_to = (*p)[NR::X];
933         gdouble y_to;
934         if ((*p)[NR::Y] < dbox.min()[NR::Y])
935             y_to = dbox.min()[NR::Y];
936         else if ((*p)[NR::Y] > dbox.max()[NR::Y])
937             y_to = dbox.max()[NR::Y];
938         else
939             y_to = (*p)[NR::Y];
941         NR::Point const d_dt(x_to, y_to);
942         NR::Point const d_w( d_dt * _d2w );
943         NR::Point const moved_w( d_w - s_w );
945         if (autoscrollspeed == 0)
946             autoscrollspeed = prefs_get_double_attribute_limited ("options.autoscrollspeed", "value", 1, 0, 10);
948         if (autoscrollspeed != 0)
949             scroll_world (autoscrollspeed * moved_w);
951         return true;
952     }
953     return false;
956 void
957 SPDesktop::fullscreen()
959     _widget->setFullscreen();
962 void
963 SPDesktop::getWindowGeometry (gint &x, gint &y, gint &w, gint &h)
965     _widget->getGeometry (x, y, w, h);
968 void
969 SPDesktop::setWindowPosition (NR::Point p)
971     _widget->setPosition (p);
974 void
975 SPDesktop::setWindowSize (gint w, gint h)
977     _widget->setSize (w, h);
980 void
981 SPDesktop::setWindowTransient (void *p, int transient_policy)
983     _widget->setTransient (p, transient_policy);
986 void
987 SPDesktop::presentWindow()
989     _widget->present();
992 bool
993 SPDesktop::warnDialog (gchar *text)
995     return _widget->warnDialog (text);
998 void
999 SPDesktop::toggleRulers()
1001     _widget->toggleRulers();
1004 void
1005 SPDesktop::toggleScrollbars()
1007     _widget->toggleScrollbars();
1010 void
1011 SPDesktop::layoutWidget()
1013     _widget->layout();
1016 void
1017 SPDesktop::destroyWidget()
1019     _widget->destroy();
1022 bool
1023 SPDesktop::shutdown()
1025     return _widget->shutdown();
1028 void
1029 SPDesktop::setToolboxFocusTo (gchar const *label)
1031     _widget->setToolboxFocusTo (label);
1034 void
1035 SPDesktop::setToolboxAdjustmentValue (gchar const* id, double val)
1037     _widget->setToolboxAdjustmentValue (id, val);
1040 bool
1041 SPDesktop::isToolboxButtonActive (gchar const *id)
1043     return _widget->isToolboxButtonActive (id);
1046 void
1047 SPDesktop::emitToolSubselectionChanged(gpointer data)
1049         _tool_subselection_changed.emit(data);
1050         inkscape_subselection_changed (this);
1053 void
1054 SPDesktop::updateNow()
1056   sp_canvas_update_now(canvas);
1059 void
1060 SPDesktop::enableInteraction()
1062   _widget->enableInteraction();
1065 void SPDesktop::disableInteraction()
1067   _widget->disableInteraction();
1070 //----------------------------------------------------------------------
1071 // Callback implementations. The virtual ones are connected by the view.
1073 void
1074 SPDesktop::onPositionSet (double x, double y)
1076     _widget->viewSetPosition (NR::Point(x,y));
1079 void
1080 SPDesktop::onResized (double x, double y)
1082    // Nothing called here
1085 /**
1086  * Redraw callback; queues Gtk redraw; connected by View.
1087  */
1088 void
1089 SPDesktop::onRedrawRequested ()
1091     if (main) {
1092         _widget->requestCanvasUpdate();
1093     }
1096 void
1097 SPDesktop::updateCanvasNow()
1099   _widget->requestCanvasUpdateAndWait();
1102 /**
1103  * Associate document with desktop.
1104  */
1105 /// \todo fixme: refactor SPDesktop::init to use setDocument
1106 void
1107 SPDesktop::setDocument (SPDocument *doc)
1109     if (this->doc() && doc) {
1110         namedview->hide(this);
1111         sp_item_invoke_hide (SP_ITEM (sp_document_root (this->doc())), dkey);
1112     }
1114     if (_layer_hierarchy) {
1115         _layer_hierarchy->clear();
1116         delete _layer_hierarchy;
1117     }
1118     _layer_hierarchy = new Inkscape::ObjectHierarchy(NULL);
1119     _layer_hierarchy->connectAdded(sigc::bind(sigc::ptr_fun(_layer_activated), this));
1120     _layer_hierarchy->connectRemoved(sigc::bind(sigc::ptr_fun(_layer_deactivated), this));
1121     _layer_hierarchy->connectChanged(sigc::bind(sigc::ptr_fun(_layer_hierarchy_changed), this));
1122     _layer_hierarchy->setTop(SP_DOCUMENT_ROOT(doc));
1124     /* setup EventLog */
1125     event_log = new Inkscape::EventLog(doc);
1126     doc->addUndoObserver(*event_log);
1128     _commit_connection.disconnect();
1129     _commit_connection = doc->connectCommit(sigc::mem_fun(*this, &SPDesktop::updateNow));
1131     /// \todo fixme: This condition exists to make sure the code
1132     /// inside is called only once on initialization. But there
1133     /// are surely more safe methods to accomplish this.
1134     if (drawing) {
1135         NRArenaItem *ai;
1137         namedview = sp_document_namedview (doc, NULL);
1138         _modified_connection = namedview->connectModified(sigc::bind<2>(sigc::ptr_fun(&_namedview_modified), this));
1139         number = namedview->getViewCount();
1141         ai = sp_item_invoke_show (SP_ITEM (sp_document_root (doc)),
1142                 SP_CANVAS_ARENA (drawing)->arena,
1143                 dkey,
1144                 SP_ITEM_SHOW_DISPLAY);
1145         if (ai) {
1146             nr_arena_item_add_child (SP_CANVAS_ARENA (drawing)->root, ai, NULL);
1147             nr_arena_item_unref (ai);
1148         }
1149         namedview->show(this);
1150         /* Ugly hack */
1151         activate_guides (true);
1152         /* Ugly hack */
1153         _namedview_modified (namedview, SP_OBJECT_MODIFIED_FLAG, this);
1154     }
1156     _document_replaced_signal.emit (this, doc);
1158     View::setDocument (doc);
1161 void
1162 SPDesktop::onStatusMessage
1163 (Inkscape::MessageType type, gchar const *message)
1165     if (_widget) {
1166         _widget->setMessage(type, message);
1167     }
1170 void
1171 SPDesktop::onDocumentURISet (gchar const* uri)
1173     _widget->setTitle(uri);
1176 /**
1177  * Resized callback.
1178  */
1179 void
1180 SPDesktop::onDocumentResized (gdouble width, gdouble height)
1182     _doc2dt[5] = height;
1183     sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (drawing), _doc2dt);
1184     NR::Rect const a(NR::Point(0, 0), NR::Point(width, height));
1185     SP_CTRLRECT(page)->setRectangle(a);
1186     SP_CTRLRECT(page_border)->setRectangle(a);
1190 void
1191 SPDesktop::_onActivate (SPDesktop* dt)
1193     if (!dt->_widget) return;
1194     dt->_widget->activateDesktop();
1197 void
1198 SPDesktop::_onDeactivate (SPDesktop* dt)
1200     if (!dt->_widget) return;
1201     dt->_widget->deactivateDesktop();
1204 void
1205 SPDesktop::_onSelectionModified
1206 (Inkscape::Selection *selection, guint flags, SPDesktop *dt)
1208     if (!dt->_widget) return;
1209     dt->_widget->updateScrollbars (expansion(dt->_d2w));
1212 static void
1213 _onSelectionChanged
1214 (Inkscape::Selection *selection, SPDesktop *desktop)
1216     /** \todo
1217      * only change the layer for single selections, or what?
1218      * This seems reasonable -- for multiple selections there can be many
1219      * different layers involved.
1220      */
1221     SPItem *item=selection->singleItem();
1222     if (item) {
1223         SPObject *layer=desktop->layerForObject(item);
1224         if ( layer && layer != desktop->currentLayer() ) {
1225             desktop->setCurrentLayer(layer);
1226         }
1227     }
1230 /**
1231  * Calls event handler of current event context.
1232  * \param arena Unused
1233  * \todo fixme
1234  */
1235 static gint
1236 _arena_handler (SPCanvasArena *arena, NRArenaItem *ai, GdkEvent *event, SPDesktop *desktop)
1238     if (ai) {
1239         SPItem *spi = (SPItem*)NR_ARENA_ITEM_GET_DATA (ai);
1240         return sp_event_context_item_handler (desktop->event_context, spi, event);
1241     } else {
1242         return sp_event_context_root_handler (desktop->event_context, event);
1243     }
1246 static void
1247 _layer_activated(SPObject *layer, SPDesktop *desktop) {
1248     g_return_if_fail(SP_IS_GROUP(layer));
1249     SP_GROUP(layer)->setLayerDisplayMode(desktop->dkey, SPGroup::LAYER);
1252 /// Callback
1253 static void
1254 _layer_deactivated(SPObject *layer, SPDesktop *desktop) {
1255     g_return_if_fail(SP_IS_GROUP(layer));
1256     SP_GROUP(layer)->setLayerDisplayMode(desktop->dkey, SPGroup::GROUP);
1259 /// Callback
1260 static void
1261 _layer_hierarchy_changed(SPObject *top, SPObject *bottom,
1262                                          SPDesktop *desktop)
1264     desktop->_layer_changed_signal.emit (bottom);
1267 /// Called when document is starting to be rebuilt.
1268 static void
1269 _reconstruction_start (SPDesktop * desktop)
1271     // printf("Desktop, starting reconstruction\n");
1272     desktop->_reconstruction_old_layer_id = g_strdup(SP_OBJECT_ID(desktop->currentLayer()));
1273     desktop->_layer_hierarchy->setBottom(desktop->currentRoot());
1275     /*
1276     GSList const * selection_objs = desktop->selection->list();
1277     for (; selection_objs != NULL; selection_objs = selection_objs->next) {
1279     }
1280     */
1281     desktop->selection->clear();
1283     // printf("Desktop, starting reconstruction end\n");
1286 /// Called when document rebuild is finished.
1287 static void
1288 _reconstruction_finish (SPDesktop * desktop)
1290     // printf("Desktop, finishing reconstruction\n");
1291     if (desktop->_reconstruction_old_layer_id == NULL)
1292         return;
1294     SPObject * newLayer = SP_OBJECT_DOCUMENT(desktop->namedview)->getObjectById(desktop->_reconstruction_old_layer_id);
1295     if (newLayer != NULL)
1296         desktop->setCurrentLayer(newLayer);
1298     g_free(desktop->_reconstruction_old_layer_id);
1299     desktop->_reconstruction_old_layer_id = NULL;
1300     // printf("Desktop, finishing reconstruction end\n");
1301     return;
1304 /**
1305  * Namedview_modified callback.
1306  */
1307 static void
1308 _namedview_modified (SPObject *obj, guint flags, SPDesktop *desktop)
1310     SPNamedView *nv=SP_NAMEDVIEW(obj);
1312     if (flags & SP_OBJECT_MODIFIED_FLAG) {
1314         /* Recalculate snap distances */
1315         /* FIXME: why is the desktop getting involved in setting up something
1316         ** that is entirely to do with the namedview?
1317         */
1318         _update_snap_distances (desktop);
1320         /* Show/hide page background */
1321         if (nv->pagecolor & 0xff) {
1322             sp_canvas_item_show (desktop->table);
1323             ((CtrlRect *) desktop->table)->setColor(0x00000000, true, nv->pagecolor);
1324             sp_canvas_item_move_to_z (desktop->table, 0);
1325         } else {
1326             sp_canvas_item_hide (desktop->table);
1327         }
1329         /* Show/hide page border */
1330         if (nv->showborder) {
1331             // show
1332             sp_canvas_item_show (desktop->page_border);
1333             // set color and shadow
1334             ((CtrlRect *) desktop->page_border)->setColor(nv->bordercolor, false, 0x00000000);
1335             if (nv->pageshadow) {
1336                 ((CtrlRect *) desktop->page_border)->setShadow(nv->pageshadow, nv->bordercolor);
1337             }
1338             // place in the z-order stack
1339             if (nv->borderlayer == SP_BORDER_LAYER_BOTTOM) {
1340                  sp_canvas_item_move_to_z (desktop->page_border, 2);
1341             } else {
1342                 int order = sp_canvas_item_order (desktop->page_border);
1343                 int morder = sp_canvas_item_order (desktop->drawing);
1344                 if (morder > order) sp_canvas_item_raise (desktop->page_border,
1345                                     morder - order);
1346             }
1347         } else {
1348                 sp_canvas_item_hide (desktop->page_border);
1349                 if (nv->pageshadow) {
1350                     ((CtrlRect *) desktop->page)->setShadow(0, 0x00000000);
1351                 }
1352         }
1353         
1354         /* Show/hide page shadow */
1355         if (nv->showpageshadow && nv->pageshadow) {
1356             ((CtrlRect *) desktop->page_border)->setShadow(nv->pageshadow, nv->bordercolor);
1357         } else {
1358             ((CtrlRect *) desktop->page_border)->setShadow(0, 0x00000000);
1359         }
1361         if (SP_RGBA32_A_U(nv->pagecolor) < 128 ||
1362             (SP_RGBA32_R_U(nv->pagecolor) +
1363              SP_RGBA32_G_U(nv->pagecolor) +
1364              SP_RGBA32_B_U(nv->pagecolor)) >= 384) {
1365             // the background color is light or transparent, use black outline
1366             SP_CANVAS_ARENA (desktop->drawing)->arena->outlinecolor = prefs_get_int_attribute("options.wireframecolors", "onlight", 0xff);
1367         } else { // use white outline
1368             SP_CANVAS_ARENA (desktop->drawing)->arena->outlinecolor = prefs_get_int_attribute("options.wireframecolors", "ondark", 0xffffffff);
1369         }
1370     }
1373 /**
1374  * Callback to reset snapper's distances.
1375  */
1376 static void
1377 _update_snap_distances (SPDesktop *desktop)
1379     SPUnit const &px = sp_unit_get_by_id(SP_UNIT_PX);
1381     SPNamedView &nv = *desktop->namedview;
1383     nv.snap_manager.grid.setDistance(sp_convert_distance_full(nv.gridtolerance,
1384                                                                       *nv.gridtoleranceunit,
1385                                                                       px));
1386     nv.snap_manager.axonomgrid.setDistance(sp_convert_distance_full(nv.gridtolerance,
1387                                                                       *nv.gridtoleranceunit,
1388                                                                       px));
1389     nv.snap_manager.guide.setDistance(sp_convert_distance_full(nv.guidetolerance,
1390                                                                        *nv.guidetoleranceunit,
1391                                                                        px));
1392     nv.snap_manager.object.setDistance(sp_convert_distance_full(nv.objecttolerance,
1393                                                                         *nv.objecttoleranceunit,
1394                                                                         px));
1398 NR::Matrix SPDesktop::w2d() const
1400     return _w2d;
1403 NR::Point SPDesktop::w2d(NR::Point const &p) const
1405     return p * _w2d;
1408 NR::Point SPDesktop::d2w(NR::Point const &p) const
1410     return p * _d2w;
1413 NR::Matrix SPDesktop::doc2dt() const
1415     return _doc2dt;
1418 NR::Point SPDesktop::doc2dt(NR::Point const &p) const
1420     return p * _doc2dt;
1423 NR::Point SPDesktop::dt2doc(NR::Point const &p) const
1425     return p / _doc2dt;
1429 /**
1430  * Pop event context from desktop's context stack. Never used.
1431  */
1432 // void
1433 // SPDesktop::pop_event_context (unsigned int key)
1434 // {
1435 //    SPEventContext *ec = NULL;
1436 //
1437 //    if (event_context && event_context->key == key) {
1438 //        g_return_if_fail (event_context);
1439 //        g_return_if_fail (event_context->next);
1440 //        ec = event_context;
1441 //        sp_event_context_deactivate (ec);
1442 //        event_context = ec->next;
1443 //        sp_event_context_activate (event_context);
1444 //        _event_context_changed_signal.emit (this, ec);
1445 //    }
1446 //
1447 //    SPEventContext *ref = event_context;
1448 //    while (ref && ref->next && ref->next->key != key)
1449 //        ref = ref->next;
1450 //
1451 //    if (ref && ref->next) {
1452 //        ec = ref->next;
1453 //        ref->next = ec->next;
1454 //    }
1455 //
1456 //    if (ec) {
1457 //        sp_event_context_finish (ec);
1458 //        g_object_unref (G_OBJECT (ec));
1459 //    }
1460 // }
1462 /*
1463   Local Variables:
1464   mode:c++
1465   c-file-style:"stroustrup"
1466   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1467   indent-tabs-mode:nil
1468   fill-column:99
1469   End:
1470 */
1471 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :