// SPDX-License-Identifier: GPL-2.0-or-later /** @file * Generic object attribute editor *//* * Authors: * see git history * Kris De Gussem * * Copyright (C) 2018 Authors * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include <2geom/rect.h> #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "actions/actions-tools.h" #include "desktop.h" #include "live_effects/effect-enum.h" #include "mod360.h" #include "object/sp-anchor.h" #include "object/sp-ellipse.h" #include "object/sp-image.h" #include "object/sp-lpe-item.h" #include "object/sp-namedview.h" #include "object/sp-object.h" #include "object/sp-path.h" #include "object/sp-rect.h" #include "object/sp-star.h" #include "object/tags.h" #include "streq.h" #include "ui/builder-utils.h" #include "ui/dialog/object-attributes.h" #include "ui/icon-names.h" #include "ui/tools/node-tool.h" #include "ui/util.h" #include "ui/widget/image-properties.h" #include "ui/widget/spinbutton.h" #include "ui/widget/style-swatch.h" #include "widgets/sp-attribute-widget.h" #include "xml/href-attribute-helper.h" #include "live_effects/lpeobject-reference.h" #include "live_effects/lpeobject.h" #include "live_effects/effect.h" namespace Inkscape { namespace UI { void sp_apply_lpeffect(SPDesktop* desktop, SPLPEItem* item, LivePathEffect::EffectType etype); namespace Dialog { struct SPAttrDesc { gchar const *label; gchar const *attribute; }; static const SPAttrDesc anchor_desc[] = { { N_("Href:"), "xlink:href"}, { N_("Target:"), "target"}, { N_("Type:"), "xlink:type"}, // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkRoleAttribute // Identifies the type of the related resource with an absolute URI { N_("Role:"), "xlink:role"}, // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkArcRoleAttribute // For situations where the nature/role alone isn't enough, this offers an additional URI defining the purpose of the link. { N_("Arcrole:"), "xlink:arcrole"}, // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkTitleAttribute { N_("Title:"), "xlink:title"}, { N_("Show:"), "xlink:show"}, // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkActuateAttribute { N_("Actuate:"), "xlink:actuate"}, { nullptr, nullptr} }; ObjectAttributes::ObjectAttributes() : DialogBase("/dialogs/objectattr/", "ObjectAttributes"), _builder(create_builder("object-attributes.glade")), _main_panel(get_widget(_builder, "main-panel")), _obj_title(get_widget(_builder, "main-obj-name")), _style_swatch(nullptr, _("Item's fill, stroke and opacity"), Gtk::ORIENTATION_HORIZONTAL) { auto& main = get_widget(_builder, "main-widget"); _obj_title.set_text(""); _style_swatch.set_hexpand(false); _style_swatch.set_valign(Gtk::ALIGN_CENTER); get_widget(_builder, "main-header").pack_end(_style_swatch, false, true); add(main); create_panels(); _style_swatch.hide(); } void ObjectAttributes::widget_setup() { if (_update.pending() || !getDesktop()) return; auto selection = getDesktop()->getSelection(); auto item = selection->singleItem(); auto scoped(_update.block()); auto panel = get_panel(item); if (panel != _current_panel && _current_panel) { _current_panel->update_panel(nullptr, nullptr); _main_panel.remove(_current_panel->widget()); _obj_title.set_text(""); } _current_panel = panel; _current_item = nullptr; Glib::ustring title = panel ? panel->get_title() : ""; if (!panel) { if (item) { if (auto name = item->displayName()) { title = name; } } else if (selection->size() > 1) { title = _("Multiple objects selected"); } } _obj_title.set_markup("" + Glib::Markup::escape_text(title) + ""); if (!panel) { _style_swatch.hide(); return; } _main_panel.pack_start(panel->widget(), true, true); bool show_style = false; if (panel->supports_fill_stroke()) { if (auto style = item ? item->style : nullptr) { _style_swatch.setStyle(style); show_style = true; } } widget_show(_style_swatch, show_style); panel->update_panel(item, getDesktop()); panel->widget().show(); _current_item = item; // TODO // show no of LPEs? // show locked status? } void ObjectAttributes::update_panel(SPObject* item) { if (!_current_panel) return; if (_current_panel->supports_fill_stroke()) { if (auto style = item ? item->style : nullptr) { _style_swatch.setStyle(style); } } _current_panel->update_panel(item, getDesktop()); } void ObjectAttributes::desktopReplaced() { //todo; if anything } void ObjectAttributes::selectionChanged(Selection* selection) { widget_setup(); } void ObjectAttributes::selectionModified(Selection* _selection, guint flags) { if (_update.pending() || !getDesktop() || !_current_panel) return; auto selection = getDesktop()->getSelection(); if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG)) { auto item = selection->singleItem(); if (item == _current_item) { update_panel(item); } else { g_warning("ObjectAttributes: missed selection change?"); } } } /////////////////////////////////////////////////////////////////////////////// std::tuple round_values(double x, double y) { auto a = std::round(x); auto b = std::round(y); return std::make_tuple(a != x || b != y, a, b); } std::tuple round_values(Gtk::SpinButton& x, Gtk::SpinButton& y) { return round_values(x.get_adjustment()->get_value(), y.get_adjustment()->get_value()); } const LivePathEffectObject* find_lpeffect(SPLPEItem* item, LivePathEffect::EffectType etype) { if (!item) return nullptr; auto lpe = item->getFirstPathEffectOfType(Inkscape::LivePathEffect::FILLET_CHAMFER); if (!lpe) return nullptr; return lpe->getLPEObj(); } void remove_lpeffect(SPLPEItem* item, LivePathEffect::EffectType type) { if (auto effect = find_lpeffect(item, type)) { item->setCurrentPathEffect(effect); item->removeCurrentPathEffect(false); DocumentUndo::done(item->document, _("Removed live path effect"), INKSCAPE_ICON("dialog-path-effects")); } } std::optional get_number(SPItem* item, const char* attribute) { if (!item) return {}; auto val = item->getAttribute(attribute); if (!val) return {}; return item->getRepr()->getAttributeDouble(attribute); } void align_star_shape(SPStar* path) { if (!path || !path->sides) return; auto arg1 = path->arg[0]; auto arg2 = path->arg[1]; auto delta = arg2 - arg1; auto top = -M_PI / 2; auto odd = path->sides & 1; if (odd) { arg1 = top; } else { arg1 = top - M_PI / path->sides; } arg2 = arg1 + delta; path->setAttributeDouble("sodipodi:arg1", arg1); path->setAttributeDouble("sodipodi:arg2", arg2); path->updateRepr(); } /////////////////////////////////////////////////////////////////////////////// details::AttributesPanel::AttributesPanel() { _tracker = std::make_unique(Inkscape::Util::UNIT_TYPE_LINEAR); //todo: // auto init_units = desktop->getNamedView()->display_units; // _tracker->setActiveUnit(init_units); } void details::AttributesPanel::update_panel(SPObject* object, SPDesktop* desktop) { if (object) { auto scoped(_update.block()); auto units = object->document->getNamedView() ? object->document->getNamedView()->display_units : nullptr; // auto init_units = desktop->getNamedView()->display_units; if (units) _tracker->setActiveUnit(units); } _desktop = desktop; if (!_update.pending()) { update(object); } } void details::AttributesPanel::change_value_px(SPObject* object, const Glib::RefPtr& adj, const char* attr, std::function&& setter) { if (_update.pending() || !object) return; auto scoped(_update.block()); const auto unit = _tracker->getActiveUnit(); auto value = Util::Quantity::convert(adj->get_value(), unit, "px"); if (value != 0 || attr == nullptr) { setter(value); } else if (attr) { object->removeAttribute(attr); } DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle")); } void details::AttributesPanel::change_angle(SPObject* object, const Glib::RefPtr& adj, std::function&& setter) { if (_update.pending() || !object) return; auto scoped(_update.block()); auto value = degree_to_radians_mod2pi(adj->get_value()); setter(value); DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle")); } void details::AttributesPanel::change_value(SPObject* object, const Glib::RefPtr& adj, std::function&& setter) { if (_update.pending() || !object) return; auto scoped(_update.block()); auto value = adj ? adj->get_value() : 0; setter(value); DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle")); } /////////////////////////////////////////////////////////////////////////////// class ImagePanel : public details::AttributesPanel { public: ImagePanel() { _title = _("Image"); _show_fill_stroke = false; _panel = std::make_unique(); _widget = _panel.get(); } ~ImagePanel() override = default; void update(SPObject* object) override { _panel->update(cast(object)); } private: std::unique_ptr _panel; }; /////////////////////////////////////////////////////////////////////////////// class AnchorPanel : public details::AttributesPanel { public: AnchorPanel() { _title = _("Anchor"); _show_fill_stroke = false; _table = std::make_unique(); _table->show(); _table->set_hexpand(); _table->set_vexpand(false); _widget = _table.get(); } ~AnchorPanel() override = default; void update(SPObject* object) override { auto anchor = cast(object); auto changed = _anchor != anchor; _anchor = anchor; if (!anchor) return; std::vector labels; std::vector attrs; if (changed) { int len = 0; while (anchor_desc[len].label) { labels.emplace_back(anchor_desc[len].label); attrs.emplace_back(anchor_desc[len].attribute); len += 1; } _table->set_object(anchor, labels, attrs, (GtkWidget*)_table->gobj()); } else { _table->reread_properties(); } } private: std::unique_ptr _table; SPAnchor* _anchor = nullptr; }; /////////////////////////////////////////////////////////////////////////////// class RectPanel : public details::AttributesPanel { public: RectPanel(Glib::RefPtr builder) : _main(get_widget(builder, "rect-main")), _width(get_derived_widget(builder, "rect-width")), _height(get_derived_widget(builder, "rect-height")), _rx(get_derived_widget(builder, "rect-rx")), _ry(get_derived_widget(builder, "rect-ry")), _sharp(get_widget(builder, "rect-sharp")), _round(get_widget(builder, "rect-corners")) { _title = _("Rectangle"); _widget = &_main; _width.get_adjustment()->signal_value_changed().connect([=](){ change_value_px(_rect, _width.get_adjustment(), "width", [=](double w){ _rect->setVisibleWidth(w); }); }); _height.get_adjustment()->signal_value_changed().connect([=](){ change_value_px(_rect, _height.get_adjustment(), "height", [=](double h){ _rect->setVisibleHeight(h); }); }); _rx.get_adjustment()->signal_value_changed().connect([=](){ change_value_px(_rect, _rx.get_adjustment(), "rx", [=](double rx){ _rect->setVisibleRx(rx); }); }); _ry.get_adjustment()->signal_value_changed().connect([=](){ change_value_px(_rect, _ry.get_adjustment(), "ry", [=](double ry){ _rect->setVisibleRy(ry); }); }); get_widget(builder, "rect-round").signal_clicked().connect([=](){ auto [changed, x, y] = round_values(_width, _height); if (changed) { _width.get_adjustment()->set_value(x); _height.get_adjustment()->set_value(y); } }); _sharp.signal_clicked().connect([=](){ if (!_rect) return; // remove rounded corners if LPE is there (first one found) remove_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER); _rx.get_adjustment()->set_value(0); _ry.get_adjustment()->set_value(0); }); _round.signal_clicked().connect([=](){ if (!_rect || !_desktop) return; // switch to node tool to show handles set_active_tool(_desktop, "Node"); // rx/ry need to be reset first, LPE doesn't handle them too well _rx.get_adjustment()->set_value(0); _ry.get_adjustment()->set_value(0); // add flexible corners effect if not yet present if (!find_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER)) { sp_apply_lpeffect(_desktop, _rect, LivePathEffect::FILLET_CHAMFER); } }); } ~RectPanel() override = default; void update(SPObject* object) override { _rect = cast(object); if (!_rect) return; auto scoped(_update.block()); _width.set_value(_rect->width.value); _height.set_value(_rect->height.value); _rx.set_value(_rect->rx.value); _ry.set_value(_rect->ry.value); auto lpe = find_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER); _sharp.set_sensitive(_rect->rx.value > 0 || _rect->ry.value > 0 || lpe); _round.set_sensitive(!lpe); } private: SPRect* _rect = nullptr; Gtk::Widget& _main; Inkscape::UI::Widget::SpinButton& _width; Inkscape::UI::Widget::SpinButton& _height; Inkscape::UI::Widget::SpinButton& _rx; Inkscape::UI::Widget::SpinButton& _ry; Gtk::Button& _sharp; Gtk::Button& _round; }; /////////////////////////////////////////////////////////////////////////////// class EllipsePanel : public details::AttributesPanel { public: EllipsePanel(Glib::RefPtr builder) : _main(get_widget(builder, "ellipse-main")), _rx(get_derived_widget(builder, "el-rx")), _ry(get_derived_widget(builder, "el-ry")), _start(get_derived_widget(builder, "el-start")), _end(get_derived_widget(builder, "el-end")), _slice(get_widget(builder, "el-slice")), _arc(get_widget(builder, "el-arc")), _chord(get_widget(builder, "el-chord")), _whole(get_widget(builder, "el-whole")) { _title = _("Ellipse"); _widget = &_main; _type[0] = &_slice; _type[1] = &_arc; _type[2] = &_chord; int type = 0; for (auto btn : _type) { btn->signal_toggled().connect([=](){ set_type(type); }); type++; } _whole.signal_clicked().connect([=](){ _start.get_adjustment()->set_value(0); _end.get_adjustment()->set_value(0); }); auto normalize = [=](){ _ellipse->normalize(); _ellipse->updateRepr(); _ellipse->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); }; _rx.get_adjustment()->signal_value_changed().connect([=](){ change_value_px(_ellipse, _rx.get_adjustment(), nullptr, [=](double rx){ _ellipse->setVisibleRx(rx); normalize(); }); }); _ry.get_adjustment()->signal_value_changed().connect([=](){ change_value_px(_ellipse, _ry.get_adjustment(), nullptr, [=](double ry){ _ellipse->setVisibleRy(ry); normalize(); }); }); _start.get_adjustment()->signal_value_changed().connect([=](){ change_angle(_ellipse, _start.get_adjustment(), [=](double s){ _ellipse->start = s; normalize(); }); }); _end.get_adjustment()->signal_value_changed().connect([=](){ change_angle(_ellipse, _end.get_adjustment(), [=](double e){ _ellipse->end = e; normalize(); }); }); get_widget(builder, "el-round").signal_clicked().connect([=](){ auto [changed, x, y] = round_values(_rx, _ry); if (changed && x > 0 && y > 0) { _rx.get_adjustment()->set_value(x); _ry.get_adjustment()->set_value(y); } }); } ~EllipsePanel() override = default; void update(SPObject* object) override { _ellipse = cast(object); if (!_ellipse) return; auto scoped(_update.block()); _rx.set_value(_ellipse->rx.value); _ry.set_value(_ellipse->ry.value); _start.set_value(radians_to_degree_mod360(_ellipse->start)); _end.set_value(radians_to_degree_mod360(_ellipse->end)); _slice.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE); _arc.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); _chord.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD); auto slice = !_ellipse->is_whole(); _whole.set_sensitive(slice); for (auto btn : _type) { btn->set_sensitive(slice); } } void set_type(int type) { if (!_ellipse) return; auto scoped(_update.block()); Glib::ustring arc_type = "slice"; bool open = false; switch (type) { case 0: arc_type = "slice"; open = false; break; case 1: arc_type = "arc"; open = true; break; case 2: arc_type = "chord"; open = true; // For backward compat, not truly open but chord most like arc. break; default: std::cerr << "Ellipse type change - bad arc type: " << type << std::endl; break; } _ellipse->setAttribute("sodipodi:open", open ? "true" : nullptr); _ellipse->setAttribute("sodipodi:arc-type", arc_type.c_str()); _ellipse->updateRepr(); DocumentUndo::done(_ellipse->document, _("Change arc type"), INKSCAPE_ICON("draw-ellipse")); } private: SPGenericEllipse* _ellipse = nullptr; Gtk::Widget& _main; Inkscape::UI::Widget::SpinButton& _rx; Inkscape::UI::Widget::SpinButton& _ry; Inkscape::UI::Widget::SpinButton& _start; Inkscape::UI::Widget::SpinButton& _end; Gtk::RadioButton& _slice; Gtk::RadioButton& _arc; Gtk::RadioButton& _chord; Gtk::Button& _whole; Gtk::RadioButton* _type[3]; }; /////////////////////////////////////////////////////////////////////////////// class StarPanel : public details::AttributesPanel { public: StarPanel(Glib::RefPtr builder) : _main(get_widget(builder, "star-main")), _corners(get_derived_widget(builder, "star-corners")), _ratio(get_derived_widget(builder, "star-ratio")), _rounded(get_derived_widget(builder, "star-rounded")), _rand(get_derived_widget(builder, "star-rand")), _poly(get_widget(builder, "star-poly")), _star(get_widget(builder, "star-star")), _align(get_widget(builder, "star-align")), _clear_rnd(get_widget(builder, "star-rnd-clear")), _clear_round(get_widget(builder, "star-round-clear")), _clear_ratio(get_widget(builder, "star-ratio-clear")) { _title = _("Star"); _widget = &_main; _corners.get_adjustment()->signal_value_changed().connect([=](){ change_value(_path, _corners.get_adjustment(), [=](double sides) { _path->setAttributeDouble("sodipodi:sides", (int)sides); auto arg1 = get_number(_path, "sodipodi:arg1").value_or(0.5); _path->setAttributeDouble("sodipodi:arg2", arg1 + M_PI / sides); _path->updateRepr(); }); }); _rounded.get_adjustment()->signal_value_changed().connect([=](){ change_value(_path, _rounded.get_adjustment(), [=](double rounded) { _path->setAttributeDouble("inkscape:rounded", rounded); _path->updateRepr(); }); }); _ratio.get_adjustment()->signal_value_changed().connect([=](){ change_value(_path, _ratio.get_adjustment(), [=](double ratio){ auto r1 = get_number(_path, "sodipodi:r1").value_or(1.0); auto r2 = get_number(_path, "sodipodi:r2").value_or(1.0); if (r2 < r1) { _path->setAttributeDouble("sodipodi:r2", r1 * ratio); } else { _path->setAttributeDouble("sodipodi:r1", r2 * ratio); } _path->updateRepr(); }); }); _rand.get_adjustment()->signal_value_changed().connect([=](){ change_value(_path, _rand.get_adjustment(), [=](double rnd){ _path->setAttributeDouble("inkscape:randomized", rnd); _path->updateRepr(); }); }); _clear_rnd.signal_clicked().connect([=](){ _rand.get_adjustment()->set_value(0); }); _clear_round.signal_clicked().connect([=](){ _rounded.get_adjustment()->set_value(0); }); _clear_ratio.signal_clicked().connect([=](){ _ratio.get_adjustment()->set_value(0.5); }); _poly.signal_toggled().connect([=](){ set_flat(true); }); _star.signal_toggled().connect([=](){ set_flat(false); }); _align.signal_clicked().connect([=](){ change_value(_path, {}, [=](double) { align_star_shape(_path); }); }); } ~StarPanel() override = default; void update(SPObject* object) override { _path = cast(object); if (!_path) return; auto scoped(_update.block()); _corners.set_value(_path->sides); double r1 = get_number(_path, "sodipodi:r1").value_or(0.5); double r2 = get_number(_path, "sodipodi:r2").value_or(0.5); if (r2 < r1) { _ratio.set_value(r1 > 0 ? r2 / r1 : 0.5); } else { _ratio.set_value(r2 > 0 ? r1 / r2 : 0.5); } _rounded.set_value(_path->rounded); _rand.set_value(_path->randomized); widget_show(_clear_rnd, _path->randomized != 0); widget_show(_clear_round, _path->rounded != 0); widget_show(_clear_ratio, std::abs(_ratio.get_value() - 0.5) > 0.0005); _poly.set_active(_path->flatsided); _star.set_active(!_path->flatsided); } void set_flat(bool flat) { change_value(_path, {}, [=](double){ _path->setAttribute("inkscape:flatsided", flat ? "true" : "false"); _path->updateRepr(); }); // adjust corners/sides _corners.get_adjustment()->set_lower(flat ? 3 : 2); if (flat && _corners.get_value() < 3) { _corners.get_adjustment()->set_value(3); } } private: SPStar* _path = nullptr; Gtk::Widget& _main; Inkscape::UI::Widget::SpinButton& _corners; Inkscape::UI::Widget::SpinButton& _ratio; Inkscape::UI::Widget::SpinButton& _rounded; Inkscape::UI::Widget::SpinButton& _rand; Gtk::Button& _clear_rnd; Gtk::Button& _clear_round; Gtk::Button& _clear_ratio; Gtk::Button& _align; Gtk::RadioButton& _poly; Gtk::RadioButton& _star; }; /////////////////////////////////////////////////////////////////////////////// class TextPanel : public details::AttributesPanel { public: TextPanel(Glib::RefPtr builder) : _main(get_widget(builder, "text-main")) { // TODO - text panel } private: Gtk::Widget& _main; }; /////////////////////////////////////////////////////////////////////////////// std::string get_key(SPObject* object) { if (!object) return {}; return typeid(*object).name(); } details::AttributesPanel* ObjectAttributes::get_panel(SPObject* object) { if (!object) return nullptr; std::string name = get_key(object); auto it = _panels.find(name); return it == _panels.end() ? nullptr : it->second.get(); } void ObjectAttributes::create_panels() { _panels[typeid(SPImage).name()] = std::make_unique(); _panels[typeid(SPRect).name()] = std::make_unique(_builder); _panels[typeid(SPGenericEllipse).name()] = std::make_unique(_builder); _panels[typeid(SPStar).name()] = std::make_unique(_builder); _panels[typeid(SPAnchor).name()] = std::make_unique(); } } } } /* 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:fileencoding=utf-8:textwidth=99 :