8f6d6f1e379ca16f6836d404382bfde68db48a09
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 "desktop-affine.h"
39 #include "message-stack.h"
40 #include "message-context.h"
41 #include "pixmaps/cursor-text.xpm"
42 #include "pixmaps/cursor-text-insert.xpm"
43 #include <glibmm/i18n.h>
44 #include "object-edit.h"
45 #include "xml/repr.h"
46 #include "xml/node-event-vector.h"
47 #include "prefs-utils.h"
48 #include "rubberband.h"
49 #include "sp-metrics.h"
50 #include "context-fns.h"
51 #include "verbs.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 event_context->shape_repr = NULL;
135 event_context->shape_knot_holder = NULL;
137 tc->imc = NULL;
139 tc->text = NULL;
140 tc->pdoc = NR::Point(0, 0);
141 new (&tc->text_sel_start) Inkscape::Text::Layout::iterator();
142 new (&tc->text_sel_end) Inkscape::Text::Layout::iterator();
143 new (&tc->text_selection_quads) std::vector<SPCanvasItem*>();
145 tc->unimode = false;
147 tc->cursor = NULL;
148 tc->indicator = NULL;
149 tc->frame = NULL;
150 tc->grabbed = NULL;
151 tc->timeout = 0;
152 tc->show = FALSE;
153 tc->phase = 0;
154 tc->nascent_object = 0;
155 tc->over_text = 0;
156 tc->dragging = 0;
157 tc->creating = 0;
159 new (&tc->sel_changed_connection) sigc::connection();
160 new (&tc->sel_modified_connection) sigc::connection();
161 new (&tc->style_set_connection) sigc::connection();
162 new (&tc->style_query_connection) sigc::connection();
163 }
165 static void
166 sp_text_context_dispose(GObject *obj)
167 {
168 SPTextContext *tc = SP_TEXT_CONTEXT(obj);
169 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
170 tc->style_query_connection.~connection();
171 tc->style_set_connection.~connection();
172 tc->sel_changed_connection.~connection();
173 tc->sel_modified_connection.~connection();
174 tc->text_sel_end.~iterator();
175 tc->text_sel_start.~iterator();
176 tc->text_selection_quads.~vector();
177 if (G_OBJECT_CLASS(parent_class)->dispose) {
178 G_OBJECT_CLASS(parent_class)->dispose(obj);
179 }
180 if (tc->grabbed) {
181 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
182 tc->grabbed = NULL;
183 }
185 Inkscape::Rubberband::get()->stop();
187 if (ec->shape_knot_holder) {
188 sp_knot_holder_destroy(ec->shape_knot_holder);
189 ec->shape_knot_holder = NULL;
190 }
191 if (ec->shape_repr) { // remove old listener
192 sp_repr_remove_listener_by_data(ec->shape_repr, ec);
193 Inkscape::GC::release(ec->shape_repr);
194 ec->shape_repr = 0;
195 }
196 }
198 static Inkscape::XML::NodeEventVector ec_shape_repr_events = {
199 NULL, /* child_added */
200 NULL, /* child_removed */
201 ec_shape_event_attr_changed,
202 NULL, /* content_changed */
203 NULL /* order_changed */
204 };
206 static void
207 sp_text_context_setup(SPEventContext *ec)
208 {
209 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
210 SPDesktop *desktop = ec->desktop;
212 tc->cursor = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLLINE, NULL);
213 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), 100, 0, 100, 100);
214 sp_ctrlline_set_rgba32(SP_CTRLLINE(tc->cursor), 0x000000ff);
215 sp_canvas_item_hide(tc->cursor);
217 tc->indicator = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLRECT, NULL);
218 SP_CTRLRECT(tc->indicator)->setRectangle(NR::Rect(NR::Point(0, 0), NR::Point(100, 100)));
219 SP_CTRLRECT(tc->indicator)->setColor(0x0000ff7f, false, 0);
220 sp_canvas_item_hide(tc->indicator);
222 tc->frame = sp_canvas_item_new(sp_desktop_controls(desktop), SP_TYPE_CTRLRECT, NULL);
223 SP_CTRLRECT(tc->frame)->setRectangle(NR::Rect(NR::Point(0, 0), NR::Point(100, 100)));
224 SP_CTRLRECT(tc->frame)->setColor(0x0000ff7f, false, 0);
225 sp_canvas_item_hide(tc->frame);
227 tc->timeout = gtk_timeout_add(250, (GtkFunction) sp_text_context_timeout, ec);
229 tc->imc = gtk_im_multicontext_new();
230 if (tc->imc) {
231 GtkWidget *canvas = GTK_WIDGET(sp_desktop_canvas(desktop));
233 /* im preedit handling is very broken in inkscape for
234 * multi-byte characters. See bug 1086769.
235 * We need to let the IM handle the preediting, and
236 * just take in the characters when they're finished being
237 * entered.
238 */
239 gtk_im_context_set_use_preedit(tc->imc, FALSE);
240 gtk_im_context_set_client_window(tc->imc, canvas->window);
242 g_signal_connect(G_OBJECT(canvas), "focus_in_event", G_CALLBACK(sptc_focus_in), tc);
243 g_signal_connect(G_OBJECT(canvas), "focus_out_event", G_CALLBACK(sptc_focus_out), tc);
244 g_signal_connect(G_OBJECT(tc->imc), "commit", G_CALLBACK(sptc_commit), tc);
246 if (GTK_WIDGET_HAS_FOCUS(canvas)) {
247 sptc_focus_in(canvas, NULL, tc);
248 }
249 }
251 if (((SPEventContextClass *) parent_class)->setup)
252 ((SPEventContextClass *) parent_class)->setup(ec);
254 SPItem *item = sp_desktop_selection(ec->desktop)->singleItem();
255 if (item && SP_IS_FLOWTEXT (item) && SP_FLOWTEXT(item)->has_internal_frame()) {
256 ec->shape_knot_holder = sp_item_knot_holder(item, ec->desktop);
257 Inkscape::XML::Node *shape_repr = SP_OBJECT_REPR(SP_FLOWTEXT(item)->get_frame(NULL));
258 if (shape_repr) {
259 ec->shape_repr = shape_repr;
260 Inkscape::GC::anchor(shape_repr);
261 sp_repr_add_listener(shape_repr, &ec_shape_repr_events, ec);
262 }
263 }
265 tc->sel_changed_connection = sp_desktop_selection(desktop)->connectChanged(
266 sigc::bind(sigc::ptr_fun(&sp_text_context_selection_changed), tc)
267 );
268 tc->sel_modified_connection = sp_desktop_selection(desktop)->connectModified(
269 sigc::bind(sigc::ptr_fun(&sp_text_context_selection_modified), tc)
270 );
271 tc->style_set_connection = desktop->connectSetStyle(
272 sigc::bind(sigc::ptr_fun(&sp_text_context_style_set), tc)
273 );
274 tc->style_query_connection = desktop->connectQueryStyle(
275 sigc::bind(sigc::ptr_fun(&sp_text_context_style_query), tc)
276 );
278 sp_text_context_selection_changed(sp_desktop_selection(desktop), tc);
280 if (prefs_get_int_attribute("tools.text", "selcue", 0) != 0) {
281 ec->enableSelectionCue();
282 }
283 if (prefs_get_int_attribute("tools.text", "gradientdrag", 0) != 0) {
284 ec->enableGrDrag();
285 }
286 }
288 static void
289 sp_text_context_finish(SPEventContext *ec)
290 {
291 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
293 if (ec->desktop) {
294 sp_signal_disconnect_by_data(sp_desktop_canvas(ec->desktop), tc);
295 }
297 ec->enableGrDrag(false);
299 tc->style_set_connection.disconnect();
300 tc->style_query_connection.disconnect();
301 tc->sel_changed_connection.disconnect();
302 tc->sel_modified_connection.disconnect();
304 sp_text_context_forget_text(SP_TEXT_CONTEXT(ec));
306 if (tc->imc) {
307 g_object_unref(G_OBJECT(tc->imc));
308 tc->imc = NULL;
309 }
311 if (tc->timeout) {
312 gtk_timeout_remove(tc->timeout);
313 tc->timeout = 0;
314 }
316 if (tc->cursor) {
317 gtk_object_destroy(GTK_OBJECT(tc->cursor));
318 tc->cursor = NULL;
319 }
321 if (tc->indicator) {
322 gtk_object_destroy(GTK_OBJECT(tc->indicator));
323 tc->indicator = NULL;
324 }
326 if (tc->frame) {
327 gtk_object_destroy(GTK_OBJECT(tc->frame));
328 tc->frame = NULL;
329 }
331 for (std::vector<SPCanvasItem*>::iterator it = tc->text_selection_quads.begin() ;
332 it != tc->text_selection_quads.end() ; ++it) {
333 sp_canvas_item_hide(*it);
334 gtk_object_destroy(*it);
335 }
336 tc->text_selection_quads.clear();
337 }
340 static gint
341 sp_text_context_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event)
342 {
343 SPTextContext *tc = SP_TEXT_CONTEXT(event_context);
344 SPDesktop *desktop = event_context->desktop;
345 SPItem *item_ungrouped;
347 gint ret = FALSE;
349 sp_text_context_validate_cursor_iterators(tc);
351 switch (event->type) {
352 case GDK_BUTTON_PRESS:
353 if (event->button.button == 1 && !event_context->space_panning) {
354 // find out clicked item, disregarding groups
355 item_ungrouped = desktop->item_at_point(NR::Point(event->button.x, event->button.y), TRUE);
356 if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
357 sp_desktop_selection(desktop)->set(item_ungrouped);
358 if (tc->text) {
359 // find out click point in document coordinates
360 NR::Point p = desktop->w2d(NR::Point(event->button.x, event->button.y));
361 // set the cursor closest to that point
362 tc->text_sel_start = tc->text_sel_end = sp_te_get_position_by_coords(tc->text, p);
363 // update display
364 sp_text_context_update_cursor(tc);
365 sp_text_context_update_text_selection(tc);
366 tc->dragging = 1;
367 }
368 ret = TRUE;
369 }
370 }
371 break;
372 case GDK_2BUTTON_PRESS:
373 if (event->button.button == 1 && tc->text) {
374 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
375 if (layout) {
376 if (!layout->isStartOfWord(tc->text_sel_start))
377 tc->text_sel_start.prevStartOfWord();
378 if (!layout->isEndOfWord(tc->text_sel_end))
379 tc->text_sel_end.nextEndOfWord();
380 sp_text_context_update_cursor(tc);
381 sp_text_context_update_text_selection(tc);
382 tc->dragging = 2;
383 ret = TRUE;
384 }
385 }
386 break;
387 case GDK_3BUTTON_PRESS:
388 if (event->button.button == 1 && tc->text) {
389 tc->text_sel_start.thisStartOfLine();
390 tc->text_sel_end.thisEndOfLine();
391 sp_text_context_update_cursor(tc);
392 sp_text_context_update_text_selection(tc);
393 tc->dragging = 3;
394 ret = TRUE;
395 }
396 break;
397 case GDK_BUTTON_RELEASE:
398 if (event->button.button == 1 && tc->dragging && !event_context->space_panning) {
399 tc->dragging = 0;
400 ret = TRUE;
401 }
402 break;
403 case GDK_MOTION_NOTIFY:
404 if (event->motion.state & GDK_BUTTON1_MASK && tc->dragging && !event_context->space_panning) {
405 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
406 if (!layout) break;
407 // find out click point in document coordinates
408 NR::Point p = desktop->w2d(NR::Point(event->button.x, event->button.y));
409 // set the cursor closest to that point
410 Inkscape::Text::Layout::iterator new_end = sp_te_get_position_by_coords(tc->text, p);
411 if (tc->dragging == 2) {
412 // double-click dragging: go by word
413 if (new_end < tc->text_sel_start) {
414 if (!layout->isStartOfWord(new_end))
415 new_end.prevStartOfWord();
416 } else
417 if (!layout->isEndOfWord(new_end))
418 new_end.nextEndOfWord();
419 } else if (tc->dragging == 3) {
420 // triple-click dragging: go by line
421 if (new_end < tc->text_sel_start)
422 new_end.thisStartOfLine();
423 else
424 new_end.thisEndOfLine();
425 }
426 // update display
427 if (tc->text_sel_end != new_end) {
428 tc->text_sel_end = new_end;
429 sp_text_context_update_cursor(tc);
430 sp_text_context_update_text_selection(tc);
431 }
432 ret = TRUE;
433 break;
434 }
435 // find out item under mouse, disregarding groups
436 item_ungrouped = desktop->item_at_point(NR::Point(event->button.x, event->button.y), TRUE);
437 if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
438 sp_canvas_item_show(tc->indicator);
439 NR::Maybe<NR::Rect> ibbox = sp_item_bbox_desktop(item_ungrouped);
440 if (ibbox) {
441 SP_CTRLRECT(tc->indicator)->setRectangle(*ibbox);
442 }
444 event_context->cursor_shape = cursor_text_insert_xpm;
445 event_context->hot_x = 7;
446 event_context->hot_y = 10;
447 sp_event_context_update_cursor(event_context);
448 sp_text_context_update_text_selection(tc);
450 if (SP_IS_TEXT (item_ungrouped)) {
451 desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text."));
452 } else {
453 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."));
454 }
456 tc->over_text = true;
458 ret = TRUE;
459 }
460 break;
461 default:
462 break;
463 }
465 if (!ret) {
466 if (((SPEventContextClass *) parent_class)->item_handler)
467 ret = ((SPEventContextClass *) parent_class)->item_handler(event_context, item, event);
468 }
470 return ret;
471 }
473 static void
474 sp_text_context_setup_text(SPTextContext *tc)
475 {
476 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
478 /* Create <text> */
479 Inkscape::XML::Document *xml_doc = sp_document_repr_doc(SP_EVENT_CONTEXT_DESKTOP(ec)->doc());
480 Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text");
481 rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
483 /* Set style */
484 sp_desktop_apply_style_tool(SP_EVENT_CONTEXT_DESKTOP(ec), rtext, "tools.text", true);
486 sp_repr_set_svg_double(rtext, "x", tc->pdoc[NR::X]);
487 sp_repr_set_svg_double(rtext, "y", tc->pdoc[NR::Y]);
489 /* Create <tspan> */
490 Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
491 rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
492 rtext->addChild(rtspan, NULL);
493 Inkscape::GC::release(rtspan);
495 /* Create TEXT */
496 Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
497 rtspan->addChild(rstring, NULL);
498 Inkscape::GC::release(rstring);
499 SPItem *text_item = SP_ITEM(ec->desktop->currentLayer()->appendChildRepr(rtext));
500 /* fixme: Is selection::changed really immediate? */
501 /* yes, it's immediate .. why does it matter? */
502 sp_desktop_selection(ec->desktop)->set(text_item);
503 Inkscape::GC::release(rtext);
504 text_item->transform = SP_ITEM(ec->desktop->currentRoot())->getRelativeTransform(ec->desktop->currentLayer());
505 text_item->updateRepr();
506 sp_document_done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT,
507 _("Create text"));
508 }
510 /**
511 * Insert the character indicated by tc.uni to replace the current selection,
512 * and reset tc.uni/tc.unipos to empty string.
513 *
514 * \pre tc.uni/tc.unipos non-empty.
515 */
516 static void
517 insert_uni_char(SPTextContext *const tc)
518 {
519 g_return_if_fail(tc->unipos
520 && tc->unipos < sizeof(tc->uni)
521 && tc->uni[tc->unipos] == '\0');
522 unsigned int uv;
523 sscanf(tc->uni, "%x", &uv);
524 tc->unipos = 0;
525 tc->uni[tc->unipos] = '\0';
527 if ( !g_unichar_isprint(static_cast<gunichar>(uv))
528 && !(g_unichar_validate(static_cast<gunichar>(uv)) && (g_unichar_type(static_cast<gunichar>(uv)) == G_UNICODE_PRIVATE_USE) ) ) {
529 // This may be due to bad input, so it goes to statusbar.
530 tc->desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE,
531 _("Non-printable character"));
532 } else {
533 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
534 sp_text_context_setup_text(tc);
535 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
536 }
538 gchar u[10];
539 guint const len = g_unichar_to_utf8(uv, u);
540 u[len] = '\0';
542 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, u);
543 sp_text_context_update_cursor(tc);
544 sp_text_context_update_text_selection(tc);
545 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_DIALOG_TRANSFORM,
546 _("Insert Unicode character"));
547 }
548 }
550 static void
551 hex_to_printable_utf8_buf(char const *const hex, char *utf8)
552 {
553 unsigned int uv;
554 sscanf(hex, "%x", &uv);
555 if (!g_unichar_isprint((gunichar) uv)) {
556 uv = 0xfffd;
557 }
558 guint const len = g_unichar_to_utf8(uv, utf8);
559 utf8[len] = '\0';
560 }
562 static void
563 show_curr_uni_char(SPTextContext *const tc)
564 {
565 g_return_if_fail(tc->unipos < sizeof(tc->uni)
566 && tc->uni[tc->unipos] == '\0');
567 if (tc->unipos) {
568 char utf8[10];
569 hex_to_printable_utf8_buf(tc->uni, utf8);
571 /* Status bar messages are in pango markup, so we need xml escaping. */
572 if (utf8[1] == '\0') {
573 switch(utf8[0]) {
574 case '<': strcpy(utf8, "<"); break;
575 case '>': strcpy(utf8, ">"); break;
576 case '&': strcpy(utf8, "&"); break;
577 default: break;
578 }
579 }
580 tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
581 _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8);
582 } else {
583 tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
584 }
585 }
587 static gint
588 sp_text_context_root_handler(SPEventContext *const event_context, GdkEvent *const event)
589 {
590 SPTextContext *const tc = SP_TEXT_CONTEXT(event_context);
592 SPDesktop *desktop = event_context->desktop;
594 sp_canvas_item_hide(tc->indicator);
596 sp_text_context_validate_cursor_iterators(tc);
598 event_context->tolerance = prefs_get_int_attribute_limited("options.dragtolerance", "value", 0, 0, 100);
600 switch (event->type) {
601 case GDK_BUTTON_PRESS:
602 if (event->button.button == 1 && !event_context->space_panning) {
604 if (Inkscape::have_viable_layer(desktop, desktop->messageStack()) == false) {
605 return TRUE;
606 }
608 // save drag origin
609 event_context->xp = (gint) event->button.x;
610 event_context->yp = (gint) event->button.y;
611 event_context->within_tolerance = true;
613 NR::Point const button_pt(event->button.x, event->button.y);
614 tc->p0 = desktop->w2d(button_pt);
615 Inkscape::Rubberband::get()->start(desktop, tc->p0);
616 sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
617 GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK |
618 GDK_POINTER_MOTION_MASK,
619 NULL, event->button.time);
620 tc->grabbed = SP_CANVAS_ITEM(desktop->acetate);
621 tc->creating = 1;
623 /* Processed */
624 return TRUE;
625 }
626 break;
627 case GDK_MOTION_NOTIFY:
628 if (tc->over_text) {
629 tc->over_text = 0;
630 // update cursor and statusbar: we are not over a text object now
631 event_context->cursor_shape = cursor_text_xpm;
632 event_context->hot_x = 7;
633 event_context->hot_y = 7;
634 sp_event_context_update_cursor(event_context);
635 desktop->event_context->defaultMessageContext()->clear();
636 }
638 if (tc->creating && event->motion.state & GDK_BUTTON1_MASK && !event_context->space_panning) {
639 if ( event_context->within_tolerance
640 && ( abs( (gint) event->motion.x - event_context->xp ) < event_context->tolerance )
641 && ( abs( (gint) event->motion.y - event_context->yp ) < event_context->tolerance ) ) {
642 break; // do not drag if we're within tolerance from origin
643 }
644 // Once the user has moved farther than tolerance from the original location
645 // (indicating they intend to draw, not click), then always process the
646 // motion notify coordinates as given (no snapping back to origin)
647 event_context->within_tolerance = false;
649 NR::Point const motion_pt(event->motion.x, event->motion.y);
650 NR::Point const p = desktop->w2d(motion_pt);
652 Inkscape::Rubberband::get()->move(p);
653 gobble_motion_events(GDK_BUTTON1_MASK);
655 // status text
656 GString *xs = SP_PX_TO_METRIC_STRING(fabs((p - tc->p0)[NR::X]), desktop->namedview->getDefaultMetric());
657 GString *ys = SP_PX_TO_METRIC_STRING(fabs((p - tc->p0)[NR::Y]), desktop->namedview->getDefaultMetric());
658 event_context->_message_context->setF(Inkscape::NORMAL_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs->str, ys->str);
659 g_string_free(xs, FALSE);
660 g_string_free(ys, FALSE);
662 }
663 break;
664 case GDK_BUTTON_RELEASE:
665 if (event->button.button == 1 && !event_context->space_panning) {
667 if (tc->grabbed) {
668 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
669 tc->grabbed = NULL;
670 }
672 Inkscape::Rubberband::get()->stop();
674 if (tc->creating && event_context->within_tolerance) {
675 /* Button 1, set X & Y & new item */
676 sp_desktop_selection(desktop)->clear();
677 NR::Point dtp = desktop->w2d(NR::Point(event->button.x, event->button.y));
678 tc->pdoc = sp_desktop_dt2root_xy_point(desktop, dtp);
680 tc->show = TRUE;
681 tc->phase = 1;
682 tc->nascent_object = 1; // new object was just created
684 /* Cursor */
685 sp_canvas_item_show(tc->cursor);
686 // Cursor height is defined by the new text object's font size; it needs to be set
687 // articifically here, for the text object does not exist yet:
688 double cursor_height = sp_desktop_get_font_size_tool(desktop);
689 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), dtp, dtp + NR::Point(0, cursor_height));
690 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
692 event_context->within_tolerance = false;
693 } else if (tc->creating) {
694 NR::Point const button_pt(event->button.x, event->button.y);
695 NR::Point p1 = desktop->w2d(button_pt);
696 double cursor_height = sp_desktop_get_font_size_tool(desktop);
697 if (fabs(p1[NR::Y] - tc->p0[NR::Y]) > cursor_height) {
698 // otherwise even one line won't fit; most probably a slip of hand (even if bigger than tolerance)
699 SPItem *ft = create_flowtext_with_internal_frame (desktop, tc->p0, p1);
700 /* Set style */
701 sp_desktop_apply_style_tool(desktop, SP_OBJECT_REPR(ft), "tools.text", true);
702 sp_desktop_selection(desktop)->set(ft);
703 desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created."));
704 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
705 _("Create flowed text"));
706 } else {
707 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The frame is <b>too small</b> for the current font size. Flowed text not created."));
708 }
709 }
710 tc->creating = false;
711 return TRUE;
712 }
713 break;
714 case GDK_KEY_PRESS: {
715 guint const group0_keyval = get_group0_keyval(&event->key);
717 if (group0_keyval == GDK_KP_Add ||
718 group0_keyval == GDK_KP_Subtract) {
719 if (!(event->key.state & GDK_MOD2_MASK)) // mod2 is NumLock; if on, type +/- keys
720 break; // otherwise pass on keypad +/- so they can zoom
721 }
723 if ((tc->text) || (tc->nascent_object)) {
724 // there is an active text object in this context, or a new object was just created
726 if (tc->unimode || !tc->imc
727 || (MOD__CTRL && MOD__SHIFT) // input methods tend to steal this for unimode,
728 // but we have our own so make sure they don't swallow it
729 || !gtk_im_context_filter_keypress(tc->imc, (GdkEventKey*) event)) {
730 //IM did not consume the key, or we're in unimode
732 if (!MOD__CTRL_ONLY && tc->unimode) {
733 /* TODO: ISO 14755 (section 3 Definitions) says that we should also
734 accept the first 6 characters of alphabets other than the latin
735 alphabet "if the Latin alphabet is not used". The below is also
736 reasonable (viz. hope that the user's keyboard includes latin
737 characters and force latin interpretation -- just as we do for our
738 keyboard shortcuts), but differs from the ISO 14755
739 recommendation. */
740 switch (group0_keyval) {
741 case GDK_space:
742 case GDK_KP_Space: {
743 if (tc->unipos) {
744 insert_uni_char(tc);
745 }
746 /* Stay in unimode. */
747 show_curr_uni_char(tc);
748 return TRUE;
749 }
751 case GDK_BackSpace: {
752 g_return_val_if_fail(tc->unipos < sizeof(tc->uni), TRUE);
753 if (tc->unipos) {
754 tc->uni[--tc->unipos] = '\0';
755 }
756 show_curr_uni_char(tc);
757 return TRUE;
758 }
760 case GDK_Return:
761 case GDK_KP_Enter: {
762 if (tc->unipos) {
763 insert_uni_char(tc);
764 }
765 /* Exit unimode. */
766 tc->unimode = false;
767 event_context->defaultMessageContext()->clear();
768 return TRUE;
769 }
771 case GDK_Escape: {
772 // Cancel unimode.
773 tc->unimode = false;
774 gtk_im_context_reset(tc->imc);
775 event_context->defaultMessageContext()->clear();
776 return TRUE;
777 }
779 case GDK_Shift_L:
780 case GDK_Shift_R:
781 break;
783 default: {
784 if (g_ascii_isxdigit(group0_keyval)) {
785 g_return_val_if_fail(tc->unipos < sizeof(tc->uni) - 1, TRUE);
786 tc->uni[tc->unipos++] = group0_keyval;
787 tc->uni[tc->unipos] = '\0';
788 if (tc->unipos == 8) {
789 /* This behaviour is partly to allow us to continue to
790 use a fixed-length buffer for tc->uni. Reason for
791 choosing the number 8 is that it's the length of
792 ``canonical form'' mentioned in the ISO 14755 spec.
793 An advantage over choosing 6 is that it allows using
794 backspace for typos & misremembering when entering a
795 6-digit number. */
796 insert_uni_char(tc);
797 }
798 show_curr_uni_char(tc);
799 return TRUE;
800 } else {
801 /* The intent is to ignore but consume characters that could be
802 typos for hex digits. Gtk seems to ignore & consume all
803 non-hex-digits, and we do similar here. Though note that some
804 shortcuts (like keypad +/- for zoom) get processed before
805 reaching this code. */
806 return TRUE;
807 }
808 }
809 }
810 }
812 bool (Inkscape::Text::Layout::iterator::*cursor_movement_operator)() = NULL;
814 /* Neither unimode nor IM consumed key; process text tool shortcuts */
815 switch (group0_keyval) {
816 case GDK_x:
817 case GDK_X:
818 if (MOD__ALT_ONLY) {
819 desktop->setToolboxFocusTo ("altx-text");
820 return TRUE;
821 }
822 break;
823 case GDK_space:
824 if (MOD__CTRL_ONLY) {
825 /* No-break space */
826 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
827 sp_text_context_setup_text(tc);
828 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
829 }
830 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, "\302\240");
831 sp_text_context_update_cursor(tc);
832 sp_text_context_update_text_selection(tc);
833 desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No-break space"));
834 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
835 _("Insert no-break space"));
836 return TRUE;
837 }
838 break;
839 case GDK_U:
840 case GDK_u:
841 if (MOD__CTRL_ONLY || (MOD__CTRL && MOD__SHIFT)) {
842 if (tc->unimode) {
843 tc->unimode = false;
844 event_context->defaultMessageContext()->clear();
845 } else {
846 tc->unimode = true;
847 tc->unipos = 0;
848 event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
849 }
850 if (tc->imc) {
851 gtk_im_context_reset(tc->imc);
852 }
853 return TRUE;
854 }
855 break;
856 case GDK_B:
857 case GDK_b:
858 if (MOD__CTRL_ONLY && tc->text) {
859 SPStyle const *style = sp_te_style_at_position(tc->text, std::min(tc->text_sel_start, tc->text_sel_end));
860 SPCSSAttr *css = sp_repr_css_attr_new();
861 if (style->font_weight.computed == SP_CSS_FONT_WEIGHT_NORMAL
862 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_100
863 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_200
864 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_300
865 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_400)
866 sp_repr_css_set_property(css, "font-weight", "bold");
867 else
868 sp_repr_css_set_property(css, "font-weight", "normal");
869 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
870 sp_repr_css_attr_unref(css);
871 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
872 _("Make bold"));
873 sp_text_context_update_cursor(tc);
874 sp_text_context_update_text_selection(tc);
875 return TRUE;
876 }
877 break;
878 case GDK_I:
879 case GDK_i:
880 if (MOD__CTRL_ONLY && tc->text) {
881 SPStyle const *style = sp_te_style_at_position(tc->text, std::min(tc->text_sel_start, tc->text_sel_end));
882 SPCSSAttr *css = sp_repr_css_attr_new();
883 if (style->font_style.computed == SP_CSS_FONT_STYLE_NORMAL)
884 sp_repr_css_set_property(css, "font-style", "italic");
885 else
886 sp_repr_css_set_property(css, "font-style", "normal");
887 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
888 sp_repr_css_attr_unref(css);
889 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
890 _("Make italic"));
891 sp_text_context_update_cursor(tc);
892 sp_text_context_update_text_selection(tc);
893 return TRUE;
894 }
895 break;
897 case GDK_A:
898 case GDK_a:
899 if (MOD__CTRL_ONLY && tc->text) {
900 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
901 if (layout) {
902 tc->text_sel_start = layout->begin();
903 tc->text_sel_end = layout->end();
904 sp_text_context_update_cursor(tc);
905 sp_text_context_update_text_selection(tc);
906 return TRUE;
907 }
908 }
909 break;
911 case GDK_Return:
912 case GDK_KP_Enter:
913 {
914 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
915 sp_text_context_setup_text(tc);
916 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
917 }
919 iterator_pair enter_pair;
920 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, enter_pair);
921 tc->text_sel_start = tc->text_sel_end = enter_pair.first;
923 tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
925 sp_text_context_update_cursor(tc);
926 sp_text_context_update_text_selection(tc);
927 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
928 _("New line"));
929 return TRUE;
930 }
931 case GDK_BackSpace:
932 if (tc->text) { // if nascent_object, do nothing, but return TRUE; same for all other delete and move keys
934 bool noSelection = false;
936 if (tc->text_sel_start == tc->text_sel_end) {
937 tc->text_sel_start.prevCursorPosition();
938 noSelection = true;
939 }
941 iterator_pair bspace_pair;
942 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, bspace_pair);
944 if (noSelection) {
945 if (success) {
946 tc->text_sel_start = tc->text_sel_end = bspace_pair.first;
947 } else { // nothing deleted
948 tc->text_sel_start = tc->text_sel_end = bspace_pair.second;
949 }
950 } else {
951 if (success) {
952 tc->text_sel_start = tc->text_sel_end = bspace_pair.first;
953 } else { // nothing deleted
954 tc->text_sel_start = bspace_pair.first;
955 tc->text_sel_end = bspace_pair.second;
956 }
957 }
959 sp_text_context_update_cursor(tc);
960 sp_text_context_update_text_selection(tc);
961 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
962 _("Backspace"));
963 }
964 return TRUE;
965 case GDK_Delete:
966 case GDK_KP_Delete:
967 if (tc->text) {
968 bool noSelection = false;
970 if (tc->text_sel_start == tc->text_sel_end) {
971 tc->text_sel_end.nextCursorPosition();
972 noSelection = true;
973 }
975 iterator_pair del_pair;
976 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, del_pair);
978 if (noSelection) {
979 tc->text_sel_start = tc->text_sel_end = del_pair.first;
980 } else {
981 if (success) {
982 tc->text_sel_start = tc->text_sel_end = del_pair.first;
983 } else { // nothing deleted
984 tc->text_sel_start = del_pair.first;
985 tc->text_sel_end = del_pair.second;
986 }
987 }
990 sp_text_context_update_cursor(tc);
991 sp_text_context_update_text_selection(tc);
992 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
993 _("Delete"));
994 }
995 return TRUE;
996 case GDK_Left:
997 case GDK_KP_Left:
998 case GDK_KP_4:
999 if (tc->text) {
1000 if (MOD__ALT) {
1001 if (MOD__SHIFT)
1002 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(-10, 0));
1003 else
1004 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(-1, 0));
1005 sp_text_context_update_cursor(tc);
1006 sp_text_context_update_text_selection(tc);
1007 sp_document_maybe_done(sp_desktop_document(desktop), "kern:left", SP_VERB_CONTEXT_TEXT,
1008 _("Kern to the left"));
1009 } else {
1010 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorLeftWithControl
1011 : &Inkscape::Text::Layout::iterator::cursorLeft;
1012 break;
1013 }
1014 }
1015 return TRUE;
1016 case GDK_Right:
1017 case GDK_KP_Right:
1018 case GDK_KP_6:
1019 if (tc->text) {
1020 if (MOD__ALT) {
1021 if (MOD__SHIFT)
1022 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(10, 0));
1023 else
1024 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(1, 0));
1025 sp_text_context_update_cursor(tc);
1026 sp_text_context_update_text_selection(tc);
1027 sp_document_maybe_done(sp_desktop_document(desktop), "kern:right", SP_VERB_CONTEXT_TEXT,
1028 _("Kern to the right"));
1029 } else {
1030 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorRightWithControl
1031 : &Inkscape::Text::Layout::iterator::cursorRight;
1032 break;
1033 }
1034 }
1035 return TRUE;
1036 case GDK_Up:
1037 case GDK_KP_Up:
1038 case GDK_KP_8:
1039 if (tc->text) {
1040 if (MOD__ALT) {
1041 if (MOD__SHIFT)
1042 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, -10));
1043 else
1044 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, -1));
1045 sp_text_context_update_cursor(tc);
1046 sp_text_context_update_text_selection(tc);
1047 sp_document_maybe_done(sp_desktop_document(desktop), "kern:up", SP_VERB_CONTEXT_TEXT,
1048 _("Kern up"));
1050 } else {
1051 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorUpWithControl
1052 : &Inkscape::Text::Layout::iterator::cursorUp;
1053 break;
1054 }
1055 }
1056 return TRUE;
1057 case GDK_Down:
1058 case GDK_KP_Down:
1059 case GDK_KP_2:
1060 if (tc->text) {
1061 if (MOD__ALT) {
1062 if (MOD__SHIFT)
1063 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, 10));
1064 else
1065 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, 1));
1066 sp_text_context_update_cursor(tc);
1067 sp_text_context_update_text_selection(tc);
1068 sp_document_maybe_done(sp_desktop_document(desktop), "kern:down", SP_VERB_CONTEXT_TEXT,
1069 _("Kern down"));
1071 } else {
1072 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorDownWithControl
1073 : &Inkscape::Text::Layout::iterator::cursorDown;
1074 break;
1075 }
1076 }
1077 return TRUE;
1078 case GDK_Home:
1079 case GDK_KP_Home:
1080 if (tc->text) {
1081 if (MOD__CTRL)
1082 cursor_movement_operator = &Inkscape::Text::Layout::iterator::thisStartOfShape;
1083 else
1084 cursor_movement_operator = &Inkscape::Text::Layout::iterator::thisStartOfLine;
1085 break;
1086 }
1087 return TRUE;
1088 case GDK_End:
1089 case GDK_KP_End:
1090 if (tc->text) {
1091 if (MOD__CTRL)
1092 cursor_movement_operator = &Inkscape::Text::Layout::iterator::nextStartOfShape;
1093 else
1094 cursor_movement_operator = &Inkscape::Text::Layout::iterator::thisEndOfLine;
1095 break;
1096 }
1097 return TRUE;
1098 case GDK_Escape:
1099 if (tc->creating) {
1100 tc->creating = 0;
1101 if (tc->grabbed) {
1102 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
1103 tc->grabbed = NULL;
1104 }
1105 Inkscape::Rubberband::get()->stop();
1106 } else {
1107 sp_desktop_selection(desktop)->clear();
1108 }
1109 tc->nascent_object = FALSE;
1110 return TRUE;
1111 case GDK_bracketleft:
1112 if (tc->text) {
1113 if (MOD__ALT || MOD__CTRL) {
1114 if (MOD__ALT) {
1115 if (MOD__SHIFT) {
1116 // FIXME: alt+shift+[] does not work, don't know why
1117 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1118 } else {
1119 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1120 }
1121 } else {
1122 sp_te_adjust_rotation(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -90);
1123 }
1124 sp_document_maybe_done(sp_desktop_document(desktop), "textrot:ccw", SP_VERB_CONTEXT_TEXT,
1125 _("Rotate counterclockwise"));
1126 sp_text_context_update_cursor(tc);
1127 sp_text_context_update_text_selection(tc);
1128 return TRUE;
1129 }
1130 }
1131 break;
1132 case GDK_bracketright:
1133 if (tc->text) {
1134 if (MOD__ALT || MOD__CTRL) {
1135 if (MOD__ALT) {
1136 if (MOD__SHIFT) {
1137 // FIXME: alt+shift+[] does not work, don't know why
1138 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1139 } else {
1140 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1141 }
1142 } else {
1143 sp_te_adjust_rotation(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 90);
1144 }
1145 sp_document_maybe_done(sp_desktop_document(desktop), "textrot:cw", SP_VERB_CONTEXT_TEXT,
1146 _("Rotate clockwise"));
1147 sp_text_context_update_cursor(tc);
1148 sp_text_context_update_text_selection(tc);
1149 return TRUE;
1150 }
1151 }
1152 break;
1153 case GDK_less:
1154 case GDK_comma:
1155 if (tc->text) {
1156 if (MOD__ALT) {
1157 if (MOD__CTRL) {
1158 if (MOD__SHIFT)
1159 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1160 else
1161 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1162 sp_document_maybe_done(sp_desktop_document(desktop), "linespacing:dec", SP_VERB_CONTEXT_TEXT,
1163 _("Contract line spacing"));
1165 } else {
1166 if (MOD__SHIFT)
1167 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1168 else
1169 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1170 sp_document_maybe_done(sp_desktop_document(desktop), "letterspacing:dec", SP_VERB_CONTEXT_TEXT,
1171 _("Contract letter spacing"));
1173 }
1174 sp_text_context_update_cursor(tc);
1175 sp_text_context_update_text_selection(tc);
1176 return TRUE;
1177 }
1178 }
1179 break;
1180 case GDK_greater:
1181 case GDK_period:
1182 if (tc->text) {
1183 if (MOD__ALT) {
1184 if (MOD__CTRL) {
1185 if (MOD__SHIFT)
1186 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1187 else
1188 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1189 sp_document_maybe_done(sp_desktop_document(desktop), "linespacing:inc", SP_VERB_CONTEXT_TEXT,
1190 _("Expand line spacing"));
1192 } else {
1193 if (MOD__SHIFT)
1194 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1195 else
1196 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1197 sp_document_maybe_done(sp_desktop_document(desktop), "letterspacing:inc", SP_VERB_CONTEXT_TEXT,
1198 _("Expand letter spacing"));
1200 }
1201 sp_text_context_update_cursor(tc);
1202 sp_text_context_update_text_selection(tc);
1203 return TRUE;
1204 }
1205 }
1206 break;
1207 default:
1208 break;
1209 }
1211 if (cursor_movement_operator) {
1212 Inkscape::Text::Layout::iterator old_start = tc->text_sel_start;
1213 Inkscape::Text::Layout::iterator old_end = tc->text_sel_end;
1214 (tc->text_sel_end.*cursor_movement_operator)();
1215 if (!MOD__SHIFT)
1216 tc->text_sel_start = tc->text_sel_end;
1217 if (old_start != tc->text_sel_start || old_end != tc->text_sel_end) {
1218 sp_text_context_update_cursor(tc);
1219 sp_text_context_update_text_selection(tc);
1220 }
1221 return TRUE;
1222 }
1224 } else return TRUE; // return the "I took care of it" value if it was consumed by the IM
1225 } else { // do nothing if there's no object to type in - the key will be sent to parent context,
1226 // except up/down that are swallowed to prevent the zoom field from activation
1227 if ((group0_keyval == GDK_Up ||
1228 group0_keyval == GDK_Down ||
1229 group0_keyval == GDK_KP_Up ||
1230 group0_keyval == GDK_KP_Down )
1231 && !MOD__CTRL_ONLY) {
1232 return TRUE;
1233 } else if (group0_keyval == GDK_Escape) { // cancel rubberband
1234 if (tc->creating) {
1235 tc->creating = 0;
1236 if (tc->grabbed) {
1237 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
1238 tc->grabbed = NULL;
1239 }
1240 Inkscape::Rubberband::get()->stop();
1241 }
1242 }
1243 }
1244 break;
1245 }
1247 case GDK_KEY_RELEASE:
1248 if (!tc->unimode && tc->imc && gtk_im_context_filter_keypress(tc->imc, (GdkEventKey*) event)) {
1249 return TRUE;
1250 }
1251 break;
1252 default:
1253 break;
1254 }
1256 // if nobody consumed it so far
1257 if (((SPEventContextClass *) parent_class)->root_handler) { // and there's a handler in parent context,
1258 return ((SPEventContextClass *) parent_class)->root_handler(event_context, event); // send event to parent
1259 } else {
1260 return FALSE; // return "I did nothing" value so that global shortcuts can be activated
1261 }
1262 }
1264 /**
1265 Attempts to paste system clipboard into the currently edited text, returns true on success
1266 */
1267 bool
1268 sp_text_paste_inline(SPEventContext *ec)
1269 {
1270 if (!SP_IS_TEXT_CONTEXT(ec))
1271 return false;
1273 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
1275 if ((tc->text) || (tc->nascent_object)) {
1276 // there is an active text object in this context, or a new object was just created
1278 Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
1279 Glib::ustring const text = refClipboard->wait_for_text();
1281 if (!text.empty()) {
1283 if (!tc->text) { // create text if none (i.e. if nascent_object)
1284 sp_text_context_setup_text(tc);
1285 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
1286 }
1288 // using indices is slow in ustrings. Whatever.
1289 Glib::ustring::size_type begin = 0;
1290 for ( ; ; ) {
1291 Glib::ustring::size_type end = text.find('\n', begin);
1292 if (end == Glib::ustring::npos) {
1293 if (begin != text.length())
1294 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());
1295 break;
1296 }
1297 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());
1298 tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
1299 begin = end + 1;
1300 }
1301 sp_document_done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT,
1302 _("Paste text"));
1304 return true;
1305 }
1306 } // FIXME: else create and select a new object under cursor!
1308 return false;
1309 }
1311 /**
1312 Gets the raw characters that comprise the currently selected text, converting line
1313 breaks into lf characters.
1314 */
1315 Glib::ustring
1316 sp_text_get_selected_text(SPEventContext const *ec)
1317 {
1318 if (!SP_IS_TEXT_CONTEXT(ec))
1319 return "";
1320 SPTextContext const *tc = SP_TEXT_CONTEXT(ec);
1321 if (tc->text == NULL)
1322 return "";
1324 return sp_te_get_string_multiline(tc->text, tc->text_sel_start, tc->text_sel_end);
1325 }
1327 /**
1328 Deletes the currently selected characters. Returns false if there is no
1329 text selection currently.
1330 */
1331 bool sp_text_delete_selection(SPEventContext *ec)
1332 {
1333 if (!SP_IS_TEXT_CONTEXT(ec))
1334 return false;
1335 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
1336 if (tc->text == NULL)
1337 return false;
1339 if (tc->text_sel_start == tc->text_sel_end)
1340 return false;
1342 iterator_pair pair;
1343 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, pair);
1346 if (success) {
1347 tc->text_sel_start = tc->text_sel_end = pair.first;
1348 } else { // nothing deleted
1349 tc->text_sel_start = pair.first;
1350 tc->text_sel_end = pair.second;
1351 }
1353 sp_text_context_update_cursor(tc);
1354 sp_text_context_update_text_selection(tc);
1356 return true;
1357 }
1359 /**
1360 * \param selection Should not be NULL.
1361 */
1362 static void
1363 sp_text_context_selection_changed(Inkscape::Selection *selection, SPTextContext *tc)
1364 {
1365 g_assert(selection != NULL);
1367 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
1369 if (ec->shape_knot_holder) { // destroy knotholder
1370 sp_knot_holder_destroy(ec->shape_knot_holder);
1371 ec->shape_knot_holder = NULL;
1372 }
1374 if (ec->shape_repr) { // remove old listener
1375 sp_repr_remove_listener_by_data(ec->shape_repr, ec);
1376 Inkscape::GC::release(ec->shape_repr);
1377 ec->shape_repr = 0;
1378 }
1380 SPItem *item = selection->singleItem();
1381 if (item && SP_IS_FLOWTEXT (item) && SP_FLOWTEXT(item)->has_internal_frame()) {
1382 ec->shape_knot_holder = sp_item_knot_holder(item, ec->desktop);
1383 Inkscape::XML::Node *shape_repr = SP_OBJECT_REPR(SP_FLOWTEXT(item)->get_frame(NULL));
1384 if (shape_repr) {
1385 ec->shape_repr = shape_repr;
1386 Inkscape::GC::anchor(shape_repr);
1387 sp_repr_add_listener(shape_repr, &ec_shape_repr_events, ec);
1388 }
1389 }
1391 if (tc->text && (item != tc->text)) {
1392 sp_text_context_forget_text(tc);
1393 }
1394 tc->text = NULL;
1396 if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
1397 tc->text = item;
1398 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1399 if (layout)
1400 tc->text_sel_start = tc->text_sel_end = layout->end();
1401 } else {
1402 tc->text = NULL;
1403 }
1405 // we update cursor without scrolling, because this position may not be final;
1406 // item_handler moves cusros to the point of click immediately
1407 sp_text_context_update_cursor(tc, false);
1408 sp_text_context_update_text_selection(tc);
1409 }
1411 static void
1412 sp_text_context_selection_modified(Inkscape::Selection *selection, guint flags, SPTextContext *tc)
1413 {
1414 sp_text_context_update_cursor(tc);
1415 sp_text_context_update_text_selection(tc);
1416 }
1418 static bool
1419 sp_text_context_style_set(SPCSSAttr const *css, SPTextContext *tc)
1420 {
1421 if (tc->text == NULL)
1422 return false;
1423 if (tc->text_sel_start == tc->text_sel_end)
1424 return false; // will get picked up by the parent and applied to the whole text object
1426 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
1427 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT,
1428 _("Set text style"));
1429 sp_text_context_update_cursor(tc);
1430 sp_text_context_update_text_selection(tc);
1432 return true;
1433 }
1435 static int
1436 sp_text_context_style_query(SPStyle *style, int property, SPTextContext *tc)
1437 {
1438 if (tc->text == NULL)
1439 return QUERY_STYLE_NOTHING;
1440 const Inkscape::Text::Layout *layout = te_get_layout(tc->text);
1441 if (layout == NULL)
1442 return QUERY_STYLE_NOTHING;
1443 sp_text_context_validate_cursor_iterators(tc);
1445 GSList *styles_list = NULL;
1447 Inkscape::Text::Layout::iterator begin_it, end_it;
1448 if (tc->text_sel_start < tc->text_sel_end) {
1449 begin_it = tc->text_sel_start;
1450 end_it = tc->text_sel_end;
1451 } else {
1452 begin_it = tc->text_sel_end;
1453 end_it = tc->text_sel_start;
1454 }
1455 if (begin_it == end_it)
1456 if (!begin_it.prevCharacter())
1457 end_it.nextCharacter();
1458 for (Inkscape::Text::Layout::iterator it = begin_it ; it < end_it ; it.nextStartOfSpan()) {
1459 SPObject const *pos_obj = 0;
1460 void *rawptr = 0;
1461 layout->getSourceOfCharacter(it, &rawptr);
1462 pos_obj = SP_OBJECT(rawptr);
1463 if (pos_obj == 0) continue;
1464 while (SP_IS_STRING(pos_obj) && SP_OBJECT_PARENT(pos_obj)) {
1465 pos_obj = SP_OBJECT_PARENT(pos_obj); // SPStrings don't have style
1466 }
1467 styles_list = g_slist_prepend(styles_list, (gpointer)pos_obj);
1468 }
1470 int result = sp_desktop_query_style_from_list (styles_list, style, property);
1472 g_slist_free(styles_list);
1473 return result;
1474 }
1476 static void
1477 sp_text_context_validate_cursor_iterators(SPTextContext *tc)
1478 {
1479 if (tc->text == NULL)
1480 return;
1481 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1482 if (layout) { // undo can change the text length without us knowing it
1483 layout->validateIterator(&tc->text_sel_start);
1484 layout->validateIterator(&tc->text_sel_end);
1485 }
1486 }
1488 static void
1489 sp_text_context_update_cursor(SPTextContext *tc, bool scroll_to_see)
1490 {
1491 GdkRectangle im_cursor = { 0, 0, 1, 1 };
1493 // due to interruptible display, tc may already be destroyed during a display update before
1494 // the cursor update (can't do both atomically, alas)
1495 if (!tc->desktop) return;
1497 if (tc->text) {
1498 NR::Point p0, p1;
1499 sp_te_get_cursor_coords(tc->text, tc->text_sel_end, p0, p1);
1500 NR::Point const d0 = p0 * sp_item_i2d_affine(SP_ITEM(tc->text));
1501 NR::Point const d1 = p1 * sp_item_i2d_affine(SP_ITEM(tc->text));
1503 // scroll to show cursor
1504 if (scroll_to_see) {
1505 NR::Point const dm = (d0 + d1) / 2;
1506 // unlike mouse moves, here we must scroll all the way at first shot, so we override the autoscrollspeed
1507 SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(&dm, 1.0);
1508 }
1510 sp_canvas_item_show(tc->cursor);
1511 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), d0, d1);
1513 /* fixme: ... need another transformation to get canvas widget coordinate space? */
1514 im_cursor.x = (int) floor(d0[NR::X]);
1515 im_cursor.y = (int) floor(d0[NR::Y]);
1516 im_cursor.width = (int) floor(d1[NR::X]) - im_cursor.x;
1517 im_cursor.height = (int) floor(d1[NR::Y]) - im_cursor.y;
1519 tc->show = TRUE;
1520 tc->phase = 1;
1522 if (SP_IS_FLOWTEXT(tc->text)) {
1523 SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (NULL); // first frame only
1524 if (frame) {
1525 sp_canvas_item_show(tc->frame);
1526 NR::Maybe<NR::Rect> frame_bbox = sp_item_bbox_desktop(frame);
1527 if (frame_bbox) {
1528 SP_CTRLRECT(tc->frame)->setRectangle(*frame_bbox);
1529 }
1530 }
1531 SP_EVENT_CONTEXT(tc)->_message_context->set(Inkscape::NORMAL_MESSAGE, _("Type flowed text; <b>Enter</b> to start new paragraph."));
1532 } else {
1533 SP_EVENT_CONTEXT(tc)->_message_context->set(Inkscape::NORMAL_MESSAGE, _("Type text; <b>Enter</b> to start new line."));
1534 }
1536 } else {
1537 sp_canvas_item_hide(tc->cursor);
1538 sp_canvas_item_hide(tc->frame);
1539 tc->show = FALSE;
1540 if (!tc->nascent_object) {
1541 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
1542 }
1543 }
1545 if (tc->imc) {
1546 gtk_im_context_set_cursor_location(tc->imc, &im_cursor);
1547 }
1548 SP_EVENT_CONTEXT(tc)->desktop->emitToolSubselectionChanged((gpointer)tc);
1549 }
1551 static void sp_text_context_update_text_selection(SPTextContext *tc)
1552 {
1553 // due to interruptible display, tc may already be destroyed during a display update before
1554 // the selection update (can't do both atomically, alas)
1555 if (!tc->desktop) return;
1557 for (std::vector<SPCanvasItem*>::iterator it = tc->text_selection_quads.begin() ; it != tc->text_selection_quads.end() ; it++) {
1558 sp_canvas_item_hide(*it);
1559 gtk_object_destroy(*it);
1560 }
1561 tc->text_selection_quads.clear();
1563 std::vector<NR::Point> quads;
1564 if (tc->text != NULL)
1565 quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, sp_item_i2d_affine(tc->text));
1566 for (unsigned i = 0 ; i < quads.size() ; i += 4) {
1567 SPCanvasItem *quad_canvasitem;
1568 quad_canvasitem = sp_canvas_item_new(sp_desktop_controls(tc->desktop), SP_TYPE_CTRLQUADR, NULL);
1569 sp_ctrlquadr_set_rgba32(SP_CTRLQUADR(quad_canvasitem), 0x000000ff);
1570 sp_ctrlquadr_set_coords(SP_CTRLQUADR(quad_canvasitem), quads[i], quads[i+1], quads[i+2], quads[i+3]);
1571 sp_canvas_item_show(quad_canvasitem);
1572 tc->text_selection_quads.push_back(quad_canvasitem);
1573 }
1574 }
1576 static gint
1577 sp_text_context_timeout(SPTextContext *tc)
1578 {
1579 if (tc->show) {
1580 if (tc->phase) {
1581 tc->phase = 0;
1582 sp_canvas_item_hide(tc->cursor);
1583 } else {
1584 tc->phase = 1;
1585 sp_canvas_item_show(tc->cursor);
1586 }
1587 }
1589 return TRUE;
1590 }
1592 static void
1593 sp_text_context_forget_text(SPTextContext *tc)
1594 {
1595 if (! tc->text) return;
1596 SPItem *ti = tc->text;
1597 /* We have to set it to zero,
1598 * or selection changed signal messes everything up */
1599 tc->text = NULL;
1600 if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) {
1601 Inkscape::XML::Node *text_repr=SP_OBJECT_REPR(ti);
1602 // the repr may already have been unparented
1603 // if we were called e.g. as the result of
1604 // an undo or the element being removed from
1605 // the XML editor
1606 if ( text_repr && sp_repr_parent(text_repr) ) {
1607 sp_repr_unparent(text_repr);
1608 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT,
1609 _("Remove empty text"));
1610 }
1611 }
1612 }
1614 gint
1615 sptc_focus_in(GtkWidget *widget, GdkEventFocus *event, SPTextContext *tc)
1616 {
1617 gtk_im_context_focus_in(tc->imc);
1618 return FALSE;
1619 }
1621 gint
1622 sptc_focus_out(GtkWidget *widget, GdkEventFocus *event, SPTextContext *tc)
1623 {
1624 gtk_im_context_focus_out(tc->imc);
1625 return FALSE;
1626 }
1628 static void
1629 sptc_commit(GtkIMContext *imc, gchar *string, SPTextContext *tc)
1630 {
1631 if (!tc->text) {
1632 sp_text_context_setup_text(tc);
1633 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
1634 }
1636 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, string);
1637 sp_text_context_update_cursor(tc);
1638 sp_text_context_update_text_selection(tc);
1640 sp_document_done(SP_OBJECT_DOCUMENT(tc->text), SP_VERB_CONTEXT_TEXT,
1641 _("Type text"));
1642 }
1645 /*
1646 Local Variables:
1647 mode:c++
1648 c-file-style:"stroustrup"
1649 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1650 indent-tabs-mode:nil
1651 fill-column:99
1652 End:
1653 */
1654 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :