Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
xml-treeview.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * A widget showing the XML tree.
4 *
5 * Authors:
6 * Tavmjong Bah
7 * MenTaLguY <mental@rydia.net> (Original C version)
8 *
9 * Copyright (C)
10 * Tavmjong Bah 2024
11 * MenTaLguY 2002
12 *
13 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14 *
15 * TODO:
16 * Lazy load.
17 */
18
19#include "xml-treeview.h"
20
21#include <cassert>
22
23#include <gtkmm/dragsource.h>
24#include <gtkmm/droptarget.h>
25#include <gtkmm/treemodel.h>
26#include <gtkmm/treerowreference.h>
27#include <gtkmm/treestore.h>
28
29#include "document.h"
30#include "object/sp-defs.h" // D&D in <defs>, <glyph>, etc.
31#include "object/sp-glyph.h"
32#include "object/sp-item.h"
33#include "object/sp-mask.h"
34#include "object/sp-pattern.h"
35#include "object/sp-root.h" // -> SPGroup -> SPLPEItem -> SPItem
36#include "object/sp-text.h"
37#include "object/sp-tspan.h"
38#include "util/cast.h" // cast<> of SPObjects
39#include "ui/syntax.h" // XMLFormatter
40#include "xml/node.h"
41#include "xml/simple-node.h"
42#include "xml/node-observer.h"
43#include "util/value-utils.h"
44
45using namespace Inkscape::Util;
46
47namespace Inkscape::UI::Widget {
48namespace {
49
50struct XMLDnDRow
51{
53};
54
55} // namespace
56
57class ModelColumns final : public Gtk::TreeModel::ColumnRecord
58{
59public:
60 ModelColumns()
61 {
62 add(node);
63 add(markup);
64 add(text);
65 }
66
67 Gtk::TreeModelColumn<Inkscape::XML::Node*> node;
68 Gtk::TreeModelColumn<Glib::ustring> markup;
69 Gtk::TreeModelColumn<Glib::ustring> text;
70};
71
72/************ NodeWatcher ************/
73
74class NodeWatcher : public Inkscape::XML::NodeObserver
75{
76public:
77 NodeWatcher() = delete;
78 NodeWatcher(XmlTreeView *xml_tree_view, Inkscape::XML::Node *node, Gtk::TreeRow *row);
79 ~NodeWatcher() override;
80
81 std::unordered_map<Inkscape::XML::Node const *,
82 std::unique_ptr<NodeWatcher>> child_watchers;
83
84private:
85 void update_row();
86
87 // Treeview routines
88 Gtk::TreeNodeChildren get_children() const;
89 void add_child (Inkscape::XML::Node *child); // Add a NodeWatcher for a child.
90 void add_children(); // Add children to this node. (Maybe not needed.)
91
92 // XML routines
93 void move_child(Inkscape::XML::Node &child, Inkscape::XML::Node *sibling);
94
95 Gtk::TreeModel::iterator get_child_iterator(Inkscape::XML::Node *node) const;
96
97 // Notifiers
98 void notifyContentChanged(Inkscape::XML::Node & /* node */,
99 Inkscape::Util::ptr_shared /* old_content */,
100 Inkscape::Util::ptr_shared new_content) override
101 {
102 update_row();
103 }
104
105 void notifyChildAdded(Inkscape::XML::Node &node,
107 Inkscape::XML::Node *prev) override
108 {
109 assert (this->node == &node);
110
111 add_child(&child);
112 move_child(child, prev);
113 }
114
115 void notifyChildRemoved(Inkscape::XML::Node &node,
117 Inkscape::XML::Node *) override
118 {
119 assert (this->node == &node);
120
121 if (child_watchers.erase(&child) > 0) {
122 return;
123 }
124
125 std::cerr << "NodeWatcher::notifyChildRemoved: failed to remove child!"
126 << std::endl;
127 }
128
129 void notifyChildOrderChanged(Inkscape::XML::Node &parent,
131 Inkscape::XML::Node * /* old parent */,
132 Inkscape::XML::Node *new_prev) override
133 {
134 assert (this->node == &parent);
135
136 move_child(child, new_prev);
137 }
138
139 void notifyAttributeChanged(Inkscape::XML::Node &node,
140 GQuark key,
143 {
144 // Only worry about 'id' or 'inkscape::label' changes.
145 auto const attribute = g_quark_to_string(key);
146 if (std::strcmp(attribute, "id") == 0 ||
147 std::strcmp(attribute, "inkscape:label") == 0) {
148 update_row();
149 }
150 }
151
152 void notifyElementNameChanged(Inkscape::XML::Node &node,
153 GQuark,
154 GQuark) override
155 {
156 update_row();
157 }
158
159 // Variables
161 XmlTreeView *xml_tree_view;
162 Gtk::TreeModel::RowReference row_ref;
163};
164
165NodeWatcher::NodeWatcher(XmlTreeView *xml_tree_view, Inkscape::XML::Node *node, Gtk::TreeRow *row)
166 : xml_tree_view(xml_tree_view)
167 , node(node)
168 , row_ref()
169{
170 if (row != nullptr) {
171 assert(row->children().empty());
172
173 auto store = xml_tree_view->store;
174
175 auto row_iter = row->get_iter();
176 auto path = store->get_path(row_iter);
177 row_ref = Gtk::TreeModel::RowReference(store, path);
178
179 update_row();
180 }
181
182 node->addObserver(*this);
183
184 add_children(); // Recursively add all descendents.
185}
186
187NodeWatcher::~NodeWatcher()
188{
189 node->removeObserver(*this);
190 Gtk::TreeModel::Path path;
191 if (bool(row_ref) && (path = row_ref.get_path())) {
192 if (auto iter = xml_tree_view->store->get_iter(path)) {
193 xml_tree_view->store->erase(iter);
194 }
195 }
196 child_watchers.clear();
197}
198
199void
200NodeWatcher::update_row()
201{
202 Glib::ustring start;
203 Glib::ustring end;
205 switch (node->type()) {
206 case NodeType::ELEMENT_NODE: start = "<"; end = ">"; break;
207 case NodeType::TEXT_NODE: start = "\""; end = "\""; break;
208 case NodeType::COMMENT_NODE: start = "<!--"; end = "-->"; break;
209 case NodeType::PI_NODE: start = "<?"; end = "?>"; break;
210 case NodeType::DOCUMENT_NODE: break;
211 default: std::cerr << "NodeWatcher::NodeWatcher: unhandled NodeType!" << std::endl;
212 }
213
214 Glib::ustring content;
215 Glib::ustring text;
216 Glib::ustring markup;
217 switch (node->type()) {
218 case NodeType::ELEMENT_NODE: {
219
220 content = node->name();
221
222 // Remove namespace "svg", it's just visual noise.
223 auto pos = content.find("svg:");
224 if (pos != Glib::ustring::npos) {
225 content.erase(pos, 4);
226 }
227
228 // Markup text with color.
229 xml_tree_view->formatter->openTag(content.c_str());
230
231 // char const *id = node->attribute("id");
232 if (char const *id = node->attribute("id")) {
233 content += " id=\"";
234 content += id;
235 content += "\"";
236 xml_tree_view->formatter->addAttribute("id", id);
237 }
238
239 if (char const *label = node->attribute("inkscape::label")) {
240 content += " inkscape:label=\"";
241 content += label;
242 content += "\"";
243 xml_tree_view->formatter->addAttribute("inkscape:label", label);
244 }
245
246 text = start + content + end;
247 markup = xml_tree_view->formatter->finishTag();
248
249 break;
250 }
251 case NodeType::TEXT_NODE:
252 case NodeType::COMMENT_NODE:
253 case NodeType::PI_NODE:
254 {
255 if (auto simple_node = dynamic_cast<Inkscape::XML::SimpleNode*>(node)) {
256 if (simple_node->content()) {
257 content = simple_node->content();
258 }
259 }
260 text = start + content + end;
261 markup = xml_tree_view->formatter->formatContent(text.c_str(), false);
262 break;
263 }
264 case NodeType::DOCUMENT_NODE: break;
265 default: std::cerr << "NodeWatcher::NodeWatcher: unhandled NodeType!" << std::endl;
266 }
267
268 auto path = row_ref.get_path();
269 if (!path) {
270 std::cerr << "NodeWatcher::update_row: no path!" << std::endl;
271 return;
272 }
273 auto row_iter = xml_tree_view->store->get_iter(path);
274 if (!row_iter) {
275 std::cerr << "NodeWatcher::update_row: no row_iter!" << std::endl;
276 return;
277 }
278
279 (*row_iter)[xml_tree_view->model_columns->node ] = node;
280 (*row_iter)[xml_tree_view->model_columns->text ] = text;
281 (*row_iter)[xml_tree_view->model_columns->markup] = markup;
282}
283
284
285Gtk::TreeNodeChildren
286NodeWatcher::get_children() const
287{
288 Gtk::TreeModel::Path path;
289 if (row_ref && (path = row_ref.get_path())) {
290 return xml_tree_view->store->get_iter(path)->children();
291 }
292 assert (!row_ref);
293 return xml_tree_view->store->children();
294}
295
296void
297NodeWatcher::add_child(Inkscape::XML::Node *child)
298{
299 assert (child);
300
301 auto children = get_children();
302 Gtk::TreeModel::Row row = *(xml_tree_view->store->append(children));
303
304 auto &watcher = child_watchers[child];
305 assert (!watcher);
306 watcher.reset(new NodeWatcher(xml_tree_view, child, &row));
307}
308
309void
310NodeWatcher::add_children()
311{
312 for (auto *child = node->firstChild(); child != nullptr; child = child->next()) {
313 add_child(child);
314 }
315}
316
320void
321NodeWatcher::move_child(Inkscape::XML::Node &child, Inkscape::XML::Node *sibling)
322{
323 auto child_iter = get_child_iterator(&child);
324 if (!child_iter) {
325 std::cerr << "NodeWatcher::move_child: no child iterator!" << std::endl;
326 return;
327 }
328
329 auto sibling_iter = get_child_iterator(sibling); // Can be null.
330 // move() puts the child before the sibling, but we need it before...
331 if (sibling_iter) {
332 sibling_iter++;
333 } else {
334 sibling_iter = get_children().begin(); // Messy!
335 }
336 xml_tree_view->store->move( child_iter, sibling_iter);
337}
338
339Gtk::TreeModel::iterator
340NodeWatcher::get_child_iterator(Inkscape::XML::Node *node) const
341{
342 auto child_rows = get_children();
343
344 if (!node) {
345 return child_rows.end();
346 }
347
348 for (auto &row : child_rows) {
349 if (xml_tree_view->get_repr(row) == node) {
350 return row.get_iter();
351 }
352 }
353
354 std::cerr << "NodeWatcher::get_child_iterator: failed to find interator!" << std::endl;
355
356 return child_rows.begin();
357}
358
359/************ NodeRenderer ***********/
360
361class NodeRenderer : public Gtk::CellRendererText {
362public:
363 NodeRenderer()
364 : Glib::ObjectBase(typeid(CellRendererText))
365 , Gtk::CellRendererText()
366 , property_plain_text(*this, "plain", "-") {}
367
368 Glib::Property<Glib::ustring> property_plain_text;
369
370 void snapshot_vfunc(Glib::RefPtr<Gtk::Snapshot> const &snapshot,
371 Gtk::Widget &widget,
372 const Gdk::Rectangle &background_area,
373 const Gdk::Rectangle &cell_area,
374 Gtk::CellRendererState flags) override
375 {
376 if ((bool)(flags & Gtk::CellRendererState::SELECTED)) {
377 // Use plain text instead of marked-up text to render selected nodes, for legibility.
378 property_text() = property_plain_text.get_value();
379 }
380 Gtk::CellRendererText::snapshot_vfunc(snapshot, widget, background_area, cell_area, flags);
381 }
382};
383
384/************ XmlTreeView ************/
385
386XmlTreeView::XmlTreeView()
387{
388 set_name("XmlTreeView");
389 set_headers_visible(false);
390 set_reorderable(false); // Don't interfere with D&D via controllers!
391 set_enable_search(true);
392
393 model_columns = std::make_unique<ModelColumns>();
394 store = Gtk::TreeStore::create(*model_columns);
395 set_model(store);
396
397 // Text rendering
398 formatter = std::unique_ptr<Inkscape::UI::Syntax::XMLFormatter>(new Inkscape::UI::Syntax::XMLFormatter());
399 text_renderer = Gtk::make_managed<NodeRenderer>();
400 auto text_column = Gtk::make_managed<Gtk::TreeViewColumn>();
401 text_column->pack_start(*text_renderer, true);
402 text_column->set_expand(true);
403 text_column->add_attribute(*text_renderer, "markup", model_columns->markup);
404 text_column->add_attribute(*text_renderer, "plain", model_columns->text);
405 append_column(*text_column);
406
407 enable_model_drag_source ();
408 auto const drag = Gtk::DragSource::create();
409 drag->set_actions(Gdk::DragAction::MOVE);
410 drag->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
411 drag->signal_prepare().connect([this, &drag = *drag](auto &&...args) { return on_prepare(drag, args...); }, false); // before
412 add_controller(drag);
413
414 auto const drop = Gtk::DropTarget::create(GlibValue::type<XMLDnDRow>(), Gdk::DragAction::MOVE);
415 drop->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
416 drop->signal_motion().connect(sigc::mem_fun(*this, &XmlTreeView::on_drag_motion), false); // before
417 drop->signal_drop().connect(sigc::mem_fun(*this, &XmlTreeView::on_drag_drop), false); // before
418 add_controller(drop);
419}
420
421// Build TreeView model, starting with root.
422void
423XmlTreeView::build_tree(SPDocument* document_in)
424{
425 document = document_in;
426
427 root_watcher.reset();
428
429 if (!document) {
430 return;
431 }
432
433 auto root = document->getReprRoot();
434 if (!root) {
435 std::cerr << "XMLTreeView::set_root_watcher: No XML root!" << std::endl;
436 return;
437 }
438
439 Gtk::TreeModel::Row row = *(store->prepend());
440 root_watcher = std::make_unique<NodeWatcher>(this, root, &row);
441}
442
444XmlTreeView::get_repr(Gtk::TreeModel::ConstRow const &row) const
445{
446 return row[model_columns->node];
447}
448
452void
453XmlTreeView::select_node(Inkscape::XML::Node *node, bool edit)
454{
455 auto selection = get_selection();
456
457 if (node) {
458 store->foreach_iter([this, node, edit, selection](const Gtk::TreeModel::iterator &it) {
459 if ((*it)[model_columns->node] == node) {
460
461 // Ensure node is shown
462 auto path = store->get_path(it);
463 expand_to_path(path);
464 auto column = get_column(0);
465 scroll_to_cell(path, *column, 0.66, 0.0);
466
467 selection->unselect_all();
468 selection->select(it);
469 set_cursor(path, *column, edit);
470
471 return true; // stop
472 }
473 return false; // continue
474 });
475 } else {
476 selection->unselect_all();
477 }
478}
479
480/*
481 * Set style for formatting tree.
482 */
483void
484XmlTreeView::set_style(Inkscape::UI::Syntax::XMLStyles const &new_style)
485{
486 if (formatter) {
487 formatter->setStyle(new_style);
488 }
489}
490
491Glib::RefPtr<Gdk::ContentProvider>
492XmlTreeView::on_prepare(Gtk::DragSource &controller, double x, double y)
493{
494 Gtk::TreeModel::Path path;
495 Gtk::TreeView::DropPosition pos;
496 get_dest_row_at_pos(x, y, path, pos);
497
498 // Glib::ustring drag_label;
499 Inkscape::XML::Node *node = nullptr;
500
501 if (path) {
502 // Don't drag root element <svg::svg/>!
503 if (path.to_string() == "0") { // Gtkmm missing get_depth() function!
504 return nullptr;
505 }
506
507 static GQuark const CODE_sodipodi_namedview = g_quark_from_static_string("sodipodi:namedview");
508 static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs");
509
510 if (auto row_iter = store->get_iter(path)) {
511 node = (*row_iter)[model_columns->node];
512
513 // Don't drag, document holds pointers to these elements which must stay valid.
514 if (node->code() == CODE_sodipodi_namedview ||
515 node->code() == CODE_svg_defs) {
516 return nullptr;
517 }
518
519 } else {
520 return nullptr;
521 }
522
523 // Set icon (or else icon is determined by provider value).
524 auto surface = create_row_drag_icon(path);
525 controller.set_icon(surface, x, 12);
526 }
527
528 return Gdk::ContentProvider::create(GlibValue::create<XMLDnDRow>(XMLDnDRow{node}));
529}
530
531Gdk::DragAction XmlTreeView::on_drag_motion(double const x, double const y)
532{
533 Gtk::TreeModel::Path path;
534 Gtk::TreeView::DropPosition pos;
535 get_dest_row_at_pos(x, y, path, pos);
536
537 if (path) {
538 if (auto row_iter = store->get_iter(path)) {
539 // std::cout << " Over " << (*row_iter)[model_columns->text] << std::endl;
540
541 Inkscape::XML::Node *node = (*row_iter)[model_columns->node];
542 bool const drop_into =
543 pos != Gtk::TreeView::DropPosition::BEFORE &&
544 pos != Gtk::TreeView::DropPosition::AFTER;
545
546 // Only xml element nodes can have children.
547 if (drop_into && node->type() != Inkscape::XML::NodeType::ELEMENT_NODE) {
548 unset_drag_dest_row();
549 return Gdk::DragAction{};
550 }
551
552 return Gdk::DragAction::MOVE;
553 }
554 }
555
556 return Gdk::DragAction::MOVE; // Drag at bottom moves object to end.
557}
558
559bool XmlTreeView::on_drag_drop(Glib::ValueBase const &value, double x, double y)
560{
561 auto pointer = GlibValue::get<XMLDnDRow>(value);
562 assert(pointer);
563 auto node = pointer->node;
564 assert(node);
565
566 Glib:: ustring id = ( node->attribute("id") ? node->attribute("id") : "Not element");
567
568 Gtk::TreeModel::Path path;
569 Gtk::TreeView::DropPosition pos;
570 get_dest_row_at_pos(x, y, path, pos);
571
572 if (!path) {
573 if (is_blank_at_pos(x, y)) {
574 // Move to end, why is this so hard?
575
576 // Find first child of "svg:svg".
577 path = Gtk::TreePath("0:0");
578 auto row_iter = store->get_iter(path);
579 Inkscape::XML::Node *child_node = (*row_iter)[model_columns->node];
580 // Now count children.
581 int n = 0;
582 while (child_node->next()) {
583 child_node = child_node->next();
584 ++n;
585 }
586 Glib::ustring path_index = "0:" + std::to_string(n);
587 path = Gtk::TreePath(path_index);
588 pos = Gtk::TreeView::DropPosition::AFTER;
589 } else {
590 return true;
591 }
592 }
593
594 auto row_iter = store->get_iter(path);
595 assert (row_iter);
596
597 Inkscape::XML::Node *drop_node = (*row_iter)[model_columns->node];
598
599 if (node == drop_node) {
600 // Don't drop onto self!
601 return false;
602 }
603
604 bool const drop_into =
605 pos != Gtk::TreeView::DropPosition::BEFORE && // 0
606 pos != Gtk::TreeView::DropPosition::AFTER; // 1
607
608 auto parent_node = node->parent();
609 auto drop_parent_node = drop_node->parent();
610
611 // Glib:: ustring drop_id = (drop_node->attribute("id") ? drop_node->attribute("id") : "Not element");
612 // std::cout << " node: " << node->name() << " (" << id << ")"
613 // << " parent: " << parent_node->name()
614 // << " drop node: " << drop_node->name() << "(" << drop_id << ")"
615 // << " drop parent: " << drop_parent_node->name()
616 // << " pos: " << (int)pos
617 // << " drop_into: " << std::boolalpha << drop_into
618 // <<std::endl;
619
620 if (drop_into) {
621 // Only drop into containers!
622 assert (document);
623 auto item = document->getObjectByRepr(drop_node);
624 if (item && (
625 is<SPDefs> (item) ||
626 is<SPGlyph> (item) ||
627 is<SPGroup> (item) || // (SPRoot, SPMarker, SPBox3D, SPSwitch, SPAnchor, SPSymbol)
628 is<SPMask> (item) ||
629 is<SPPattern>(item) ||
630 is<SPTSpan> (item) ||
631 is<SPText> (item)
632 )) {
633 parent_node->removeChild(node);
634 drop_node->addChild(node, nullptr);
635 }
636 } else {
637 if (pos == Gtk::TreeView::DropPosition::BEFORE) {
638 drop_node = drop_node->prev();
639 }
640 if (parent_node == drop_parent_node) {
641 parent_node->changeOrder(node, drop_node);
642 } else {
643 parent_node->removeChild(node);
644 drop_parent_node->addChild(node, drop_node);
645 }
646 }
647
648 // while (node->parent()) { node = node->parent(); }
649 // node->recursivePrintTree(0);
650
651 return true;
652}
653
654} // namespace Inkscape::UI::Widget
655
656/*
657 Local Variables:
658 mode:c++
659 c-file-style:"stroustrup"
660 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
661 indent-tabs-mode:nil
662 fill-column:99
663 End:
664*/
665// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
Fragment store
Definition canvas.cpp:155
Hand-rolled LLVM-style RTTI system for class hierarchies where dynamic_cast isn't fast enough.
A formatter for XML syntax, based on Pango markup.
Definition syntax.h:64
Interface for XML node observers.
Interface for refcounted XML nodes.
Definition node.h:80
virtual Node * parent()=0
Get the parent of this node.
virtual Node * next()=0
Get the next sibling of this node.
virtual Node * prev()=0
virtual void addChild(Node *child, Node *after)=0
Insert another node as a child of this node.
virtual void changeOrder(Node *child, Node *after)=0
Move a given node in this node's child order.
virtual void removeChild(Node *child)=0
Remove a child of this node.
Default implementation of the XML node stored in memory.
Definition simple-node.h:37
Typed SVG document implementation.
Definition document.h:101
Inkscape::XML::Node * getReprRoot()
Definition document.h:206
RootCluster root
static char const *const parent
Definition dir-util.cpp:70
SPItem * item
Inkscape::XML::Node * node
Geom::Point start
Glib::ustring label
Geom::Point end
Definition desktop.h:50
Custom widgets.
Definition desktop.h:126
std::vector< Gtk::Widget * > get_children(Gtk::Widget &widget)
Get a vector of the widgetʼs children, from get_first_child() through each get_next_sibling().
Definition util.cpp:141
Miscellaneous supporting code.
Definition document.h:93
NodeType
Enumeration containing all supported node types.
Definition node.h:40
@ ELEMENT_NODE
Regular element node, e.g. <group />.
Interface for XML node observers.
static cairo_user_data_key_t key
Ocnode * child[8]
Definition quantize.cpp:33
GC-managed XML node implementation.
Some things pertinent to all visible shapes: SPItem, SPItemView, SPItemCtx.
guint32 GQuark
SVG <pattern> implementation.
SPRoot: SVG <svg> implementation.
TODO: insert short description here.
The styles used for simple XML syntax highlighting.
Definition syntax.h:46
Wrapper for the GLib value API.
XML::Node * node
Interface for XML nodes.