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 gobble_motion_events(GDK_BUTTON1_MASK);
433 ret = TRUE;
434 break;
435 }
436 // find out item under mouse, disregarding groups
437 item_ungrouped = desktop->item_at_point(NR::Point(event->button.x, event->button.y), TRUE);
438 if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) {
439 sp_canvas_item_show(tc->indicator);
440 NR::Maybe<NR::Rect> ibbox = sp_item_bbox_desktop(item_ungrouped);
441 if (ibbox) {
442 SP_CTRLRECT(tc->indicator)->setRectangle(*ibbox);
443 }
445 event_context->cursor_shape = cursor_text_insert_xpm;
446 event_context->hot_x = 7;
447 event_context->hot_y = 10;
448 sp_event_context_update_cursor(event_context);
449 sp_text_context_update_text_selection(tc);
451 if (SP_IS_TEXT (item_ungrouped)) {
452 desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text."));
453 } else {
454 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."));
455 }
457 tc->over_text = true;
459 ret = TRUE;
460 }
461 break;
462 default:
463 break;
464 }
466 if (!ret) {
467 if (((SPEventContextClass *) parent_class)->item_handler)
468 ret = ((SPEventContextClass *) parent_class)->item_handler(event_context, item, event);
469 }
471 return ret;
472 }
474 static void
475 sp_text_context_setup_text(SPTextContext *tc)
476 {
477 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
479 /* Create <text> */
480 Inkscape::XML::Document *xml_doc = sp_document_repr_doc(SP_EVENT_CONTEXT_DESKTOP(ec)->doc());
481 Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text");
482 rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
484 /* Set style */
485 sp_desktop_apply_style_tool(SP_EVENT_CONTEXT_DESKTOP(ec), rtext, "tools.text", true);
487 sp_repr_set_svg_double(rtext, "x", tc->pdoc[NR::X]);
488 sp_repr_set_svg_double(rtext, "y", tc->pdoc[NR::Y]);
490 /* Create <tspan> */
491 Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
492 rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
493 rtext->addChild(rtspan, NULL);
494 Inkscape::GC::release(rtspan);
496 /* Create TEXT */
497 Inkscape::XML::Node *rstring = xml_doc->createTextNode("");
498 rtspan->addChild(rstring, NULL);
499 Inkscape::GC::release(rstring);
500 SPItem *text_item = SP_ITEM(ec->desktop->currentLayer()->appendChildRepr(rtext));
501 /* fixme: Is selection::changed really immediate? */
502 /* yes, it's immediate .. why does it matter? */
503 sp_desktop_selection(ec->desktop)->set(text_item);
504 Inkscape::GC::release(rtext);
505 text_item->transform = SP_ITEM(ec->desktop->currentRoot())->getRelativeTransform(ec->desktop->currentLayer());
506 text_item->updateRepr();
507 sp_document_done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT,
508 _("Create text"));
509 }
511 /**
512 * Insert the character indicated by tc.uni to replace the current selection,
513 * and reset tc.uni/tc.unipos to empty string.
514 *
515 * \pre tc.uni/tc.unipos non-empty.
516 */
517 static void
518 insert_uni_char(SPTextContext *const tc)
519 {
520 g_return_if_fail(tc->unipos
521 && tc->unipos < sizeof(tc->uni)
522 && tc->uni[tc->unipos] == '\0');
523 unsigned int uv;
524 sscanf(tc->uni, "%x", &uv);
525 tc->unipos = 0;
526 tc->uni[tc->unipos] = '\0';
528 if ( !g_unichar_isprint(static_cast<gunichar>(uv))
529 && !(g_unichar_validate(static_cast<gunichar>(uv)) && (g_unichar_type(static_cast<gunichar>(uv)) == G_UNICODE_PRIVATE_USE) ) ) {
530 // This may be due to bad input, so it goes to statusbar.
531 tc->desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE,
532 _("Non-printable character"));
533 } else {
534 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
535 sp_text_context_setup_text(tc);
536 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
537 }
539 gchar u[10];
540 guint const len = g_unichar_to_utf8(uv, u);
541 u[len] = '\0';
543 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, u);
544 sp_text_context_update_cursor(tc);
545 sp_text_context_update_text_selection(tc);
546 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_DIALOG_TRANSFORM,
547 _("Insert Unicode character"));
548 }
549 }
551 static void
552 hex_to_printable_utf8_buf(char const *const hex, char *utf8)
553 {
554 unsigned int uv;
555 sscanf(hex, "%x", &uv);
556 if (!g_unichar_isprint((gunichar) uv)) {
557 uv = 0xfffd;
558 }
559 guint const len = g_unichar_to_utf8(uv, utf8);
560 utf8[len] = '\0';
561 }
563 static void
564 show_curr_uni_char(SPTextContext *const tc)
565 {
566 g_return_if_fail(tc->unipos < sizeof(tc->uni)
567 && tc->uni[tc->unipos] == '\0');
568 if (tc->unipos) {
569 char utf8[10];
570 hex_to_printable_utf8_buf(tc->uni, utf8);
572 /* Status bar messages are in pango markup, so we need xml escaping. */
573 if (utf8[1] == '\0') {
574 switch(utf8[0]) {
575 case '<': strcpy(utf8, "<"); break;
576 case '>': strcpy(utf8, ">"); break;
577 case '&': strcpy(utf8, "&"); break;
578 default: break;
579 }
580 }
581 tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE,
582 _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8);
583 } else {
584 tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
585 }
586 }
588 static gint
589 sp_text_context_root_handler(SPEventContext *const event_context, GdkEvent *const event)
590 {
591 SPTextContext *const tc = SP_TEXT_CONTEXT(event_context);
593 SPDesktop *desktop = event_context->desktop;
595 sp_canvas_item_hide(tc->indicator);
597 sp_text_context_validate_cursor_iterators(tc);
599 event_context->tolerance = prefs_get_int_attribute_limited("options.dragtolerance", "value", 0, 0, 100);
601 switch (event->type) {
602 case GDK_BUTTON_PRESS:
603 if (event->button.button == 1 && !event_context->space_panning) {
605 if (Inkscape::have_viable_layer(desktop, desktop->messageStack()) == false) {
606 return TRUE;
607 }
609 // save drag origin
610 event_context->xp = (gint) event->button.x;
611 event_context->yp = (gint) event->button.y;
612 event_context->within_tolerance = true;
614 NR::Point const button_pt(event->button.x, event->button.y);
615 tc->p0 = desktop->w2d(button_pt);
616 Inkscape::Rubberband::get()->start(desktop, tc->p0);
617 sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate),
618 GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK |
619 GDK_POINTER_MOTION_MASK,
620 NULL, event->button.time);
621 tc->grabbed = SP_CANVAS_ITEM(desktop->acetate);
622 tc->creating = 1;
624 /* Processed */
625 return TRUE;
626 }
627 break;
628 case GDK_MOTION_NOTIFY:
629 if (tc->over_text) {
630 tc->over_text = 0;
631 // update cursor and statusbar: we are not over a text object now
632 event_context->cursor_shape = cursor_text_xpm;
633 event_context->hot_x = 7;
634 event_context->hot_y = 7;
635 sp_event_context_update_cursor(event_context);
636 desktop->event_context->defaultMessageContext()->clear();
637 }
639 if (tc->creating && event->motion.state & GDK_BUTTON1_MASK && !event_context->space_panning) {
640 if ( event_context->within_tolerance
641 && ( abs( (gint) event->motion.x - event_context->xp ) < event_context->tolerance )
642 && ( abs( (gint) event->motion.y - event_context->yp ) < event_context->tolerance ) ) {
643 break; // do not drag if we're within tolerance from origin
644 }
645 // Once the user has moved farther than tolerance from the original location
646 // (indicating they intend to draw, not click), then always process the
647 // motion notify coordinates as given (no snapping back to origin)
648 event_context->within_tolerance = false;
650 NR::Point const motion_pt(event->motion.x, event->motion.y);
651 NR::Point const p = desktop->w2d(motion_pt);
653 Inkscape::Rubberband::get()->move(p);
654 gobble_motion_events(GDK_BUTTON1_MASK);
656 // status text
657 GString *xs = SP_PX_TO_METRIC_STRING(fabs((p - tc->p0)[NR::X]), desktop->namedview->getDefaultMetric());
658 GString *ys = SP_PX_TO_METRIC_STRING(fabs((p - tc->p0)[NR::Y]), desktop->namedview->getDefaultMetric());
659 event_context->_message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs->str, ys->str);
660 g_string_free(xs, FALSE);
661 g_string_free(ys, FALSE);
663 }
664 break;
665 case GDK_BUTTON_RELEASE:
666 if (event->button.button == 1 && !event_context->space_panning) {
668 if (tc->grabbed) {
669 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
670 tc->grabbed = NULL;
671 }
673 Inkscape::Rubberband::get()->stop();
675 if (tc->creating && event_context->within_tolerance) {
676 /* Button 1, set X & Y & new item */
677 sp_desktop_selection(desktop)->clear();
678 NR::Point dtp = desktop->w2d(NR::Point(event->button.x, event->button.y));
679 tc->pdoc = sp_desktop_dt2root_xy_point(desktop, dtp);
681 tc->show = TRUE;
682 tc->phase = 1;
683 tc->nascent_object = 1; // new object was just created
685 /* Cursor */
686 sp_canvas_item_show(tc->cursor);
687 // Cursor height is defined by the new text object's font size; it needs to be set
688 // articifically here, for the text object does not exist yet:
689 double cursor_height = sp_desktop_get_font_size_tool(desktop);
690 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), dtp, dtp + NR::Point(0, cursor_height));
691 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
693 event_context->within_tolerance = false;
694 } else if (tc->creating) {
695 NR::Point const button_pt(event->button.x, event->button.y);
696 NR::Point p1 = desktop->w2d(button_pt);
697 double cursor_height = sp_desktop_get_font_size_tool(desktop);
698 if (fabs(p1[NR::Y] - tc->p0[NR::Y]) > cursor_height) {
699 // otherwise even one line won't fit; most probably a slip of hand (even if bigger than tolerance)
700 SPItem *ft = create_flowtext_with_internal_frame (desktop, tc->p0, p1);
701 /* Set style */
702 sp_desktop_apply_style_tool(desktop, SP_OBJECT_REPR(ft), "tools.text", true);
703 sp_desktop_selection(desktop)->set(ft);
704 desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created."));
705 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
706 _("Create flowed text"));
707 } else {
708 desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The frame is <b>too small</b> for the current font size. Flowed text not created."));
709 }
710 }
711 tc->creating = false;
712 return TRUE;
713 }
714 break;
715 case GDK_KEY_PRESS: {
716 guint const group0_keyval = get_group0_keyval(&event->key);
718 if (group0_keyval == GDK_KP_Add ||
719 group0_keyval == GDK_KP_Subtract) {
720 if (!(event->key.state & GDK_MOD2_MASK)) // mod2 is NumLock; if on, type +/- keys
721 break; // otherwise pass on keypad +/- so they can zoom
722 }
724 if ((tc->text) || (tc->nascent_object)) {
725 // there is an active text object in this context, or a new object was just created
727 if (tc->unimode || !tc->imc
728 || (MOD__CTRL && MOD__SHIFT) // input methods tend to steal this for unimode,
729 // but we have our own so make sure they don't swallow it
730 || !gtk_im_context_filter_keypress(tc->imc, (GdkEventKey*) event)) {
731 //IM did not consume the key, or we're in unimode
733 if (!MOD__CTRL_ONLY && tc->unimode) {
734 /* TODO: ISO 14755 (section 3 Definitions) says that we should also
735 accept the first 6 characters of alphabets other than the latin
736 alphabet "if the Latin alphabet is not used". The below is also
737 reasonable (viz. hope that the user's keyboard includes latin
738 characters and force latin interpretation -- just as we do for our
739 keyboard shortcuts), but differs from the ISO 14755
740 recommendation. */
741 switch (group0_keyval) {
742 case GDK_space:
743 case GDK_KP_Space: {
744 if (tc->unipos) {
745 insert_uni_char(tc);
746 }
747 /* Stay in unimode. */
748 show_curr_uni_char(tc);
749 return TRUE;
750 }
752 case GDK_BackSpace: {
753 g_return_val_if_fail(tc->unipos < sizeof(tc->uni), TRUE);
754 if (tc->unipos) {
755 tc->uni[--tc->unipos] = '\0';
756 }
757 show_curr_uni_char(tc);
758 return TRUE;
759 }
761 case GDK_Return:
762 case GDK_KP_Enter: {
763 if (tc->unipos) {
764 insert_uni_char(tc);
765 }
766 /* Exit unimode. */
767 tc->unimode = false;
768 event_context->defaultMessageContext()->clear();
769 return TRUE;
770 }
772 case GDK_Escape: {
773 // Cancel unimode.
774 tc->unimode = false;
775 gtk_im_context_reset(tc->imc);
776 event_context->defaultMessageContext()->clear();
777 return TRUE;
778 }
780 case GDK_Shift_L:
781 case GDK_Shift_R:
782 break;
784 default: {
785 if (g_ascii_isxdigit(group0_keyval)) {
786 g_return_val_if_fail(tc->unipos < sizeof(tc->uni) - 1, TRUE);
787 tc->uni[tc->unipos++] = group0_keyval;
788 tc->uni[tc->unipos] = '\0';
789 if (tc->unipos == 8) {
790 /* This behaviour is partly to allow us to continue to
791 use a fixed-length buffer for tc->uni. Reason for
792 choosing the number 8 is that it's the length of
793 ``canonical form'' mentioned in the ISO 14755 spec.
794 An advantage over choosing 6 is that it allows using
795 backspace for typos & misremembering when entering a
796 6-digit number. */
797 insert_uni_char(tc);
798 }
799 show_curr_uni_char(tc);
800 return TRUE;
801 } else {
802 /* The intent is to ignore but consume characters that could be
803 typos for hex digits. Gtk seems to ignore & consume all
804 non-hex-digits, and we do similar here. Though note that some
805 shortcuts (like keypad +/- for zoom) get processed before
806 reaching this code. */
807 return TRUE;
808 }
809 }
810 }
811 }
813 bool (Inkscape::Text::Layout::iterator::*cursor_movement_operator)() = NULL;
815 /* Neither unimode nor IM consumed key; process text tool shortcuts */
816 switch (group0_keyval) {
817 case GDK_x:
818 case GDK_X:
819 if (MOD__ALT_ONLY) {
820 desktop->setToolboxFocusTo ("altx-text");
821 return TRUE;
822 }
823 break;
824 case GDK_space:
825 if (MOD__CTRL_ONLY) {
826 /* No-break space */
827 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
828 sp_text_context_setup_text(tc);
829 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
830 }
831 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, "\302\240");
832 sp_text_context_update_cursor(tc);
833 sp_text_context_update_text_selection(tc);
834 desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No-break space"));
835 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
836 _("Insert no-break space"));
837 return TRUE;
838 }
839 break;
840 case GDK_U:
841 case GDK_u:
842 if (MOD__CTRL_ONLY || (MOD__CTRL && MOD__SHIFT)) {
843 if (tc->unimode) {
844 tc->unimode = false;
845 event_context->defaultMessageContext()->clear();
846 } else {
847 tc->unimode = true;
848 tc->unipos = 0;
849 event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): "));
850 }
851 if (tc->imc) {
852 gtk_im_context_reset(tc->imc);
853 }
854 return TRUE;
855 }
856 break;
857 case GDK_B:
858 case GDK_b:
859 if (MOD__CTRL_ONLY && tc->text) {
860 SPStyle const *style = sp_te_style_at_position(tc->text, std::min(tc->text_sel_start, tc->text_sel_end));
861 SPCSSAttr *css = sp_repr_css_attr_new();
862 if (style->font_weight.computed == SP_CSS_FONT_WEIGHT_NORMAL
863 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_100
864 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_200
865 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_300
866 || style->font_weight.computed == SP_CSS_FONT_WEIGHT_400)
867 sp_repr_css_set_property(css, "font-weight", "bold");
868 else
869 sp_repr_css_set_property(css, "font-weight", "normal");
870 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
871 sp_repr_css_attr_unref(css);
872 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
873 _("Make bold"));
874 sp_text_context_update_cursor(tc);
875 sp_text_context_update_text_selection(tc);
876 return TRUE;
877 }
878 break;
879 case GDK_I:
880 case GDK_i:
881 if (MOD__CTRL_ONLY && tc->text) {
882 SPStyle const *style = sp_te_style_at_position(tc->text, std::min(tc->text_sel_start, tc->text_sel_end));
883 SPCSSAttr *css = sp_repr_css_attr_new();
884 if (style->font_style.computed == SP_CSS_FONT_STYLE_NORMAL)
885 sp_repr_css_set_property(css, "font-style", "italic");
886 else
887 sp_repr_css_set_property(css, "font-style", "normal");
888 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
889 sp_repr_css_attr_unref(css);
890 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
891 _("Make italic"));
892 sp_text_context_update_cursor(tc);
893 sp_text_context_update_text_selection(tc);
894 return TRUE;
895 }
896 break;
898 case GDK_A:
899 case GDK_a:
900 if (MOD__CTRL_ONLY && tc->text) {
901 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
902 if (layout) {
903 tc->text_sel_start = layout->begin();
904 tc->text_sel_end = layout->end();
905 sp_text_context_update_cursor(tc);
906 sp_text_context_update_text_selection(tc);
907 return TRUE;
908 }
909 }
910 break;
912 case GDK_Return:
913 case GDK_KP_Enter:
914 {
915 if (!tc->text) { // printable key; create text if none (i.e. if nascent_object)
916 sp_text_context_setup_text(tc);
917 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
918 }
920 iterator_pair enter_pair;
921 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, enter_pair);
922 (void)success; // TODO cleanup
923 tc->text_sel_start = tc->text_sel_end = enter_pair.first;
925 tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
927 sp_text_context_update_cursor(tc);
928 sp_text_context_update_text_selection(tc);
929 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
930 _("New line"));
931 return TRUE;
932 }
933 case GDK_BackSpace:
934 if (tc->text) { // if nascent_object, do nothing, but return TRUE; same for all other delete and move keys
936 bool noSelection = false;
938 if (tc->text_sel_start == tc->text_sel_end) {
939 tc->text_sel_start.prevCursorPosition();
940 noSelection = true;
941 }
943 iterator_pair bspace_pair;
944 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, bspace_pair);
946 if (noSelection) {
947 if (success) {
948 tc->text_sel_start = tc->text_sel_end = bspace_pair.first;
949 } else { // nothing deleted
950 tc->text_sel_start = tc->text_sel_end = bspace_pair.second;
951 }
952 } else {
953 if (success) {
954 tc->text_sel_start = tc->text_sel_end = bspace_pair.first;
955 } else { // nothing deleted
956 tc->text_sel_start = bspace_pair.first;
957 tc->text_sel_end = bspace_pair.second;
958 }
959 }
961 sp_text_context_update_cursor(tc);
962 sp_text_context_update_text_selection(tc);
963 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
964 _("Backspace"));
965 }
966 return TRUE;
967 case GDK_Delete:
968 case GDK_KP_Delete:
969 if (tc->text) {
970 bool noSelection = false;
972 if (tc->text_sel_start == tc->text_sel_end) {
973 tc->text_sel_end.nextCursorPosition();
974 noSelection = true;
975 }
977 iterator_pair del_pair;
978 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, del_pair);
980 if (noSelection) {
981 tc->text_sel_start = tc->text_sel_end = del_pair.first;
982 } else {
983 if (success) {
984 tc->text_sel_start = tc->text_sel_end = del_pair.first;
985 } else { // nothing deleted
986 tc->text_sel_start = del_pair.first;
987 tc->text_sel_end = del_pair.second;
988 }
989 }
992 sp_text_context_update_cursor(tc);
993 sp_text_context_update_text_selection(tc);
994 sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT,
995 _("Delete"));
996 }
997 return TRUE;
998 case GDK_Left:
999 case GDK_KP_Left:
1000 case GDK_KP_4:
1001 if (tc->text) {
1002 if (MOD__ALT) {
1003 gint mul = 1 + gobble_key_events(
1004 get_group0_keyval(&event->key), 0); // with any mask
1005 if (MOD__SHIFT)
1006 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(mul*-10, 0));
1007 else
1008 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(mul*-1, 0));
1009 sp_text_context_update_cursor(tc);
1010 sp_text_context_update_text_selection(tc);
1011 sp_document_maybe_done(sp_desktop_document(desktop), "kern:left", SP_VERB_CONTEXT_TEXT,
1012 _("Kern to the left"));
1013 } else {
1014 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorLeftWithControl
1015 : &Inkscape::Text::Layout::iterator::cursorLeft;
1016 break;
1017 }
1018 }
1019 return TRUE;
1020 case GDK_Right:
1021 case GDK_KP_Right:
1022 case GDK_KP_6:
1023 if (tc->text) {
1024 if (MOD__ALT) {
1025 gint mul = 1 + gobble_key_events(
1026 get_group0_keyval(&event->key), 0); // with any mask
1027 if (MOD__SHIFT)
1028 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(mul*10, 0));
1029 else
1030 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(mul*1, 0));
1031 sp_text_context_update_cursor(tc);
1032 sp_text_context_update_text_selection(tc);
1033 sp_document_maybe_done(sp_desktop_document(desktop), "kern:right", SP_VERB_CONTEXT_TEXT,
1034 _("Kern to the right"));
1035 } else {
1036 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorRightWithControl
1037 : &Inkscape::Text::Layout::iterator::cursorRight;
1038 break;
1039 }
1040 }
1041 return TRUE;
1042 case GDK_Up:
1043 case GDK_KP_Up:
1044 case GDK_KP_8:
1045 if (tc->text) {
1046 if (MOD__ALT) {
1047 gint mul = 1 + gobble_key_events(
1048 get_group0_keyval(&event->key), 0); // with any mask
1049 if (MOD__SHIFT)
1050 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, mul*-10));
1051 else
1052 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, mul*-1));
1053 sp_text_context_update_cursor(tc);
1054 sp_text_context_update_text_selection(tc);
1055 sp_document_maybe_done(sp_desktop_document(desktop), "kern:up", SP_VERB_CONTEXT_TEXT,
1056 _("Kern up"));
1058 } else {
1059 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorUpWithControl
1060 : &Inkscape::Text::Layout::iterator::cursorUp;
1061 break;
1062 }
1063 }
1064 return TRUE;
1065 case GDK_Down:
1066 case GDK_KP_Down:
1067 case GDK_KP_2:
1068 if (tc->text) {
1069 if (MOD__ALT) {
1070 gint mul = 1 + gobble_key_events(
1071 get_group0_keyval(&event->key), 0); // with any mask
1072 if (MOD__SHIFT)
1073 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, mul*10));
1074 else
1075 sp_te_adjust_kerning_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, NR::Point(0, mul*1));
1076 sp_text_context_update_cursor(tc);
1077 sp_text_context_update_text_selection(tc);
1078 sp_document_maybe_done(sp_desktop_document(desktop), "kern:down", SP_VERB_CONTEXT_TEXT,
1079 _("Kern down"));
1081 } else {
1082 cursor_movement_operator = MOD__CTRL ? &Inkscape::Text::Layout::iterator::cursorDownWithControl
1083 : &Inkscape::Text::Layout::iterator::cursorDown;
1084 break;
1085 }
1086 }
1087 return TRUE;
1088 case GDK_Home:
1089 case GDK_KP_Home:
1090 if (tc->text) {
1091 if (MOD__CTRL)
1092 cursor_movement_operator = &Inkscape::Text::Layout::iterator::thisStartOfShape;
1093 else
1094 cursor_movement_operator = &Inkscape::Text::Layout::iterator::thisStartOfLine;
1095 break;
1096 }
1097 return TRUE;
1098 case GDK_End:
1099 case GDK_KP_End:
1100 if (tc->text) {
1101 if (MOD__CTRL)
1102 cursor_movement_operator = &Inkscape::Text::Layout::iterator::nextStartOfShape;
1103 else
1104 cursor_movement_operator = &Inkscape::Text::Layout::iterator::thisEndOfLine;
1105 break;
1106 }
1107 return TRUE;
1108 case GDK_Escape:
1109 if (tc->creating) {
1110 tc->creating = 0;
1111 if (tc->grabbed) {
1112 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
1113 tc->grabbed = NULL;
1114 }
1115 Inkscape::Rubberband::get()->stop();
1116 } else {
1117 sp_desktop_selection(desktop)->clear();
1118 }
1119 tc->nascent_object = FALSE;
1120 return TRUE;
1121 case GDK_bracketleft:
1122 if (tc->text) {
1123 if (MOD__ALT || MOD__CTRL) {
1124 if (MOD__ALT) {
1125 if (MOD__SHIFT) {
1126 // FIXME: alt+shift+[] does not work, don't know why
1127 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1128 } else {
1129 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1130 }
1131 } else {
1132 sp_te_adjust_rotation(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -90);
1133 }
1134 sp_document_maybe_done(sp_desktop_document(desktop), "textrot:ccw", SP_VERB_CONTEXT_TEXT,
1135 _("Rotate counterclockwise"));
1136 sp_text_context_update_cursor(tc);
1137 sp_text_context_update_text_selection(tc);
1138 return TRUE;
1139 }
1140 }
1141 break;
1142 case GDK_bracketright:
1143 if (tc->text) {
1144 if (MOD__ALT || MOD__CTRL) {
1145 if (MOD__ALT) {
1146 if (MOD__SHIFT) {
1147 // FIXME: alt+shift+[] does not work, don't know why
1148 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1149 } else {
1150 sp_te_adjust_rotation_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1151 }
1152 } else {
1153 sp_te_adjust_rotation(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 90);
1154 }
1155 sp_document_maybe_done(sp_desktop_document(desktop), "textrot:cw", SP_VERB_CONTEXT_TEXT,
1156 _("Rotate clockwise"));
1157 sp_text_context_update_cursor(tc);
1158 sp_text_context_update_text_selection(tc);
1159 return TRUE;
1160 }
1161 }
1162 break;
1163 case GDK_less:
1164 case GDK_comma:
1165 if (tc->text) {
1166 if (MOD__ALT) {
1167 if (MOD__CTRL) {
1168 if (MOD__SHIFT)
1169 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1170 else
1171 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1172 sp_document_maybe_done(sp_desktop_document(desktop), "linespacing:dec", SP_VERB_CONTEXT_TEXT,
1173 _("Contract line spacing"));
1175 } else {
1176 if (MOD__SHIFT)
1177 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -10);
1178 else
1179 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, -1);
1180 sp_document_maybe_done(sp_desktop_document(desktop), "letterspacing:dec", SP_VERB_CONTEXT_TEXT,
1181 _("Contract letter spacing"));
1183 }
1184 sp_text_context_update_cursor(tc);
1185 sp_text_context_update_text_selection(tc);
1186 return TRUE;
1187 }
1188 }
1189 break;
1190 case GDK_greater:
1191 case GDK_period:
1192 if (tc->text) {
1193 if (MOD__ALT) {
1194 if (MOD__CTRL) {
1195 if (MOD__SHIFT)
1196 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1197 else
1198 sp_te_adjust_linespacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1199 sp_document_maybe_done(sp_desktop_document(desktop), "linespacing:inc", SP_VERB_CONTEXT_TEXT,
1200 _("Expand line spacing"));
1202 } else {
1203 if (MOD__SHIFT)
1204 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 10);
1205 else
1206 sp_te_adjust_tspan_letterspacing_screen(tc->text, tc->text_sel_start, tc->text_sel_end, desktop, 1);
1207 sp_document_maybe_done(sp_desktop_document(desktop), "letterspacing:inc", SP_VERB_CONTEXT_TEXT,
1208 _("Expand letter spacing"));
1210 }
1211 sp_text_context_update_cursor(tc);
1212 sp_text_context_update_text_selection(tc);
1213 return TRUE;
1214 }
1215 }
1216 break;
1217 default:
1218 break;
1219 }
1221 if (cursor_movement_operator) {
1222 Inkscape::Text::Layout::iterator old_start = tc->text_sel_start;
1223 Inkscape::Text::Layout::iterator old_end = tc->text_sel_end;
1224 (tc->text_sel_end.*cursor_movement_operator)();
1225 if (!MOD__SHIFT)
1226 tc->text_sel_start = tc->text_sel_end;
1227 if (old_start != tc->text_sel_start || old_end != tc->text_sel_end) {
1228 sp_text_context_update_cursor(tc);
1229 sp_text_context_update_text_selection(tc);
1230 }
1231 return TRUE;
1232 }
1234 } else return TRUE; // return the "I took care of it" value if it was consumed by the IM
1235 } else { // do nothing if there's no object to type in - the key will be sent to parent context,
1236 // except up/down that are swallowed to prevent the zoom field from activation
1237 if ((group0_keyval == GDK_Up ||
1238 group0_keyval == GDK_Down ||
1239 group0_keyval == GDK_KP_Up ||
1240 group0_keyval == GDK_KP_Down )
1241 && !MOD__CTRL_ONLY) {
1242 return TRUE;
1243 } else if (group0_keyval == GDK_Escape) { // cancel rubberband
1244 if (tc->creating) {
1245 tc->creating = 0;
1246 if (tc->grabbed) {
1247 sp_canvas_item_ungrab(tc->grabbed, GDK_CURRENT_TIME);
1248 tc->grabbed = NULL;
1249 }
1250 Inkscape::Rubberband::get()->stop();
1251 }
1252 }
1253 }
1254 break;
1255 }
1257 case GDK_KEY_RELEASE:
1258 if (!tc->unimode && tc->imc && gtk_im_context_filter_keypress(tc->imc, (GdkEventKey*) event)) {
1259 return TRUE;
1260 }
1261 break;
1262 default:
1263 break;
1264 }
1266 // if nobody consumed it so far
1267 if (((SPEventContextClass *) parent_class)->root_handler) { // and there's a handler in parent context,
1268 return ((SPEventContextClass *) parent_class)->root_handler(event_context, event); // send event to parent
1269 } else {
1270 return FALSE; // return "I did nothing" value so that global shortcuts can be activated
1271 }
1272 }
1274 /**
1275 Attempts to paste system clipboard into the currently edited text, returns true on success
1276 */
1277 bool
1278 sp_text_paste_inline(SPEventContext *ec)
1279 {
1280 if (!SP_IS_TEXT_CONTEXT(ec))
1281 return false;
1283 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
1285 if ((tc->text) || (tc->nascent_object)) {
1286 // there is an active text object in this context, or a new object was just created
1288 Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
1289 Glib::ustring const text = refClipboard->wait_for_text();
1291 if (!text.empty()) {
1293 if (!tc->text) { // create text if none (i.e. if nascent_object)
1294 sp_text_context_setup_text(tc);
1295 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
1296 }
1298 // using indices is slow in ustrings. Whatever.
1299 Glib::ustring::size_type begin = 0;
1300 for ( ; ; ) {
1301 Glib::ustring::size_type end = text.find('\n', begin);
1302 if (end == Glib::ustring::npos) {
1303 if (begin != text.length())
1304 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());
1305 break;
1306 }
1307 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());
1308 tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start);
1309 begin = end + 1;
1310 }
1311 sp_document_done(sp_desktop_document(ec->desktop), SP_VERB_CONTEXT_TEXT,
1312 _("Paste text"));
1314 return true;
1315 }
1316 } // FIXME: else create and select a new object under cursor!
1318 return false;
1319 }
1321 /**
1322 Gets the raw characters that comprise the currently selected text, converting line
1323 breaks into lf characters.
1324 */
1325 Glib::ustring
1326 sp_text_get_selected_text(SPEventContext const *ec)
1327 {
1328 if (!SP_IS_TEXT_CONTEXT(ec))
1329 return "";
1330 SPTextContext const *tc = SP_TEXT_CONTEXT(ec);
1331 if (tc->text == NULL)
1332 return "";
1334 return sp_te_get_string_multiline(tc->text, tc->text_sel_start, tc->text_sel_end);
1335 }
1337 /**
1338 Deletes the currently selected characters. Returns false if there is no
1339 text selection currently.
1340 */
1341 bool sp_text_delete_selection(SPEventContext *ec)
1342 {
1343 if (!SP_IS_TEXT_CONTEXT(ec))
1344 return false;
1345 SPTextContext *tc = SP_TEXT_CONTEXT(ec);
1346 if (tc->text == NULL)
1347 return false;
1349 if (tc->text_sel_start == tc->text_sel_end)
1350 return false;
1352 iterator_pair pair;
1353 bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, pair);
1356 if (success) {
1357 tc->text_sel_start = tc->text_sel_end = pair.first;
1358 } else { // nothing deleted
1359 tc->text_sel_start = pair.first;
1360 tc->text_sel_end = pair.second;
1361 }
1363 sp_text_context_update_cursor(tc);
1364 sp_text_context_update_text_selection(tc);
1366 return true;
1367 }
1369 /**
1370 * \param selection Should not be NULL.
1371 */
1372 static void
1373 sp_text_context_selection_changed(Inkscape::Selection *selection, SPTextContext *tc)
1374 {
1375 g_assert(selection != NULL);
1377 SPEventContext *ec = SP_EVENT_CONTEXT(tc);
1379 if (ec->shape_knot_holder) { // destroy knotholder
1380 sp_knot_holder_destroy(ec->shape_knot_holder);
1381 ec->shape_knot_holder = NULL;
1382 }
1384 if (ec->shape_repr) { // remove old listener
1385 sp_repr_remove_listener_by_data(ec->shape_repr, ec);
1386 Inkscape::GC::release(ec->shape_repr);
1387 ec->shape_repr = 0;
1388 }
1390 SPItem *item = selection->singleItem();
1391 if (item && SP_IS_FLOWTEXT (item) && SP_FLOWTEXT(item)->has_internal_frame()) {
1392 ec->shape_knot_holder = sp_item_knot_holder(item, ec->desktop);
1393 Inkscape::XML::Node *shape_repr = SP_OBJECT_REPR(SP_FLOWTEXT(item)->get_frame(NULL));
1394 if (shape_repr) {
1395 ec->shape_repr = shape_repr;
1396 Inkscape::GC::anchor(shape_repr);
1397 sp_repr_add_listener(shape_repr, &ec_shape_repr_events, ec);
1398 }
1399 }
1401 if (tc->text && (item != tc->text)) {
1402 sp_text_context_forget_text(tc);
1403 }
1404 tc->text = NULL;
1406 if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) {
1407 tc->text = item;
1408 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1409 if (layout)
1410 tc->text_sel_start = tc->text_sel_end = layout->end();
1411 } else {
1412 tc->text = NULL;
1413 }
1415 // we update cursor without scrolling, because this position may not be final;
1416 // item_handler moves cusros to the point of click immediately
1417 sp_text_context_update_cursor(tc, false);
1418 sp_text_context_update_text_selection(tc);
1419 }
1421 static void
1422 sp_text_context_selection_modified(Inkscape::Selection */*selection*/, guint /*flags*/, SPTextContext *tc)
1423 {
1424 sp_text_context_update_cursor(tc);
1425 sp_text_context_update_text_selection(tc);
1426 }
1428 static bool
1429 sp_text_context_style_set(SPCSSAttr const *css, SPTextContext *tc)
1430 {
1431 if (tc->text == NULL)
1432 return false;
1433 if (tc->text_sel_start == tc->text_sel_end)
1434 return false; // will get picked up by the parent and applied to the whole text object
1436 sp_te_apply_style(tc->text, tc->text_sel_start, tc->text_sel_end, css);
1437 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT,
1438 _("Set text style"));
1439 sp_text_context_update_cursor(tc);
1440 sp_text_context_update_text_selection(tc);
1442 return true;
1443 }
1445 static int
1446 sp_text_context_style_query(SPStyle *style, int property, SPTextContext *tc)
1447 {
1448 if (tc->text == NULL)
1449 return QUERY_STYLE_NOTHING;
1450 const Inkscape::Text::Layout *layout = te_get_layout(tc->text);
1451 if (layout == NULL)
1452 return QUERY_STYLE_NOTHING;
1453 sp_text_context_validate_cursor_iterators(tc);
1455 GSList *styles_list = NULL;
1457 Inkscape::Text::Layout::iterator begin_it, end_it;
1458 if (tc->text_sel_start < tc->text_sel_end) {
1459 begin_it = tc->text_sel_start;
1460 end_it = tc->text_sel_end;
1461 } else {
1462 begin_it = tc->text_sel_end;
1463 end_it = tc->text_sel_start;
1464 }
1465 if (begin_it == end_it)
1466 if (!begin_it.prevCharacter())
1467 end_it.nextCharacter();
1468 for (Inkscape::Text::Layout::iterator it = begin_it ; it < end_it ; it.nextStartOfSpan()) {
1469 SPObject const *pos_obj = 0;
1470 void *rawptr = 0;
1471 layout->getSourceOfCharacter(it, &rawptr);
1472 if (!rawptr || !SP_IS_OBJECT(rawptr))
1473 continue;
1474 pos_obj = SP_OBJECT(rawptr);
1475 while (SP_IS_STRING(pos_obj) && SP_OBJECT_PARENT(pos_obj)) {
1476 pos_obj = SP_OBJECT_PARENT(pos_obj); // SPStrings don't have style
1477 }
1478 styles_list = g_slist_prepend(styles_list, (gpointer)pos_obj);
1479 }
1481 int result = sp_desktop_query_style_from_list (styles_list, style, property);
1483 g_slist_free(styles_list);
1484 return result;
1485 }
1487 static void
1488 sp_text_context_validate_cursor_iterators(SPTextContext *tc)
1489 {
1490 if (tc->text == NULL)
1491 return;
1492 Inkscape::Text::Layout const *layout = te_get_layout(tc->text);
1493 if (layout) { // undo can change the text length without us knowing it
1494 layout->validateIterator(&tc->text_sel_start);
1495 layout->validateIterator(&tc->text_sel_end);
1496 }
1497 }
1499 static void
1500 sp_text_context_update_cursor(SPTextContext *tc, bool scroll_to_see)
1501 {
1502 GdkRectangle im_cursor = { 0, 0, 1, 1 };
1504 // due to interruptible display, tc may already be destroyed during a display update before
1505 // the cursor update (can't do both atomically, alas)
1506 if (!tc->desktop) return;
1508 if (tc->text) {
1509 NR::Point p0, p1;
1510 sp_te_get_cursor_coords(tc->text, tc->text_sel_end, p0, p1);
1511 NR::Point const d0 = p0 * sp_item_i2d_affine(SP_ITEM(tc->text));
1512 NR::Point const d1 = p1 * sp_item_i2d_affine(SP_ITEM(tc->text));
1514 // scroll to show cursor
1515 if (scroll_to_see) {
1516 NR::Point const dm = (d0 + d1) / 2;
1517 // unlike mouse moves, here we must scroll all the way at first shot, so we override the autoscrollspeed
1518 SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(&dm, 1.0);
1519 }
1521 sp_canvas_item_show(tc->cursor);
1522 sp_ctrlline_set_coords(SP_CTRLLINE(tc->cursor), d0, d1);
1524 /* fixme: ... need another transformation to get canvas widget coordinate space? */
1525 im_cursor.x = (int) floor(d0[NR::X]);
1526 im_cursor.y = (int) floor(d0[NR::Y]);
1527 im_cursor.width = (int) floor(d1[NR::X]) - im_cursor.x;
1528 im_cursor.height = (int) floor(d1[NR::Y]) - im_cursor.y;
1530 tc->show = TRUE;
1531 tc->phase = 1;
1533 if (SP_IS_FLOWTEXT(tc->text)) {
1534 SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (NULL); // first frame only
1535 if (frame) {
1536 sp_canvas_item_show(tc->frame);
1537 NR::Maybe<NR::Rect> frame_bbox = sp_item_bbox_desktop(frame);
1538 if (frame_bbox) {
1539 SP_CTRLRECT(tc->frame)->setRectangle(*frame_bbox);
1540 }
1541 }
1542 SP_EVENT_CONTEXT(tc)->_message_context->set(Inkscape::NORMAL_MESSAGE, _("Type flowed text; <b>Enter</b> to start new paragraph."));
1543 } else {
1544 SP_EVENT_CONTEXT(tc)->_message_context->set(Inkscape::NORMAL_MESSAGE, _("Type text; <b>Enter</b> to start new line."));
1545 }
1547 } else {
1548 sp_canvas_item_hide(tc->cursor);
1549 sp_canvas_item_hide(tc->frame);
1550 tc->show = FALSE;
1551 if (!tc->nascent_object) {
1552 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
1553 }
1554 }
1556 if (tc->imc) {
1557 gtk_im_context_set_cursor_location(tc->imc, &im_cursor);
1558 }
1559 SP_EVENT_CONTEXT(tc)->desktop->emitToolSubselectionChanged((gpointer)tc);
1560 }
1562 static void sp_text_context_update_text_selection(SPTextContext *tc)
1563 {
1564 // due to interruptible display, tc may already be destroyed during a display update before
1565 // the selection update (can't do both atomically, alas)
1566 if (!tc->desktop) return;
1568 for (std::vector<SPCanvasItem*>::iterator it = tc->text_selection_quads.begin() ; it != tc->text_selection_quads.end() ; it++) {
1569 sp_canvas_item_hide(*it);
1570 gtk_object_destroy(*it);
1571 }
1572 tc->text_selection_quads.clear();
1574 std::vector<NR::Point> quads;
1575 if (tc->text != NULL)
1576 quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, sp_item_i2d_affine(tc->text));
1577 for (unsigned i = 0 ; i < quads.size() ; i += 4) {
1578 SPCanvasItem *quad_canvasitem;
1579 quad_canvasitem = sp_canvas_item_new(sp_desktop_controls(tc->desktop), SP_TYPE_CTRLQUADR, NULL);
1580 sp_ctrlquadr_set_rgba32(SP_CTRLQUADR(quad_canvasitem), 0x000000ff);
1581 sp_ctrlquadr_set_coords(SP_CTRLQUADR(quad_canvasitem), quads[i], quads[i+1], quads[i+2], quads[i+3]);
1582 sp_canvas_item_show(quad_canvasitem);
1583 tc->text_selection_quads.push_back(quad_canvasitem);
1584 }
1585 }
1587 static gint
1588 sp_text_context_timeout(SPTextContext *tc)
1589 {
1590 if (tc->show) {
1591 if (tc->phase) {
1592 tc->phase = 0;
1593 sp_canvas_item_hide(tc->cursor);
1594 } else {
1595 tc->phase = 1;
1596 sp_canvas_item_show(tc->cursor);
1597 }
1598 }
1600 return TRUE;
1601 }
1603 static void
1604 sp_text_context_forget_text(SPTextContext *tc)
1605 {
1606 if (! tc->text) return;
1607 SPItem *ti = tc->text;
1608 (void)ti;
1609 /* We have to set it to zero,
1610 * or selection changed signal messes everything up */
1611 tc->text = NULL;
1613 /* FIXME: this automatic deletion when nothing is inputted crashes the XML edittor and also crashes when duplicating an empty flowtext.
1614 So don't create an empty flowtext in the first place? Create it when first character is typed.
1615 */
1616 /*
1617 if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) {
1618 Inkscape::XML::Node *text_repr=SP_OBJECT_REPR(ti);
1619 // the repr may already have been unparented
1620 // if we were called e.g. as the result of
1621 // an undo or the element being removed from
1622 // the XML editor
1623 if ( text_repr && sp_repr_parent(text_repr) ) {
1624 sp_repr_unparent(text_repr);
1625 sp_document_done(sp_desktop_document(tc->desktop), SP_VERB_CONTEXT_TEXT,
1626 _("Remove empty text"));
1627 }
1628 }
1629 */
1630 }
1632 gint
1633 sptc_focus_in(GtkWidget */*widget*/, GdkEventFocus */*event*/, SPTextContext *tc)
1634 {
1635 gtk_im_context_focus_in(tc->imc);
1636 return FALSE;
1637 }
1639 gint
1640 sptc_focus_out(GtkWidget */*widget*/, GdkEventFocus */*event*/, SPTextContext *tc)
1641 {
1642 gtk_im_context_focus_out(tc->imc);
1643 return FALSE;
1644 }
1646 static void
1647 sptc_commit(GtkIMContext */*imc*/, gchar *string, SPTextContext *tc)
1648 {
1649 if (!tc->text) {
1650 sp_text_context_setup_text(tc);
1651 tc->nascent_object = 0; // we don't need it anymore, having created a real <text>
1652 }
1654 tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, string);
1655 sp_text_context_update_cursor(tc);
1656 sp_text_context_update_text_selection(tc);
1658 sp_document_done(SP_OBJECT_DOCUMENT(tc->text), SP_VERB_CONTEXT_TEXT,
1659 _("Type text"));
1660 }
1663 /*
1664 Local Variables:
1665 mode:c++
1666 c-file-style:"stroustrup"
1667 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1668 indent-tabs-mode:nil
1669 fill-column:99
1670 End:
1671 */
1672 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :