1 #define __SP_TEXT_CONTEXT_C__
3 /*
4 * SPTextContext
5 *
6 * Authors:
7 * Lauris Kaplinski <lauris@kaplinski.com>
8 * bulia byak <buliabyak@users.sf.net>
9 *
10 * Copyright (C) 1999-2005 authors
11 * Copyright (C) 2001 Ximian, Inc.
12 *
13 * Released under GNU GPL, read the file 'COPYING' for more information
14 */
16 #ifdef HAVE_CONFIG_H
17 # include <config.h>
18 #endif
20 #include <gdk/gdkkeysyms.h>
21 #include <gtk/gtkmain.h>
22 #include <display/sp-ctrlline.h>
23 #include <display/sodipodi-ctrlrect.h>
24 #include <display/sp-ctrlquadr.h>
25 #include <gtk/gtkimmulticontext.h>
26 #include <gtkmm/clipboard.h>
28 #include "macros.h"
29 #include "sp-text.h"
30 #include "sp-flowtext.h"
31 #include "document.h"
32 #include "sp-namedview.h"
33 #include "style.h"
34 #include "selection.h"
35 #include "desktop.h"
36 #include "desktop-style.h"
37 #include "desktop-handles.h"
38 #include "message-stack.h"
39 #include "message-context.h"
40 #include "pixmaps/cursor-text.xpm"
41 #include "pixmaps/cursor-text-insert.xpm"
42 #include <glibmm/i18n.h>
43 #include "object-edit.h"
44 #include "xml/repr.h"
45 #include "xml/node-event-vector.h"
46 #include "preferences.h"
47 #include "rubberband.h"
48 #include "sp-metrics.h"
49 #include "context-fns.h"
50 #include "verbs.h"
51 #include "shape-editor.h"
52 #include "selection-chemistry.h"
53 #include "text-editing.h"
55 #include "text-context.h"
58 static void sp_text_context_class_init(SPTextContextClass *klass);
59 static void sp_text_context_init(SPTextContext *text_context);
60 static void sp_text_context_dispose(GObject *obj);
62 static void sp_text_context_setup(SPEventContext *ec);
63 static void sp_text_context_finish(SPEventContext *ec);
64 static gint sp_text_context_root_handler(SPEventContext *event_context, GdkEvent *event);
65 static gint sp_text_context_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event);
67 static void sp_text_context_selection_changed(Inkscape::Selection *selection, SPTextContext *tc);
68 static void sp_text_context_selection_modified(Inkscape::Selection *selection, guint flags, SPTextContext *tc);
69 static bool sp_text_context_style_set(SPCSSAttr const *css, SPTextContext *tc);
70 static int sp_text_context_style_query(SPStyle *style, int property, SPTextContext *tc);
72 static void sp_text_context_validate_cursor_iterators(SPTextContext *tc);
73 static void sp_text_context_update_cursor(SPTextContext *tc, bool scroll_to_see = true);
74 static void sp_text_context_update_text_selection(SPTextContext *tc);
75 static gint sp_text_context_timeout(SPTextContext *tc);
76 static void sp_text_context_forget_text(SPTextContext *tc);
78 static gint sptc_focus_in(GtkWidget *widget, GdkEventFocus *event, SPTextContext *tc);
79 static gint sptc_focus_out(GtkWidget *widget, GdkEventFocus *event, SPTextContext *tc);
80 static void sptc_commit(GtkIMContext *imc, gchar *string, SPTextContext *tc);
82 static SPEventContextClass *parent_class;
84 GType
85 sp_text_context_get_type()
86 {
87 static GType type = 0;
88 if (!type) {
89 GTypeInfo info = {
90 sizeof(SPTextContextClass),
91 NULL, NULL,
92 (GClassInitFunc) sp_text_context_class_init,
93 NULL, NULL,
94 sizeof(SPTextContext),
95 4,
96 (GInstanceInitFunc) sp_text_context_init,
97 NULL, /* value_table */
98 };
99 type = g_type_register_static(SP_TYPE_EVENT_CONTEXT, "SPTextContext", &info, (GTypeFlags)0);
100 }
101 return type;
102 }
104 static void
105 sp_text_context_class_init(SPTextContextClass *klass)
106 {
107 GObjectClass *object_class=(GObjectClass *)klass;
108 SPEventContextClass *event_context_class = (SPEventContextClass *) klass;
110 parent_class = (SPEventContextClass*)g_type_class_peek_parent(klass);
112 object_class->dispose = sp_text_context_dispose;
114 event_context_class->setup = sp_text_context_setup;
115 event_context_class->finish = sp_text_context_finish;
116 event_context_class->root_handler = sp_text_context_root_handler;
117 event_context_class->item_handler = sp_text_context_item_handler;
118 }
120 static void
121 sp_text_context_init(SPTextContext *tc)
122 {
123 SPEventContext *event_context = SP_EVENT_CONTEXT(tc);
125 event_context->cursor_shape = cursor_text_xpm;
126 event_context->hot_x = 7;
127 event_context->hot_y = 7;
129 event_context->xp = 0;
130 event_context->yp = 0;
131 event_context->tolerance = 0;
132 event_context->within_tolerance = false;
134 tc->imc = NULL;
136 tc->text = NULL;
137 tc->pdoc = Geom::Point(0, 0);
138 new (&tc->text_sel_start) Inkscape::Text::Layout::iterator();
139 new (&tc->text_sel_end) Inkscape::Text::Layout::iterator();
140 new (&tc->text_selection_quads) std::vector<SPCanvasItem*>();
142 tc->unimode = false;
144 tc->cursor = NULL;
145 tc->indicator = NULL;
146 tc->frame = NULL;
147 tc->grabbed = NULL;
148 tc->timeout = 0;
149 tc->show = FALSE;
150 tc->phase = 0;
151 tc->nascent_object = 0;
152 tc->over_text = 0;
153 tc->dragging = 0;
154 tc->creating = 0;
156 new (&tc->sel_changed_connection) sigc::connection();
157 new (&tc->sel_modified_connection) sigc::connection();
158 new (&tc->style_set_connection) sigc::connection();
159 new (&tc->style_query_connection) sigc::connection();
160 }
162 static void
163 sp_text_context_dispose(GObject *obj)
164 {
165 SPTextContext *tc = SP_TEXT_CONTEXT(obj);
166 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
167 tc->style_query_connection.~connection();
168 tc->style_set_connection.~connection();
169 tc->sel_changed_connection.~connection();
170 tc->sel_modified_connection.~connection();
172 delete ec->shape_editor;
173 ec->shape_editor = NULL;
175 tc->text_sel_end.~iterator();
176 tc->text_sel_start.~iterator();
177 tc->text_selection_quads.~vector();
178 if (G_OBJECT_CLASS(parent_class)->dispose) {
179 G_OBJECT_CLASS(parent_class)->dispose(obj);
180 }
181 if (tc->grabbed) {
182 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
183 tc->grabbed = NULL;
184 }
186 Inkscape::Rubberband::get(ec->desktop)->stop();
187 }
189 static void
190 sp_text_context_setup(SPEventContext *ec)
191 {
192 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
193 SPDesktop *desktop = ec->desktop;
194 GtkSettings* settings = gtk_settings_get_default();
195 gint timeout = 0;
196 g_object_get( settings, "gtk-cursor-blink-time", &timeout, NULL );
197 if (timeout < 0) {
198 timeout = 200;
199 } else {
200 timeout /= 2;
201 }
203 tc->cursor = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLLINE, NULL);
204 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), 100, 0, 100, 100);
205 sp_ctrlline_set_rgba32(SP_CTRLLINE(tc->cursor), 0x000000ff);
206 sp_canvas_item_hide(tc->cursor);
208 tc->indicator = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLRECT, NULL);
209 SP_CTRLRECT(tc->indicator)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100)));
210 SP_CTRLRECT(tc->indicator)->setColor(0x0000ff7f, false, 0);
211 sp_canvas_item_hide(tc->indicator);
213 tc->frame = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLRECT, NULL);
214 SP_CTRLRECT(tc->frame)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100)));
215 SP_CTRLRECT(tc->frame)->setColor(0x0000ff7f, false, 0);
216 sp_canvas_item_hide(tc->frame);
218 tc->timeout = gtk_timeout_add(timeout, (GtkFunction) sp_text_context_timeout, ec);
220 tc->imc = gtk_im_multicontext_new();
221 if (tc->imc) {
222 GtkWidget *canvas = GTK_WIDGET(sp_desktop_canvas(desktop));
224 /* im preedit handling is very broken in inkscape for
225 * multi-byte characters. See bug 1086769.
226 * We need to let the IM handle the preediting, and
227 * just take in the characters when they're finished being
228 * entered.
229 */
230 gtk_im_context_set_use_preedit(tc->imc, FALSE);
231 gtk_im_context_set_client_window(tc->imc, canvas->window);
233 g_signal_connect(G_OBJECT(canvas), "focus_in_event", G_CALLBACK(sptc_focus_in), tc);
234 g_signal_connect(G_OBJECT(canvas), "focus_out_event", G_CALLBACK(sptc_focus_out), tc);
235 g_signal_connect(G_OBJECT(tc->imc), "commit", G_CALLBACK(sptc_commit), tc);
237 if (GTK_WIDGET_HAS_FOCUS(canvas)) {
238 sptc_focus_in(canvas, NULL, tc);
239 }
240 }
242 if (((SPEventContextClass *) parent_class)->setup)
243 ((SPEventContextClass *) parent_class)->setup(ec);
245 ec->shape_editor = new ShapeEditor(ec->desktop);
247 SPItem *item = sp_desktop_selection(ec->desktop)->singleItem();
248 if (item && SP_IS_FLOWTEXT (item) && SP_FLOWTEXT(item)->has_internal_frame()) {
249 ec->shape_editor->set_item(item, SH_KNOTHOLDER);
250 }
252 tc->sel_changed_connection = sp_desktop_selection(desktop)->connectChanged(
253 sigc::bind(sigc::ptr_fun(&sp_text_context_selection_changed), tc)
254 );
255 tc->sel_modified_connection = sp_desktop_selection(desktop)->connectModified(
256 sigc::bind(sigc::ptr_fun(&sp_text_context_selection_modified), tc)
257 );
258 tc->style_set_connection = desktop->connectSetStyle(
259 sigc::bind(sigc::ptr_fun(&sp_text_context_style_set), tc)
260 );
261 tc->style_query_connection = desktop->connectQueryStyle(
262 sigc::bind(sigc::ptr_fun(&sp_text_context_style_query), tc)
263 );
265 sp_text_context_selection_changed(sp_desktop_selection(desktop), tc);
267 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
268 if (prefs->getBool("/tools/text/selcue")) {
269 ec->enableSelectionCue();
270 }
271 if (prefs->getBool("/tools/text/gradientdrag")) {
272 ec->enableGrDrag();
273 }
274 }
276 static void
277 sp_text_context_finish(SPEventContext *ec)
278 {
279 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
281 if (ec->desktop) {
282 sp_signal_disconnect_by_data(sp_desktop_canvas(ec->desktop), tc);
283 }
285 ec->enableGrDrag(false);
287 tc->style_set_connection.disconnect();
288 tc->style_query_connection.disconnect();
289 tc->sel_changed_connection.disconnect();
290 tc->sel_modified_connection.disconnect();
292 sp_text_context_forget_text(SP_TEXT_CONTEXT(ec));
294 if (tc->imc) {
295 g_object_unref(G_OBJECT(tc->imc));
296 tc->imc = NULL;
297 }
299 if (tc->timeout) {
300 gtk_timeout_remove(tc->timeout);
301 tc->timeout = 0;
302 }
304 if (tc->cursor) {
305 gtk_object_destroy(GTK_OBJECT(tc->cursor));
306 tc->cursor = NULL;
307 }
309 if (tc->indicator) {
310 gtk_object_destroy(GTK_OBJECT(tc->indicator));
311 tc->indicator = NULL;
312 }
314 if (tc->frame) {
315 gtk_object_destroy(GTK_OBJECT(tc->frame));
316 tc->frame = NULL;
317 }
319 for (std::vector<SPCanvasItem*>::iterator it = tc->text_selection_quads.begin() ;
320 it != tc->text_selection_quads.end() ; ++it) {
321 sp_canvas_item_hide(*it);
322 gtk_object_destroy(*it);
323 }
324 tc->text_selection_quads.clear();
325 }
328 static gint
329 sp_text_context_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event)
330 {
331 SPTextContext *tc = SP_TEXT_CONTEXT(event_context);
332 SPDesktop *desktop = event_context->desktop;
333 SPItem *item_ungrouped;
335 gint ret = FALSE;
337 sp_text_context_validate_cursor_iterators(tc);
339 switch (event->type) {
340 case GDK_BUTTON_PRESS:
341 if (event->button.button == 1 && !event_context->space_panning) {
342 // find out clicked item, disregarding groups
343 item_ungrouped = desktop->item_at_point(Geom::Point(event->button.x, event->button.y), TRUE);
344 if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
345 sp_desktop_selection(desktop)->set(item_ungrouped);
346 if (tc->text) {
347 // find out click point in document coordinates
348 Geom::Point p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
349 // set the cursor closest to that point
350 tc->text_sel_start = tc->text_sel_end = sp_te_get_position_by_coords(tc->text, p);
351 // update display
352 sp_text_context_update_cursor(tc);
353 sp_text_context_update_text_selection(tc);
354 tc->dragging = 1;
355 sp_event_context_snap_window_open(event_context);
356 }
357 ret = TRUE;
358 }
359 }
360 break;
361 case GDK_2BUTTON_PRESS:
362 if (event->button.button == 1 && tc->text) {
363 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
364 if (layout) {
365 if (!layout->isStartOfWord(tc->text_sel_start))
366 tc->text_sel_start.prevStartOfWord();
367 if (!layout->isEndOfWord(tc->text_sel_end))
368 tc->text_sel_end.nextEndOfWord();
369 sp_text_context_update_cursor(tc);
370 sp_text_context_update_text_selection(tc);
371 tc->dragging = 2;
372 sp_event_context_snap_window_open(event_context);
373 ret = TRUE;
374 }
375 }
376 break;
377 case GDK_3BUTTON_PRESS:
378 if (event->button.button == 1 && tc->text) {
379 tc->text_sel_start.thisStartOfLine();
380 tc->text_sel_end.thisEndOfLine();
381 sp_text_context_update_cursor(tc);
382 sp_text_context_update_text_selection(tc);
383 tc->dragging = 3;
384 sp_event_context_snap_window_open(event_context);
385 ret = TRUE;
386 }
387 break;
388 case GDK_BUTTON_RELEASE:
389 if (event->button.button == 1 && tc->dragging && !event_context->space_panning) {
390 tc->dragging = 0;
391 sp_event_context_snap_window_closed(event_context, false); //button release will also occur on a double-click; in that case suppress warnings
392 ret = TRUE;
393 }
394 break;
395 case GDK_MOTION_NOTIFY:
396 if (event->motion.state & GDK_BUTTON1_MASK && tc->dragging && !event_context->space_panning) {
397 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
398 if (!layout) break;
399 // find out click point in document coordinates
400 Geom::Point p = desktop->w2d(Geom::Point(event->button.x, event->button.y));
401 // set the cursor closest to that point
402 Inkscape::Text::Layout::iterator new_end = sp_te_get_position_by_coords(tc->text, p);
403 if (tc->dragging == 2) {
404 // double-click dragging: go by word
405 if (new_end < tc->text_sel_start) {
406 if (!layout->isStartOfWord(new_end))
407 new_end.prevStartOfWord();
408 } else
409 if (!layout->isEndOfWord(new_end))
410 new_end.nextEndOfWord();
411 } else if (tc->dragging == 3) {
412 // triple-click dragging: go by line
413 if (new_end < tc->text_sel_start)
414 new_end.thisStartOfLine();
415 else
416 new_end.thisEndOfLine();
417 }
418 // update display
419 if (tc->text_sel_end != new_end) {
420 tc->text_sel_end = new_end;
421 sp_text_context_update_cursor(tc);
422 sp_text_context_update_text_selection(tc);
423 }
424 gobble_motion_events(GDK_BUTTON1_MASK);
425 ret = TRUE;
426 break;
427 }
428 // find out item under mouse, disregarding groups
429 item_ungrouped = desktop->item_at_point(Geom::Point(event->button.x, event->button.y), TRUE);
430 if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
431 sp_canvas_item_show(tc->indicator);
432 Geom::OptRect ibbox = sp_item_bbox_desktop(item_ungrouped);
433 if (ibbox) {
434 SP_CTRLRECT(tc->indicator)->setRectangle(*ibbox);
435 }
437 event_context->cursor_shape = cursor_text_insert_xpm;
438 event_context->hot_x = 7;
439 event_context->hot_y = 10;
440 sp_event_context_update_cursor(event_context);
441 sp_text_context_update_text_selection(tc);
443 if (SP_IS_TEXT (item_ungrouped)) {
444 desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text."));
445 } else {
446 desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to edit the flowed text, <b>drag</b> to select part of the text."));
447 }
449 tc->over_text = true;
451 ret = TRUE;
452 }
453 break;
454 default:
455 break;
456 }
458 if (!ret) {
459 if (((SPEventContextClass *) parent_class)->item_handler)
460 ret = ((SPEventContextClass *) parent_class)->item_handler(event_context, item, event);
461 }
463 return ret;
464 }
466 static void
467 sp_text_context_setup_text(SPTextContext *tc)
468 {
469 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
471 /* Create <text> */
472 Inkscape::XML::Document *xml_doc = sp_document_repr_doc(SP_EVENT_CONTEXT_DESKTOP(ec)->doc());
473 Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text");
474 rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
476 /* Set style */
477 sp_desktop_apply_style_tool(SP_EVENT_CONTEXT_DESKTOP(ec), rtext, "/tools/text", true);
479 sp_repr_set_svg_double(rtext, "x", tc->pdoc[Geom::X]);
480 sp_repr_set_svg_double(rtext, "y", tc->pdoc[Geom::Y]);
482 /* Create <tspan> */
483 Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
484 rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
485 rtext->addChild(rtspan, NULL);
486 Inkscape::GC::release(rtspan);
488 /* Create TEXT */
489 Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
490 rtspan->addChild(rstring, NULL);
491 Inkscape::GC::release(rstring);
492 SPItem *text_item = SP_ITEM(ec->desktop->currentLayer()->appendChildRepr(rtext));
493 /* fixme: Is selection::changed really immediate? */
494 /* yes, it's immediate .. why does it matter? */
495 sp_desktop_selection(ec->desktop)->set(text_item);
496 Inkscape::GC::release(rtext);
497 text_item->transform = sp_item_i2doc_affine(SP_ITEM(ec->desktop->currentLayer())).inverse();
499 text_item->updateRepr();
500 sp_document_done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT,
501 _("Create text"));
502 }
504 /**
505 * Insert the character indicated by tc.uni to replace the current selection,
506 * and reset tc.uni/tc.unipos to empty string.
507 *
508 * \pre tc.uni/tc.unipos non-empty.
509 */
510 static void
511 insert_uni_char(SPTextContext *const tc)
512 {
513 g_return_if_fail(tc->unipos
514 && tc->unipos < sizeof(tc->uni)
515 && tc->uni[tc->unipos] == '\0');
516 unsigned int uv;
517 sscanf(tc->uni, "%x", &uv);
518 tc->unipos = 0;
519 tc->uni[tc->unipos] = '\0';
521 if ( !g_unichar_isprint(static_cast<gunichar>(uv))
522 && !(g_unichar_validate(static_cast<gunichar>(uv)) && (g_unichar_type(static_cast<gunichar>(uv)) == G_UNICODE_PRIVATE_USE) ) ) {
523 // This may be due to bad input, so it goes to statusbar.
524 tc->desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE,
525 _("Non-printable character"));
526 } else {
527 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
528 sp_text_context_setup_text(tc);
529 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
530 }
532 gchar u[10];
533 guint const len = g_unichar_to_utf8(uv, u);
534 u[len] = '\0';
536 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, u);
537 sp_text_context_update_cursor(tc);
538 sp_text_context_update_text_selection(tc);
539 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_DIALOG_TRANSFORM,
540 _("Insert Unicode character"));
541 }
542 }
544 static void
545 hex_to_printable_utf8_buf(char const *const hex, char *utf8)
546 {
547 unsigned int uv;
548 sscanf(hex, "%x", &uv);
549 if (!g_unichar_isprint((gunichar) uv)) {
550 uv = 0xfffd;
551 }
552 guint const len = g_unichar_to_utf8(uv, utf8);
553 utf8[len] = '\0';
554 }
556 static void
557 show_curr_uni_char(SPTextContext *const tc)
558 {
559 g_return_if_fail(tc->unipos < sizeof(tc->uni)
560 && tc->uni[tc->unipos] == '\0');
561 if (tc->unipos) {
562 char utf8[10];
563 hex_to_printable_utf8_buf(tc->uni, utf8);
565 /* Status bar messages are in pango markup, so we need xml escaping. */
566 if (utf8[1] == '\0') {
567 switch(utf8[0]) {
568 case '<': strcpy(utf8, "<"); break;
569 case '>': strcpy(utf8, ">"); break;
570 case '&': strcpy(utf8, "&"); break;
571 default: break;
572 }
573 }
574 tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
575 _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8);
576 } else {
577 tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
578 }
579 }
581 static gint
582 sp_text_context_root_handler(SPEventContext *const event_context, GdkEvent *const event)
583 {
584 SPTextContext *const tc = SP_TEXT_CONTEXT(event_context);
586 SPDesktop *desktop = event_context->desktop;
588 sp_canvas_item_hide(tc->indicator);
590 sp_text_context_validate_cursor_iterators(tc);
592 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
593 event_context->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
595 switch (event->type) {
596 case GDK_BUTTON_PRESS:
597 if (event->button.button == 1 && !event_context->space_panning) {
599 if (Inkscape::have_viable_layer(desktop, desktop->messageStack()) == false) {
600 return TRUE;
601 }
603 // save drag origin
604 event_context->xp = (gint) event->button.x;
605 event_context->yp = (gint) event->button.y;
606 event_context->within_tolerance = true;
608 Geom::Point const button_pt(event->button.x, event->button.y);
609 tc->p0 = desktop->w2d(button_pt);
610 Inkscape::Rubberband::get(desktop)->start(desktop, tc->p0);
611 sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
612 GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK,
613 NULL, event->button.time);
614 tc->grabbed = SP_CANVAS_ITEM(desktop->acetate);
615 tc->creating = 1;
617 /* Processed */
618 return TRUE;
619 }
620 break;
621 case GDK_MOTION_NOTIFY:
622 if (tc->over_text) {
623 tc->over_text = 0;
624 // update cursor and statusbar: we are not over a text object now
625 event_context->cursor_shape = cursor_text_xpm;
626 event_context->hot_x = 7;
627 event_context->hot_y = 7;
628 sp_event_context_update_cursor(event_context);
629 desktop->event_context->defaultMessageContext()->clear();
630 }
632 if (tc->creating && event->motion.state & GDK_BUTTON1_MASK && !event_context->space_panning) {
633 if ( event_context->within_tolerance
634 && ( abs( (gint) event->motion.x - event_context->xp ) < event_context->tolerance )
635 && ( abs( (gint) event->motion.y - event_context->yp ) < event_context->tolerance ) ) {
636 break; // do not drag if we're within tolerance from origin
637 }
638 // Once the user has moved farther than tolerance from the original location
639 // (indicating they intend to draw, not click), then always process the
640 // motion notify coordinates as given (no snapping back to origin)
641 event_context->within_tolerance = false;
643 Geom::Point const motion_pt(event->motion.x, event->motion.y);
644 Geom::Point const p = desktop->w2d(motion_pt);
646 Inkscape::Rubberband::get(desktop)->move(p);
647 gobble_motion_events(GDK_BUTTON1_MASK);
649 // status text
650 GString *xs = SP_PX_TO_METRIC_STRING(fabs((p - tc->p0)[Geom::X]), desktop->namedview->getDefaultMetric());
651 GString *ys = SP_PX_TO_METRIC_STRING(fabs((p - tc->p0)[Geom::Y]), desktop->namedview->getDefaultMetric());
652 event_context->_message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs->str, ys->str);
653 g_string_free(xs, FALSE);
654 g_string_free(ys, FALSE);
656 }
657 break;
658 case GDK_BUTTON_RELEASE:
659 if (event->button.button == 1 && !event_context->space_panning) {
661 if (tc->grabbed) {
662 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
663 tc->grabbed = NULL;
664 }
666 Inkscape::Rubberband::get(desktop)->stop();
668 if (tc->creating && event_context->within_tolerance) {
669 /* Button 1, set X & Y & new item */
670 sp_desktop_selection(desktop)->clear();
671 Geom::Point dtp = desktop->w2d(Geom::Point(event->button.x, event->button.y));
672 tc->pdoc = desktop->dt2doc(dtp);
674 tc->show = TRUE;
675 tc->phase = 1;
676 tc->nascent_object = 1; // new object was just created
678 /* Cursor */
679 sp_canvas_item_show(tc->cursor);
680 // Cursor height is defined by the new text object's font size; it needs to be set
681 // articifically here, for the text object does not exist yet:
682 double cursor_height = sp_desktop_get_font_size_tool(desktop);
683 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), dtp, dtp + Geom::Point(0, cursor_height));
684 event_context->_message_context->set(Inkscape::NORMAL_MESSAGE, _("Type text; <b>Enter</b> to start new line.")); // FIXME:: this is a copy of a string from _update_cursor below, do not desync
686 event_context->within_tolerance = false;
687 } else if (tc->creating) {
688 Geom::Point const button_pt(event->button.x, event->button.y);
689 Geom::Point p1 = desktop->w2d(button_pt);
690 double cursor_height = sp_desktop_get_font_size_tool(desktop);
691 if (fabs(p1[Geom::Y] - tc->p0[Geom::Y]) > cursor_height) {
692 // otherwise even one line won't fit; most probably a slip of hand (even if bigger than tolerance)
693 SPItem *ft = create_flowtext_with_internal_frame (desktop, tc->p0, p1);
694 /* Set style */
695 sp_desktop_apply_style_tool(desktop, SP_OBJECT_REPR(ft), "/tools/text", true);
696 sp_desktop_selection(desktop)->set(ft);
697 desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created."));
698 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
699 _("Create flowed text"));
700 } else {
701 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The frame is <b>too small</b> for the current font size. Flowed text not created."));
702 }
703 }
704 tc->creating = false;
705 return TRUE;
706 }
707 break;
708 case GDK_KEY_PRESS: {
709 guint const group0_keyval = get_group0_keyval(&event->key);
711 if (group0_keyval == GDK_KP_Add ||
712 group0_keyval == GDK_KP_Subtract) {
713 if (!(event->key.state & GDK_MOD2_MASK)) // mod2 is NumLock; if on, type +/- keys
714 break; // otherwise pass on keypad +/- so they can zoom
715 }
717 if ((tc->text) || (tc->nascent_object)) {
718 // there is an active text object in this context, or a new object was just created
720 if (tc->unimode || !tc->imc
721 || (MOD__CTRL && MOD__SHIFT) // input methods tend to steal this for unimode,
722 // but we have our own so make sure they don't swallow it
723 || !gtk_im_context_filter_keypress(tc->imc, (GdkEventKey*) event)) {
724 //IM did not consume the key, or we're in unimode
726 if (!MOD__CTRL_ONLY && tc->unimode) {
727 /* TODO: ISO 14755 (section 3 Definitions) says that we should also
728 accept the first 6 characters of alphabets other than the latin
729 alphabet "if the Latin alphabet is not used". The below is also
730 reasonable (viz. hope that the user's keyboard includes latin
731 characters and force latin interpretation -- just as we do for our
732 keyboard shortcuts), but differs from the ISO 14755
733 recommendation. */
734 switch (group0_keyval) {
735 case GDK_space:
736 case GDK_KP_Space: {
737 if (tc->unipos) {
738 insert_uni_char(tc);
739 }
740 /* Stay in unimode. */
741 show_curr_uni_char(tc);
742 return TRUE;
743 }
745 case GDK_BackSpace: {
746 g_return_val_if_fail(tc->unipos < sizeof(tc->uni), TRUE);
747 if (tc->unipos) {
748 tc->uni[--tc->unipos] = '\0';
749 }
750 show_curr_uni_char(tc);
751 return TRUE;
752 }
754 case GDK_Return:
755 case GDK_KP_Enter: {
756 if (tc->unipos) {
757 insert_uni_char(tc);
758 }
759 /* Exit unimode. */
760 tc->unimode = false;
761 event_context->defaultMessageContext()->clear();
762 return TRUE;
763 }
765 case GDK_Escape: {
766 // Cancel unimode.
767 tc->unimode = false;
768 gtk_im_context_reset(tc->imc);
769 event_context->defaultMessageContext()->clear();
770 return TRUE;
771 }
773 case GDK_Shift_L:
774 case GDK_Shift_R:
775 break;
777 default: {
778 if (g_ascii_isxdigit(group0_keyval)) {
779 g_return_val_if_fail(tc->unipos < sizeof(tc->uni) - 1, TRUE);
780 tc->uni[tc->unipos++] = group0_keyval;
781 tc->uni[tc->unipos] = '\0';
782 if (tc->unipos == 8) {
783 /* This behaviour is partly to allow us to continue to
784 use a fixed-length buffer for tc->uni. Reason for
785 choosing the number 8 is that it's the length of
786 ``canonical form'' mentioned in the ISO 14755 spec.
787 An advantage over choosing 6 is that it allows using
788 backspace for typos & misremembering when entering a
789 6-digit number. */
790 insert_uni_char(tc);
791 }
792 show_curr_uni_char(tc);
793 return TRUE;
794 } else {
795 /* The intent is to ignore but consume characters that could be
796 typos for hex digits. Gtk seems to ignore & consume all
797 non-hex-digits, and we do similar here. Though note that some
798 shortcuts (like keypad +/- for zoom) get processed before
799 reaching this code. */
800 return TRUE;
801 }
802 }
803 }
804 }
806 Inkscape::Text::Layout::iterator old_start = tc->text_sel_start;
807 Inkscape::Text::Layout::iterator old_end = tc->text_sel_end;
808 bool cursor_moved = false;
809 int screenlines = 1;
810 if (tc->text) {
811 double spacing = sp_te_get_average_linespacing(tc->text);
812 Geom::Rect const d = desktop->get_display_area();
813 screenlines = (int) floor(fabs(d.min()[Geom::Y] - d.max()[Geom::Y])/spacing) - 1;
814 if (screenlines <= 0)
815 screenlines = 1;
816 }
818 /* Neither unimode nor IM consumed key; process text tool shortcuts */
819 switch (group0_keyval) {
820 case GDK_x:
821 case GDK_X:
822 if (MOD__ALT_ONLY) {
823 desktop->setToolboxFocusTo ("altx-text");
824 return TRUE;
825 }
826 break;
827 case GDK_space:
828 if (MOD__CTRL_ONLY) {
829 /* No-break space */
830 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
831 sp_text_context_setup_text(tc);
832 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
833 }
834 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, "\302\240");
835 sp_text_context_update_cursor(tc);
836 sp_text_context_update_text_selection(tc);
837 desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No-break space"));
838 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
839 _("Insert no-break space"));
840 return TRUE;
841 }
842 break;
843 case GDK_U:
844 case GDK_u:
845 if (MOD__CTRL_ONLY || (MOD__CTRL && MOD__SHIFT)) {
846 if (tc->unimode) {
847 tc->unimode = false;
848 event_context->defaultMessageContext()->clear();
849 } else {
850 tc->unimode = true;
851 tc->unipos = 0;
852 event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
853 }
854 if (tc->imc) {
855 gtk_im_context_reset(tc->imc);
856 }
857 return TRUE;
858 }
859 break;
860 case GDK_B:
861 case GDK_b:
862 if (MOD__CTRL_ONLY && tc->text) {
863 SPStyle const *style = sp_te_style_at_position(tc->text, std::min(tc->text_sel_start, tc->text_sel_end));
864 SPCSSAttr *css = sp_repr_css_attr_new();
865 if (style->font_weight.computed == SP_CSS_FONT_WEIGHT_NORMAL
866 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_100
867 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_200
868 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_300
869 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_400)
870 sp_repr_css_set_property(css, "font-weight", "bold");
871 else
872 sp_repr_css_set_property(css, "font-weight", "normal");
873 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
874 sp_repr_css_attr_unref(css);
875 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
876 _("Make bold"));
877 sp_text_context_update_cursor(tc);
878 sp_text_context_update_text_selection(tc);
879 return TRUE;
880 }
881 break;
882 case GDK_I:
883 case GDK_i:
884 if (MOD__CTRL_ONLY && tc->text) {
885 SPStyle const *style = sp_te_style_at_position(tc->text, std::min(tc->text_sel_start, tc->text_sel_end));
886 SPCSSAttr *css = sp_repr_css_attr_new();
887 if (style->font_style.computed == SP_CSS_FONT_STYLE_NORMAL)
888 sp_repr_css_set_property(css, "font-style", "italic");
889 else
890 sp_repr_css_set_property(css, "font-style", "normal");
891 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
892 sp_repr_css_attr_unref(css);
893 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
894 _("Make italic"));
895 sp_text_context_update_cursor(tc);
896 sp_text_context_update_text_selection(tc);
897 return TRUE;
898 }
899 break;
901 case GDK_A:
902 case GDK_a:
903 if (MOD__CTRL_ONLY && tc->text) {
904 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
905 if (layout) {
906 tc->text_sel_start = layout->begin();
907 tc->text_sel_end = layout->end();
908 sp_text_context_update_cursor(tc);
909 sp_text_context_update_text_selection(tc);
910 return TRUE;
911 }
912 }
913 break;
915 case GDK_Return:
916 case GDK_KP_Enter:
917 {
918 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
919 sp_text_context_setup_text(tc);
920 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
921 }
923 iterator_pair enter_pair;
924 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, enter_pair);
925 (void)success; // TODO cleanup
926 tc->text_sel_start = tc->text_sel_end = enter_pair.first;
928 tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
930 sp_text_context_update_cursor(tc);
931 sp_text_context_update_text_selection(tc);
932 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
933 _("New line"));
934 return TRUE;
935 }
936 case GDK_BackSpace:
937 if (tc->text) { // if nascent_object, do nothing, but return TRUE; same for all other delete and move keys
939 bool noSelection = false;
941 if (tc->text_sel_start == tc->text_sel_end) {
942 tc->text_sel_start.prevCursorPosition();
943 noSelection = true;
944 }
946 iterator_pair bspace_pair;
947 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, bspace_pair);
949 if (noSelection) {
950 if (success) {
951 tc->text_sel_start = tc->text_sel_end = bspace_pair.first;
952 } else { // nothing deleted
953 tc->text_sel_start = tc->text_sel_end = bspace_pair.second;
954 }
955 } else {
956 if (success) {
957 tc->text_sel_start = tc->text_sel_end = bspace_pair.first;
958 } else { // nothing deleted
959 tc->text_sel_start = bspace_pair.first;
960 tc->text_sel_end = bspace_pair.second;
961 }
962 }
964 sp_text_context_update_cursor(tc);
965 sp_text_context_update_text_selection(tc);
966 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
967 _("Backspace"));
968 }
969 return TRUE;
970 case GDK_Delete:
971 case GDK_KP_Delete:
972 if (tc->text) {
973 bool noSelection = false;
975 if (tc->text_sel_start == tc->text_sel_end) {
976 tc->text_sel_end.nextCursorPosition();
977 noSelection = true;
978 }
980 iterator_pair del_pair;
981 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, del_pair);
983 if (noSelection) {
984 tc->text_sel_start = tc->text_sel_end = del_pair.first;
985 } else {
986 if (success) {
987 tc->text_sel_start = tc->text_sel_end = del_pair.first;
988 } else { // nothing deleted
989 tc->text_sel_start = del_pair.first;
990 tc->text_sel_end = del_pair.second;
991 }
992 }
995 sp_text_context_update_cursor(tc);
996 sp_text_context_update_text_selection(tc);
997 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
998 _("Delete"));
999 }
1000 return TRUE;
1001 case GDK_Left:
1002 case GDK_KP_Left:
1003 case GDK_KP_4:
1004 if (tc->text) {
1005 if (MOD__ALT) {
1006 gint mul = 1 + gobble_key_events(
1007 get_group0_keyval(&event->key), 0); // with any mask
1008 if (MOD__SHIFT)
1009 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(mul*-10, 0));
1010 else
1011 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(mul*-1, 0));
1012 sp_text_context_update_cursor(tc);
1013 sp_text_context_update_text_selection(tc);
1014 sp_document_maybe_done(sp_desktop_document(desktop), "kern:left", SP_VERB_CONTEXT_TEXT,
1015 _("Kern to the left"));
1016 } else {
1017 if (MOD__CTRL)
1018 tc->text_sel_end.cursorLeftWithControl();
1019 else
1020 tc->text_sel_end.cursorLeft();
1021 cursor_moved = true;
1022 break;
1023 }
1024 }
1025 return TRUE;
1026 case GDK_Right:
1027 case GDK_KP_Right:
1028 case GDK_KP_6:
1029 if (tc->text) {
1030 if (MOD__ALT) {
1031 gint mul = 1 + gobble_key_events(
1032 get_group0_keyval(&event->key), 0); // with any mask
1033 if (MOD__SHIFT)
1034 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(mul*10, 0));
1035 else
1036 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(mul*1, 0));
1037 sp_text_context_update_cursor(tc);
1038 sp_text_context_update_text_selection(tc);
1039 sp_document_maybe_done(sp_desktop_document(desktop), "kern:right", SP_VERB_CONTEXT_TEXT,
1040 _("Kern to the right"));
1041 } else {
1042 if (MOD__CTRL)
1043 tc->text_sel_end.cursorRightWithControl();
1044 else
1045 tc->text_sel_end.cursorRight();
1046 cursor_moved = true;
1047 break;
1048 }
1049 }
1050 return TRUE;
1051 case GDK_Up:
1052 case GDK_KP_Up:
1053 case GDK_KP_8:
1054 if (tc->text) {
1055 if (MOD__ALT) {
1056 gint mul = 1 + gobble_key_events(
1057 get_group0_keyval(&event->key), 0); // with any mask
1058 if (MOD__SHIFT)
1059 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(0, mul*-10));
1060 else
1061 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(0, mul*-1));
1062 sp_text_context_update_cursor(tc);
1063 sp_text_context_update_text_selection(tc);
1064 sp_document_maybe_done(sp_desktop_document(desktop), "kern:up", SP_VERB_CONTEXT_TEXT,
1065 _("Kern up"));
1067 } else {
1068 if (MOD__CTRL)
1069 tc->text_sel_end.cursorUpWithControl();
1070 else
1071 tc->text_sel_end.cursorUp();
1072 cursor_moved = true;
1073 break;
1074 }
1075 }
1076 return TRUE;
1077 case GDK_Down:
1078 case GDK_KP_Down:
1079 case GDK_KP_2:
1080 if (tc->text) {
1081 if (MOD__ALT) {
1082 gint mul = 1 + gobble_key_events(
1083 get_group0_keyval(&event->key), 0); // with any mask
1084 if (MOD__SHIFT)
1085 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(0, mul*10));
1086 else
1087 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, Geom::Point(0, mul*1));
1088 sp_text_context_update_cursor(tc);
1089 sp_text_context_update_text_selection(tc);
1090 sp_document_maybe_done(sp_desktop_document(desktop), "kern:down", SP_VERB_CONTEXT_TEXT,
1091 _("Kern down"));
1093 } else {
1094 if (MOD__CTRL)
1095 tc->text_sel_end.cursorDownWithControl();
1096 else
1097 tc->text_sel_end.cursorDown();
1098 cursor_moved = true;
1099 break;
1100 }
1101 }
1102 return TRUE;
1103 case GDK_Home:
1104 case GDK_KP_Home:
1105 if (tc->text) {
1106 if (MOD__CTRL)
1107 tc->text_sel_end.thisStartOfShape();
1108 else
1109 tc->text_sel_end.thisStartOfLine();
1110 cursor_moved = true;
1111 break;
1112 }
1113 return TRUE;
1114 case GDK_End:
1115 case GDK_KP_End:
1116 if (tc->text) {
1117 if (MOD__CTRL)
1118 tc->text_sel_end.nextStartOfShape();
1119 else
1120 tc->text_sel_end.thisEndOfLine();
1121 cursor_moved = true;
1122 break;
1123 }
1124 return TRUE;
1125 case GDK_Page_Down:
1126 case GDK_KP_Page_Down:
1127 if (tc->text) {
1128 tc->text_sel_end.cursorDown(screenlines);
1129 cursor_moved = true;
1130 break;
1131 }
1132 return TRUE;
1133 case GDK_Page_Up:
1134 case GDK_KP_Page_Up:
1135 if (tc->text) {
1136 tc->text_sel_end.cursorUp(screenlines);
1137 cursor_moved = true;
1138 break;
1139 }
1140 return TRUE;
1141 case GDK_Escape:
1142 if (tc->creating) {
1143 tc->creating = 0;
1144 if (tc->grabbed) {
1145 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
1146 tc->grabbed = NULL;
1147 }
1148 Inkscape::Rubberband::get(desktop)->stop();
1149 } else {
1150 sp_desktop_selection(desktop)->clear();
1151 }
1152 tc->nascent_object = FALSE;
1153 return TRUE;
1154 case GDK_bracketleft:
1155 if (tc->text) {
1156 if (MOD__ALT || MOD__CTRL) {
1157 if (MOD__ALT) {
1158 if (MOD__SHIFT) {
1159 // FIXME: alt+shift+[] does not work, don't know why
1160 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1161 } else {
1162 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1163 }
1164 } else {
1165 sp_te_adjust_rotation(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -90);
1166 }
1167 sp_document_maybe_done(sp_desktop_document(desktop), "textrot:ccw", SP_VERB_CONTEXT_TEXT,
1168 _("Rotate counterclockwise"));
1169 sp_text_context_update_cursor(tc);
1170 sp_text_context_update_text_selection(tc);
1171 return TRUE;
1172 }
1173 }
1174 break;
1175 case GDK_bracketright:
1176 if (tc->text) {
1177 if (MOD__ALT || MOD__CTRL) {
1178 if (MOD__ALT) {
1179 if (MOD__SHIFT) {
1180 // FIXME: alt+shift+[] does not work, don't know why
1181 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1182 } else {
1183 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1184 }
1185 } else {
1186 sp_te_adjust_rotation(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 90);
1187 }
1188 sp_document_maybe_done(sp_desktop_document(desktop), "textrot:cw", SP_VERB_CONTEXT_TEXT,
1189 _("Rotate clockwise"));
1190 sp_text_context_update_cursor(tc);
1191 sp_text_context_update_text_selection(tc);
1192 return TRUE;
1193 }
1194 }
1195 break;
1196 case GDK_less:
1197 case GDK_comma:
1198 if (tc->text) {
1199 if (MOD__ALT) {
1200 if (MOD__CTRL) {
1201 if (MOD__SHIFT)
1202 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1203 else
1204 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1205 sp_document_maybe_done(sp_desktop_document(desktop), "linespacing:dec", SP_VERB_CONTEXT_TEXT,
1206 _("Contract line spacing"));
1208 } else {
1209 if (MOD__SHIFT)
1210 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1211 else
1212 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1213 sp_document_maybe_done(sp_desktop_document(desktop), "letterspacing:dec", SP_VERB_CONTEXT_TEXT,
1214 _("Contract letter spacing"));
1216 }
1217 sp_text_context_update_cursor(tc);
1218 sp_text_context_update_text_selection(tc);
1219 return TRUE;
1220 }
1221 }
1222 break;
1223 case GDK_greater:
1224 case GDK_period:
1225 if (tc->text) {
1226 if (MOD__ALT) {
1227 if (MOD__CTRL) {
1228 if (MOD__SHIFT)
1229 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1230 else
1231 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1232 sp_document_maybe_done(sp_desktop_document(desktop), "linespacing:inc", SP_VERB_CONTEXT_TEXT,
1233 _("Expand line spacing"));
1235 } else {
1236 if (MOD__SHIFT)
1237 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1238 else
1239 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1240 sp_document_maybe_done(sp_desktop_document(desktop), "letterspacing:inc", SP_VERB_CONTEXT_TEXT,
1241 _("Expand letter spacing"));
1243 }
1244 sp_text_context_update_cursor(tc);
1245 sp_text_context_update_text_selection(tc);
1246 return TRUE;
1247 }
1248 }
1249 break;
1250 default:
1251 break;
1252 }
1254 if (cursor_moved) {
1255 if (!MOD__SHIFT)
1256 tc->text_sel_start = tc->text_sel_end;
1257 if (old_start != tc->text_sel_start || old_end != tc->text_sel_end) {
1258 sp_text_context_update_cursor(tc);
1259 sp_text_context_update_text_selection(tc);
1260 }
1261 return TRUE;
1262 }
1264 } else return TRUE; // return the "I took care of it" value if it was consumed by the IM
1265 } else { // do nothing if there's no object to type in - the key will be sent to parent context,
1266 // except up/down that are swallowed to prevent the zoom field from activation
1267 if ((group0_keyval == GDK_Up ||
1268 group0_keyval == GDK_Down ||
1269 group0_keyval == GDK_KP_Up ||
1270 group0_keyval == GDK_KP_Down )
1271 && !MOD__CTRL_ONLY) {
1272 return TRUE;
1273 } else if (group0_keyval == GDK_Escape) { // cancel rubberband
1274 if (tc->creating) {
1275 tc->creating = 0;
1276 if (tc->grabbed) {
1277 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
1278 tc->grabbed = NULL;
1279 }
1280 Inkscape::Rubberband::get(desktop)->stop();
1281 }
1282 } else if ((group0_keyval == GDK_x || group0_keyval == GDK_X) && MOD__ALT_ONLY) {
1283 desktop->setToolboxFocusTo ("altx-text");
1284 return TRUE;
1285 }
1286 }
1287 break;
1288 }
1290 case GDK_KEY_RELEASE:
1291 if (!tc->unimode && tc->imc && gtk_im_context_filter_keypress(tc->imc, (GdkEventKey*) event)) {
1292 return TRUE;
1293 }
1294 break;
1295 default:
1296 break;
1297 }
1299 // if nobody consumed it so far
1300 if (((SPEventContextClass *) parent_class)->root_handler) { // and there's a handler in parent context,
1301 return ((SPEventContextClass *) parent_class)->root_handler(event_context, event); // send event to parent
1302 } else {
1303 return FALSE; // return "I did nothing" value so that global shortcuts can be activated
1304 }
1305 }
1307 /**
1308 Attempts to paste system clipboard into the currently edited text, returns true on success
1309 */
1310 bool
1311 sp_text_paste_inline(SPEventContext *ec)
1312 {
1313 if (!SP_IS_TEXT_CONTEXT(ec))
1314 return false;
1316 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
1318 if ((tc->text) || (tc->nascent_object)) {
1319 // there is an active text object in this context, or a new object was just created
1321 Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
1322 Glib::ustring const clip_text = refClipboard->wait_for_text();
1324 if (!clip_text.empty()) {
1325 // Fix for 244940
1326 // The XML standard defines the following as valid characters
1327 // (Extensible Markup Language (XML) 1.0 (Fourth Edition) paragraph 2.2)
1328 // char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
1329 // Since what comes in off the paste buffer will go right into XML, clean
1330 // the text here.
1331 Glib::ustring text(clip_text);
1332 Glib::ustring::iterator itr = text.begin();
1333 gunichar paste_string_uchar;
1335 while(itr != text.end())
1336 {
1337 paste_string_uchar = *itr;
1339 // Make sure we don't have a control character. We should really check
1340 // for the whole range above... Add the rest of the invalid cases from
1341 // above if we find additional issues
1342 if(paste_string_uchar >= 0x00000020 ||
1343 paste_string_uchar == 0x00000009 ||
1344 paste_string_uchar == 0x0000000A ||
1345 paste_string_uchar == 0x0000000D) {
1346 itr++;
1347 } else {
1348 itr = text.erase(itr);
1349 }
1350 }
1352 if (!tc->text) { // create text if none (i.e. if nascent_object)
1353 sp_text_context_setup_text(tc);
1354 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
1355 }
1357 // using indices is slow in ustrings. Whatever.
1358 Glib::ustring::size_type begin = 0;
1359 for ( ; ; ) {
1360 Glib::ustring::size_type end = text.find('\n', begin);
1361 if (end == Glib::ustring::npos) {
1362 if (begin != text.length())
1363 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin).c_str());
1364 break;
1365 }
1366 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin, end - begin).c_str());
1367 tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
1368 begin = end + 1;
1369 }
1370 sp_document_done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT,
1371 _("Paste text"));
1373 return true;
1374 }
1375 } // FIXME: else create and select a new object under cursor!
1377 return false;
1378 }
1380 /**
1381 Gets the raw characters that comprise the currently selected text, converting line
1382 breaks into lf characters.
1383 */
1384 Glib::ustring
1385 sp_text_get_selected_text(SPEventContext const *ec)
1386 {
1387 if (!SP_IS_TEXT_CONTEXT(ec))
1388 return "";
1389 SPTextContext const *tc = SP_TEXT_CONTEXT(ec);
1390 if (tc->text == NULL)
1391 return "";
1393 return sp_te_get_string_multiline(tc->text, tc->text_sel_start, tc->text_sel_end);
1394 }
1396 SPCSSAttr *
1397 sp_text_get_style_at_cursor(SPEventContext const *ec)
1398 {
1399 if (!SP_IS_TEXT_CONTEXT(ec))
1400 return NULL;
1401 SPTextContext const *tc = SP_TEXT_CONTEXT(ec);
1402 if (tc->text == NULL)
1403 return NULL;
1405 SPObject const *obj = sp_te_object_at_position(tc->text, tc->text_sel_end);
1406 if (obj)
1407 return take_style_from_item((SPItem *) obj);
1408 return NULL;
1409 }
1411 /**
1412 Deletes the currently selected characters. Returns false if there is no
1413 text selection currently.
1414 */
1415 bool sp_text_delete_selection(SPEventContext *ec)
1416 {
1417 if (!SP_IS_TEXT_CONTEXT(ec))
1418 return false;
1419 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
1420 if (tc->text == NULL)
1421 return false;
1423 if (tc->text_sel_start == tc->text_sel_end)
1424 return false;
1426 iterator_pair pair;
1427 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, pair);
1430 if (success) {
1431 tc->text_sel_start = tc->text_sel_end = pair.first;
1432 } else { // nothing deleted
1433 tc->text_sel_start = pair.first;
1434 tc->text_sel_end = pair.second;
1435 }
1437 sp_text_context_update_cursor(tc);
1438 sp_text_context_update_text_selection(tc);
1440 return true;
1441 }
1443 /**
1444 * \param selection Should not be NULL.
1445 */
1446 static void
1447 sp_text_context_selection_changed(Inkscape::Selection *selection, SPTextContext *tc)
1448 {
1449 g_assert(selection != NULL);
1451 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
1453 ec->shape_editor->unset_item(SH_KNOTHOLDER);
1454 SPItem *item = selection->singleItem();
1455 if (item && SP_IS_FLOWTEXT (item) && SP_FLOWTEXT(item)->has_internal_frame()) {
1456 ec->shape_editor->set_item(item, SH_KNOTHOLDER);
1457 }
1459 if (tc->text && (item != tc->text)) {
1460 sp_text_context_forget_text(tc);
1461 }
1462 tc->text = NULL;
1464 if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
1465 tc->text = item;
1466 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1467 if (layout)
1468 tc->text_sel_start = tc->text_sel_end = layout->end();
1469 } else {
1470 tc->text = NULL;
1471 }
1473 // we update cursor without scrolling, because this position may not be final;
1474 // item_handler moves cusros to the point of click immediately
1475 sp_text_context_update_cursor(tc, false);
1476 sp_text_context_update_text_selection(tc);
1477 }
1479 static void
1480 sp_text_context_selection_modified(Inkscape::Selection */*selection*/, guint /*flags*/, SPTextContext *tc)
1481 {
1482 sp_text_context_update_cursor(tc);
1483 sp_text_context_update_text_selection(tc);
1484 }
1486 static bool
1487 sp_text_context_style_set(SPCSSAttr const *css, SPTextContext *tc)
1488 {
1489 if (tc->text == NULL)
1490 return false;
1491 if (tc->text_sel_start == tc->text_sel_end)
1492 return false; // will get picked up by the parent and applied to the whole text object
1494 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
1495 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT,
1496 _("Set text style"));
1497 sp_text_context_update_cursor(tc);
1498 sp_text_context_update_text_selection(tc);
1500 return true;
1501 }
1503 static int
1504 sp_text_context_style_query(SPStyle *style, int property, SPTextContext *tc)
1505 {
1506 if (tc->text == NULL)
1507 return QUERY_STYLE_NOTHING;
1508 const Inkscape::Text::Layout *layout = te_get_layout(tc->text);
1509 if (layout == NULL)
1510 return QUERY_STYLE_NOTHING;
1511 sp_text_context_validate_cursor_iterators(tc);
1513 GSList *styles_list = NULL;
1515 Inkscape::Text::Layout::iterator begin_it, end_it;
1516 if (tc->text_sel_start < tc->text_sel_end) {
1517 begin_it = tc->text_sel_start;
1518 end_it = tc->text_sel_end;
1519 } else {
1520 begin_it = tc->text_sel_end;
1521 end_it = tc->text_sel_start;
1522 }
1523 if (begin_it == end_it)
1524 if (!begin_it.prevCharacter())
1525 end_it.nextCharacter();
1526 for (Inkscape::Text::Layout::iterator it = begin_it ; it < end_it ; it.nextStartOfSpan()) {
1527 SPObject const *pos_obj = 0;
1528 void *rawptr = 0;
1529 layout->getSourceOfCharacter(it, &rawptr);
1530 if (!rawptr || !SP_IS_OBJECT(rawptr))
1531 continue;
1532 pos_obj = SP_OBJECT(rawptr);
1533 while (SP_IS_STRING(pos_obj) && SP_OBJECT_PARENT(pos_obj)) {
1534 pos_obj = SP_OBJECT_PARENT(pos_obj); // SPStrings don't have style
1535 }
1536 styles_list = g_slist_prepend(styles_list, (gpointer)pos_obj);
1537 }
1539 int result = sp_desktop_query_style_from_list (styles_list, style, property);
1541 g_slist_free(styles_list);
1542 return result;
1543 }
1545 static void
1546 sp_text_context_validate_cursor_iterators(SPTextContext *tc)
1547 {
1548 if (tc->text == NULL)
1549 return;
1550 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1551 if (layout) { // undo can change the text length without us knowing it
1552 layout->validateIterator(&tc->text_sel_start);
1553 layout->validateIterator(&tc->text_sel_end);
1554 }
1555 }
1557 static void
1558 sp_text_context_update_cursor(SPTextContext *tc, bool scroll_to_see)
1559 {
1560 GdkRectangle im_cursor = { 0, 0, 1, 1 };
1562 // due to interruptible display, tc may already be destroyed during a display update before
1563 // the cursor update (can't do both atomically, alas)
1564 if (!tc->desktop) return;
1566 if (tc->text) {
1567 Geom::Point p0, p1;
1568 sp_te_get_cursor_coords(tc->text, tc->text_sel_end, p0, p1);
1569 Geom::Point const d0 = p0 * sp_item_i2d_affine(SP_ITEM(tc->text));
1570 Geom::Point const d1 = p1 * sp_item_i2d_affine(SP_ITEM(tc->text));
1572 // scroll to show cursor
1573 if (scroll_to_see) {
1574 Geom::Point const center = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().midpoint();
1575 if (Geom::L2(d0 - center) > Geom::L2(d1 - center))
1576 // unlike mouse moves, here we must scroll all the way at first shot, so we override the autoscrollspeed
1577 SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d0, 1.0);
1578 else
1579 SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d1, 1.0);
1580 }
1582 sp_canvas_item_show(tc->cursor);
1583 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), d0, d1);
1585 /* fixme: ... need another transformation to get canvas widget coordinate space? */
1586 im_cursor.x = (int) floor(d0[Geom::X]);
1587 im_cursor.y = (int) floor(d0[Geom::Y]);
1588 im_cursor.width = (int) floor(d1[Geom::X]) - im_cursor.x;
1589 im_cursor.height = (int) floor(d1[Geom::Y]) - im_cursor.y;
1591 tc->show = TRUE;
1592 tc->phase = 1;
1594 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1595 int const nChars = layout->iteratorToCharIndex(layout->end());
1596 if (SP_IS_FLOWTEXT(tc->text)) {
1597 SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (NULL); // first frame only
1598 if (frame) {
1599 sp_canvas_item_show(tc->frame);
1600 Geom::OptRect frame_bbox = sp_item_bbox_desktop(frame);
1601 if (frame_bbox) {
1602 SP_CTRLRECT(tc->frame)->setRectangle(*frame_bbox);
1603 }
1604 }
1605 SP_EVENT_CONTEXT(tc)->_message_context->setF(Inkscape::NORMAL_MESSAGE, _("Type or edit flowed text (%d characters); <b>Enter</b> to start new paragraph."), nChars);
1606 } else {
1607 SP_EVENT_CONTEXT(tc)->_message_context->setF(Inkscape::NORMAL_MESSAGE, _("Type or edit text (%d characters); <b>Enter</b> to start new line."), nChars);
1608 }
1610 } else {
1611 sp_canvas_item_hide(tc->cursor);
1612 sp_canvas_item_hide(tc->frame);
1613 tc->show = FALSE;
1614 if (!tc->nascent_object) {
1615 SP_EVENT_CONTEXT(tc)->_message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to select or create text, <b>drag</b> to create flowed text; then type.")); // FIXME: this is a copy of string from tools-switch, do not desync
1616 }
1617 }
1619 if (tc->imc) {
1620 gtk_im_context_set_cursor_location(tc->imc, &im_cursor);
1621 }
1622 SP_EVENT_CONTEXT(tc)->desktop->emitToolSubselectionChanged((gpointer)tc);
1623 }
1625 static void sp_text_context_update_text_selection(SPTextContext *tc)
1626 {
1627 // due to interruptible display, tc may already be destroyed during a display update before
1628 // the selection update (can't do both atomically, alas)
1629 if (!tc->desktop) return;
1631 for (std::vector<SPCanvasItem*>::iterator it = tc->text_selection_quads.begin() ; it != tc->text_selection_quads.end() ; it++) {
1632 sp_canvas_item_hide(*it);
1633 gtk_object_destroy(*it);
1634 }
1635 tc->text_selection_quads.clear();
1637 std::vector<Geom::Point> quads;
1638 if (tc->text != NULL)
1639 quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, sp_item_i2d_affine(tc->text));
1640 for (unsigned i = 0 ; i < quads.size() ; i += 4) {
1641 SPCanvasItem *quad_canvasitem;
1642 quad_canvasitem = sp_canvas_item_new(sp_desktop_controls(tc->desktop), SP_TYPE_CTRLQUADR, NULL);
1643 // FIXME: make the color settable in prefs
1644 // for now, use semitrasparent blue, as cairo cannot do inversion :(
1645 sp_ctrlquadr_set_rgba32(SP_CTRLQUADR(quad_canvasitem), 0x00777777);
1646 sp_ctrlquadr_set_coords(SP_CTRLQUADR(quad_canvasitem), quads[i], quads[i+1], quads[i+2], quads[i+3]);
1647 sp_canvas_item_show(quad_canvasitem);
1648 tc->text_selection_quads.push_back(quad_canvasitem);
1649 }
1650 }
1652 static gint
1653 sp_text_context_timeout(SPTextContext *tc)
1654 {
1655 if (tc->show) {
1656 sp_canvas_item_show(tc->cursor);
1657 if (tc->phase) {
1658 tc->phase = 0;
1659 sp_ctrlline_set_rgba32(SP_CTRLLINE(tc->cursor), 0x000000ff);
1660 } else {
1661 tc->phase = 1;
1662 sp_ctrlline_set_rgba32(SP_CTRLLINE(tc->cursor), 0xffffffff);
1663 }
1664 }
1666 return TRUE;
1667 }
1669 static void
1670 sp_text_context_forget_text(SPTextContext *tc)
1671 {
1672 if (! tc->text) return;
1673 SPItem *ti = tc->text;
1674 (void)ti;
1675 /* We have to set it to zero,
1676 * or selection changed signal messes everything up */
1677 tc->text = NULL;
1679 /* FIXME: this automatic deletion when nothing is inputted crashes the XML edittor and also crashes when duplicating an empty flowtext.
1680 So don't create an empty flowtext in the first place? Create it when first character is typed.
1681 */
1682 /*
1683 if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) {
1684 Inkscape::XML::Node *text_repr=SP_OBJECT_REPR(ti);
1685 // the repr may already have been unparented
1686 // if we were called e.g. as the result of
1687 // an undo or the element being removed from
1688 // the XML editor
1689 if ( text_repr && sp_repr_parent(text_repr) ) {
1690 sp_repr_unparent(text_repr);
1691 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT,
1692 _("Remove empty text"));
1693 }
1694 }
1695 */
1696 }
1698 gint
1699 sptc_focus_in(GtkWidget */*widget*/, GdkEventFocus */*event*/, SPTextContext *tc)
1700 {
1701 gtk_im_context_focus_in(tc->imc);
1702 return FALSE;
1703 }
1705 gint
1706 sptc_focus_out(GtkWidget */*widget*/, GdkEventFocus */*event*/, SPTextContext *tc)
1707 {
1708 gtk_im_context_focus_out(tc->imc);
1709 return FALSE;
1710 }
1712 static void
1713 sptc_commit(GtkIMContext */*imc*/, gchar *string, SPTextContext *tc)
1714 {
1715 if (!tc->text) {
1716 sp_text_context_setup_text(tc);
1717 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
1718 }
1720 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, string);
1721 sp_text_context_update_cursor(tc);
1722 sp_text_context_update_text_selection(tc);
1724 sp_document_done(SP_OBJECT_DOCUMENT(tc->text), SP_VERB_CONTEXT_TEXT,
1725 _("Type text"));
1726 }
1728 void
1729 sp_text_context_place_cursor (SPTextContext *tc, SPObject *text, Inkscape::Text::Layout::iterator where)
1730 {
1731 SP_EVENT_CONTEXT_DESKTOP (tc)->selection->set (text);
1732 tc->text_sel_start = tc->text_sel_end = where;
1733 sp_text_context_update_cursor(tc);
1734 sp_text_context_update_text_selection(tc);
1735 }
1737 void
1738 sp_text_context_place_cursor_at (SPTextContext *tc, SPObject *text, Geom::Point const p)
1739 {
1740 SP_EVENT_CONTEXT_DESKTOP (tc)->selection->set (text);
1741 sp_text_context_place_cursor (tc, text, sp_te_get_position_by_coords(tc->text, p));
1742 }
1744 Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(SPTextContext *tc, SPObject *text)
1745 {
1746 if (text != tc->text)
1747 return NULL;
1748 return &(tc->text_sel_end);
1749 }
1752 /*
1753 Local Variables:
1754 mode:c++
1755 c-file-style:"stroustrup"
1756 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1757 indent-tabs-mode:nil
1758 fill-column:99
1759 End:
1760 */
1761 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :