diff options
Diffstat (limited to 'src/widgets/sp-xmlview-tree.cpp')
-rw-r--r-- | src/widgets/sp-xmlview-tree.cpp | 936 |
1 files changed, 936 insertions, 0 deletions
diff --git a/src/widgets/sp-xmlview-tree.cpp b/src/widgets/sp-xmlview-tree.cpp new file mode 100644 index 0000000..a64e25d --- /dev/null +++ b/src/widgets/sp-xmlview-tree.cpp @@ -0,0 +1,936 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Specialization of GtkTreeView for the XML tree view + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2002 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-xmlview-tree.h" + +#include <cstring> +#include <glibmm/markup.h> +#include <glibmm/property.h> +#include <glibmm/ustring.h> +#include <gmodule.h> +#include <gtkmm/cellrenderer.h> +#include <gtkmm/cellrenderertext.h> +#include <memory> +#include <string> + +#include "ui/syntax.h" +#include "xml/node-observer.h" +#include "xml/node.h" + +namespace { + +struct NodeData +{ + SPXMLViewTree *tree; + GtkTreeRowReference *rowref; + Inkscape::XML::Node *repr; + bool expanded = false; //< true if tree view has been expanded to this node + bool dragging = false; + std::unique_ptr<Inkscape::XML::NodeObserver> observer; + + NodeData(SPXMLViewTree *tree, GtkTreeIter *node, Inkscape::XML::Node *repr); + ~NodeData(); +}; + +// currently dragged node +Inkscape::XML::Node *dragging_repr = nullptr; + +} // namespace + +enum { STORE_TEXT_COL = 0, STORE_DATA_COL, STORE_MARKUP_COL, STORE_N_COLS }; + +static void sp_xmlview_tree_destroy(GtkWidget * object); + +static NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter); + +static void add_node(SPXMLViewTree *tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node *repr); + +static gboolean ref_to_sibling (NodeData *node, Inkscape::XML::Node * ref, GtkTreeIter *); +static gboolean repr_to_child (NodeData *node, Inkscape::XML::Node * repr, GtkTreeIter *); +static GtkTreeRowReference *tree_iter_to_ref(SPXMLViewTree *, GtkTreeIter *); +static gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref); + +static gboolean search_equal_func(GtkTreeModel *, gint column, const gchar *key, GtkTreeIter *, gpointer search_data); +static gboolean foreach_func(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data); + +static void on_row_changed(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data); +static void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata); +static void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata); +static gboolean do_drag_motion(GtkWidget *, GdkDragContext *, gint x, gint y, guint time, gpointer user_data); + +static bool get_first_child(NodeData *data, GtkTreeIter *child_iter); +static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter); +static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen = 200); + +namespace { + +static auto null_to_empty(char const *str) +{ + return str ? str : ""; +} + +class ElementNodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + ElementNodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + + void notifyChildAdded(Inkscape::XML::Node&, Inkscape::XML::Node &child_, Inkscape::XML::Node *ref) override + { + GtkTreeIter before; + Inkscape::XML::Node *child = &child_; + + if (_nodedata->tree->blocked) return; + + if (!ref_to_sibling (_nodedata, ref, &before)) { + return; + } + + GtkTreeIter data_iter; + tree_ref_to_iter(_nodedata->tree, &data_iter, _nodedata->rowref); + + if (!_nodedata->expanded) { + auto model = GTK_TREE_MODEL(_nodedata->tree->store); + GtkTreeIter childiter; + if (!gtk_tree_model_iter_children(model, &childiter, &data_iter)) { + // no children yet, add a dummy + child = nullptr; + } else if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) { + // already has a dummy child + return; + } + } + + add_node(_nodedata->tree, &data_iter, &before, child); + } + + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark key_, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared) override + { + auto const key = g_quark_to_string(key_); + if (std::strcmp(key, "id") != 0 && std::strcmp(key, "inkscape:label") != 0) + return; + elementAttrOrNameChangedUpdate(&node); + } + + void notifyElementNameChanged(Inkscape::XML::Node &node, GQuark, GQuark) override + { + elementAttrOrNameChangedUpdate(&node); + } + + void notifyChildOrderChanged(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *, + Inkscape::XML::Node *newref) override + { + GtkTreeIter before, node; + + if (_nodedata->tree->blocked) + return; + + ref_to_sibling(_nodedata, newref, &before); + repr_to_child(_nodedata, &child, &node); + + if (gtk_tree_store_iter_is_valid(_nodedata->tree->store, &before)) { + gtk_tree_store_move_before(_nodedata->tree->store, &node, &before); + } else { + repr_to_child(_nodedata, newref, &before); + gtk_tree_store_move_after(_nodedata->tree->store, &node, &before); + } + } + + void notifyChildRemoved(Inkscape::XML::Node &repr, Inkscape::XML::Node &child, Inkscape::XML::Node *) override + { + if (_nodedata->tree->blocked) + return; + + GtkTreeIter iter; + if (repr_to_child(_nodedata, &child, &iter)) { + delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(_nodedata->tree->store), &iter); + gtk_tree_store_remove(_nodedata->tree->store, &iter); + } else if (!repr.firstChild() && get_first_child(_nodedata, &iter)) { + // remove dummy when all children gone + remove_dummy_rows(_nodedata->tree->store, &iter); + } else { + return; + } + +#ifndef GTK_ISSUE_2510_IS_FIXED + // https://gitlab.gnome.org/GNOME/gtk/issues/2510 + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(_nodedata->tree))); +#endif + } + + void elementAttrOrNameChangedUpdate(Inkscape::XML::Node *repr) + { + if (_nodedata->tree->blocked) { + return; + } + + auto node_name = Glib::ustring(null_to_empty(repr->name())); + auto pos = node_name.find("svg:"); + if (pos != Glib::ustring::npos) { + // Do not decorate element names the with default namespace "svg"; it is just visual noise. + node_name.erase(pos, 4); + } + + // Build a plain-text and a markup-enabled representation of the node. + auto &formatter = *_nodedata->tree->formatter; + auto display_text = Glib::ustring::compose("<%1", node_name); + formatter.openTag(node_name.c_str()); + + if (auto id_value = repr->attribute("id")) { + display_text += Glib::ustring::compose(" id=\"%1\"", id_value); + formatter.addAttribute("id", id_value); + } + if (auto label_value = repr->attribute("inkscape:label")) { + display_text += Glib::ustring::compose(" inkscape:label=\"%1\"", label_value); + formatter.addAttribute("inkscape:label", label_value); + } + display_text += ">"; + auto markup = formatter.finishTag(); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, display_text.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +class TextNodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + TextNodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + + void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared new_content) override + { + if (_nodedata->tree->blocked) + return; + + auto text_content = std::string("\"").append(null_to_empty(new_content.pointer())).append("\""); + sp_remove_newlines_and_tabs(text_content); + + auto &formatter = *_nodedata->tree->formatter; + auto markup = formatter.formatContent(text_content.c_str(), false); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, text_content.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +class CommentNodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + CommentNodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared new_content) override + { + if (_nodedata->tree->blocked) + return; + + auto comment = std::string("<!--").append(null_to_empty(new_content.pointer())).append("-->"); + sp_remove_newlines_and_tabs(comment); + + auto &formatter = *_nodedata->tree->formatter; + auto markup = formatter.formatComment(comment.c_str(), false); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, comment.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +class PINodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + PINodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + void notifyContentChanged(Inkscape::XML::Node &repr, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared new_content) override + { + if (_nodedata->tree->blocked) + return; + + auto processing_instr = std::string("<?").append(repr.name()).append(" ").append(null_to_empty(new_content.pointer())).append("?>"); + sp_remove_newlines_and_tabs(processing_instr); + + auto &formatter = *_nodedata->tree->formatter; + auto markup = formatter.formatProlog(processing_instr.c_str()); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, processing_instr.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +} // namespace + +/** + * Get an iterator to the first child of `data` + * @param data handle which references a row + * @param[out] child_iter On success: valid iterator to first child + * @return False if the node has no children + */ +static bool get_first_child(NodeData *data, GtkTreeIter *child_iter) +{ + GtkTreeIter iter; + return tree_ref_to_iter(data->tree, &iter, data->rowref) && + gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), child_iter, &iter); +} + +/** + * @param iter First dummy row on that level + * @pre all rows on the same level are dummies + * @pre iter is valid + * @post iter is invalid + * @post level is empty + */ +static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter) +{ + do { + g_assert(nullptr == sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(store), iter)); + gtk_tree_store_remove(store, iter); + } while (gtk_tree_store_iter_is_valid(store, iter)); +} + +static gboolean on_test_expand_row( // + GtkTreeView *tree_view, // + GtkTreeIter *iter, // + GtkTreePath *path, // + gpointer) +{ + auto tree = SP_XMLVIEW_TREE(tree_view); + auto model = GTK_TREE_MODEL(tree->store); + + GtkTreeIter childiter; + bool has_children = gtk_tree_model_iter_children(model, &childiter, iter); + g_assert(has_children); + + if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) { + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + + remove_dummy_rows(tree->store, &childiter); + + // insert real rows + data->expanded = true; + ElementNodeObserver e(data); + data->repr->synthesizeEvents(e); + } + + return false; +} + +/** Node name renderer for XML tree. + * It knows to use markup, but falls back to plain text for selected nodes. + */ +class NodeRenderer : public Gtk::CellRendererText { +public: + NodeRenderer(): + Glib::ObjectBase(typeid(CellRendererText)), + Gtk::CellRendererText(), + _property_plain_text(*this, "plain", "-") {} + + // "text" and "markup" properties from CellRendererText are in use for marked up text; + // we need a separate property for plain text (placeholder_text could be hijacked potentially) + Glib::Property<Glib::ustring> _property_plain_text; + + void render_vfunc(const Cairo::RefPtr<Cairo::Context>& ctx, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override { + if (flags & Gtk::CELL_RENDERER_SELECTED) { + // use plain text instead of marked up text to render selected nodes, so they are legible + property_text() = _property_plain_text.get_value(); + } + Gtk::CellRendererText::render_vfunc(ctx, widget, background_area, cell_area, flags); + } +}; + +GtkWidget *sp_xmlview_tree_new(Inkscape::XML::Node * repr, void * /*factory*/, void * /*data*/) +{ + SPXMLViewTree *tree = SP_XMLVIEW_TREE(g_object_new (SP_TYPE_XMLVIEW_TREE, nullptr)); + tree->_tree_move = new sigc::signal<void ()>(); + + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(tree), FALSE); + gtk_tree_view_set_reorderable (GTK_TREE_VIEW(tree), TRUE); + gtk_tree_view_set_enable_search (GTK_TREE_VIEW(tree), TRUE); + gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW(tree), search_equal_func, nullptr, nullptr); + + auto r = new NodeRenderer(); + tree->renderer = r; + + GtkCellRenderer* renderer = r->Gtk::CellRenderer::gobj(); + GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("", renderer, + "markup", STORE_MARKUP_COL, // marked up text for decorated color output + "plain", STORE_TEXT_COL, // plain text for searching and rendering selected nodes + nullptr); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); + + sp_xmlview_tree_set_repr (tree, repr); + + g_signal_connect(GTK_TREE_VIEW(tree), "drag-begin", G_CALLBACK(on_drag_begin), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "drag-end", G_CALLBACK(on_drag_end), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "drag-motion", G_CALLBACK(do_drag_motion), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "test-expand-row", G_CALLBACK(on_test_expand_row), nullptr); + + tree->formatter = new Inkscape::UI::Syntax::XMLFormatter(); + + return GTK_WIDGET(tree); +} + +G_DEFINE_TYPE(SPXMLViewTree, sp_xmlview_tree, GTK_TYPE_TREE_VIEW); + +void sp_xmlview_tree_class_init(SPXMLViewTreeClass * klass) +{ + auto widget_class = GTK_WIDGET_CLASS(klass); + widget_class->destroy = sp_xmlview_tree_destroy; +} + +void +sp_xmlview_tree_init (SPXMLViewTree * tree) +{ + tree->repr = nullptr; + tree->blocked = 0; +} + +void sp_xmlview_tree_destroy(GtkWidget * object) +{ + SPXMLViewTree * tree = SP_XMLVIEW_TREE (object); + + delete tree->renderer; + tree->renderer = nullptr; + delete tree->formatter; + tree->formatter = nullptr; + delete tree->_tree_move; + tree->_tree_move = nullptr; + + sp_xmlview_tree_set_repr (tree, nullptr); + + GTK_WIDGET_CLASS(sp_xmlview_tree_parent_class)->destroy (object); +} + +/* + * Add a new row to the tree + */ +void +add_node (SPXMLViewTree * tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node * repr) +{ + g_assert (tree != nullptr); + + if (before && !gtk_tree_store_iter_is_valid(tree->store, before)) { + before = nullptr; + } + + GtkTreeIter iter; + gtk_tree_store_insert_before (tree->store, &iter, parent, before); + + if (!gtk_tree_store_iter_is_valid(tree->store, &iter)) { + return; + } + + if (!repr) { + // no need to store any data + return; + } + + auto data = new NodeData(tree, &iter, repr); + + g_assert(data != nullptr); + + gtk_tree_store_set(tree->store, &iter, STORE_DATA_COL, data, -1); + + if (repr->type() == Inkscape::XML::NodeType::TEXT_NODE) { + data->observer = std::make_unique<TextNodeObserver>(data); + } else if (repr->type() == Inkscape::XML::NodeType::COMMENT_NODE) { + data->observer = std::make_unique<CommentNodeObserver>(data); + } else if (repr->type() == Inkscape::XML::NodeType::PI_NODE) { + data->observer = std::make_unique<PINodeObserver>(data); + } else if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE) { + data->observer = std::make_unique<ElementNodeObserver>(data); + } + + if (data->observer) { + /* cheat a little to get the text updated on nodes without id */ + if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE && repr->attribute("id") == nullptr) { + data->observer->notifyAttributeChanged(*repr, g_quark_from_static_string("id"), + Inkscape::Util::ptr_shared(), Inkscape::Util::ptr_shared()); + } + repr->addObserver(*data->observer); + repr->synthesizeEvents(*data->observer); + } +} + +static gboolean remove_all_listeners(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer) +{ + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + delete data; + return false; +} + +NodeData::NodeData(SPXMLViewTree *tree, GtkTreeIter *iter, Inkscape::XML::Node *repr) + : tree(tree) + , rowref(tree_iter_to_ref(tree, iter)) + , repr(repr) +{ + if (repr) { + Inkscape::GC::anchor(repr); + } +} + +NodeData::~NodeData() +{ + if (repr) { + if (observer) { + repr->removeObserver(*observer); + } + Inkscape::GC::release(repr); + } + gtk_tree_row_reference_free(rowref); +} + +/** + * Truncate `val` to `maxlen` unicode characters and replace newlines and tabs + * with placeholder symbols. The string is modified in place. + * @param[in,out] val String in UTF-8 encoding + */ +static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen) +{ + if (g_utf8_strlen(val.data(), maxlen * 2) > maxlen) { + size_t newlen = g_utf8_offset_to_pointer(val.data(), maxlen - 3) - val.data(); + val.resize(newlen); + val.append("…"); + } + + struct + { + const char *query; + const char *replacement; + } replacements[] = { + {"\r\n", "⏎"}, + {"\n", "⏎"}, + {"\t", "⇥"}, + }; + + for (auto const &item : replacements) { + for (size_t pos = 0; (pos = val.find(item.query, pos)) != std::string::npos;) { + val.replace(pos, strlen(item.query), item.replacement); + } + } +} + +/* + * Save the source path on drag start, will need it in on_row_changed() when moving a row + */ +void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata) +{ + SPXMLViewTree *tree = static_cast<SPXMLViewTree *>(userdata); + if (!tree) { + return; + } + + GtkTreeModel *model = nullptr; + GtkTreeIter iter; + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + if (gtk_tree_selection_get_selected(selection, &model, &iter)) { + NodeData *data = sp_xmlview_tree_node_get_data(model, &iter); + if (data) { + data->dragging = true; + dragging_repr = data->repr; + } + } +} + +/** + * Finalize what happened in `on_row_changed` and clean up what was set up in `on_drag_begin` + */ +void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata) +{ + if (!dragging_repr) + return; + + auto tree = static_cast<SPXMLViewTree *>(userdata); + auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + bool failed = false; + + GtkTreeIter iter; + if (sp_xmlview_tree_get_repr_node(tree, dragging_repr, &iter)) { + NodeData *data = sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(tree->store), &iter); + + if (data && data->dragging) { + // dragging flag was not cleared in `on_row_changed`, this indicates a failed drag + data->dragging = false; + failed = true; + } else { + // Reselect the dragged row + gtk_tree_selection_select_iter(selection, &iter); + } + } else { +#ifndef GTK_ISSUE_2510_IS_FIXED + // https://gitlab.gnome.org/GNOME/gtk/issues/2510 + gtk_tree_selection_unselect_all(selection); +#endif + } + + dragging_repr = nullptr; + + if (!failed) { + // Signal that a drag and drop has completed successfully + tree->_tree_move->emit(); + } +} + +/* + * Main drag & drop function + * Get the old and new paths, and change the Inkscape::XML::Node repr's + */ +void on_row_changed(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data) +{ + NodeData *data = sp_xmlview_tree_node_get_data(tree_model, iter); + + if (!data || !data->dragging) { + return; + } + data->dragging = false; + + SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data); + + gtk_tree_row_reference_free(data->rowref); + data->rowref = tree_iter_to_ref(tree, iter); + + GtkTreeIter new_parent; + if (!gtk_tree_model_iter_parent(tree_model, &new_parent, iter)) { + //No parent of drop location + return; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(tree_model, iter); + Inkscape::XML::Node *before_repr = nullptr; + + // Find the sibling node before iter + GtkTreeIter before_iter = *iter; + if (gtk_tree_model_iter_previous(tree_model, &before_iter)) { + before_repr = sp_xmlview_tree_node_get_repr(tree_model, &before_iter); + } + + // Drop onto oneself causes assert in changeOrder() below, ignore + if (repr == before_repr) + return; + + auto repr_old_parent = repr->parent(); + auto repr_new_parent = sp_xmlview_tree_node_get_repr(tree_model, &new_parent); + + tree->blocked++; + + if (repr_old_parent == repr_new_parent) { + repr_old_parent->changeOrder(repr, before_repr); + } else { + repr_old_parent->removeChild(repr); + repr_new_parent->addChild(repr, before_repr); + } + + NodeData *data_new_parent = sp_xmlview_tree_node_get_data(tree_model, &new_parent); + if (data_new_parent && data_new_parent->expanded) { + // Reselect the dragged row in `on_drag_end` instead of here, because of + // https://gitlab.gnome.org/GNOME/gtk/-/issues/2510 + } else { + // convert to dummy node + delete data; + gtk_tree_store_set(tree->store, iter, STORE_DATA_COL, nullptr, -1); + } + + tree->blocked--; +} + +/* + * Set iter to ref or node data's child with the same repr or first child + */ +gboolean ref_to_sibling (NodeData *data, Inkscape::XML::Node *repr, GtkTreeIter *iter) +{ + if (repr) { + if (!repr_to_child (data, repr, iter)) { + return false; + } + gtk_tree_model_iter_next (GTK_TREE_MODEL(data->tree->store), iter); + } else { + GtkTreeIter data_iter; + if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) { + return false; + } + gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), iter, &data_iter); + } + return true; +} + +/* + * Set iter to the node data's child with the same repr + */ +gboolean repr_to_child (NodeData *data, Inkscape::XML::Node * repr, GtkTreeIter *iter) +{ + GtkTreeIter data_iter; + GtkTreeModel *model = GTK_TREE_MODEL(data->tree->store); + gboolean valid = false; + + if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) { + return false; + } + + /* + * The node we are looking for is likely to be the last one, so check it first. + */ + gint n_children = gtk_tree_model_iter_n_children (model, &data_iter); + if (n_children > 1) { + valid = gtk_tree_model_iter_nth_child (model, iter, &data_iter, n_children-1); + if (valid && sp_xmlview_tree_node_get_repr (model, iter) == repr) { + //g_message("repr_to_child hit %d", n_children); + return valid; + } + } + + valid = gtk_tree_model_iter_children(model, iter, &data_iter); + while (valid && sp_xmlview_tree_node_get_repr (model, iter) != repr) { + valid = gtk_tree_model_iter_next(model, iter); + } + + return valid; +} + +/* + * Get a matching GtkTreeRowReference for a GtkTreeIter + */ +GtkTreeRowReference *tree_iter_to_ref (SPXMLViewTree * tree, GtkTreeIter* iter) +{ + GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), iter); + GtkTreeRowReference *ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(tree->store), path); + gtk_tree_path_free(path); + + return ref; +} + +/* + * Get a matching GtkTreeIter for a GtkTreeRowReference + */ +gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref) +{ + GtkTreePath* path = gtk_tree_row_reference_get_path(ref); + if (!path) { + return false; + } + gboolean const valid = // + gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), iter, path); + gtk_tree_path_free(path); + + return valid; +} + +/* + * Disable drag and drop target on : root node and non-element nodes + */ +gboolean do_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data) +{ + GtkTreePath *path = nullptr; + GtkTreeViewDropPosition pos; + gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW(widget), x, y, &path, &pos); + + int action = 0; + + if (!dragging_repr) { + goto finally; + } + + if (path) { + SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data); + GtkTreeIter iter; + gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), &iter, path); + auto repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), &iter); + + bool const drop_into = pos != GTK_TREE_VIEW_DROP_BEFORE && // + pos != GTK_TREE_VIEW_DROP_AFTER; + + // 0. don't drop on self (also handled by on_row_changed but nice to not have drop highlight for it) + if (repr == dragging_repr) { + goto finally; + } + + // 1. only xml elements can have children + if (drop_into && repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE) { + goto finally; + } + + // 3. elements must be at least children of the root <svg:svg> element + if (gtk_tree_path_get_depth(path) < 2) { + goto finally; + } + + // 4. drag node specific limitations + { + // nodes which can't be re-parented (because the document holds pointers to them which must stay valid) + static GQuark const CODE_sodipodi_namedview = g_quark_from_static_string("sodipodi:namedview"); + static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs"); + + bool const no_reparenting = dragging_repr->code() == CODE_sodipodi_namedview || // + dragging_repr->code() == CODE_svg_defs; + + if (no_reparenting && (drop_into || dragging_repr->parent() != repr->parent())) { + goto finally; + } + } + + action = GDK_ACTION_MOVE; + } + +finally: + if (action == 0) { + // remove drop highlight + gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(widget), nullptr, pos /* ignored */); + } + + gtk_tree_path_free(path); + gdk_drag_status (context, (GdkDragAction)action, time); + + return (action == 0); +} + +/* + * Set the tree selection and scroll to the row with the given repr + */ +void +sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr) +{ + if ( tree->repr == repr ) return; + + if (tree->store) { + gtk_tree_view_set_model(GTK_TREE_VIEW(tree), nullptr); + gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), remove_all_listeners, nullptr); + g_object_unref(tree->store); + tree->store = nullptr; + } + + if (tree->repr) { + Inkscape::GC::release(tree->repr); + } + tree->repr = repr; + if (repr) { + tree->store = gtk_tree_store_new(STORE_N_COLS, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_STRING); + + Inkscape::GC::anchor(repr); + add_node(tree, nullptr, nullptr, repr); + + // Set the tree model here, after all data is inserted + gtk_tree_view_set_model (GTK_TREE_VIEW(tree), GTK_TREE_MODEL(tree->store)); + g_signal_connect(G_OBJECT(tree->store), "row-changed", G_CALLBACK(on_row_changed), tree); + + GtkTreePath *path = gtk_tree_path_new_from_indices(0, -1); + gtk_tree_view_expand_to_path (GTK_TREE_VIEW(tree), path); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW(tree), path, nullptr, true, 0.5, 0.0); + gtk_tree_path_free(path); + } +} + +/* + * Return the node data at a given GtkTreeIter position + */ +NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter) +{ + NodeData *data = nullptr; + gtk_tree_model_get(model, iter, STORE_DATA_COL, &data, -1); + return data; +} + +/* + * Return the repr at a given GtkTreeIter position + */ +Inkscape::XML::Node * +sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * iter) +{ + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + return data ? data->repr : nullptr; +} + +struct IterByReprData { + const Inkscape::XML::Node *repr; //< in + GtkTreeIter *iter; //< out +}; + +/* + * Find a GtkTreeIter position in the tree by repr + * @return True if the node was found + */ +gboolean +sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *iter) +{ + iter->stamp = 0; // invalidate iterator + IterByReprData funcdata = { repr, iter }; + gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), foreach_func, &funcdata); + return iter->stamp != 0; +} + +gboolean foreach_func(GtkTreeModel *model, GtkTreePath * /*path*/, GtkTreeIter *iter, gpointer user_data) +{ + auto funcdata = static_cast<IterByReprData *>(user_data); + if (sp_xmlview_tree_node_get_repr(model, iter) == funcdata->repr) { + *funcdata->iter = *iter; + return TRUE; + } + + return FALSE; +} + +/* + * Callback function for string searches in the tree + * Return a match on any substring + */ +gboolean search_equal_func(GtkTreeModel *model, gint /*column*/, const gchar *key, GtkTreeIter *iter, gpointer /*search_data*/) +{ + gchar *text = nullptr; + gtk_tree_model_get(model, iter, STORE_TEXT_COL, &text, -1); + + gboolean match = (strstr(text, key) != nullptr); + + g_free(text); + + return !match; +} + +/* + 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=4:softtabstop=4:fileencoding=utf-8 : |