diff --git a/share/ui/menus.xml b/share/ui/menus.xml index a1b777e599bb730fd51bbace7f2d4a9bdd769db2..65654a2bf5c3a66966c43d4200bc357ed2a7c388 100644 --- a/share/ui/menus.xml +++ b/share/ui/menus.xml @@ -281,6 +281,7 @@ + diff --git a/src/object/sp-object.cpp b/src/object/sp-object.cpp index e44bd435f42c63074339b86ecedfa63617784230..d64905412fc9d535ca2284186578f955e209a18a 100644 --- a/src/object/sp-object.cpp +++ b/src/object/sp-object.cpp @@ -224,6 +224,16 @@ gchar const* SPObject::getId() const { return id; } +/** + * Returns the id as a url param, in the form 'url(#{id})' + */ +std::string SPObject::getUrl() const { + if (id) { + return std::string("url(#") + id + ")"; + } + return ""; +} + Inkscape::XML::Node * SPObject::getRepr() { return repr; } diff --git a/src/object/sp-object.h b/src/object/sp-object.h index 6388f3ba4b61cc96e395043d7a288751df0e5849..dff1283d588ee5b8f74341861652799c09d89ba3 100644 --- a/src/object/sp-object.h +++ b/src/object/sp-object.h @@ -215,6 +215,11 @@ public: */ char const* getId() const; + /** + * Get the id in a URL format. + */ + std::string getUrl() const; + /** * Returns the XML representation of tree */ diff --git a/src/object/sp-text.cpp b/src/object/sp-text.cpp index dad90d9f870ac1cbab9dd894ee37b20b4c7fa610..5ad11a6f17ab50779a49311b1ad67cd69677d3d8 100644 --- a/src/object/sp-text.cpp +++ b/src/object/sp-text.cpp @@ -512,7 +512,7 @@ void SPText::_buildLayoutInit() // Find union of all exclusion shapes Shape *exclusion_shape = nullptr; if(style->shape_subtract.set) { - exclusion_shape = _buildExclusionShape(); + exclusion_shape = getExclusionShape(); } // Find inside shape curves @@ -764,7 +764,7 @@ unsigned SPText::_buildLayoutInput(SPObject *object, Inkscape::Text::Layout::Opt return length; } -Shape* SPText::_buildExclusionShape() const +Shape* SPText::getExclusionShape() const { std::unique_ptr result(new Shape()); // Union of all exclusion shapes std::unique_ptr shape_temp(new Shape()); diff --git a/src/object/sp-text.h b/src/object/sp-text.h index 353844b4b64f064c6c233d0d88eb3dadd3e605be..2fe5fdbdd6f7a675ec93ceffc9aa6cc927202441 100644 --- a/src/object/sp-text.h +++ b/src/object/sp-text.h @@ -69,6 +69,9 @@ public: bool _optimizeTextpathText = false; + /** Union all exclusion shapes. */ + Shape* getExclusionShape() const; + private: /** Initializes layout from (i.e. this node). */ @@ -81,9 +84,6 @@ private: that we don't get a spurious extra one at the end of the flow. */ unsigned _buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath); - /** Union all exclusion shapes. */ - Shape* _buildExclusionShape() const; - /** Find first x/y values which may be in a descendent element. */ SVGLength* _getFirstXLength(); SVGLength* _getFirstYLength(); diff --git a/src/text-chemistry.cpp b/src/text-chemistry.cpp index ff81efce8cbfc8d5e6140316b8c7571a476cde33..0770e22118465153ca0abf6d8b28e0457e630a99 100644 --- a/src/text-chemistry.cpp +++ b/src/text-chemistry.cpp @@ -274,6 +274,39 @@ text_remove_all_kerns() } } +void +text_flow_shape_subtract() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + return; + + SPDocument *doc = desktop->getDocument(); + Inkscape::Selection *selection = desktop->getSelection(); + SPItem *text = text_or_flowtext_in_selection(selection); + + if (SP_IS_TEXT(text)) { + // Make list of all shapes. + Glib::ustring shapes; + auto items = selection->items(); + for (auto item : items) { + if (SP_IS_SHAPE(item)) { + if (!shapes.empty()) shapes += " "; + shapes += item->getUrl(); + } + } + + // Set 'shape-subtract' property. + text->style->shape_subtract.read(shapes.c_str()); + text->updateRepr(); + + DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, _("Flow text subtract shape")); + } else { + // SVG 1.2 Flowed Text + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Subtraction not available for SVG 1.2 Flowed text.")); + } +} + void text_flow_into_shape() { @@ -290,7 +323,7 @@ text_flow_into_shape() SPItem *shape = shape_in_selection(selection); if (!text || !shape || boost::distance(selection->items()) < 2) { - desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a text and one or more paths or shapes to flow text into frame.")); + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a text and one or more paths or shapes to flow text.")); return; } @@ -299,48 +332,34 @@ text_flow_into_shape() // SVG 2 Text if (SP_IS_TEXT(text)) { - unsigned shape_count = 0; - // Make list of all shapes. - Glib::ustring shape_inside; + Glib::ustring shapes; auto items = selection->items(); for (auto item : items) { if (SP_IS_SHAPE(item)) { - shape_inside += "url(#"; - shape_inside += item->getId(); - shape_inside += ") "; - - // can only take one shape into account for transform compensation - if (++shape_count > 1) - continue; - - // compensate transform - auto const new_transform = i2i_affine(item->parent, text->parent); - auto const ex = text->transform.descrim() / new_transform.descrim(); - static_cast(text)->_adjustFontsizeRecursive(text, ex); - text->transform = new_transform; - } - } + if (!shapes.empty()) { + shapes += " "; + } else { + // can only take one shape into account for transform compensation + // compensate transform + auto const new_transform = i2i_affine(item->parent, text->parent); + auto const ex = text->transform.descrim() / new_transform.descrim(); + static_cast(text)->_adjustFontsizeRecursive(text, ex); + text->transform = new_transform; + } - // Remove extra space at end. - if (shape_inside.length() > 1) { - shape_inside.erase (shape_inside.length() - 1); + shapes += item->getUrl(); + } } // Set 'shape-inside' property. - text->style->shape_inside.read(shape_inside.c_str()); + text->style->shape_inside.read(shapes.c_str()); text->style->white_space.read("pre"); // Respect new lines. - text->updateRepr(); - } - - DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, - _("Flow text into shape")); - + DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, _("Flow text into shape")); + } } else { - // SVG 1.2 Flowed Text - if (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)) { // remove transform from text, but recursively scale text's fontsize by the expansion auto ex = i2i_affine(text, shape->parent).descrim(); @@ -402,18 +421,16 @@ text_flow_into_shape() Inkscape::GC::release(para_repr); } } - } - - text->deleteObject(true); + } - DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, - _("Flow text into shape")); + text->deleteObject(true); - desktop->getSelection()->set(SP_ITEM(root_object)); + DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, _("Flow text into shape")); - Inkscape::GC::release(root_repr); - Inkscape::GC::release(region_repr); + desktop->getSelection()->set(SP_ITEM(root_object)); + Inkscape::GC::release(root_repr); + Inkscape::GC::release(region_repr); } } diff --git a/src/text-chemistry.h b/src/text-chemistry.h index 5d2df62c8be7b9614adc41a5b90290e0b8bfd241..1c38a9a3ddcde1da5e1c19569b8b2bcd62831b78 100644 --- a/src/text-chemistry.h +++ b/src/text-chemistry.h @@ -25,6 +25,7 @@ void text_put_on_path(); void text_remove_from_path(); void text_remove_all_kerns(); void text_flow_into_shape(); +void text_flow_shape_subtract(); void text_unflow(); void flowtext_to_text(); enum text_ref_t { TEXT_REF_DEF = 0x1, TEXT_REF_EXTERNAL = 0x2, TEXT_REF_INTERNAL = 0x4, }; diff --git a/src/ui/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp index 74805f89bbd44f386bc5cb4a4600f268af5bf8b8..34c26d3a5a47f0be7d7ff264f4907380e3b4bf67 100644 --- a/src/ui/shape-editor-knotholders.cpp +++ b/src/ui/shape-editor-knotholders.cpp @@ -1844,6 +1844,113 @@ TextKnotHolderEntityInlineSize::knot_click(unsigned int state) } } +/** + * Shape padding editor knot positioned top right corner of first object + */ +class TextKnotHolderEntityShapePadding : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +Geom::Point +TextKnotHolderEntityShapePadding::knot_get() const +{ + SPText *text = dynamic_cast(item); + g_assert(text != nullptr); + Geom::Point corner; + if (text->has_shape_inside()) { + auto shape = text->get_first_shape_dependency(); + Geom::OptRect bounds = shape->geometricBounds(); + if (bounds) { + corner = (*bounds).corner(1); + if (text->style->shape_padding.set) { + auto padding = text->style->shape_padding.computed; + corner *= Geom::Affine(Geom::Translate(-padding, padding)); + } + corner *= shape->transform; + } + } + return corner; +} + +void +TextKnotHolderEntityShapePadding::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + // Text in a shape: rectangle + SPText *text = dynamic_cast(item); + + if (text->has_shape_inside()) { + auto shape = text->get_first_shape_dependency(); + Geom::OptRect bounds = shape->geometricBounds(); + if (bounds) { + Geom::Point const point_a = snap_knot_position(p, state); + Geom::Point point_b = point_a * shape->transform.inverse(); + auto padding = (*bounds).corner(1)[Geom::X] - point_b[Geom::X]; + gchar *pad = g_strdup_printf("%f", padding); + text->style->shape_padding.read(pad); + g_free(pad); + + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + text->updateRepr(); + } + } +} + + +/** + * Shape margin editor knot positioned top right corner of each object + */ +class TextKnotHolderEntityShapeMargin : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void set_shape(SPShape *shape) { linked_shape = shape; } + SPShape *linked_shape; +}; + +Geom::Point +TextKnotHolderEntityShapeMargin::knot_get() const +{ + Geom::Point corner; + if (linked_shape == nullptr) return corner; + + Geom::OptRect bounds = linked_shape->geometricBounds(); + if (bounds) { + corner = (*bounds).corner(1); + if (linked_shape->style->shape_margin.set) { + auto margin = linked_shape->style->shape_margin.computed; + corner *= Geom::Affine(Geom::Translate(margin, -margin)); + } + corner *= linked_shape->transform; + } + return corner; +} + +void +TextKnotHolderEntityShapeMargin::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + g_assert(linked_shape != nullptr); + + Geom::OptRect bounds = linked_shape->geometricBounds(); + if (bounds) { + Geom::Point const point_a = snap_knot_position(p, state); + Geom::Point point_b = point_a * linked_shape->transform.inverse(); + auto margin = (*bounds).corner(1)[Geom::X] - point_b[Geom::X]; + gchar *pad = g_strdup_printf("%f", -margin); + linked_shape->style->shape_margin.read(pad); + g_free(pad); + + linked_shape->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + linked_shape->updateRepr(); + } +} + + + + class TextKnotHolderEntityShapeInside : public KnotHolderEntity { public: Geom::Point knot_get() const override; @@ -1860,7 +1967,7 @@ TextKnotHolderEntityShapeInside::knot_get() const // we have a crash on undo cration so remove assert // g_assert(text->style->shape_inside.set); Geom::Point p; - if (text->style->shape_inside.set) { + if (text->has_shape_inside()) { Geom::OptRect frame = text->get_frame(); if (frame) { p = (*frame).corner(2); @@ -1902,11 +2009,33 @@ TextKnotHolder::TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderRel if (text->style->shape_inside.set) { // 'shape-inside' - TextKnotHolderEntityShapeInside *entity_shapeinside = new TextKnotHolderEntityShapeInside(); - entity_shapeinside->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Text:shapeinside", - _("Adjust the rectangular region of the text.")); - entity.push_back(entity_shapeinside); + if (text->get_first_rectangle()) { + auto entity_shapeinside = new TextKnotHolderEntityShapeInside(); + entity_shapeinside->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Text:shapeinside", + _("Adjust the rectangular region of the text.")); + entity.push_back(entity_shapeinside); + } + + auto entity_shapepadding = new TextKnotHolderEntityShapePadding(); + entity_shapepadding->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Text:shapepadding", + _("Adjust the text shape padding.")); + entity.push_back(entity_shapepadding); + + // Add knots for shape subtraction margins + if (text->style->shape_subtract.set) { + for (auto *href : text->style->shape_subtract.hrefs) { + auto shape = href->getObject(); + if (dynamic_cast(shape)) { + auto entity_shapemargin = new TextKnotHolderEntityShapeMargin(); + entity_shapemargin->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Text:shapemargin", + _("Adjust the shape's text margin.")); + entity_shapemargin->set_shape(shape); + entity_shapemargin->update_knot(); + entity.push_back(entity_shapemargin); + } + } + } } else { // 'inline-size' or normal text diff --git a/src/ui/tools/text-tool.cpp b/src/ui/tools/text-tool.cpp index 4d0d7f58853195e79901a4c058edab1fdf3be2f4..099cd464da2231d832d188bd1fcbe1f5a641198d 100644 --- a/src/ui/tools/text-tool.cpp +++ b/src/ui/tools/text-tool.cpp @@ -42,6 +42,8 @@ #include "display/control/canvas-item-rect.h" #include "display/control/canvas-item-bpath.h" #include "display/curve.h" +#include "livarot/Path.h" +#include "livarot/Shape.h" #include "object/sp-flowtext.h" #include "object/sp-namedview.h" @@ -118,12 +120,18 @@ void TextTool::setup() { indicator->set_shadow(0xffffff7f, 1); indicator->hide(); - // The rectangle box outlining wrapping the shape for text in a shape. + // The shape that the text is flowing into frame = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); frame->set_fill(0x00 /* zero alpha */, SP_WIND_RULE_NONZERO); frame->set_stroke(0x0000ff7f); frame->hide(); + // A second frame for showing the padding of the above frame + padding_frame = new Inkscape::CanvasItemBpath(desktop->getCanvasControls()); + padding_frame->set_fill(0x00 /* zero alpha */, SP_WIND_RULE_NONZERO); + padding_frame->set_stroke(0xccccccdf); + padding_frame->hide(); + this->timeout = g_timeout_add(timeout, (GSourceFunc) sp_text_context_timeout, this); this->imc = gtk_im_multicontext_new(); @@ -154,10 +162,7 @@ void TextTool::setup() { this->shape_editor = new ShapeEditor(this->desktop); SPItem *item = this->desktop->getSelection()->singleItem(); - if (item && ( - (SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) || - (SP_IS_TEXT(item) && !SP_TEXT(item)->has_shape_inside()) ) - ) { + if (item && (SP_IS_FLOWTEXT(item) || SP_IS_TEXT(item))) { this->shape_editor->set_item(item); } @@ -224,6 +229,11 @@ void TextTool::finish() { this->frame = nullptr; } + if (this->padding_frame) { + delete padding_frame; + this->padding_frame = nullptr; + } + for (auto & text_selection_quad : text_selection_quads) { text_selection_quad->hide(); delete text_selection_quad; @@ -1480,23 +1490,17 @@ bool sp_text_delete_selection(ToolBase *ec) void TextTool::_selectionChanged(Inkscape::Selection *selection) { g_assert(selection != nullptr); - - shape_editor->unset_item(); SPItem *item = selection->singleItem(); - if (item && ( - (SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) || - (SP_IS_TEXT(item) && - !(SP_TEXT(item)->has_shape_inside() && !SP_TEXT(item)->get_first_rectangle())) - )) { - shape_editor->set_item(item); - } if (this->text && (item != this->text)) { sp_text_context_forget_text(this); } this->text = nullptr; + shape_editor->unset_item(); if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + shape_editor->set_item(item); + this->text = item; Inkscape::Text::Layout const *layout = te_get_layout(this->text); if (layout) @@ -1679,6 +1683,8 @@ static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see) } std::vector shapes; + Shape *exclusion_shape = nullptr; + double padding; // Frame around text if (SP_IS_FLOWTEXT(tc->text)) { @@ -1687,11 +1693,19 @@ static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see) tc->message_context->setF(Inkscape::NORMAL_MESSAGE, ngettext("Type or edit flowed text (%d character%s); Enter to start new paragraph.", "Type or edit flowed text (%d characters%s); Enter to start new paragraph.", nChars), nChars, trunc); - } else if (SP_IS_TEXT(tc->text)) { - if (tc->text->style->shape_inside.set) { - for (auto const *href : tc->text->style->shape_inside.hrefs) { + } else if (auto text = dynamic_cast(tc->text)) { + if (text->style->shape_inside.set) { + for (auto const *href : text->style->shape_inside.hrefs) { shapes.push_back(href->getObject()); } + if (text->style->shape_padding.set) { + // Calculate it here so we never show padding on FlowText or non-flowed Text (even if set) + padding = text->style->shape_padding.computed; + } + if(text->style->shape_subtract.set) { + // Find union of all exclusion shapes for later use + exclusion_shape = text->getExclusionShape(); + } } else { for (SPObject &child : tc->text->children) { if (auto textpath = dynamic_cast(&child)) { @@ -1717,11 +1731,50 @@ static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see) } if (!curve.is_empty()) { + + + if (padding) { + // See sp-text.cpp function _buildLayoutInit() + Path *temp = new Path; + Path *padded = new Path; + + temp->LoadPathVector(curve.get_pathvector()); + temp->OutsideOutline(padded, padding, join_round, butt_straight, 20.0); + padded->Convert(0.25); // Convert to polyline + + Shape* sh = new Shape; + padded->Fill(sh, 0); + Shape *uncross = new Shape; + uncross->ConvertToShape(sh); + + // Remove exclusions plus margins from padding frame + Shape *copy = new Shape; + if (exclusion_shape && exclusion_shape->hasEdges()) { + copy->Booleen(uncross, const_cast(exclusion_shape), bool_op_diff); + } else { + copy->Copy(uncross); + } + copy->ConvertToForme(padded); + padded->Transform(tc->text->i2dt_affine()); + tc->padding_frame->set_bpath(padded->MakePathVector()); + tc->padding_frame->show(); + + delete temp; + delete padded; + delete sh; + delete uncross; + delete copy; + } else { + tc->padding_frame->hide(); + } + + // Transform curve after doing padding. curve.transform(tc->text->i2dt_affine()); tc->frame->set_bpath(&curve); tc->frame->show(); } else { tc->frame->hide(); + tc->padding_frame->hide(); } } else { diff --git a/src/ui/tools/text-tool.h b/src/ui/tools/text-tool.h index ce06d1e29dd30b43b70fa50133ec23446ed9d03f..7026b1fbab504852d30b7455d99c9bc3c00e8d09 100644 --- a/src/ui/tools/text-tool.h +++ b/src/ui/tools/text-tool.h @@ -66,6 +66,7 @@ public: Inkscape::CanvasItemCurve *cursor = nullptr; Inkscape::CanvasItemRect *indicator = nullptr; Inkscape::CanvasItemBpath *frame = nullptr; // Highlighting flowtext shapes or textpath path + Inkscape::CanvasItemBpath *padding_frame = nullptr; // Highlighting flowtext padding std::vector text_selection_quads; gint timeout = 0; diff --git a/src/verbs.cpp b/src/verbs.cpp index 4f61898bf7f3ca1336f56f0412c0eaf713a6b423..d84b856f248f5c018a216e2650748b6999ae06cd 100644 --- a/src/verbs.cpp +++ b/src/verbs.cpp @@ -1611,6 +1611,9 @@ void ObjectVerb::perform( SPAction *action, void *data) case SP_VERB_OBJECT_FLOW_TEXT: text_flow_into_shape(); break; + case SP_VERB_OBJECT_FLOW_SUBTRACT: + text_flow_shape_subtract(); + break; case SP_VERB_OBJECT_UNFLOW_TEXT: text_unflow(); break; @@ -2656,6 +2659,9 @@ Verb *Verb::_base_verbs[] = { new ObjectVerb(SP_VERB_OBJECT_FLOW_TEXT, "ObjectFlowText", N_("_Flow into Frame"), N_("Put text into a frame (path or shape), creating a flowed text linked to the frame object"), "text-flow-into-frame"), + new ObjectVerb(SP_VERB_OBJECT_FLOW_SUBTRACT, "ObjectFlowSubtract", N_("Set _Subtraction Frames"), + N_("Flow text around a frame (path or shape), only available for SVG 2.0 Flow text."), + "text-flow-subtract-frame"), new ObjectVerb(SP_VERB_OBJECT_UNFLOW_TEXT, "ObjectUnFlowText", N_("_Unflow"), N_("Remove text from frame (creates a single-line text object)"), INKSCAPE_ICON("text-unflow")), new ObjectVerb(SP_VERB_OBJECT_FLOWTEXT_TO_TEXT, "ObjectFlowtextToText", N_("_Convert to Text"), diff --git a/src/verbs.h b/src/verbs.h index d1abce533a537015224124470a2531253bb4c8b6..ee38e8320af30cabd03614842bdb8e2a4444f8da 100644 --- a/src/verbs.h +++ b/src/verbs.h @@ -182,6 +182,7 @@ enum { SP_VERB_OBJECT_FLATTEN, SP_VERB_OBJECT_TO_CURVE, SP_VERB_OBJECT_FLOW_TEXT, + SP_VERB_OBJECT_FLOW_SUBTRACT, SP_VERB_OBJECT_UNFLOW_TEXT, SP_VERB_OBJECT_FLOWTEXT_TO_TEXT, SP_VERB_OBJECT_FLIP_HORIZONTAL,