summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/elements/_selected.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
commit35a96bde514a8897f6f0fcc41c5833bf63df2e2a (patch)
tree657d15a03cc46bd099fc2c6546a7a4ad43815d9f /share/extensions/inkex/elements/_selected.py
parentInitial commit. (diff)
downloadinkscape-upstream/1.0.2.tar.xz
inkscape-upstream/1.0.2.zip
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/inkex/elements/_selected.py')
-rw-r--r--share/extensions/inkex/elements/_selected.py159
1 files changed, 159 insertions, 0 deletions
diff --git a/share/extensions/inkex/elements/_selected.py b/share/extensions/inkex/elements/_selected.py
new file mode 100644
index 0000000..c09c526
--- /dev/null
+++ b/share/extensions/inkex/elements/_selected.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc.,Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+When elements are selected, these structures provide an advanced API.
+"""
+
+from collections import OrderedDict
+
+class ElementList(OrderedDict):
+ """
+ A list of elements, selected by id, iterator or xpath
+
+ This may look like a dictionary, but it is really a list of elements.
+ The default iterator is the element objects themselves (not keys) and it is
+ possible to key elements by their numerical index.
+
+ It is also possible to look up items by their id and the element object itself.
+ """
+ def __init__(self, svg, _iter=None):
+ self.svg = svg
+ self.ids = OrderedDict()
+ super(ElementList, self).__init__()
+ if _iter:
+ self.set(*list(_iter))
+
+ def __getitem__(self, key):
+ return super(ElementList, self).__getitem__(self._to_key(key))
+
+ def __contains__(self, key):
+ return super(ElementList, self).__contains__(self._to_key(key))
+
+ def __setitem__(self, orig_key, elem):
+ from ._base import BaseElement
+ if orig_key != elem and orig_key != elem.get('id'):
+ raise ValueError("Refusing to set bad key in ElementList {}".format(orig_key))
+ if isinstance(elem, str):
+ key = elem
+ elem = self.svg.getElementById(elem)
+ if elem is None:
+ return
+ if isinstance(elem, BaseElement):
+ # Selection is a list of elements to select
+ key = elem.xml_path
+ element_id = elem.get('id')
+ if element_id is not None:
+ self.ids[element_id] = key
+ super(ElementList, self).__setitem__(key, elem)
+ else:
+ kind = type(elem).__name__
+ raise ValueError("Unknown element type: {}".format(kind))
+
+ def _to_key(self, key, default=None):
+ """Takes a key (id, element, etc) and returns an xml_path key"""
+ from ._base import BaseElement
+ if self and key is None:
+ key = default
+ if isinstance(key, int):
+ return list(self.keys())[key]
+ elif isinstance(key, BaseElement):
+ return key.xml_path
+ elif isinstance(key, str) and key[0] != '/':
+ return self.ids.get(key, key)
+ return key
+
+ def clear(self):
+ """Also clear ids"""
+ self.ids.clear()
+ super(ElementList, self).clear()
+
+ def set(self, *ids):
+ """
+ Sets the currently selected elements to these ids, any existing
+ selection is cleared.
+
+ Arguments a list of element ids, element objects or
+ a single xpath expression starting with "//".
+
+ All element objects must have an id to be correctly set.
+
+ >>> selection.set("rect123", "path456", "text789")
+ >>> selection.set(elem1, elem2, elem3)
+ >>> selection.set("//rect")
+ """
+ self.clear()
+ self.add(*ids)
+
+ def pop(self, key=None):
+ """Remove the key item or remove the last item selected"""
+ item = super(ElementList, self).pop(self._to_key(key, default=-1))
+ self.ids.pop(item.get('id'))
+ return item
+
+ def add(self, *ids):
+ """Like set() but does not clear first"""
+ # Allow selecting of xpath elements directly
+ if len(ids) == 1 and isinstance(ids[0], str) and ids[0].startswith('//'):
+ ids = self.svg.xpath(ids[0])
+
+ for elem in ids:
+ self[elem] = elem # This doesn't matter
+
+ def paint_order(self):
+ """Get the selected elements, but ordered by their appearance in the document"""
+ new_list = ElementList(self.svg)
+ new_list.set(*[elem for _, elem in sorted(self.items(), key=lambda x: x[0])])
+ return new_list
+
+ def filter(self, *types):
+ """Filter selected elements of the given type, returns a new SelectedElements object"""
+ return ElementList(self.svg, [e for e in self.values() if not types or isinstance(e, types)])
+
+ def get(self, *types):
+ """Like filter, but will enter each element searching for any child of the given types"""
+ def _recurse(elem):
+ if not types or isinstance(elem, types):
+ yield elem
+ for child in elem:
+ for item in _recurse(child):
+ yield item
+ return ElementList(self.svg, [r for e in self.values() for r in _recurse(e)])
+
+ def id_dict(self):
+ """For compatibility, return regular dictionary of id -> element pairs"""
+ return OrderedDict([(eid, self[xid]) for eid, xid in self.ids.items()])
+
+ def bounding_box(self):
+ """
+ Gets a :class:`inkex.transforms.BoundingBox` object for the selected items.
+
+ Text objects have a bounding box without width or height that only
+ reflects the coordinate of their anchor. If a text object is a part of
+ the selection's boundary, the bounding box may be inaccurate.
+
+ When no object is selected or when the object's location cannot be
+ determined (e.g. empty group or layer), all coordinates will be None.
+ """
+ return sum([elem.bounding_box() for elem in self.values()], None)
+
+ def first(self):
+ """Returns the first item in the selected list"""
+ for elem in self.values():
+ return elem
+ return None