// SPDX-License-Identifier: GPL-2.0-or-later /** * Specialization of GtkTreeView for the XML tree view * * Authors: * MenTaLguY * * Copyright (C) 2002 MenTaLguY * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "sp-xmlview-tree.h" #include #include #include #include #include #include #include #include #include #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 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(""); 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(""); 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 _property_plain_text; void render_vfunc(const Cairo::RefPtr& 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(); 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(data); } else if (repr->type() == Inkscape::XML::NodeType::COMMENT_NODE) { data->observer = std::make_unique(data); } else if (repr->type() == Inkscape::XML::NodeType::PI_NODE) { data->observer = std::make_unique(data); } else if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE) { data->observer = std::make_unique(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(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(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 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(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 :