// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) Theodore Janeczko 2012 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "live_effects/parameter/patharray.h" #include <2geom/coord.h> #include <2geom/point.h> #include #include #include #include #include #include #include "display/curve.h" #include "document-undo.h" #include "document.h" #include "inkscape.h" #include "live_effects/effect.h" #include "live_effects/lpe-bspline.h" #include "live_effects/lpe-spiro.h" #include "live_effects/lpeobject-reference.h" #include "live_effects/lpeobject.h" #include "live_effects/parameter/patharray.h" #include "object/sp-shape.h" #include "object/sp-text.h" #include "object/uri.h" #include "originalpath.h" #include "svg/stringstream.h" #include "svg/svg.h" #include "ui/clipboard.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" namespace Inkscape { namespace LivePathEffect { class PathArrayParam::ModelColumns : public Gtk::TreeModel::ColumnRecord { public: ModelColumns() { add(_colObject); add(_colLabel); add(_colReverse); add(_colVisible); } ~ModelColumns() override = default; Gtk::TreeModelColumn _colObject; Gtk::TreeModelColumn _colLabel; Gtk::TreeModelColumn _colReverse; Gtk::TreeModelColumn _colVisible; }; PathArrayParam::PathArrayParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, Inkscape::UI::Widget::Registry *wr, Effect *effect) : Parameter(label, tip, key, wr, effect) , _vector() { _tree = nullptr; _scroller = nullptr; _model = nullptr; // refresh widgets on load to allow to remove the // memory leak calling initui here param_effect->refresh_widgets = true; oncanvas_editable = true; _from_original_d = false; _allow_only_bspline_spiro = false; } PathArrayParam::~PathArrayParam() { while (!_vector.empty()) { PathAndDirectionAndVisible *w = _vector.back(); unlink(w); } delete _model; } void PathArrayParam::initui() { SPDesktop * desktop = SP_ACTIVE_DESKTOP; if (!desktop) { return; } if (!_tree) { _tree = manage(new Gtk::TreeView()); _model = new ModelColumns(); _store = Gtk::TreeStore::create(*_model); _tree->set_model(_store); _tree->set_reorderable(true); _tree->enable_model_drag_dest (Gdk::ACTION_MOVE); Gtk::CellRendererToggle *toggle_reverse = manage(new Gtk::CellRendererToggle()); int reverseColNum = _tree->append_column(_("Reverse"), *toggle_reverse) - 1; Gtk::TreeViewColumn* col_reverse = _tree->get_column(reverseColNum); toggle_reverse->set_activatable(true); toggle_reverse->signal_toggled().connect(sigc::mem_fun(*this, &PathArrayParam::on_reverse_toggled)); col_reverse->add_attribute(toggle_reverse->property_active(), _model->_colReverse); Gtk::CellRendererToggle *toggle_visible = manage(new Gtk::CellRendererToggle()); int visibleColNum = _tree->append_column(_("Visible"), *toggle_visible) - 1; Gtk::TreeViewColumn* col_visible = _tree->get_column(visibleColNum); toggle_visible->set_activatable(true); toggle_visible->signal_toggled().connect(sigc::mem_fun(*this, &PathArrayParam::on_visible_toggled)); col_visible->add_attribute(toggle_visible->property_active(), _model->_colVisible); Gtk::CellRendererText *text_renderer = manage(new Gtk::CellRendererText()); int nameColNum = _tree->append_column(_("Name"), *text_renderer) - 1; Gtk::TreeView::Column *name_column = _tree->get_column(nameColNum); name_column->add_attribute(text_renderer->property_text(), _model->_colLabel); _tree->set_expander_column(*_tree->get_column(nameColNum) ); _tree->set_search_column(_model->_colLabel); _scroller = Gtk::manage(new Gtk::ScrolledWindow()); //quick little hack -- newer versions of gtk gave the item zero space allotment _scroller->set_size_request(-1, 120); _scroller->add(*_tree); _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); //_scroller.set_shadow_type(Gtk::SHADOW_IN); } param_readSVGValue(param_getSVGValue().c_str()); } void PathArrayParam::on_reverse_toggled(const Glib::ustring &path) { Gtk::TreeModel::iterator iter = _store->get_iter(path); Gtk::TreeModel::Row row = *iter; PathAndDirectionAndVisible *w = row[_model->_colObject]; row[_model->_colReverse] = !row[_model->_colReverse]; w->reversed = row[_model->_colReverse]; param_write_to_repr(param_getSVGValue().c_str()); DocumentUndo::done(param_effect->getSPDoc(), _("Link path parameter to path"), INKSCAPE_ICON("dialog-path-effects")); } void PathArrayParam::on_visible_toggled(const Glib::ustring &path) { Gtk::TreeModel::iterator iter = _store->get_iter(path); Gtk::TreeModel::Row row = *iter; PathAndDirectionAndVisible *w = row[_model->_colObject]; row[_model->_colVisible] = !row[_model->_colVisible]; w->visibled = row[_model->_colVisible]; param_write_to_repr(param_getSVGValue().c_str()); DocumentUndo::done(param_effect->getSPDoc(), _("Toggle path parameter visibility"), INKSCAPE_ICON("dialog-path-effects")); } void PathArrayParam::param_set_default() {} Gtk::Widget *PathArrayParam::param_newWidget() { Gtk::Box* vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); Gtk::Box* hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); _tree = nullptr; _model = nullptr; _scroller = nullptr; initui(); vbox->pack_start(*_scroller, Gtk::PACK_EXPAND_WIDGET); { // Paste path to link button Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); pButton->set_relief(Gtk::RELIEF_NONE); pIcon->show(); pButton->add(*pIcon); pButton->show(); pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_link_button_click)); hbox->pack_start(*pButton, Gtk::PACK_SHRINK); pButton->set_tooltip_text(_("Link to path in clipboard")); } { // Remove linked path Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("list-remove", Gtk::ICON_SIZE_BUTTON)); Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); pButton->set_relief(Gtk::RELIEF_NONE); pIcon->show(); pButton->add(*pIcon); pButton->show(); pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_remove_button_click)); hbox->pack_start(*pButton, Gtk::PACK_SHRINK); pButton->set_tooltip_text(_("Remove Path")); } { // Move Down Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_BUTTON)); Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); pButton->set_relief(Gtk::RELIEF_NONE); pIcon->show(); pButton->add(*pIcon); pButton->show(); pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_down_button_click)); hbox->pack_end(*pButton, Gtk::PACK_SHRINK); pButton->set_tooltip_text(_("Move Down")); } { // Move Down Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_BUTTON)); Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); pButton->set_relief(Gtk::RELIEF_NONE); pIcon->show(); pButton->add(*pIcon); pButton->show(); pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_up_button_click)); hbox->pack_end(*pButton, Gtk::PACK_SHRINK); pButton->set_tooltip_text(_("Move Up")); } vbox->pack_end(*hbox, Gtk::PACK_SHRINK); vbox->show_all_children(true); return vbox; } bool PathArrayParam::_selectIndex(const Gtk::TreeIter &iter, int *i) { if ((*i)-- <= 0) { _tree->get_selection()->select(iter); return true; } return false; } std::vector PathArrayParam::param_get_satellites() { std::vector objs; for (auto &iter : _vector) { if (iter && iter->ref.isAttached()) { SPObject *obj = iter->ref.getObject(); if (obj) { objs.push_back(obj); } } } return objs; } void PathArrayParam::on_up_button_click() { Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; int i = -1; std::vector::iterator piter = _vector.begin(); for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); piter = iter, i++, ++iter) { if (*iter == row[_model->_colObject]) { _vector.erase(iter); _vector.insert(piter, row[_model->_colObject]); break; } } param_write_to_repr(param_getSVGValue().c_str()); DocumentUndo::done(param_effect->getSPDoc(), _("Move path up"), INKSCAPE_ICON("dialog-path-effects")); _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &PathArrayParam::_selectIndex), &i)); } } void PathArrayParam::on_down_button_click() { Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; int i = 0; for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); i++, ++iter) { if (*iter == row[_model->_colObject]) { std::vector::iterator niter = _vector.erase(iter); if (niter != _vector.end()) { ++niter; i++; } _vector.insert(niter, row[_model->_colObject]); break; } } param_write_to_repr(param_getSVGValue().c_str()); DocumentUndo::done(param_effect->getSPDoc(), _("Move path down"), INKSCAPE_ICON("dialog-path-effects")); _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &PathArrayParam::_selectIndex), &i)); } } void PathArrayParam::on_remove_button_click() { Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); if (iter) { Gtk::TreeModel::Row row = *iter; unlink(row[_model->_colObject]); param_write_to_repr(param_getSVGValue().c_str()); DocumentUndo::done(param_effect->getSPDoc(), _("Remove path"), INKSCAPE_ICON("dialog-path-effects")); } } void PathArrayParam::on_link_button_click() { Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); std::vector pathsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "svg:path"); std::vector textsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "svg:text"); pathsid.insert(pathsid.end(), textsid.begin(), textsid.end()); if (pathsid.empty()) { return; } bool foundOne = false; Inkscape::SVGOStringStream os; for (auto iter : _vector) { if (foundOne) { os << "|"; } else { foundOne = true; } os << iter->href << "," << (iter->reversed ? "1" : "0") << "," << (iter->visibled ? "1" : "0"); } for (auto pathid : pathsid) { // add '#' at start to make it an uri. pathid.insert(pathid.begin(), '#'); if (foundOne) { os << "|"; } else { foundOne = true; } os << pathid.c_str() << ",0,1"; } param_write_to_repr(os.str().c_str()); DocumentUndo::done(param_effect->getSPDoc(), _("Link patharray parameter to path"), INKSCAPE_ICON("dialog-path-effects")); } void PathArrayParam::unlink(PathAndDirectionAndVisible *to) { to->linked_modified_connection.disconnect(); to->linked_delete_connection.disconnect(); to->ref.detach(); to->_pathvector = Geom::PathVector(); if (to->href) { g_free(to->href); to->href = nullptr; } for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); ++iter) { if (*iter == to) { PathAndDirectionAndVisible *w = *iter; _vector.erase(iter); delete w; return; } } } void PathArrayParam::start_listening() { for (auto w : _vector) { linked_changed(nullptr,w->ref.getObject(), w); } } void PathArrayParam::linked_delete(SPObject * /*deleted*/, PathAndDirectionAndVisible * /*to*/) { // unlink(to); param_write_to_repr(param_getSVGValue().c_str()); } bool PathArrayParam::_updateLink(const Gtk::TreeIter &iter, PathAndDirectionAndVisible *pd) { Gtk::TreeModel::Row row = *iter; if (row[_model->_colObject] == pd) { SPObject *obj = pd->ref.getObject(); row[_model->_colLabel] = obj && obj->getId() ? ( obj->label() ? obj->label() : obj->getId() ) : pd->href; return true; } return false; } void PathArrayParam::linked_changed(SPObject * /*old_obj*/, SPObject *new_obj, PathAndDirectionAndVisible *to) { to->linked_delete_connection.disconnect(); to->linked_modified_connection.disconnect(); if (new_obj && SP_IS_ITEM(new_obj)) { to->linked_delete_connection = new_obj->connectDelete( sigc::bind(sigc::mem_fun(*this, &PathArrayParam::linked_delete), to)); to->linked_modified_connection = new_obj->connectModified( sigc::bind(sigc::mem_fun(*this, &PathArrayParam::linked_modified), to)); linked_modified(new_obj, SP_OBJECT_MODIFIED_FLAG, to); } else { to->_pathvector = Geom::PathVector(); param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); if (_store.get()) { _store->foreach_iter( sigc::bind(sigc::mem_fun(*this, &PathArrayParam::_updateLink), to)); } } } void PathArrayParam::setPathVector(SPObject *linked_obj, guint /*flags*/, PathAndDirectionAndVisible *to) { if (!to) { return; } std::unique_ptr curve; SPText *text = dynamic_cast(linked_obj); if (auto shape = dynamic_cast(linked_obj)) { SPLPEItem * lpe_item = SP_LPE_ITEM(linked_obj); if (_from_original_d) { curve = SPCurve::copy(shape->curveForEdit()); } else if (_allow_only_bspline_spiro && lpe_item && lpe_item->hasPathEffect()){ curve = SPCurve::copy(shape->curveForEdit()); PathEffectList lpelist = lpe_item->getEffectList(); PathEffectList::iterator i; for (i = lpelist.begin(); i != lpelist.end(); ++i) { LivePathEffectObject *lpeobj = (*i)->lpeobject; if (lpeobj) { Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); if (dynamic_cast(lpe)) { Geom::PathVector hp; LivePathEffect::sp_bspline_do_effect(curve.get(), 0, hp); } else if (dynamic_cast(lpe)) { LivePathEffect::sp_spiro_do_effect(curve.get()); } } } } else { curve = SPCurve::copy(shape->curve()); } } else if (text) { bool hidden = text->isHidden(); if (hidden) { if (to->_pathvector.empty()) { text->setHidden(false); curve = text->getNormalizedBpath(); text->setHidden(true); } else { if (curve == nullptr) { curve = std::make_unique(); } curve->set_pathvector(to->_pathvector); } } else { curve = text->getNormalizedBpath(); } } if (curve == nullptr) { // curve invalid, set empty pathvector to->_pathvector = Geom::PathVector(); } else { to->_pathvector = curve->get_pathvector(); } } void PathArrayParam::linked_modified(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible *to) { if (!_updating && param_effect->getSPDoc()->isSensitive() && flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { if (!to) { return; } setPathVector(linked_obj, flags, to); param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); if (_store.get()) { _store->foreach_iter( sigc::bind(sigc::mem_fun(*this, &PathArrayParam::_updateLink), to)); } } } bool PathArrayParam::param_readSVGValue(const gchar *strvalue) { if (strvalue) { while (!_vector.empty()) { PathAndDirectionAndVisible *w = _vector.back(); unlink(w); } if (_store.get()) { _store->clear(); } gchar ** strarray = g_strsplit(strvalue, "|", 0); bool write = false; for (gchar ** iter = strarray; *iter != nullptr; iter++) { if ((*iter)[0] == '#') { gchar ** substrarray = g_strsplit(*iter, ",", 0); SPObject * old_ref = param_effect->getSPDoc()->getObjectByHref(*substrarray); if (old_ref) { SPObject * successor = old_ref->_successor; Glib::ustring id = *substrarray; if (successor) { id = successor->getId(); id.insert(id.begin(), '#'); write = true; } *(substrarray) = g_strdup(id.c_str()); } PathAndDirectionAndVisible* w = new PathAndDirectionAndVisible((SPObject *)param_effect->getLPEObj()); w->href = g_strdup(*substrarray); w->reversed = *(substrarray+1) != nullptr && (*(substrarray+1))[0] == '1'; //Like this to make backwards compatible, new value added in 0.93 w->visibled = *(substrarray+2) == nullptr || (*(substrarray+2))[0] == '1'; w->linked_changed_connection = w->ref.changedSignal().connect( sigc::bind(sigc::mem_fun(*this, &PathArrayParam::linked_changed), w)); w->ref.attach(URI(w->href)); _vector.push_back(w); if (_store.get()) { Gtk::TreeModel::iterator iter = _store->append(); Gtk::TreeModel::Row row = *iter; SPObject *obj = w->ref.getObject(); row[_model->_colObject] = w; row[_model->_colLabel] = obj ? ( obj->label() ? obj->label() : obj->getId() ) : w->href; row[_model->_colReverse] = w->reversed; row[_model->_colVisible] = w->visibled; } g_strfreev (substrarray); } } g_strfreev (strarray); if (write) { auto full = param_getSVGValue(); param_write_to_repr(full.c_str()); } return true; } return false; } Glib::ustring PathArrayParam::param_getSVGValue() const { Inkscape::SVGOStringStream os; bool foundOne = false; for (auto iter : _vector) { if (foundOne) { os << "|"; } else { foundOne = true; } os << iter->href << "," << (iter->reversed ? "1" : "0") << "," << (iter->visibled ? "1" : "0"); } return os.str(); } Glib::ustring PathArrayParam::param_getDefaultSVGValue() const { return ""; } void PathArrayParam::update() { for (auto & iter : _vector) { SPObject *linked_obj = iter->ref.getObject(); linked_modified(linked_obj, SP_OBJECT_MODIFIED_FLAG, iter); } } } /* namespace LivePathEffect */ } /* namespace Inkscape */ /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :