1 /** \file
2 * SPKnot implementation
3 *
4 * Authors:
5 * Lauris Kaplinski <lauris@kaplinski.com>
6 * bulia byak <buliabyak@users.sf.net>
7 * Abhishek Sharma
8 *
9 * Copyright (C) 1999-2005 authors
10 * Copyright (C) 2001-2002 Ximian, Inc.
11 *
12 * Released under GNU GPL, read the file 'COPYING' for more information
13 */
15 #ifdef HAVE_CONFIG_H
16 # include <config.h>
17 #endif
18 #include <gdk/gdkkeysyms.h>
19 #include <glibmm/i18n.h>
20 #include "helper/sp-marshal.h"
21 #include "display/sodipodi-ctrl.h"
22 #include "desktop.h"
23 #include "desktop-handles.h"
24 #include "knot.h"
25 #include "document.h"
26 #include "preferences.h"
27 #include "message-stack.h"
28 #include "message-context.h"
29 #include "event-context.h"
31 using Inkscape::DocumentUndo;
33 #define KNOT_EVENT_MASK (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | \
34 GDK_POINTER_MOTION_MASK | \
35 GDK_POINTER_MOTION_HINT_MASK | \
36 GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK)
38 static bool nograb = false;
40 static bool grabbed = FALSE;
41 static bool moved = FALSE;
43 static gint xp = 0, yp = 0; // where drag started
44 static gint tolerance = 0;
45 static bool within_tolerance = false;
47 static bool transform_escaped = false; // true iff resize or rotate was cancelled by esc.
49 enum {
50 EVENT,
51 CLICKED,
52 DOUBLECLICKED,
53 GRABBED,
54 UNGRABBED,
55 MOVED,
56 REQUEST,
57 DISTANCE,
58 LAST_SIGNAL
59 };
61 static void sp_knot_class_init(SPKnotClass *klass);
62 static void sp_knot_init(SPKnot *knot);
63 static void sp_knot_dispose(GObject *object);
65 static int sp_knot_handler(SPCanvasItem *item, GdkEvent *event, SPKnot *knot);
66 static void sp_knot_set_ctrl_state(SPKnot *knot);
68 static GObjectClass *parent_class;
69 static guint knot_signals[LAST_SIGNAL] = { 0 };
71 /**
72 * Registers SPKnot class and returns its type number.
73 */
74 GType sp_knot_get_type()
75 {
76 static GType type = 0;
77 if (!type) {
78 GTypeInfo info = {
79 sizeof(SPKnotClass),
80 NULL, /* base_init */
81 NULL, /* base_finalize */
82 (GClassInitFunc) sp_knot_class_init,
83 NULL, /* class_finalize */
84 NULL, /* class_data */
85 sizeof (SPKnot),
86 16, /* n_preallocs */
87 (GInstanceInitFunc) sp_knot_init,
88 NULL
89 };
90 type = g_type_register_static (G_TYPE_OBJECT, "SPKnot", &info, (GTypeFlags) 0);
91 }
92 return type;
93 }
95 /**
96 * SPKnot vtable initialization.
97 */
98 static void sp_knot_class_init(SPKnotClass *klass)
99 {
100 GObjectClass *object_class = (GObjectClass*)klass;
102 parent_class = (GObjectClass*) g_type_class_peek_parent(klass);
104 object_class->dispose = sp_knot_dispose;
106 knot_signals[EVENT] = g_signal_new("event",
107 G_TYPE_FROM_CLASS(klass),
108 G_SIGNAL_RUN_LAST,
109 G_STRUCT_OFFSET(SPKnotClass, event),
110 NULL, NULL,
111 sp_marshal_BOOLEAN__POINTER,
112 G_TYPE_BOOLEAN, 1,
113 GDK_TYPE_EVENT);
115 knot_signals[CLICKED] = g_signal_new("clicked",
116 G_TYPE_FROM_CLASS(klass),
117 G_SIGNAL_RUN_FIRST,
118 G_STRUCT_OFFSET(SPKnotClass, clicked),
119 NULL, NULL,
120 g_cclosure_marshal_VOID__UINT,
121 G_TYPE_NONE, 1,
122 G_TYPE_UINT);
124 knot_signals[DOUBLECLICKED] = g_signal_new("doubleclicked",
125 G_TYPE_FROM_CLASS(klass),
126 G_SIGNAL_RUN_FIRST,
127 G_STRUCT_OFFSET(SPKnotClass, doubleclicked),
128 NULL, NULL,
129 g_cclosure_marshal_VOID__UINT,
130 G_TYPE_NONE, 1,
131 G_TYPE_UINT);
133 knot_signals[GRABBED] = g_signal_new("grabbed",
134 G_TYPE_FROM_CLASS(klass),
135 G_SIGNAL_RUN_FIRST,
136 G_STRUCT_OFFSET(SPKnotClass, grabbed),
137 NULL, NULL,
138 g_cclosure_marshal_VOID__UINT,
139 G_TYPE_NONE, 1,
140 G_TYPE_UINT);
142 knot_signals[UNGRABBED] = g_signal_new("ungrabbed",
143 G_TYPE_FROM_CLASS(klass),
144 G_SIGNAL_RUN_FIRST,
145 G_STRUCT_OFFSET(SPKnotClass, ungrabbed),
146 NULL, NULL,
147 g_cclosure_marshal_VOID__UINT,
148 G_TYPE_NONE, 1,
149 G_TYPE_UINT);
151 knot_signals[MOVED] = g_signal_new("moved",
152 G_TYPE_FROM_CLASS(klass),
153 G_SIGNAL_RUN_FIRST,
154 G_STRUCT_OFFSET(SPKnotClass, moved),
155 NULL, NULL,
156 sp_marshal_NONE__POINTER_UINT,
157 G_TYPE_NONE, 2,
158 G_TYPE_POINTER, G_TYPE_UINT);
160 knot_signals[REQUEST] = g_signal_new("request",
161 G_TYPE_FROM_CLASS(klass),
162 G_SIGNAL_RUN_LAST,
163 G_STRUCT_OFFSET(SPKnotClass, request),
164 NULL, NULL,
165 sp_marshal_BOOLEAN__POINTER_UINT,
166 G_TYPE_BOOLEAN, 2,
167 G_TYPE_POINTER, G_TYPE_UINT);
169 knot_signals[DISTANCE] = g_signal_new("distance",
170 G_TYPE_FROM_CLASS(klass),
171 G_SIGNAL_RUN_LAST,
172 G_STRUCT_OFFSET(SPKnotClass, distance),
173 NULL, NULL,
174 sp_marshal_DOUBLE__POINTER_UINT,
175 G_TYPE_DOUBLE, 2,
176 G_TYPE_POINTER, G_TYPE_UINT);
178 const gchar *nograbenv = getenv("INKSCAPE_NO_GRAB");
179 nograb = (nograbenv && *nograbenv && (*nograbenv != '0'));
180 }
182 /**
183 * Callback for SPKnot initialization.
184 */
185 static void sp_knot_init(SPKnot *knot)
186 {
187 knot->desktop = NULL;
188 knot->item = NULL;
189 knot->flags = 0;
191 knot->size = 8;
192 knot->pos = Geom::Point(0, 0);
193 knot->grabbed_rel_pos = Geom::Point(0, 0);
194 knot->anchor = GTK_ANCHOR_CENTER;
195 knot->shape = SP_KNOT_SHAPE_SQUARE;
196 knot->mode = SP_KNOT_MODE_XOR;
197 knot->tip = NULL;
198 knot->_event_handler_id = 0;
199 knot->pressure = 0;
201 knot->fill[SP_KNOT_STATE_NORMAL] = 0xffffff00;
202 knot->fill[SP_KNOT_STATE_MOUSEOVER] = 0xff0000ff;
203 knot->fill[SP_KNOT_STATE_DRAGGING] = 0x0000ffff;
205 knot->stroke[SP_KNOT_STATE_NORMAL] = 0x01000000;
206 knot->stroke[SP_KNOT_STATE_MOUSEOVER] = 0x01000000;
207 knot->stroke[SP_KNOT_STATE_DRAGGING] = 0x01000000;
209 knot->image[SP_KNOT_STATE_NORMAL] = NULL;
210 knot->image[SP_KNOT_STATE_MOUSEOVER] = NULL;
211 knot->image[SP_KNOT_STATE_DRAGGING] = NULL;
213 knot->cursor[SP_KNOT_STATE_NORMAL] = NULL;
214 knot->cursor[SP_KNOT_STATE_MOUSEOVER] = NULL;
215 knot->cursor[SP_KNOT_STATE_DRAGGING] = NULL;
217 knot->saved_cursor = NULL;
218 knot->pixbuf = NULL;
219 }
221 /**
222 * Called before SPKnot destruction.
223 */
224 static void sp_knot_dispose(GObject *object)
225 {
226 SPKnot *knot = (SPKnot *) object;
228 if ((knot->flags & SP_KNOT_GRABBED) && gdk_pointer_is_grabbed ()) {
229 // This happens e.g. when deleting a node in node tool while dragging it
230 gdk_pointer_ungrab (GDK_CURRENT_TIME);
231 }
233 if (knot->_event_handler_id > 0)
234 {
235 g_signal_handler_disconnect(G_OBJECT (knot->item), knot->_event_handler_id);
236 knot->_event_handler_id = 0;
237 }
239 if (knot->item) {
240 gtk_object_destroy (GTK_OBJECT (knot->item));
241 knot->item = NULL;
242 }
244 for (gint i = 0; i < SP_KNOT_VISIBLE_STATES; i++) {
245 if (knot->cursor[i]) {
246 gdk_cursor_unref(knot->cursor[i]);
247 knot->cursor[i] = NULL;
248 }
249 }
251 if (knot->tip) {
252 g_free(knot->tip);
253 knot->tip = NULL;
254 }
256 if (parent_class->dispose) {
257 (*parent_class->dispose) (object);
258 }
259 }
261 /**
262 * Update knot for dragging and tell canvas an item was grabbed.
263 */
264 void sp_knot_start_dragging(SPKnot *knot, Geom::Point const &p, gint x, gint y, guint32 etime)
265 {
266 // save drag origin
267 xp = x;
268 yp = y;
269 within_tolerance = true;
271 knot->grabbed_rel_pos = p - knot->pos;
272 knot->drag_origin = knot->pos;
273 if (!nograb) {
274 sp_canvas_item_grab(knot->item,
275 KNOT_EVENT_MASK,
276 knot->cursor[SP_KNOT_STATE_DRAGGING],
277 etime);
278 }
279 sp_knot_set_flag(knot, SP_KNOT_GRABBED, TRUE);
280 grabbed = TRUE;
281 }
283 /**
284 * Called to handle events on knots.
285 */
286 static int sp_knot_handler(SPCanvasItem */*item*/, GdkEvent *event, SPKnot *knot)
287 {
288 g_assert(knot != NULL);
289 g_assert(SP_IS_KNOT(knot));
291 /* Run client universal event handler, if present */
293 gboolean consumed = FALSE;
295 g_signal_emit(knot, knot_signals[EVENT], 0, event, &consumed);
297 if (consumed) {
298 return TRUE;
299 }
301 g_object_ref(knot);
302 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
303 tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
305 switch (event->type) {
306 case GDK_2BUTTON_PRESS:
307 if (event->button.button == 1) {
308 g_signal_emit(knot, knot_signals[DOUBLECLICKED], 0, event->button.state);
310 grabbed = FALSE;
311 moved = FALSE;
312 consumed = TRUE;
313 }
314 break;
315 case GDK_BUTTON_PRESS:
316 if (event->button.button == 1 && !knot->desktop->event_context->space_panning) {
317 Geom::Point const p = knot->desktop->w2d(Geom::Point(event->button.x, event->button.y));
318 sp_knot_start_dragging(knot, p, (gint) event->button.x, (gint) event->button.y, event->button.time);
319 consumed = TRUE;
320 }
321 break;
322 case GDK_BUTTON_RELEASE:
323 if (event->button.button == 1 && !knot->desktop->event_context->space_panning) {
324 // If we have any pending snap event, then invoke it now
325 if (knot->desktop->event_context->_delayed_snap_event) {
326 sp_event_context_snap_watchdog_callback(knot->desktop->event_context->_delayed_snap_event);
327 }
329 sp_event_context_discard_delayed_snap_event(knot->desktop->event_context);
331 knot->pressure = 0;
332 if (transform_escaped) {
333 transform_escaped = false;
334 consumed = TRUE;
335 } else {
336 sp_knot_set_flag(knot, SP_KNOT_GRABBED, FALSE);
337 if (!nograb) {
338 sp_canvas_item_ungrab(knot->item, event->button.time);
339 }
340 if (moved) {
341 sp_knot_set_flag(knot,
342 SP_KNOT_DRAGGING,
343 FALSE);
344 g_signal_emit(knot,
345 knot_signals[UNGRABBED], 0,
346 event->button.state);
347 knot->_ungrabbed_signal.emit(knot);
348 } else {
349 g_signal_emit(knot,
350 knot_signals[CLICKED], 0,
351 event->button.state);
352 knot->_click_signal.emit(knot, event->button.state);
353 }
354 grabbed = FALSE;
355 moved = FALSE;
356 consumed = TRUE;
357 }
358 }
359 break;
360 case GDK_MOTION_NOTIFY:
361 if (grabbed && !knot->desktop->event_context->space_panning) {
362 consumed = TRUE;
364 if ( within_tolerance
365 && ( abs( (gint) event->motion.x - xp ) < tolerance )
366 && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) {
367 break; // do not drag if we're within tolerance from origin
368 }
370 // Once the user has moved farther than tolerance from the original location
371 // (indicating they intend to move the object, not click), then always process the
372 // motion notify coordinates as given (no snapping back to origin)
373 within_tolerance = false;
375 if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &knot->pressure))
376 knot->pressure = CLAMP (knot->pressure, 0, 1);
377 else
378 knot->pressure = 0.5;
380 if (!moved) {
381 g_signal_emit(knot,
382 knot_signals[GRABBED], 0,
383 event->motion.state);
384 sp_knot_set_flag(knot,
385 SP_KNOT_DRAGGING,
386 TRUE);
387 }
388 sp_event_context_snap_delay_handler(knot->desktop->event_context, NULL, (gpointer) knot, (GdkEventMotion *)event, DelayedSnapEvent::KNOT_HANDLER);
389 sp_knot_handler_request_position(event, knot);
390 moved = TRUE;
391 }
392 break;
393 case GDK_ENTER_NOTIFY:
394 sp_knot_set_flag(knot, SP_KNOT_MOUSEOVER, TRUE);
395 sp_knot_set_flag(knot, SP_KNOT_GRABBED, FALSE);
397 if (knot->tip) {
398 knot->desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, knot->tip);
399 }
401 grabbed = FALSE;
402 moved = FALSE;
403 consumed = TRUE;
404 break;
405 case GDK_LEAVE_NOTIFY:
406 sp_knot_set_flag(knot, SP_KNOT_MOUSEOVER, FALSE);
407 sp_knot_set_flag(knot, SP_KNOT_GRABBED, FALSE);
409 if (knot->tip) {
410 knot->desktop->event_context->defaultMessageContext()->clear();
411 }
413 grabbed = FALSE;
414 moved = FALSE;
415 consumed = TRUE;
416 break;
417 case GDK_KEY_PRESS: // keybindings for knot
418 switch (get_group0_keyval(&event->key)) {
419 case GDK_Escape:
420 sp_knot_set_flag(knot, SP_KNOT_GRABBED, FALSE);
421 if (!nograb) {
422 sp_canvas_item_ungrab(knot->item, event->button.time);
423 }
424 if (moved) {
425 sp_knot_set_flag(knot,
426 SP_KNOT_DRAGGING,
427 FALSE);
428 g_signal_emit(knot,
429 knot_signals[UNGRABBED], 0,
430 event->button.state);
431 DocumentUndo::undo(sp_desktop_document(knot->desktop));
432 knot->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Node or handle drag canceled."));
433 transform_escaped = true;
434 consumed = TRUE;
435 }
436 grabbed = FALSE;
437 moved = FALSE;
438 sp_event_context_discard_delayed_snap_event(knot->desktop->event_context);
439 break;
440 default:
441 consumed = FALSE;
442 break;
443 }
444 break;
445 default:
446 break;
447 }
449 g_object_unref(knot);
451 return consumed || grabbed;
452 }
454 void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot)
455 {
456 Geom::Point const motion_w(event->motion.x, event->motion.y);
457 Geom::Point const motion_dt = knot->desktop->w2d(motion_w);
458 Geom::Point p = motion_dt - knot->grabbed_rel_pos;
459 sp_knot_request_position (knot, p, event->motion.state);
460 knot->desktop->scroll_to_point (motion_dt);
461 knot->desktop->set_coordinate_status(knot->pos); // display the coordinate of knot, not cursor - they may be different!
462 if (event->motion.state & GDK_BUTTON1_MASK)
463 gobble_motion_events(GDK_BUTTON1_MASK);
464 }
466 /**
467 * Return new knot object.
468 */
469 SPKnot *sp_knot_new(SPDesktop *desktop, const gchar *tip)
470 {
471 g_return_val_if_fail(desktop != NULL, NULL);
473 SPKnot * knot = (SPKnot*) g_object_new(SP_TYPE_KNOT, 0);
475 knot->desktop = desktop;
476 knot->flags = SP_KNOT_VISIBLE;
477 if (tip) {
478 knot->tip = g_strdup (tip);
479 }
481 knot->item = sp_canvas_item_new(sp_desktop_controls (desktop),
482 SP_TYPE_CTRL,
483 "anchor", GTK_ANCHOR_CENTER,
484 "size", 8.0,
485 "filled", TRUE,
486 "fill_color", 0xffffff00,
487 "stroked", TRUE,
488 "stroke_color", 0x01000000,
489 "mode", SP_KNOT_MODE_XOR,
490 NULL);
492 knot->_event_handler_id = gtk_signal_connect(GTK_OBJECT(knot->item), "event",
493 GTK_SIGNAL_FUNC(sp_knot_handler), knot);
495 return knot;
496 }
498 /**
499 * Show knot on its canvas.
500 */
501 void sp_knot_show(SPKnot *knot)
502 {
503 g_return_if_fail(knot != NULL);
504 g_return_if_fail(SP_IS_KNOT (knot));
506 sp_knot_set_flag(knot, SP_KNOT_VISIBLE, TRUE);
507 }
509 /**
510 * Hide knot on its canvas.
511 */
512 void sp_knot_hide(SPKnot *knot)
513 {
514 g_return_if_fail(knot != NULL);
515 g_return_if_fail(SP_IS_KNOT(knot));
517 sp_knot_set_flag(knot, SP_KNOT_VISIBLE, FALSE);
518 }
520 /**
521 * Request or set new position for knot.
522 */
523 void sp_knot_request_position(SPKnot *knot, Geom::Point const &p, guint state)
524 {
525 g_return_if_fail(knot != NULL);
526 g_return_if_fail(SP_IS_KNOT(knot));
528 gboolean done = FALSE;
530 g_signal_emit(knot,
531 knot_signals[REQUEST], 0,
532 &p,
533 state,
534 &done);
536 /* If user did not complete, we simply move knot to new position */
538 if (!done) {
539 sp_knot_set_position (knot, p, state);
540 }
541 }
543 /**
544 * Return distance of point to knot's position; unused.
545 */
546 gdouble sp_knot_distance(SPKnot * knot, Geom::Point const &p, guint state)
547 {
548 g_return_val_if_fail(knot != NULL, 1e18);
549 g_return_val_if_fail(SP_IS_KNOT(knot), 1e18);
551 gdouble distance = Geom::L2(p - knot->pos);
553 g_signal_emit(knot,
554 knot_signals[DISTANCE], 0,
555 &p,
556 state,
557 &distance);
559 return distance;
560 }
562 /**
563 * Move knot to new position.
564 */
565 void sp_knot_set_position(SPKnot *knot, Geom::Point const &p, guint state)
566 {
567 g_return_if_fail(knot != NULL);
568 g_return_if_fail(SP_IS_KNOT (knot));
570 knot->pos = p;
572 if (knot->item) {
573 SP_CTRL(knot->item)->moveto (p);
574 }
576 g_signal_emit(knot,
577 knot_signals[MOVED], 0,
578 &p,
579 state);
580 knot->_moved_signal.emit(knot, p, state);
581 }
583 /**
584 * Move knot to new position, without emitting a MOVED signal.
585 */
586 void sp_knot_moveto(SPKnot *knot, Geom::Point const &p)
587 {
588 g_return_if_fail(knot != NULL);
589 g_return_if_fail(SP_IS_KNOT(knot));
591 knot->pos = p;
593 if (knot->item) {
594 SP_CTRL(knot->item)->moveto (p);
595 }
596 }
598 /**
599 * Returns position of knot.
600 */
601 Geom::Point sp_knot_position(SPKnot const *knot)
602 {
603 g_assert(knot != NULL);
604 g_assert(SP_IS_KNOT (knot));
606 return knot->pos;
607 }
609 /**
610 * Set flag in knot, with side effects.
611 */
612 void sp_knot_set_flag(SPKnot *knot, guint flag, bool set)
613 {
614 g_assert(knot != NULL);
615 g_assert(SP_IS_KNOT(knot));
617 if (set) {
618 knot->flags |= flag;
619 } else {
620 knot->flags &= ~flag;
621 }
623 switch (flag) {
624 case SP_KNOT_VISIBLE:
625 if (set) {
626 sp_canvas_item_show(knot->item);
627 } else {
628 sp_canvas_item_hide(knot->item);
629 }
630 break;
631 case SP_KNOT_MOUSEOVER:
632 case SP_KNOT_DRAGGING:
633 sp_knot_set_ctrl_state(knot);
634 break;
635 case SP_KNOT_GRABBED:
636 break;
637 default:
638 g_assert_not_reached();
639 break;
640 }
641 }
643 /**
644 * Update knot's pixbuf and set its control state.
645 */
646 void sp_knot_update_ctrl(SPKnot *knot)
647 {
648 if (!knot->item) {
649 return;
650 }
652 gtk_object_set(GTK_OBJECT(knot->item), "shape", knot->shape, NULL);
653 gtk_object_set(GTK_OBJECT(knot->item), "mode", knot->mode, NULL);
654 gtk_object_set(GTK_OBJECT(knot->item), "size", (gdouble) knot->size, NULL);
655 gtk_object_set(GTK_OBJECT(knot->item), "anchor", knot->anchor, NULL);
656 if (knot->pixbuf) {
657 gtk_object_set(GTK_OBJECT (knot->item), "pixbuf", knot->pixbuf, NULL);
658 }
660 sp_knot_set_ctrl_state(knot);
661 }
663 /**
664 * Set knot control state (dragging/mouseover/normal).
665 */
666 static void sp_knot_set_ctrl_state(SPKnot *knot)
667 {
668 if (knot->flags & SP_KNOT_DRAGGING) {
669 gtk_object_set(GTK_OBJECT (knot->item),
670 "fill_color",
671 knot->fill[SP_KNOT_STATE_DRAGGING],
672 NULL);
673 gtk_object_set(GTK_OBJECT (knot->item),
674 "stroke_color",
675 knot->stroke[SP_KNOT_STATE_DRAGGING],
676 NULL);
677 } else if (knot->flags & SP_KNOT_MOUSEOVER) {
678 gtk_object_set(GTK_OBJECT(knot->item),
679 "fill_color",
680 knot->fill[SP_KNOT_STATE_MOUSEOVER],
681 NULL);
682 gtk_object_set(GTK_OBJECT(knot->item),
683 "stroke_color",
684 knot->stroke[SP_KNOT_STATE_MOUSEOVER],
685 NULL);
686 } else {
687 gtk_object_set(GTK_OBJECT(knot->item),
688 "fill_color",
689 knot->fill[SP_KNOT_STATE_NORMAL],
690 NULL);
691 gtk_object_set(GTK_OBJECT(knot->item),
692 "stroke_color",
693 knot->stroke[SP_KNOT_STATE_NORMAL],
694 NULL);
695 }
696 }
700 /*
701 Local Variables:
702 mode:c++
703 c-file-style:"stroustrup"
704 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
705 indent-tabs-mode:nil
706 fill-column:99
707 End:
708 */
709 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :