Code

patch from bug 1497803 by Gustav Broberg; allow to reflow an already flowed text
[inkscape.git] / src / text-chemistry.cpp
1 #define __SP_TEXT_CHEMISTRY_C__
3 /*
4  * Text commands
5  *
6  * Authors:
7  *   bulia byak
8  *
9  * Copyright (C) 2004 authors
10  *
11  * Released under GNU GPL, read the file 'COPYING' for more information
12  */
14 #ifdef HAVE_CONFIG_H
15 # include <config.h>
16 #endif
17 #include "libnr/nr-matrix-fns.h"
18 #include "xml/repr.h"
19 #include <glibmm/i18n.h>
20 #include "sp-rect.h"
21 #include "sp-textpath.h"
22 #include "inkscape.h"
23 #include "document.h"
24 #include "message-stack.h"
25 #include "selection.h"
26 #include "style.h"
27 #include "desktop-handles.h"
28 #include "text-editing.h"
29 #include "text-chemistry.h"
30 #include "sp-flowtext.h"
31 #include "sp-flowregion.h"
32 #include "sp-flowdiv.h"
35 SPItem *
36 text_in_selection(Inkscape::Selection *selection)
37 {
38     for (GSList *items = (GSList *) selection->itemList();
39          items != NULL;
40          items = items->next) {
41         if (SP_IS_TEXT(items->data))
42             return ((SPItem *) items->data);
43     }
44     return NULL;
45 }
47 SPItem *
48 flowtext_in_selection(Inkscape::Selection *selection)
49 {
50     for (GSList *items = (GSList *) selection->itemList();
51          items != NULL;
52          items = items->next) {
53         if (SP_IS_FLOWTEXT(items->data))
54             return ((SPItem *) items->data);
55     }
56     return NULL;
57 }
59 SPItem *
60 text_or_flowtext_in_selection(Inkscape::Selection *selection)
61 {
62     for (GSList *items = (GSList *) selection->itemList();
63          items != NULL;
64          items = items->next) {
65         if (SP_IS_TEXT(items->data) || SP_IS_FLOWTEXT(items->data))
66             return ((SPItem *) items->data);
67     }
68     return NULL;
69 }
71 SPItem *
72 shape_in_selection(Inkscape::Selection *selection)
73 {
74     for (GSList *items = (GSList *) selection->itemList();
75          items != NULL;
76          items = items->next) {
77         if (SP_IS_SHAPE(items->data))
78             return ((SPItem *) items->data);
79     }
80     return NULL;
81 }
83 void
84 text_put_on_path()
85 {
86     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
87     if (!desktop)
88         return;
90     Inkscape::Selection *selection = sp_desktop_selection(desktop);
92     SPItem *text = text_or_flowtext_in_selection(selection);
93     SPItem *shape = shape_in_selection(selection);
95     if (!text || !shape || g_slist_length((GSList *) selection->itemList()) != 2) {
96         sp_desktop_message_stack(desktop)->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a text and a path</b> to put text on path."));
97         return;
98     }
100     if (SP_IS_TEXT_TEXTPATH(text)) {
101         sp_desktop_message_stack(desktop)->flash(Inkscape::ERROR_MESSAGE, _("This text object is <b>already put on a path</b>. Remove it from the path first. Use <b>Shift+D</b> to look up its path."));
102         return;
103     }
105     if (SP_IS_RECT(shape)) {
106         // rect is the only SPShape which is not <path> yet, and thus SVG forbids us from putting text on it
107         sp_desktop_message_stack(desktop)->flash(Inkscape::ERROR_MESSAGE, _("You cannot put text on a rectangle in this version. Convert rectangle to path first."));
108         return;
109     }
111     // if a flowed text is selected, convert it to a regular text object
112     if (SP_IS_FLOWTEXT(text)) {
113         Inkscape::XML::Node *repr = SP_FLOWTEXT(text)->getAsText();
114         Inkscape::XML::Node *parent = SP_OBJECT_REPR(text)->parent();
115         parent->appendChild(repr);
117         SPItem *new_item = (SPItem *) sp_desktop_document(desktop)->getObjectByRepr(repr);
118         sp_item_write_transform(new_item, repr, text->transform);
119         SP_OBJECT(new_item)->updateRepr();
121         Inkscape::GC::release(repr);
122         text->deleteObject(); // delete the orignal flowtext
124         sp_document_ensure_up_to_date(sp_desktop_document(desktop));
126         selection->clear();
128         text = new_item; // point to the new text
129     }
131     Inkscape::Text::Layout const *layout = te_get_layout(text);
132     Inkscape::Text::Layout::Alignment text_alignment = layout->paragraphAlignment(layout->begin());
134     // remove transform from text, but recursively scale text's fontsize by the expansion
135     SP_TEXT(text)->_adjustFontsizeRecursive (text, NR::expansion(SP_ITEM(text)->transform));
136     SP_OBJECT_REPR(text)->setAttribute("transform", NULL);
138     // make a list of text children
139     GSList *text_reprs = NULL;
140     for (SPObject *o = SP_OBJECT(text)->children; o != NULL; o = o->next) {
141         text_reprs = g_slist_prepend(text_reprs, SP_OBJECT_REPR(o));
142     }
144     // create textPath and put it into the text
145     Inkscape::XML::Node *textpath = sp_repr_new("svg:textPath");
146     // reference the shape
147     textpath->setAttribute("xlink:href", g_strdup_printf("#%s", SP_OBJECT_REPR(shape)->attribute("id")));
148     if (text_alignment == Inkscape::Text::Layout::RIGHT)
149         textpath->setAttribute("startOffset", "100%");
150     else if (text_alignment == Inkscape::Text::Layout::CENTER)
151         textpath->setAttribute("startOffset", "50%");
152     SP_OBJECT_REPR(text)->addChild(textpath, NULL);
154     for ( GSList *i = text_reprs ; i ; i = i->next ) {
155         // make a copy of each text child
156         Inkscape::XML::Node *copy = ((Inkscape::XML::Node *) i->data)->duplicate();
157         // We cannot have multiline in textpath, so remove line attrs from tspans
158         if (!strcmp(copy->name(), "svg:tspan")) {
159             copy->setAttribute("sodipodi:role", NULL);
160             copy->setAttribute("x", NULL);
161             copy->setAttribute("y", NULL);
162         }
163         // remove the old repr from under text
164         SP_OBJECT_REPR(text)->removeChild((Inkscape::XML::Node *) i->data);
165         // put its copy into under textPath
166         textpath->addChild(copy, NULL); // fixme: copy id
167     }
169     // x/y are useless with textpath, and confuse Batik 1.5
170     SP_OBJECT_REPR(text)->setAttribute("x", NULL);
171     SP_OBJECT_REPR(text)->setAttribute("y", NULL);
173     sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, 
174                      _("Put text on path"));
175     g_slist_free(text_reprs);
178 void
179 text_remove_from_path()
181     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
183     Inkscape::Selection *selection = sp_desktop_selection(desktop);
185     if (selection->isEmpty()) {
186         sp_desktop_message_stack(desktop)->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a text on path</b> to remove it from path."));
187         return;
188     }
190     bool did = false;
192     for (GSList *items = g_slist_copy((GSList *) selection->itemList());
193          items != NULL;
194          items = items->next) {
196         if (!SP_IS_TEXT_TEXTPATH(SP_OBJECT(items->data))) {
197             continue;
198         }
200         SPObject *tp = sp_object_first_child(SP_OBJECT(items->data));
202         did = true;
204         sp_textpath_to_text(tp);
205     }
207     if (!did) {
208         sp_desktop_message_stack(desktop)->flash(Inkscape::ERROR_MESSAGE, _("<b>No texts-on-paths</b> in the selection."));
209     } else {
210         selection->setList(g_slist_copy((GSList *) selection->itemList())); // reselect to update statusbar description
211         sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, 
212                          _("Remove text from path"));
213     }
216 void
217 text_remove_all_kerns_recursively(SPObject *o)
219     SP_OBJECT_REPR(o)->setAttribute("dx", NULL);
220     SP_OBJECT_REPR(o)->setAttribute("dy", NULL);
221     SP_OBJECT_REPR(o)->setAttribute("rotate", NULL);
223     for (SPObject *i = sp_object_first_child(o); i != NULL; i = SP_OBJECT_NEXT(i)) {
224         text_remove_all_kerns_recursively(i);
225     }
228 //FIXME: must work with text selection
229 void
230 text_remove_all_kerns()
232     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
234     Inkscape::Selection *selection = sp_desktop_selection(desktop);
236     if (selection->isEmpty()) {
237         sp_desktop_message_stack(desktop)->flash(Inkscape::WARNING_MESSAGE, _("Select <b>text(s)</b> to remove kerns from."));
238         return;
239     }
241     bool did = false;
243     for (GSList *items = g_slist_copy((GSList *) selection->itemList());
244          items != NULL;
245          items = items->next) {
247         if (!SP_IS_TEXT(SP_OBJECT(items->data))) {
248             continue;
249         }
251         text_remove_all_kerns_recursively(SP_OBJECT(items->data));
252         SP_OBJECT(items->data)->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
253         did = true;
254     }
256     if (!did) {
257         sp_desktop_message_stack(desktop)->flash(Inkscape::ERROR_MESSAGE, _("Select <b>text(s)</b> to remove kerns from."));
258     } else {
259         sp_document_done(sp_desktop_document(desktop), SP_VERB_CONTEXT_TEXT, 
260                          _("Remove manual kerns"));
261     }
264 void
265 text_flow_into_shape()
267     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
268     if (!desktop)
269         return;
271     SPDocument *doc = sp_desktop_document (desktop);
273     Inkscape::Selection *selection = sp_desktop_selection(desktop);
275     SPItem *text = text_or_flowtext_in_selection(selection);
276     SPItem *shape = shape_in_selection(selection);
278     if (!text || !shape || g_slist_length((GSList *) selection->itemList()) < 2) {
279         sp_desktop_message_stack(desktop)->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a text</b> and one or more <b>paths or shapes</b> to flow text into frame."));
280         return;
281     }
283     if (SP_IS_TEXT(text)) {
284       // remove transform from text, but recursively scale text's fontsize by the expansion
285       SP_TEXT(text)->_adjustFontsizeRecursive(text, NR::expansion(SP_ITEM(text)->transform));
286       SP_OBJECT_REPR(text)->setAttribute("transform", NULL);
287     }
289     Inkscape::XML::Node *root_repr = sp_repr_new("svg:flowRoot");
290     root_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
291     root_repr->setAttribute("style", SP_OBJECT_REPR(text)->attribute("style")); // fixme: transfer style attrs too
292     SP_OBJECT_REPR(SP_OBJECT_PARENT(shape))->appendChild(root_repr);
293     SPObject *root_object = doc->getObjectByRepr(root_repr);
294     g_return_if_fail(SP_IS_FLOWTEXT(root_object));
296     Inkscape::XML::Node *region_repr = sp_repr_new("svg:flowRegion");
297     root_repr->appendChild(region_repr);
298     SPObject *object = doc->getObjectByRepr(region_repr);
299     g_return_if_fail(SP_IS_FLOWREGION(object));
301     /* Add clones */
302     for (GSList *items = (GSList *) selection->itemList();
303          items != NULL;
304          items = items->next) {
305         SPItem *item = SP_ITEM(items->data);
306         if (SP_IS_SHAPE(item)){
307             Inkscape::XML::Node *clone = sp_repr_new("svg:use");
308             clone->setAttribute("x", "0");
309             clone->setAttribute("y", "0");
310             clone->setAttribute("xlink:href", g_strdup_printf("#%s", SP_OBJECT_REPR(item)->attribute("id")));
312             // add the new clone to the region
313             region_repr->appendChild(clone);
314         }
315     }
317     if (SP_IS_TEXT(text)) { // flow from text, as string
318         Inkscape::XML::Node *para_repr = sp_repr_new("svg:flowPara");
319         root_repr->appendChild(para_repr);
320         object = doc->getObjectByRepr(para_repr);
321         g_return_if_fail(SP_IS_FLOWPARA(object));
323         Inkscape::Text::Layout const *layout = te_get_layout(text);
324         Glib::ustring text_ustring = sp_te_get_string_multiline(text, layout->begin(), layout->end());
326         Inkscape::XML::Node *text_repr = sp_repr_new_text(text_ustring.c_str()); // FIXME: transfer all formatting! and convert newlines into flowParas!
327         para_repr->appendChild(text_repr);
329         Inkscape::GC::release(para_repr);
330         Inkscape::GC::release(text_repr);
332     } else { // reflow an already flowed text, preserving paras
333         for (SPObject *o = SP_OBJECT(text)->children; o != NULL; o = o->next) {
334             if (SP_IS_FLOWPARA(o)) {
335                 Inkscape::XML::Node *para_repr = SP_OBJECT_REPR(o)->duplicate();
336                 root_repr->appendChild(para_repr);
337                 object = doc->getObjectByRepr(para_repr);
338                 g_return_if_fail(SP_IS_FLOWPARA(object));
339                 Inkscape::GC::release(para_repr);
340             }
341         }
342     }
344     SP_OBJECT(text)->deleteObject (true);
346     sp_document_done(doc, SP_VERB_CONTEXT_TEXT,
347                      _("Flow text into shape"));
349     sp_desktop_selection(desktop)->set(SP_ITEM(root_object));
351     Inkscape::GC::release(root_repr);
352     Inkscape::GC::release(region_repr);
355 void
356 text_unflow ()
358     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
359     if (!desktop)
360         return;
362     SPDocument *doc = sp_desktop_document (desktop);
364     Inkscape::Selection *selection = sp_desktop_selection(desktop);
367     if (!flowtext_in_selection(selection) || g_slist_length((GSList *) selection->itemList()) < 1) {
368         sp_desktop_message_stack(desktop)->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a flowed text</b> to unflow it."));
369         return;
370     }
372     GSList *new_objs = NULL;
373     GSList *old_objs = NULL;
375     for (GSList *items = g_slist_copy((GSList *) selection->itemList());
376          items != NULL;
377          items = items->next) {
379         if (!SP_IS_FLOWTEXT(SP_OBJECT(items->data))) {
380             continue;
381         }
383         SPItem *flowtext = SP_ITEM(items->data);
385         /* Create <text> */
386         Inkscape::XML::Node *rtext = sp_repr_new("svg:text");
387         rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
389         /* Set style */
390         rtext->setAttribute("style", SP_OBJECT_REPR(flowtext)->attribute("style")); // fixme: transfer style attrs too; and from descendants
392         NRRect bbox;
393         sp_item_invoke_bbox(SP_ITEM(flowtext), &bbox, sp_item_i2doc_affine(SP_ITEM(flowtext)), TRUE);
394         NR::Point xy(bbox.x0, bbox.y0);
395         if (xy[NR::X] != 1e18 && xy[NR::Y] != 1e18) {
396             sp_repr_set_svg_double(rtext, "x", xy[NR::X]);
397             sp_repr_set_svg_double(rtext, "y", xy[NR::Y]);
398         }
400         /* Create <tspan> */
401         Inkscape::XML::Node *rtspan = sp_repr_new("svg:tspan");
402         rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
403         rtext->addChild(rtspan, NULL);
405         gchar *text_string = sp_te_get_string_multiline(flowtext);
406         Inkscape::XML::Node *text_repr = sp_repr_new_text(text_string); // FIXME: transfer all formatting!!!
407         free(text_string);
408         rtspan->appendChild(text_repr);
410         SP_OBJECT_REPR(SP_OBJECT_PARENT(flowtext))->appendChild(rtext);
411         SPObject *text_object = doc->getObjectByRepr(rtext);
413         new_objs = g_slist_prepend (new_objs, text_object);
414         old_objs = g_slist_prepend (old_objs, flowtext);
416         Inkscape::GC::release(rtext);
417         Inkscape::GC::release(rtspan);
418         Inkscape::GC::release(text_repr);
419     }
421     selection->clear();
422     selection->setList(new_objs);
423     for (GSList *i = old_objs; i; i = i->next) {
424         SP_OBJECT(i->data)->deleteObject (true);
425     }
427     g_slist_free (old_objs);
428     g_slist_free (new_objs);
430     sp_document_done(doc, SP_VERB_CONTEXT_TEXT, 
431                      _("Unflow flowed text"));
434 void
435 flowtext_to_text()
437     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
439     Inkscape::Selection *selection = sp_desktop_selection(desktop);
441     if (selection->isEmpty()) {
442         sp_desktop_message_stack(desktop)->flash(Inkscape::WARNING_MESSAGE, 
443                                                  _("Select <b>flowed text(s)</b> to convert."));
444         return;
445     }
447     bool did = false;
449     GSList *reprs = NULL;
450     GSList *items = g_slist_copy((GSList *) selection->itemList());
451     for (; items != NULL; items = items->next) {
452         
453         SPItem *item = (SPItem *) items->data;
455         if (!SP_IS_FLOWTEXT(item))
456             continue;
458         did = true;
460         Inkscape::XML::Node *repr = SP_FLOWTEXT(item)->getAsText();
461         Inkscape::XML::Node *parent = SP_OBJECT_REPR(item)->parent();
462         parent->appendChild(repr);
464         SPItem *new_item = (SPItem *) sp_desktop_document(desktop)->getObjectByRepr(repr);
465         sp_item_write_transform(new_item, repr, item->transform);
466         SP_OBJECT(new_item)->updateRepr();
467     
468         Inkscape::GC::release(repr);
469         item->deleteObject();
471         reprs = g_slist_prepend(reprs, repr);
472     }
474     g_slist_free(items);
476     if (did) {
477         sp_document_done(sp_desktop_document(desktop), 
478                          SP_VERB_OBJECT_FLOWTEXT_TO_TEXT,
479                          _("Convert flowed text to text"));
480         selection->setReprList(reprs);        
481     } else {
482         sp_desktop_message_stack(desktop)->
483             flash(Inkscape::ERROR_MESSAGE,
484                   _("<b>No flowed text(s)</b> to convert in the selection."));
485     }
487     g_slist_free(reprs);
491 /*
492   Local Variables:
493   mode:c++
494   c-file-style:"stroustrup"
495   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
496   indent-tabs-mode:nil
497   fill-column:99
498   End:
499 */
500 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :