bundle: update (2026-01-18)
This commit is contained in:
562
extensions/km-hershey/deps/inkex/gui/listview.py
Normal file
562
extensions/km-hershey/deps/inkex/gui/listview.py
Normal file
@@ -0,0 +1,562 @@
|
||||
#
|
||||
# Copyright 2011-2022 Martin Owens <doctormo@geek-2.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 3 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, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
"""
|
||||
Wraps the gtk treeview and iconview in something a little nicer.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Tuple, Type, Optional
|
||||
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, Pango
|
||||
|
||||
from .pixmap import PixmapManager, SizeFilter
|
||||
|
||||
GOBJ = GObject.TYPE_PYOBJECT
|
||||
|
||||
|
||||
def default(item, attr, d=None):
|
||||
"""Python logic to choose an attribute, call it if required and return"""
|
||||
if hasattr(item, attr):
|
||||
prop = getattr(item, attr)
|
||||
if callable(prop):
|
||||
prop = prop()
|
||||
return prop
|
||||
return d
|
||||
|
||||
|
||||
def cmp(a, b):
|
||||
"""Compare two objects"""
|
||||
return (a > b) - (a < b)
|
||||
|
||||
|
||||
def item_property(name, d=None):
|
||||
def inside(item):
|
||||
return default(item, name, d)
|
||||
|
||||
return inside
|
||||
|
||||
|
||||
def label(obj):
|
||||
if isinstance(obj, tuple):
|
||||
return " or ".join([label(o) for o in obj])
|
||||
if not isinstance(obj, type):
|
||||
obj = type(obj)
|
||||
return obj.__name__
|
||||
|
||||
|
||||
class BaseView:
|
||||
"""Controls for tree and icon views, a base class"""
|
||||
|
||||
widget_type: Optional[Type[Gtk.Widget]] = None
|
||||
|
||||
def __init__(self, widget, liststore=None, **kwargs):
|
||||
if not isinstance(widget, self.widget_type):
|
||||
lbl1 = label(self.widget_type)
|
||||
lbl2 = label(widget)
|
||||
raise TypeError(f"Wrong widget type: Expected {lbl1} got {lbl2}")
|
||||
|
||||
self.selected_signal = kwargs.get("selected", None)
|
||||
self._iids = []
|
||||
self._list = widget
|
||||
self.args = kwargs
|
||||
self.selected = None
|
||||
self._data = None
|
||||
self.no_dupes = True
|
||||
self._model = self.create_model(liststore or widget.get_model())
|
||||
self._list.set_model(self._model)
|
||||
self.setup()
|
||||
|
||||
self._list.connect(self.changed_signal, self.item_selected_signal)
|
||||
|
||||
def get_model(self):
|
||||
"""Returns the current data store model"""
|
||||
return self._model
|
||||
|
||||
def create_model(self, liststore):
|
||||
"""Setup the model and list"""
|
||||
if not isinstance(liststore, (Gtk.ListStore, Gtk.TreeStore)):
|
||||
lbl = label(liststore)
|
||||
raise TypeError(f"Expected List or TreeStore, got {lbl}")
|
||||
return liststore
|
||||
|
||||
def refresh(self):
|
||||
"""Attempt to refresh the listview"""
|
||||
self._list.queue_draw()
|
||||
|
||||
def setup(self):
|
||||
"""Setup columns, views, sorting etc"""
|
||||
pass
|
||||
|
||||
def get_item_id(self, item):
|
||||
"""
|
||||
Return an id set against this item.
|
||||
|
||||
If item.get_id() is set then duplicates will be ignored.
|
||||
"""
|
||||
if hasattr(item, "get_id"):
|
||||
return item.get_id()
|
||||
return None
|
||||
|
||||
def replace(self, new_item, item_iter=None):
|
||||
"""Replace all items, or a single item with object"""
|
||||
if item_iter:
|
||||
self.remove_item(item_iter)
|
||||
self.add_item(new_item)
|
||||
else:
|
||||
self.clear()
|
||||
self._data = new_item
|
||||
self.add_item(new_item)
|
||||
|
||||
def item_selected(self, item=None, *others):
|
||||
"""Base method result, called as an item is selected"""
|
||||
if self.selected != item:
|
||||
self.selected = item
|
||||
if self.selected_signal and item:
|
||||
self.selected_signal(item)
|
||||
|
||||
def remove_item(self, item=None):
|
||||
"""Remove an item from this view"""
|
||||
return self._model.remove(self.get_iter(item))
|
||||
|
||||
def check_item_id(self, item):
|
||||
"""Item id is recorded to guard against duplicates"""
|
||||
iid = self.get_item_id(item)
|
||||
if iid in self._iids and self.no_dupes:
|
||||
raise ValueError(f"Will not add duplicate row {iid}")
|
||||
if iid:
|
||||
self._iids.append(iid)
|
||||
|
||||
def __iter__(self):
|
||||
ret = []
|
||||
|
||||
def collect_all(store, treepath, treeiter):
|
||||
ret.append((self.get_item(treeiter), treepath, treeiter))
|
||||
|
||||
self._model.foreach(collect_all)
|
||||
return ret.__iter__()
|
||||
|
||||
def set_sensitive(self, sen=True):
|
||||
"""Proxy the GTK property for sensitivity"""
|
||||
self._list.set_sensitive(sen)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all items from this treeview"""
|
||||
self._iids = []
|
||||
self._model.clear()
|
||||
|
||||
def item_double_clicked(self, *items):
|
||||
"""What happens when you double click an item"""
|
||||
return items # Nothing
|
||||
|
||||
def get_item(self, item_iter):
|
||||
"""Return the object of attention from an iter"""
|
||||
return self._model[self.get_iter(item_iter)][0]
|
||||
|
||||
def get_iter(self, item, path=False):
|
||||
"""Return the iter given the item"""
|
||||
if isinstance(item, Gtk.TreePath):
|
||||
return item if path else self._model.get_iter(item)
|
||||
if isinstance(item, Gtk.TreeIter):
|
||||
return self._model.get_path(item) if path else item
|
||||
for src_item, src_path, src_iter in self:
|
||||
if item == src_item:
|
||||
return src_path if path else src_iter
|
||||
return None
|
||||
|
||||
|
||||
class TreeView(BaseView):
|
||||
"""Controls and operates a tree view."""
|
||||
|
||||
column_size = 16
|
||||
widget_type = Gtk.TreeView
|
||||
changed_signal = "cursor_changed"
|
||||
|
||||
def setup(self):
|
||||
"""Setup the treeview"""
|
||||
self._sel = self._list.get_selection()
|
||||
self._sel.set_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
self._list.connect("cursor-changed", self.item_selected_signal)
|
||||
# Separators should do something
|
||||
self._list.set_row_separator_func(TreeView.is_separator, None)
|
||||
super().setup()
|
||||
|
||||
@staticmethod
|
||||
def is_separator(model, item_iter, data):
|
||||
"""Internal function for seperator checking"""
|
||||
return isinstance(model.get_value(item_iter, 0), Separator)
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Return a list of selected item objects"""
|
||||
return [self.get_item(row) for row in self._sel.get_selected_rows()[1]]
|
||||
|
||||
def set_selected_items(self, *items):
|
||||
"""Select the given items"""
|
||||
self._sel.unselect_all()
|
||||
for item in items:
|
||||
path_item = self.get_iter(item, path=True)
|
||||
if path_item is not None:
|
||||
self._sel.select_path(path_item)
|
||||
|
||||
def is_selected(self, item):
|
||||
"""Return true if the item is selected"""
|
||||
return self._sel.iter_is_selected(self.get_iter(item))
|
||||
|
||||
def add(self, target, parent=None):
|
||||
"""Add all items from the target to the treeview"""
|
||||
for item in target:
|
||||
self.add_item(item, parent=parent)
|
||||
|
||||
def add_item(self, item, parent=None):
|
||||
"""Add a single item image to the control, returns the TreePath"""
|
||||
if item is not None:
|
||||
self.check_item_id(item)
|
||||
return self._add_item([item], self.get_iter(parent))
|
||||
raise ValueError("Item can not be None.")
|
||||
|
||||
def _add_item(self, item, parent):
|
||||
return self.get_iter(self._model.append(parent, item), path=True)
|
||||
|
||||
def item_selected_signal(self, *args, **kwargs):
|
||||
"""Signal for selecting an item"""
|
||||
return self.item_selected(*self.get_selected_items())
|
||||
|
||||
def item_button_clicked(self, _, event):
|
||||
"""Signal for mouse button click"""
|
||||
if event is None or event.type == Gdk.EventType._2BUTTON_PRESS:
|
||||
self.item_double_clicked(*self.get_selected_items())
|
||||
|
||||
def expand_item(self, item, expand=True):
|
||||
"""Expand one of our nodes"""
|
||||
self._list.expand_row(self.get_iter(item, path=True), expand)
|
||||
|
||||
def create_model(self, liststore=None):
|
||||
"""Set up an icon view for showing gallery images"""
|
||||
if liststore is None:
|
||||
liststore = Gtk.TreeStore(GOBJ)
|
||||
return super().create_model(liststore)
|
||||
|
||||
def create_column(self, name, expand=True):
|
||||
"""
|
||||
Create and pack a new column to this list.
|
||||
|
||||
name - Label in the column header
|
||||
expand - Should the column expand
|
||||
"""
|
||||
return ViewColumn(self._list, name, expand=expand)
|
||||
|
||||
def create_sort(self, *args, **kwargs):
|
||||
"""
|
||||
Create and attach a sorting view to this list.
|
||||
|
||||
see ViewSort arguments for details.
|
||||
"""
|
||||
return ViewSort(self._list, *args, **kwargs)
|
||||
|
||||
|
||||
class ComboBox(TreeView):
|
||||
"""Controls and operates a combo box list."""
|
||||
|
||||
widget_type = Gtk.ComboBox
|
||||
changed_signal = "changed"
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def get_selected_item(self):
|
||||
"""Return the selected item of this combo box"""
|
||||
return self.get_item(self._list.get_active_iter())
|
||||
|
||||
def set_selected_item(self, item):
|
||||
"""Set the given item as the selected item"""
|
||||
self._list.set_active_iter(self.get_iter(item))
|
||||
|
||||
def is_selected(self, item):
|
||||
"""Returns true if this item is the selected item"""
|
||||
return self.get_selected_item() == item
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Return a list of selected items (one)"""
|
||||
return [self.get_selected_item()]
|
||||
|
||||
|
||||
class IconView(BaseView):
|
||||
"""Allows a simpler IconView for DBus List Objects"""
|
||||
|
||||
widget_type = Gtk.IconView
|
||||
changed_signal = "selection-changed"
|
||||
|
||||
def __init__(self, widget, pixmaps, *args, **kwargs):
|
||||
super().__init__(widget, *args, **kwargs)
|
||||
self.pixmaps = pixmaps
|
||||
|
||||
def set_selected_item(self, item):
|
||||
"""Sets the selected item to this item"""
|
||||
path = self.get_iter(item, path=True)
|
||||
if path:
|
||||
self._list.set_cursor(path, None, False)
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Return the seleced item"""
|
||||
return [self.get_item(path) for path in self._list.get_selected_items()]
|
||||
|
||||
def create_model(self, liststore):
|
||||
"""Setup the icon view control and model"""
|
||||
if not liststore:
|
||||
liststore = Gtk.ListStore(GOBJ, str, GdkPixbuf.Pixbuf)
|
||||
return super().create_model(liststore)
|
||||
|
||||
def setup(self):
|
||||
"""Setup the columns for the iconview"""
|
||||
self._list.set_markup_column(1)
|
||||
self._list.set_pixbuf_column(2)
|
||||
super().setup()
|
||||
|
||||
def add(self, target):
|
||||
"""Add all items from the target to the iconview"""
|
||||
for item in target:
|
||||
self.add_item(item)
|
||||
|
||||
def add_item(self, item):
|
||||
"""Add a single item image to the control"""
|
||||
if item is not None:
|
||||
self.check_item_id(item)
|
||||
return self._add_item(item)
|
||||
raise ValueError("Item can not be None.")
|
||||
|
||||
def get_markup(self, item):
|
||||
"""Default text return for markup."""
|
||||
return default(item, "name", str(item))
|
||||
|
||||
def get_icon(self, item):
|
||||
"""Default icon return, pixbuf or gnome theme name"""
|
||||
return default(item, "icon", None)
|
||||
|
||||
def _get_icon(self, item):
|
||||
return self.pixmaps.get(self.get_icon(item), item=item)
|
||||
|
||||
def _add_item(self, item):
|
||||
"""
|
||||
Each item's properties must be stuffed into the ListStore directly
|
||||
or the IconView won't see them, but only if on auto.
|
||||
"""
|
||||
if not isinstance(item, (tuple, list)):
|
||||
item = [item, self.get_markup(item), self._get_icon(item)]
|
||||
return self._model.append(item)
|
||||
|
||||
def item_selected_signal(self, *args, **kwargs):
|
||||
"""Item has been selected"""
|
||||
return self.item_selected(*self.get_selected_items())
|
||||
|
||||
|
||||
class ViewSort(object):
|
||||
"""
|
||||
A sorting function for use is ListViews
|
||||
|
||||
ascending - Boolean which direction to sort
|
||||
contains - Contains this string
|
||||
data - A string or function to get data from each item.
|
||||
exact - Compare to this exact string instead.
|
||||
"""
|
||||
|
||||
def __init__(self, widget, data=None, ascending=False, exact=None, contains=None):
|
||||
self.tree = None
|
||||
self.data = data
|
||||
self.asc = ascending
|
||||
self.comp = exact.lower() if exact else None
|
||||
self.cont = contains
|
||||
self.tree = widget
|
||||
self.resort()
|
||||
|
||||
def get_data(self, model, list_iter):
|
||||
"""Generate sortable data from the item"""
|
||||
item = model.get_value(list_iter, 0)
|
||||
if isinstance(self.data, str):
|
||||
value = getattr(item, self.data)
|
||||
elif callable(self.data):
|
||||
value = self.data(item)
|
||||
return value
|
||||
|
||||
def sort_func(self, model, iter1, iter2, data):
|
||||
"""Called by Gtk to sort items"""
|
||||
value1 = self.get_data(model, iter1)
|
||||
value2 = self.get_data(model, iter2)
|
||||
if value1 == None or value2 == None:
|
||||
return 0
|
||||
if self.comp:
|
||||
if cmp(self.comp, value1.lower()) == 0:
|
||||
return 1
|
||||
elif cmp(self.comp, value2.lower()) == 0:
|
||||
return -1
|
||||
return 0
|
||||
elif self.cont:
|
||||
if self.cont in value1.lower():
|
||||
return 1
|
||||
elif self.cont in value2.lower():
|
||||
return -1
|
||||
return 0
|
||||
if value1 < value2:
|
||||
return 1
|
||||
if value2 < value1:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def resort(self):
|
||||
model = self.tree.get_model()
|
||||
model.set_sort_func(0, self.sort_func, None)
|
||||
if self.asc:
|
||||
model.set_sort_column_id(0, Gtk.SortType.ASCENDING)
|
||||
else:
|
||||
model.set_sort_column_id(0, Gtk.SortType.DESCENDING)
|
||||
|
||||
|
||||
class ViewColumn(object):
|
||||
"""
|
||||
Add a column to a gtk treeview.
|
||||
|
||||
name - The column name used as a label.
|
||||
expand - Set column expansion.
|
||||
"""
|
||||
|
||||
def __init__(self, widget, name, expand=False):
|
||||
if isinstance(widget, Gtk.TreeView):
|
||||
column = Gtk.TreeViewColumn((name))
|
||||
column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
|
||||
column.set_expand(expand)
|
||||
self._column = column
|
||||
widget.append_column(self._column)
|
||||
else:
|
||||
# Deal with possible drop down lists
|
||||
self._column = widget
|
||||
|
||||
def add_renderer(self, renderer, func, expand=True):
|
||||
"""Set a custom renderer"""
|
||||
self._column.pack_start(renderer, expand)
|
||||
self._column.set_cell_data_func(renderer, func, None)
|
||||
return renderer
|
||||
|
||||
def add_image_renderer(self, icon, pad=0, pixmaps=None, size=None):
|
||||
"""
|
||||
Set the image renderer
|
||||
|
||||
icon - The function that returns the image to be dsplayed.
|
||||
pad - The amount of padding around the image.
|
||||
pixmaps - The pixmap manager to use to get images.
|
||||
size - Restrict the images to this size.
|
||||
"""
|
||||
# Manager where icons will be pulled from
|
||||
filters = [SizeFilter] if size else []
|
||||
pixmaps = pixmaps or PixmapManager(
|
||||
"", pixmap_dir="./", filters=filters, size=size
|
||||
)
|
||||
|
||||
renderer = Gtk.CellRendererPixbuf()
|
||||
renderer.set_property("ypad", pad)
|
||||
renderer.set_property("xpad", pad)
|
||||
func = self.image_func(icon or self.default_icon, pixmaps)
|
||||
return self.add_renderer(renderer, func, expand=False)
|
||||
|
||||
def add_text_renderer(self, text, wrap=None, template=None):
|
||||
"""
|
||||
Set the text renderer.
|
||||
|
||||
text - the function that returns the text to be displayed.
|
||||
wrap - The wrapping setting for this renderer.
|
||||
template - A standard template used for this text markup.
|
||||
"""
|
||||
|
||||
renderer = Gtk.CellRendererText()
|
||||
if wrap is not None:
|
||||
renderer.props.wrap_width = wrap
|
||||
renderer.props.wrap_mode = Pango.WrapMode.WORD
|
||||
|
||||
renderer.props.background_set = True
|
||||
renderer.props.foreground_set = True
|
||||
|
||||
func = self.text_func(text or self.default_text, template)
|
||||
return self.add_renderer(renderer, func, expand=True)
|
||||
|
||||
@classmethod
|
||||
def clean(cls, text, markup=False):
|
||||
"""Clean text of any pango markup confusing chars"""
|
||||
if text is None:
|
||||
text = ""
|
||||
if isinstance(text, (str, int, float)):
|
||||
if markup:
|
||||
text = str(text).replace("<", "<").replace(">", ">")
|
||||
return str(text).replace("&", "&")
|
||||
elif isinstance(text, dict):
|
||||
return dict([(k, cls.clean(v)) for k, v in text.items()])
|
||||
elif isinstance(text, (list, tuple)):
|
||||
return tuple([cls.clean(value) for value in text])
|
||||
raise TypeError("Unknown value type for text: %s" % str(type(text)))
|
||||
|
||||
def get_callout(self, call, default=None):
|
||||
"""Returns the right kind of method"""
|
||||
if isinstance(call, str):
|
||||
call = item_property(call, default)
|
||||
return call
|
||||
|
||||
def text_func(self, call, template=None):
|
||||
"""Wrap up our text functionality"""
|
||||
callout = self.get_callout(call)
|
||||
|
||||
def internal(column, cell, model, item_iter, data):
|
||||
if TreeView.is_separator(model, item_iter, data):
|
||||
return
|
||||
item = model.get_value(item_iter, 0)
|
||||
markup = template is not None
|
||||
text = callout(item)
|
||||
if isinstance(template, str):
|
||||
text = template.format(self.clean(text, markup=True))
|
||||
else:
|
||||
text = self.clean(text)
|
||||
cell.set_property("markup", str(text))
|
||||
|
||||
return internal
|
||||
|
||||
def image_func(self, call, pixmaps=None):
|
||||
"""Wrap, wrap wrap the func"""
|
||||
callout = self.get_callout(call)
|
||||
|
||||
def internal(column, cell, model, item_iter, data):
|
||||
if TreeView.is_separator(model, item_iter, data):
|
||||
return
|
||||
item = model.get_value(item_iter, 0)
|
||||
icon = callout(item)
|
||||
# The or blank asks for the default icon from the pixmaps
|
||||
if isinstance(icon or "", str) and pixmaps:
|
||||
# Expect a Gnome theme icon
|
||||
icon = pixmaps.get(icon)
|
||||
elif icon:
|
||||
icon = pixmaps.apply_filters(icon)
|
||||
|
||||
cell.set_property("pixbuf", icon)
|
||||
cell.set_property("visible", True)
|
||||
|
||||
return internal
|
||||
|
||||
def default_text(self, item):
|
||||
"""Default text return for markup."""
|
||||
return default(item, "name", str(item))
|
||||
|
||||
def default_icon(self, item):
|
||||
"""Default icon return, pixbuf or gnome theme name"""
|
||||
return default(item, "icon", None)
|
||||
|
||||
|
||||
class Separator:
|
||||
"""Reprisentation of a separator in a list"""
|
||||
Reference in New Issue
Block a user