diff options
Diffstat (limited to 'src/ui/dialog/attrdialog.cpp')
-rw-r--r-- | src/ui/dialog/attrdialog.cpp | 848 |
1 files changed, 848 insertions, 0 deletions
diff --git a/src/ui/dialog/attrdialog.cpp b/src/ui/dialog/attrdialog.cpp new file mode 100644 index 0000000..88b411d --- /dev/null +++ b/src/ui/dialog/attrdialog.cpp @@ -0,0 +1,848 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#include "attrdialog.h" + +#include "preferences.h" +#include "selection.h" +#include "document-undo.h" +#include "message-context.h" +#include "message-stack.h" +#include "style.h" + +#include "io/resource.h" + +#include "ui/builder-utils.h" +#include "ui/dialog/inkscape-preferences.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/syntax.h" +#include "ui/util.h" +#include "ui/widget/shapeicon.h" +#include "util/numeric/converters.h" +#include "util/trim.h" +#include "xml/attribute-record.h" + +#include <cstddef> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/regex.h> +#include <glibmm/timer.h> +#include <glibmm/ustring.h> +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/enums.h> +#include <gtkmm/label.h> +#include <gtkmm/liststore.h> +#include <gtkmm/menuitem.h> +#include <gtkmm/object.h> +#include <gtkmm/popover.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/targetlist.h> +#include <gtkmm/textview.h> +#include <gtkmm/treeview.h> +#include <gtkmm/widget.h> +#include <memory> +#include <string> + +#include "config.h" +#if WITH_GSOURCEVIEW +# include <gtksourceview/gtksource.h> +#endif + +/** + * Return true if `node` is a text or comment node + */ +static bool is_text_or_comment_node(Inkscape::XML::Node const &node) +{ + switch (node.type()) { + case Inkscape::XML::NodeType::TEXT_NODE: + case Inkscape::XML::NodeType::COMMENT_NODE: + return true; + default: + return false; + } +} + +static Glib::ustring get_syntax_theme() +{ + return Inkscape::Preferences::get()->getString("/theme/syntax-color-theme", "-none-"); +} + +namespace Inkscape::UI::Dialog { + +// arbitrarily selected size limits +constexpr int MAX_POPOVER_HEIGHT = 450; +constexpr int MAX_POPOVER_WIDTH = 520; +constexpr int TEXT_MARGIN = 3; + +std::unique_ptr<Syntax::TextEditView> AttrDialog::init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map) +{ + auto edit = Syntax::TextEditView::create(coloring); + auto& textview = edit->getTextView(); + textview.set_wrap_mode(Gtk::WrapMode::WRAP_WORD); + + // this actually sets padding rather than margin and extends textview's background color to the sides + textview.set_top_margin(TEXT_MARGIN); + textview.set_left_margin(TEXT_MARGIN); + textview.set_right_margin(TEXT_MARGIN); + textview.set_bottom_margin(TEXT_MARGIN); + + if (map) { + textview.signal_map().connect([owner](){ + // this is not effective: text view recalculates its size on idle, so it's too early to call on 'map'; + // (note: there's no signal on a TextView to tell us that formatting has been done) + // delay adjustment; this will work if UI is fast enough, but at the cost of popup jumping, + // but at least it will be sized properly + owner->_adjust_size = Glib::signal_timeout().connect([=](){ owner->adjust_popup_edit_size(); return false; }, 50); + }); + } + + return edit; +} + +/** + * Constructor + * A treeview whose each row corresponds to an XML attribute of a selected node + * New attribute can be added by clicking '+' at bottom of the attr pane. '-' + */ +AttrDialog::AttrDialog() + : DialogBase("/dialogs/attr", "AttrDialog") + , _builder(create_builder("attribute-edit-component.glade")) + , _scrolled_text_view(get_widget<Gtk::ScrolledWindow>(_builder, "scroll-wnd")) + , _content_sw(get_widget<Gtk::ScrolledWindow>(_builder, "content-sw")) + , _scrolled_window(get_widget<Gtk::ScrolledWindow>(_builder, "scrolled-wnd")) + , _treeView(get_widget<Gtk::TreeView>(_builder, "tree-view")) + , _popover(&get_widget<Gtk::Popover>(_builder, "popup")) + , _status_box(get_widget<Gtk::Box>(_builder, "status-box")) + , _status(get_widget<Gtk::Label>(_builder, "status-label")) +{ + // Attribute value editing (with syntax highlighting). + using namespace Syntax; + _css_edit = init_text_view(this, SyntaxMode::InlineCss, true); + _svgd_edit = init_text_view(this, SyntaxMode::SvgPathData, true); + _points_edit = init_text_view(this, SyntaxMode::SvgPolyPoints, true); + _attr_edit = init_text_view(this, SyntaxMode::PlainText, true); + + // string content editing + _text_edit = init_text_view(this, SyntaxMode::PlainText, false); + _style_edit = init_text_view(this, SyntaxMode::CssStyle, false); + + set_size_request(20, 15); + + // For text and comment nodes: update XML on the fly, as users type + for (auto tv : {&_text_edit->getTextView(), &_style_edit->getTextView()}) { + tv->get_buffer()->signal_end_user_action().connect([=]() { + if (_repr) { + _repr->setContent(tv->get_buffer()->get_text().c_str()); + setUndo(_("Type text")); + } + }); + } + + _store = Gtk::ListStore::create(_attrColumns); + _treeView.set_model(_store); + + // high-res aware icon renderer for a trash can + auto delete_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon()); + delete_renderer->property_shape_type().set_value("edit-delete"); + _treeView.append_column("", *delete_renderer); + Gtk::TreeViewColumn *col = _treeView.get_column(0); + if (col) { + auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + col->set_clickable(true); + col->set_widget(*add_icon); + add_icon->set_tooltip_text(_("Add a new attribute")); + add_icon->show(); + auto button = add_icon->get_parent()->get_parent()->get_parent(); + // Assign the button event so that create happens BEFORE delete. If this code + // isn't in this exact way, the onAttrDelete is called when the header lines are pressed. + button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false); + } + delete_renderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete)); + _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed)); + + _nameRenderer = Gtk::make_managed<Gtk::CellRendererText>(); + _nameRenderer->property_editable() = true; + _nameRenderer->property_placeholder_text().set_value(_("Attribute Name")); + _nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited)); + _nameRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startNameEdit)); + _treeView.append_column(_("Name"), *_nameRenderer); + _nameCol = _treeView.get_column(1); + if (_nameCol) { + _nameCol->set_resizable(true); + _nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName); + } + + _message_stack = std::make_shared<Inkscape::MessageStack>(); + _message_context = std::make_unique<Inkscape::MessageContext>(_message_stack); + _message_changed_connection = _message_stack->connectChanged([=](MessageType, const char* message) { + _status.set_markup(message ? message : ""); + }); + + _valueRenderer = Gtk::make_managed<Gtk::CellRendererText>(); + _valueRenderer->property_editable() = true; + _valueRenderer->property_placeholder_text().set_value(_("Attribute Value")); + _valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END); + _valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited)); + _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit), true); + _treeView.append_column(_("Value"), *_valueRenderer); + _valueCol = _treeView.get_column(2); + if (_valueCol) { + _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender); + } + + set_current_textedit(_attr_edit.get()); + _scrolled_text_view.set_max_content_height(MAX_POPOVER_HEIGHT); + + auto& apply = get_widget<Gtk::Button>(_builder, "btn-ok"); + apply.signal_clicked().connect([=]() { valueEditedPop(); }); + + auto& cancel = get_widget<Gtk::Button>(_builder, "btn-cancel"); + cancel.signal_clicked().connect([=](){ + if (!_value_editing.empty()) { + _activeTextView().get_buffer()->set_text(_value_editing); + } + _popover->popdown(); + }); + + _popover->signal_closed().connect([=]() { popClosed(); }); + _popover->signal_key_press_event().connect([=](GdkEventKey* ev) { return key_callback(ev); }, false); + _popover->hide(); + + get_widget<Gtk::Button>(_builder, "btn-truncate").signal_clicked().connect([=](){ truncateDigits(); }); + + const int N = 5; + _rounding_precision = Inkscape::Preferences::get()->getIntLimited("/dialogs/attrib/precision", 2, 0, N); + for (int n = 0; n <= N; ++n) { + auto id = '_' + std::to_string(n); + auto item = &get_widget<Gtk::MenuItem>(_builder, id.c_str()); + auto action = [=](){ + _rounding_precision = n; + get_widget<Gtk::Label>(_builder, "precision").set_label(' ' + item->get_label()); + Inkscape::Preferences::get()->setInt("/dialogs/attrib/precision", n); + }; + item->signal_activate().connect(action); + + if (n == _rounding_precision) { + action(); + } + } + + attr_reset_context(0); + pack_start(get_widget<Gtk::Box>(_builder, "main-box"), Gtk::PACK_EXPAND_WIDGET); + _updating = false; +} + +AttrDialog::~AttrDialog() +{ + _current_text_edit = nullptr; + _popover->hide(); + + // remove itself from the list of node observers + setRepr(nullptr); +} + +static int fmt_number(_GMatchInfo const *match, _GString *ret, void *prec) +{ + auto number = g_match_info_fetch(match, 1); + + char *end; + double val = g_ascii_strtod(number, &end); + if (*number && (end == nullptr || end > number)) { + auto precision = *static_cast<int*>(prec); + auto fmt = Util::format_number(val, precision); + g_string_append(ret, fmt.c_str()); + } else { + g_string_append(ret, number); + } + + auto text = g_match_info_fetch(match, 2); + g_string_append(ret, text); + + g_free(number); + g_free(text); + + return false; +} + +Glib::ustring AttrDialog::round_numbers(const Glib::ustring& text, int precision) +{ + // match floating point number followed by something else (not a number); repeat + static const auto numbers = Glib::Regex::create("([-+]?(?:(?:\\d+\\.?\\d*)|(?:\\.\\d+))(?:[eE][-+]?\\d*)?)([^+\\-0-9]*)", Glib::REGEX_MULTILINE); + + return numbers->replace_eval(text, text.size(), 0, Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY, &fmt_number, &precision); +} + +/** Round the selected floating point numbers in the attribute edit popover. */ +void AttrDialog::truncateDigits() const +{ + if (!_current_text_edit) { + return; + } + + auto buffer = _current_text_edit->getTextView().get_buffer(); + auto start = buffer->begin(); + auto end = buffer->end(); + + bool const had_selection = buffer->get_has_selection(); + int start_idx = 0, end_idx = 0; + if (had_selection) { + buffer->get_selection_bounds(start, end); + start_idx = start.get_offset(); + end_idx = end.get_offset(); + } + + auto text = buffer->get_text(start, end); + auto ret = round_numbers(text, _rounding_precision); + buffer->erase(start, end); + buffer->insert_at_cursor(ret); + + if (had_selection) { + // Restore selection but note that its length may have decreased. + end_idx -= text.size() - ret.size(); + if (end_idx < start_idx) { + end_idx = start_idx; + } + buffer->select_range(buffer->get_iter_at_offset(start_idx), buffer->get_iter_at_offset(end_idx)); + } +} + +void AttrDialog::set_current_textedit(Syntax::TextEditView* edit) +{ + _current_text_edit = edit ? edit : _attr_edit.get(); + _scrolled_text_view.remove(); + _scrolled_text_view.add(_current_text_edit->getTextView()); + _scrolled_text_view.show_all(); +} + +void AttrDialog::adjust_popup_edit_size() +{ + auto vscroll = _scrolled_text_view.get_vadjustment(); + int height = vscroll->get_upper() + 2 * TEXT_MARGIN; + if (height < MAX_POPOVER_HEIGHT) { + _scrolled_text_view.set_min_content_height(height); + vscroll->set_value(vscroll->get_lower()); + } else { + _scrolled_text_view.set_min_content_height(MAX_POPOVER_HEIGHT); + } +} + +bool AttrDialog::key_callback(GdkEventKey* event) { + switch (event->keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (_popover->is_visible()) { + if (event->state & GDK_SHIFT_MASK) { + valueEditedPop(); + return true; + } + else { + // as we type and content grows, resize the popup to accommodate it + _adjust_size = Glib::signal_timeout().connect([=](){ adjust_popup_edit_size(); return false; }, 50); + } + } + break; + } + return false; +} + +/** + * Prepare value string suitable for display in a Gtk::CellRendererText + * + * Value is truncated at the first new line character (if any) and a visual indicator and ellipsis is added. + * Overall length is limited as well to prevent performance degradation for very long values. + * + * @param value Raw attribute value as UTF-8 encoded string + * @return Single-line string with fixed maximum length + */ +static Glib::ustring prepare_rendervalue(const char *value) +{ + constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons + // ~400 characters fit horizontally on a WQHD display, so 500 should be plenty + Glib::ustring renderval; + + // truncate to MAX_LENGTH + if (g_utf8_strlen(value, -1) > MAX_LENGTH) { + renderval = Glib::ustring(value, MAX_LENGTH) + "…"; + } else { + renderval = value; + } + + // truncate at first newline (if present) and add a visual indicator + auto ind = renderval.find('\n'); + if (ind != Glib::ustring::npos) { + renderval.replace(ind, Glib::ustring::npos, " ⏎ …"); + } + + return renderval; +} + +void set_mono_class(Gtk::Widget* widget, bool mono) +{ + if (!widget) { + return; + } + Glib::ustring class_name = "mono-font"; + auto style = widget->get_style_context(); + auto has_class = style->has_class(class_name); + + if (mono && !has_class) { + style->add_class(class_name); + } else if (!mono && has_class) { + style->remove_class(class_name); + } +} + +void AttrDialog::set_mono_font(bool mono) +{ + set_mono_class(&_treeView, mono); +} + +void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry)); +} + +Gtk::TextView &AttrDialog::_activeTextView() const +{ + return _current_text_edit->getTextView(); +} + +void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + _value_path = path; + Gtk::TreeIter iter = *_store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (!row || !_repr || !cell) { + return; + } + + // popover in GTK3 is clipped to dialog window (in a floating dialog); limit size: + const int dlg_width = get_allocated_width() - 10; + _popover->set_size_request(std::min(MAX_POPOVER_WIDTH, dlg_width), -1); + + auto const attribute = row[_attrColumns._attributeName]; + bool edit_in_popup = +#if WITH_GSOURCEVIEW + true; +#else + false; +#endif + bool enable_rouding = false; + + if (attribute == "style") { + set_current_textedit(_css_edit.get()); + } else if (attribute == "d" || attribute == "inkscape:original-d") { + enable_rouding = true; + set_current_textedit(_svgd_edit.get()); + } else if (attribute == "points") { + enable_rouding = true; + set_current_textedit(_points_edit.get()); + } else { + set_current_textedit(_attr_edit.get()); + edit_in_popup = false; + } + + // number rounding functionality + widget_show(get_widget<Gtk::Box>(_builder, "rounding-box"), enable_rouding); + + _activeTextView().set_size_request(std::min(MAX_POPOVER_WIDTH - 10, dlg_width), -1); + + auto theme = get_syntax_theme(); + + auto entry = dynamic_cast<Gtk::Entry*>(cell); + int width, height; + entry->get_layout()->get_pixel_size(width, height); + int colwidth = _valueCol->get_width(); + + if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] || + edit_in_popup || colwidth - 10 < width) + { + _value_editing = entry->get_text(); + Gdk::Rectangle rect; + _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect); + if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) { + rect.set_y(rect.get_y() + 20); + } + if (rect.get_x() >= dlg_width) { + rect.set_x(dlg_width - 1); + } + _popover->set_pointing_to(rect); + + auto current_value = row[_attrColumns._attributeValue]; + _current_text_edit->setStyle(theme); + _current_text_edit->setText(current_value); + + // close in-line entry + cell->property_editing_canceled() = true; + cell->remove_widget(); + // cannot dismiss it right away without warning from GTK, so delay it + Glib::signal_timeout().connect_once([=](){ + cell->editing_done(); // only this call will actually remove in-line edit widget + cell->remove_widget(); + }, 0); + // and show popup edit instead + Glib::signal_timeout().connect_once([=](){ _popover->popup(); }, 10); + } else { + entry->signal_key_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry)); + } +} + +void AttrDialog::popClosed() +{ + if (!_current_text_edit) { + return; + } + _activeTextView().get_buffer()->set_text(""); + // delay this resizing, so it is not visible as popover fades out + _close_popup = Glib::signal_timeout().connect([=](){ _scrolled_text_view.set_min_content_height(20); return false; }, 250); +} + +/** + * @brief AttrDialog::setRepr + * Set the internal xml object that I'm working on right now. + */ +void AttrDialog::setRepr(Inkscape::XML::Node * repr) +{ + if (repr == _repr) { + return; + } + if (_repr) { + _store->clear(); + _repr->removeObserver(*this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + _repr = repr; + if (repr) { + Inkscape::GC::anchor(_repr); + _repr->addObserver(*this); + + // show either attributes or content + bool show_content = is_text_or_comment_node(*_repr); + if (show_content) { + _content_sw.remove(); + auto type = repr->name(); + auto elem = repr->parent(); + if (type && strcmp(type, "string") == 0 && elem && elem->name() && strcmp(elem->name(), "svg:style") == 0) { + // editing embedded CSS style + _style_edit->setStyle(get_syntax_theme()); + _content_sw.add(_style_edit->getTextView()); + } else { + _content_sw.add(_text_edit->getTextView()); + } + } + + _repr->synthesizeEvents(*this); + _scrolled_window.set_visible(!show_content); + _content_sw.set_visible(show_content); + } +} + +void AttrDialog::setUndo(Glib::ustring const &event_description) +{ + DocumentUndo::done(getDocument(), event_description, INKSCAPE_ICON("dialog-xml-editor")); +} + +/** + * Sets the AttrDialog status bar, depending on which attr is selected. + */ +void AttrDialog::attr_reset_context(gint attr) +{ + if (attr == 0) { + _message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> attribute to edit.")); + } else { + const gchar *name = g_quark_to_string(attr); + _message_context->setF( + Inkscape::NORMAL_MESSAGE, + _("Attribute <b>%s</b> selected. Press <b>Ctrl+Enter</b> when done editing to commit changes."), name); + } +} + +/** + * @brief AttrDialog::notifyAttributeChanged + * This is called when the XML has an updated attribute + */ +void AttrDialog::notifyAttributeChanged(XML::Node&, GQuark name_, Util::ptr_shared, Util::ptr_shared new_value) +{ + if (_updating) { + return; + } + + auto const name = g_quark_to_string(name_); + + Glib::ustring renderval; + if (new_value) { + renderval = prepare_rendervalue(new_value.pointer()); + } + for (auto&& iter : _store->children()) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring col_name = row[_attrColumns._attributeName]; + if (name == col_name) { + if (new_value) { + row[_attrColumns._attributeValue] = new_value.pointer(); + row[_attrColumns._attributeValueRender] = renderval; + new_value = Util::ptr_shared(); // Don't make a new one + } else { + _store->erase(iter); + } + break; + } + } + if (new_value) { + Gtk::TreeModel::Row row = *_store->prepend(); + row[_attrColumns._attributeName] = name; + row[_attrColumns._attributeValue] = new_value.pointer(); + row[_attrColumns._attributeValueRender] = renderval; + } +} + +/** + * @brief AttrDialog::onAttrCreate + * This function is a slot to signal_clicked for '+' button panel. + */ +bool AttrDialog::onAttrCreate(GdkEventButton *event) +{ + if(event->type == GDK_BUTTON_RELEASE && event->button == 1 && this->_repr) { + Gtk::TreeIter iter = _store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + _treeView.set_cursor(path, *_nameCol, true); + grab_focus(); + return true; + } + return false; +} + +/** + * @brief AttrDialog::onAttrDelete + * @param event + * @return true + * Delete the attribute from the xml + */ +void AttrDialog::onAttrDelete(Glib::ustring path) +{ + Gtk::TreeModel::Row row = *_store->get_iter(path); + if (row) { + Glib::ustring name = row[_attrColumns._attributeName]; + { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + } +} + +void AttrDialog::notifyContentChanged(XML::Node &, + Util::ptr_shared, + Util::ptr_shared new_content) +{ + auto textview = dynamic_cast<Gtk::TextView *>(_content_sw.get_child()); + if (!textview) { + return; + } + auto buffer = textview->get_buffer(); + if (!buffer->get_modified()) { + auto str = new_content.pointer(); + buffer->set_text(str ? str : ""); + } + buffer->set_modified(false); +} + + +/** + * @brief AttrDialog::onKeyPressed + * @param event + * @return true + * Delete or create elements based on key presses + */ +bool AttrDialog::onKeyPressed(GdkEventKey *event) +{ + bool ret = false; + if (!_repr) { + return ret; + } + auto selection = _treeView.get_selection(); + auto row = *selection->get_selected(); + + switch (event->keyval) { + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: { + // Create new attribute (repeat code, fold into above event!) + Glib::ustring name = row[_attrColumns._attributeName]; + _store->erase(row); + _repr->removeAttribute(name); + setUndo(_("Delete attribute")); + ret = true; + } break; + + case GDK_KEY_plus: + case GDK_KEY_Insert: { + // Create new attribute (repeat code, fold into above event!) + Gtk::TreeIter iter = _store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + _treeView.set_cursor(path, *_nameCol, true); + grab_focus(); + ret = true; + } break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (_popover->is_visible() && (event->state & GDK_SHIFT_MASK)) { + valueEditedPop(); + ret = true; + } break; + } + + return ret; +} + +bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (event->state & GDK_SHIFT_MASK) { + int pos = entry->get_position(); + entry->insert_text("\n", 1, pos); + entry->set_position(pos + 1); + ret = true; + } + break; + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +void AttrDialog::storeMoveToNext(Gtk::TreeModel::Path modelpath) +{ + auto selection = _treeView.get_selection(); + auto iter = *(selection->get_selected()); + auto path = static_cast<Gtk::TreeModel::Path>(iter); + Gtk::TreeViewColumn *focus_column; + _treeView.get_cursor(path, focus_column); + if (path == modelpath && focus_column == _treeView.get_column(1)) { + _treeView.set_cursor(modelpath, *_valueCol, true); + } +} + +/** + * Called when the name is edited in the TreeView editable column + */ +void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name) +{ + Gtk::TreeIter iter = *_store->get_iter(path); + auto modelpath = static_cast<Gtk::TreeModel::Path>(iter); + Gtk::TreeModel::Row row = *iter; + if(row && this->_repr) { + Glib::ustring old_name = row[_attrColumns._attributeName]; + if (old_name == name) { + Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50); + grab_focus(); + return; + } + // Do not allow empty name (this would delete the attribute) + if (name.empty()) { + return; + } + // Do not allow duplicate names + const auto children = _store->children(); + for (const auto &child : children) { + if (name == child[_attrColumns._attributeName]) { + return; + } + } + if(std::any_of(name.begin(), name.end(), isspace)) { + return; + } + // Copy old value and remove old name + Glib::ustring value; + if (!old_name.empty()) { + value = row[_attrColumns._attributeValue]; + _updating = true; + _repr->removeAttribute(old_name); + _updating = false; + } + + // Do the actual renaming and set new value + row[_attrColumns._attributeName] = name; + grab_focus(); + _updating = true; + _repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values) + _updating = false; + Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50); + setUndo(_("Rename attribute")); + } +} + +void AttrDialog::valueEditedPop() +{ + valueEdited(_value_path, _current_text_edit->getText()); + _value_editing.clear(); + _popover->popdown(); +} + +/** + * @brief AttrDialog::valueEdited + * @param event + * @return + * Called when the value is edited in the TreeView editable column + */ +void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& value) +{ + if (!getDesktop()) { + return; + } + + Gtk::TreeModel::Row row = *_store->get_iter(path); + if (row && _repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + Glib::ustring old_value = row[_attrColumns._attributeValue]; + if (old_value == value || name.empty()) { + return; + } + + _repr->setAttributeOrRemoveIfEmpty(name, value); + + if (!value.empty()) { + row[_attrColumns._attributeValue] = value; + Glib::ustring renderval = prepare_rendervalue(value.c_str()); + row[_attrColumns._attributeValueRender] = renderval; + } + setUndo(_("Change attribute value")); + } +} + +} // namespace Inkscape::UI::Dialog |