bundle: update (2026-01-16)
This commit is contained in:
13
extensions/botbox3000/deps/inkex/gui/README.md
Normal file
13
extensions/botbox3000/deps/inkex/gui/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# What is inkex.gui
|
||||
|
||||
This module is a Gtk based GUI creator. It helps extensions launch their own user interfaces and can help make sure those interfaces will work on all platforms that Inkscape ships with.
|
||||
|
||||
# How do I use it
|
||||
|
||||
You can create custom user interfaces by using the [Gnome Glade builder program](https://gitlab.gnome.org/GNOME/glade). Once you have a layout of all the widgets you want, you then make a GtkApp and Window classes inside your Python program, when the GtkApp is run, the windows will be shown to the user and all signals specified for the widgets will call functions on your window class.
|
||||
|
||||
Please see the existing code for examples of how to do this.
|
||||
|
||||
# This is a fork
|
||||
|
||||
This code was originally part of the package `gtkme` which contained some parts we didn't want to ship—such as Ubuntu indicators and internet pixmaps. To avoid conflicts, our stripped down version of the `gtkme` module is renamed and placed inside of Inkscape's `inkex` module.
|
||||
58
extensions/botbox3000/deps/inkex/gui/__init__.py
Normal file
58
extensions/botbox3000/deps/inkex/gui/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#
|
||||
# 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/>
|
||||
#
|
||||
# pylint: disable=wrong-import-position
|
||||
"""
|
||||
This is a wrapper layer to make interacting with Gtk a little less painful.
|
||||
The main issues with Gtk is that it expects an awful lot of the developer,
|
||||
code which is repeated over and over and patterns which every single developer
|
||||
will use are not given easy to use convenience functions.
|
||||
|
||||
This makes Gtk programming WET, unattractive and error prone. This module steps
|
||||
inbetween and adds in all those missing bits. It's not meant to replace Gtk and
|
||||
certainly it's possible to use Gtk and threading directly.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from ..utils import DependencyError
|
||||
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "4.0")
|
||||
|
||||
# Importing while covering stderr because pygobject has broken
|
||||
# warnings support and will force import warnings on our users.
|
||||
tmp, sys.stderr = sys.stderr, None # type: ignore
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
sys.stderr = tmp # type: ignore
|
||||
except ImportError: # pragma: no cover
|
||||
raise DependencyError(
|
||||
"You are missing the required libraries for Gtk."
|
||||
" Please report this problem to the Inkscape developers."
|
||||
)
|
||||
|
||||
from .app import GtkApp
|
||||
from .window import Window
|
||||
from .listview import TreeView, IconView, ViewColumn, ViewSort, Separator
|
||||
from .pixmap import PixmapManager
|
||||
62
extensions/botbox3000/deps/inkex/gui/app.py
Normal file
62
extensions/botbox3000/deps/inkex/gui/app.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright 2011-2022 Martin Owens <doctormo@geek-2.com>
|
||||
|
||||
"""
|
||||
Wraps Gtk.Application, providing a way to load a Gtk.Builder
|
||||
with a specific ui file containing windows, and building
|
||||
a usable pythonic interface from them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from gi.repository import Gtk, Gdk, Gio
|
||||
|
||||
|
||||
class GtkApp(Gtk.Application):
|
||||
"""A very thin wrapper around Gtk.Application"""
|
||||
|
||||
app_name = None
|
||||
"""The application id"""
|
||||
|
||||
prefix = ""
|
||||
"""Folder prefix added to ui_dir"""
|
||||
|
||||
ui_dir = "./"
|
||||
"""This is often the local directory"""
|
||||
|
||||
ui_file = None
|
||||
"""If a single file is used for multiple windows"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Creates a new GtkApp."""
|
||||
self.kwargs = kwargs
|
||||
super().__init__(
|
||||
application_id=self.app_name, flags=Gio.ApplicationFlags.NON_UNIQUE
|
||||
)
|
||||
self.connect("activate", lambda _: self.on_activate())
|
||||
if self.ui_dir:
|
||||
icontheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
|
||||
icontheme.add_search_path(self.ui_dir)
|
||||
|
||||
def run_wrapped(self):
|
||||
"""Calls self.run() but catches keyboard interrupts"""
|
||||
try:
|
||||
self.run()
|
||||
except KeyboardInterrupt: # pragma: no cover
|
||||
logging.info("User Interrupted")
|
||||
|
||||
def on_activate(self):
|
||||
"""Called when the application is activated"""
|
||||
raise NotImplementedError("Must override on_activate in subclass")
|
||||
|
||||
def get_ui_file(self, window):
|
||||
"""Load any given ui file from a standard location"""
|
||||
paths = [
|
||||
os.path.join(self.ui_dir, self.prefix, f"{window}.ui"),
|
||||
os.path.join(self.ui_dir, self.prefix, f"{self.ui_file}.ui"),
|
||||
]
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
raise FileNotFoundError(f"Gtk ui file is missing: {paths}")
|
||||
330
extensions/botbox3000/deps/inkex/gui/asyncme.py
Normal file
330
extensions/botbox3000/deps/inkex/gui/asyncme.py
Normal file
@@ -0,0 +1,330 @@
|
||||
#
|
||||
# Copyright 2015 Ian Denhardt <ian@zenhack.net>
|
||||
#
|
||||
# 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/>
|
||||
#
|
||||
"""Convenience library for concurrency
|
||||
|
||||
GUI apps frequently need concurrency, for example to avoid blocking UI while
|
||||
doing some long running computation. This module provides helpers for doing
|
||||
this kind of thing.
|
||||
|
||||
The functions/methods here which spawn callables asynchronously
|
||||
don't supply a direct way to provide arguments. Instead, the user is
|
||||
expected to use a lambda, e.g::
|
||||
|
||||
holding(lck, lambda: do_stuff(1,2,3, x='hello'))
|
||||
|
||||
This is because the calling function may have additional arguments which
|
||||
could obscure the user's ability to pass arguments expected by the called
|
||||
function. For example, in the call::
|
||||
|
||||
holding(lck, lambda: run_task(blocking=True), blocking=False)
|
||||
|
||||
the blocking argument to holding might otherwise conflict with the
|
||||
blocking argument to run_task.
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from functools import wraps
|
||||
from typing import Any, Tuple
|
||||
from gi.repository import Gdk, GLib
|
||||
|
||||
|
||||
class Future:
|
||||
"""A deferred result
|
||||
|
||||
A `Future` is a result-to-be; it can be used to deliver a result
|
||||
asynchronously. Typical usage:
|
||||
|
||||
>>> def background_task(task):
|
||||
... ret = Future()
|
||||
... def _task(x):
|
||||
... return x - 4 + 2
|
||||
... thread = threading.Thread(target=lambda: ret.run(lambda: _task(7)))
|
||||
... thread.start()
|
||||
... return ret
|
||||
>>> # Do other stuff
|
||||
>>> print(ret.wait())
|
||||
5
|
||||
|
||||
:func:`run` will also propagate exceptions; see its docstring for details.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._value = None
|
||||
self._exception = None
|
||||
self._lock.acquire()
|
||||
|
||||
def is_ready(self):
|
||||
"""Return whether the result is ready"""
|
||||
result = self._lock.acquire(False)
|
||||
if result:
|
||||
self._lock.release()
|
||||
return result
|
||||
|
||||
def wait(self):
|
||||
"""Wait for the result.
|
||||
|
||||
`wait` blocks until the result is ready (either :func:`result` or
|
||||
:func:`exception` has been called), and then returns it (in the case
|
||||
of :func:`result`), or raises it (in the case of :func:`exception`).
|
||||
"""
|
||||
with self._lock:
|
||||
if self._exception is None:
|
||||
return self._value
|
||||
else:
|
||||
raise self._exception # pylint: disable=raising-bad-type
|
||||
|
||||
def result(self, value):
|
||||
"""Supply the result as a return value.
|
||||
|
||||
``value`` is the result to supply; it will be returned when
|
||||
:func:`wait` is called.
|
||||
"""
|
||||
self._value = value
|
||||
self._lock.release()
|
||||
|
||||
def exception(self, err):
|
||||
"""Supply an exception as the result.
|
||||
|
||||
Args:
|
||||
err (Exception): an exception, which will be raised when :func:`wait`
|
||||
is called.
|
||||
"""
|
||||
self._exception = err
|
||||
self._lock.release()
|
||||
|
||||
def run(self, task):
|
||||
"""Calls task(), and supplies the result.
|
||||
|
||||
If ``task`` raises an exception, pass it to :func:`exception`.
|
||||
Otherwise, pass the return value to :func:`result`.
|
||||
"""
|
||||
try:
|
||||
self.result(task())
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self.exception(err)
|
||||
|
||||
|
||||
class DebouncedSyncVar:
|
||||
"""A synchronized variable, which debounces its value
|
||||
|
||||
:class:`DebouncedSyncVar` supports three operations: put, replace, and get.
|
||||
get will only retrieve a value once it has "settled," i.e. at least
|
||||
a certain amount of time has passed since the last time the value
|
||||
was modified.
|
||||
"""
|
||||
|
||||
def __init__(self, delay_seconds=0):
|
||||
"""Create a new dsv with the supplied delay, and no initial value."""
|
||||
self._cv = threading.Condition()
|
||||
self._delay = timedelta(seconds=delay_seconds)
|
||||
|
||||
self._deadline = None
|
||||
self._value = None
|
||||
|
||||
self._have_value = False
|
||||
|
||||
def set_delay(self, delay_seconds):
|
||||
"""Set the delay in seconds of the debounce."""
|
||||
with self._cv:
|
||||
self._delay = timedelta(seconds=delay_seconds)
|
||||
|
||||
def get(self, blocking=True, remove=True) -> Tuple[Any, bool]:
|
||||
"""Retrieve a value.
|
||||
|
||||
Args:
|
||||
blocking (bool, optional): if True, block until (1) the dsv has a value
|
||||
and (2) the value has been unchanged for an amount of time greater
|
||||
than or equal to the dsv's delay. Otherwise, if these conditions
|
||||
are not met, return ``(None, False)`` immediately. Defaults to True.
|
||||
remove (bool, optional): if True, remove the value when returning it.
|
||||
Otherwise, leave it where it is.. Defaults to True.
|
||||
|
||||
Returns:
|
||||
Tuple[Any, bool]: Tuple (value, ok). ``value`` is the value of the variable
|
||||
(if successful, see above), and ok indicates whether or not a value was
|
||||
successfully retrieved.
|
||||
"""
|
||||
while True:
|
||||
with self._cv:
|
||||
# If there's no value, either wait for one or return
|
||||
# failure.
|
||||
while not self._have_value:
|
||||
if blocking:
|
||||
self._cv.wait()
|
||||
else:
|
||||
return None, False # pragma: no cover
|
||||
|
||||
now = datetime.now()
|
||||
deadline = self._deadline
|
||||
value = self._value
|
||||
if deadline <= now:
|
||||
# Okay, we're good. Remove the value if necessary, and
|
||||
# return it.
|
||||
if remove:
|
||||
self._have_value = False
|
||||
self._value = None
|
||||
self._cv.notify()
|
||||
return value, True
|
||||
|
||||
# Deadline hasn't passed yet. Either wait or return failure.
|
||||
if blocking:
|
||||
time.sleep((deadline - now).total_seconds())
|
||||
else:
|
||||
return None, False # pragma: no cover
|
||||
|
||||
def replace(self, value):
|
||||
"""Replace the current value of the dsv (if any) with ``value``.
|
||||
|
||||
replace never blocks (except briefly to acquire the lock). It does not
|
||||
wait for any unit of time to pass (though it does reset the timer on
|
||||
completion), nor does it wait for the dsv's value to appear or
|
||||
disappear.
|
||||
"""
|
||||
with self._cv:
|
||||
self._replace(value)
|
||||
|
||||
def put(self, value):
|
||||
"""Set the dsv's value to ``value``.
|
||||
|
||||
If the dsv already has a value, this blocks until the value is removed.
|
||||
Upon completion, this resets the timer.
|
||||
"""
|
||||
with self._cv:
|
||||
while self._have_value:
|
||||
self._cv.wait()
|
||||
self._replace(value)
|
||||
|
||||
def _replace(self, value):
|
||||
self._have_value = True
|
||||
self._value = value
|
||||
self._deadline = datetime.now() + self._delay
|
||||
self._cv.notify()
|
||||
|
||||
|
||||
def spawn_thread(func):
|
||||
"""Call ``func()`` in a separate thread
|
||||
|
||||
Returns the corresponding :class:`threading.Thread` object.
|
||||
"""
|
||||
thread = threading.Thread(target=func)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
def in_mainloop(func):
|
||||
"""Run f() in the gtk main loop
|
||||
|
||||
Returns a :class:`Future` object which can be used to retrieve the return
|
||||
value of the function call.
|
||||
|
||||
:func:`in_mainloop` exists because Gtk isn't threadsafe, and therefore cannot be
|
||||
manipulated except in the thread running the Gtk main loop. :func:`in_mainloop`
|
||||
can be used by other threads to manipulate Gtk safely.
|
||||
"""
|
||||
future = Future()
|
||||
|
||||
def handler(*_args, **_kwargs):
|
||||
"""Function to be called in the future"""
|
||||
future.run(func)
|
||||
|
||||
GLib.idle_add(handler, None, 0)
|
||||
return future
|
||||
|
||||
|
||||
def mainloop_only(f):
|
||||
"""A decorator which forces a function to only be run in Gtk's main loop.
|
||||
|
||||
Invoking a decorated function as ``f(*args, **kwargs)`` is equivalent to
|
||||
using the undecorated function (from a thread other than the one running
|
||||
the Gtk main loop) as::
|
||||
|
||||
in_mainloop(lambda: f(*args, **kwargs)).wait()
|
||||
|
||||
:func:`mainloop_only` should be used to decorate functions which are unsafe
|
||||
to run outside of the Gtk main loop.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
if GLib.main_depth():
|
||||
# Already in a mainloop, so just run it.
|
||||
return f(*args, **kwargs)
|
||||
return in_mainloop(lambda: f(*args, **kwargs)).wait()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def holding(lock, task, blocking=True):
|
||||
"""Run task() while holding ``lock``.
|
||||
|
||||
Args:
|
||||
blocking (bool, optional): if True, wait for the lock before running.
|
||||
Otherwise, if the lock is busy, return None immediately, and don't
|
||||
spawn `task`. Defaults to True.
|
||||
|
||||
Returns:
|
||||
Union[Future, None]: The return value is a future which can be used to retrieve
|
||||
the result of running task (or None if the task was not run).
|
||||
"""
|
||||
if not lock.acquire(False):
|
||||
return None
|
||||
ret = Future()
|
||||
|
||||
def _target():
|
||||
ret.run(task)
|
||||
if ret._exception: # pragma: no cover
|
||||
ret.wait()
|
||||
lock.release()
|
||||
|
||||
threading.Thread(target=_target).start()
|
||||
return ret
|
||||
|
||||
|
||||
def run_or_wait(func):
|
||||
"""A decorator which runs the function using :func:`holding`
|
||||
|
||||
This function creates a single lock for this function and
|
||||
waits for the lock to release before returning.
|
||||
|
||||
See :func:`holding` above, with ``blocking=True``
|
||||
"""
|
||||
lock = threading.Lock()
|
||||
|
||||
def _inner(*args, **kwargs):
|
||||
return holding(lock, lambda: func(*args, **kwargs), blocking=True)
|
||||
|
||||
return _inner
|
||||
|
||||
|
||||
def run_or_none(func):
|
||||
"""A decorator which runs the function using :func:`holding`
|
||||
|
||||
This function creates a single lock for this function and
|
||||
returns None if the process is already running (locked)
|
||||
|
||||
See :func:`holding` above with ``blocking=True``
|
||||
"""
|
||||
lock = threading.Lock()
|
||||
|
||||
def _inner(*args, **kwargs):
|
||||
return holding(lock, lambda: func(*args, **kwargs), blocking=False)
|
||||
|
||||
return _inner
|
||||
562
extensions/botbox3000/deps/inkex/gui/listview.py
Normal file
562
extensions/botbox3000/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"""
|
||||
360
extensions/botbox3000/deps/inkex/gui/pixmap.py
Normal file
360
extensions/botbox3000/deps/inkex/gui/pixmap.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#
|
||||
# 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/>
|
||||
#
|
||||
"""
|
||||
Provides wrappers for pixmap access.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from typing import List
|
||||
from collections.abc import Iterable
|
||||
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
||||
import cairo
|
||||
|
||||
ICON_THEME = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
|
||||
BILINEAR = GdkPixbuf.InterpType.BILINEAR
|
||||
HYPER = GdkPixbuf.InterpType.HYPER
|
||||
|
||||
SIZE_ASPECT = 0
|
||||
SIZE_ASPECT_GROW = 1
|
||||
SIZE_ASPECT_CROP = 2
|
||||
SIZE_STRETCH = 3
|
||||
|
||||
|
||||
class PixmapLoadError(ValueError):
|
||||
"""Failed to load a pixmap"""
|
||||
|
||||
|
||||
class PixmapFilter: # pylint: disable=too-few-public-methods
|
||||
"""Base class for filtering the pixmaps in a manager's output.
|
||||
|
||||
required - List of values required for this filter.
|
||||
|
||||
Use:
|
||||
|
||||
class Foo(PixmapManager):
|
||||
filters = [ PixmapFilterFoo ]
|
||||
|
||||
"""
|
||||
|
||||
required: List[str] = []
|
||||
optional: List[str] = []
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.enabled = True
|
||||
for key in self.required:
|
||||
if key not in kwargs:
|
||||
self.enabled = False
|
||||
else:
|
||||
setattr(self, key, kwargs[key])
|
||||
|
||||
for key in self.optional:
|
||||
if key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
|
||||
def filter(self, img, **kwargs):
|
||||
"""Run filter, replace this methodwith your own"""
|
||||
raise NotImplementedError(
|
||||
"Please add 'filter' method to your PixmapFilter class %s."
|
||||
% type(self).__name__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_size(dat):
|
||||
"""Tries to calculate a size that will work for the data"""
|
||||
if isinstance(dat, (int, float)):
|
||||
return (dat, dat)
|
||||
if isinstance(dat, Iterable) and len(dat) >= 2:
|
||||
return (dat[0], dat[1])
|
||||
return None
|
||||
|
||||
|
||||
class OverlayFilter(PixmapFilter):
|
||||
"""Adds an overlay to output images, overlay can be any name that
|
||||
the owning pixmap manager can find.
|
||||
|
||||
overlay : Name of overlay image
|
||||
position : Location of the image:
|
||||
0 - Full size (1 to 1 overlay, default)
|
||||
(x,y) - Percentage from one end to the other position 0-1
|
||||
alpha : Blending alpha, 0 - 255
|
||||
|
||||
"""
|
||||
|
||||
optional = ["position", "overlay", "alpha"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.position = (0, 0)
|
||||
self.overlay = None
|
||||
self.alpha = 255
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad_x, self.pad_y = self.to_size(self.position)
|
||||
|
||||
def get_overlay(self, **kwargs):
|
||||
if "manager" not in kwargs:
|
||||
raise ValueError("PixmapManager must be provided when adding an overlay.")
|
||||
return kwargs["manager"].get(
|
||||
kwargs.get("overlay", None) or self.overlay, no_overlay=True
|
||||
)
|
||||
|
||||
def filter(self, img, no_overlay=False, **kwargs):
|
||||
# Recursion protection
|
||||
if no_overlay:
|
||||
return img
|
||||
|
||||
overlay = self.get_overlay(**kwargs)
|
||||
if overlay:
|
||||
img = img.copy()
|
||||
|
||||
(x, y, width, height) = self.set_position(overlay, img)
|
||||
overlay.composite(
|
||||
img, x, y, width, height, x, y, 1, 1, BILINEAR, self.alpha
|
||||
)
|
||||
return img
|
||||
|
||||
def set_position(self, overlay, img):
|
||||
"""Sets the position of img on the given width and height"""
|
||||
img_w, img_h = img.get_width(), img.get_height()
|
||||
ovl_w, ovl_h = overlay.get_width(), overlay.get_height()
|
||||
return (
|
||||
max([0, (img_w - ovl_w) * self.pad_x]),
|
||||
max([0, (img_h - ovl_h) * self.pad_y]),
|
||||
min([ovl_w, img_w]),
|
||||
min([ovl_h, img_h]),
|
||||
)
|
||||
|
||||
|
||||
class SizeFilter(PixmapFilter):
|
||||
"""Resizes images to a certain size:
|
||||
|
||||
resize_mode - Way in which the size is calculated
|
||||
0 - Best Aspect, don't grow
|
||||
1 - Best Aspect, grow
|
||||
2 - Cropped Aspect
|
||||
3 - Stretch
|
||||
"""
|
||||
|
||||
required = ["size"]
|
||||
optional = ["resize_mode"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.size = None
|
||||
self.resize_mode = SIZE_ASPECT
|
||||
super().__init__(*args, **kwargs)
|
||||
self.img_w, self.img_h = self.to_size(self.size) or (0, 0)
|
||||
|
||||
def aspect(self, img_w, img_h):
|
||||
"""Get the aspect ratio of the image resized"""
|
||||
if self.resize_mode == SIZE_STRETCH:
|
||||
return (self.img_w, self.img_h)
|
||||
|
||||
if (
|
||||
self.resize_mode == SIZE_ASPECT
|
||||
and img_w < self.img_w
|
||||
and img_h < self.img_h
|
||||
):
|
||||
return (img_w, img_h)
|
||||
(pcw, pch) = (self.img_w / img_w, self.img_h / img_h)
|
||||
factor = (
|
||||
max(pcw, pch) if self.resize_mode == SIZE_ASPECT_CROP else min(pcw, pch)
|
||||
)
|
||||
return (int(img_w * factor), int(img_h * factor))
|
||||
|
||||
def filter(self, img, **kwargs):
|
||||
if self.size is not None:
|
||||
(width, height) = self.aspect(img.get_width(), img.get_height())
|
||||
return img.scale_simple(width, height, HYPER)
|
||||
return img
|
||||
|
||||
|
||||
class PadFilter(SizeFilter):
|
||||
"""Add padding to the image to make it a standard size"""
|
||||
|
||||
optional = ["padding"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.size = None
|
||||
self.padding = 0.5
|
||||
super().__init__(*args, **kwargs)
|
||||
self.pad_x, self.pad_y = self.to_size(self.padding)
|
||||
|
||||
def filter(self, img, **kwargs):
|
||||
(width, height) = (img.get_width(), img.get_height())
|
||||
if width < self.img_w or height < self.img_h:
|
||||
target = GdkPixbuf.Pixbuf.new(
|
||||
img.get_colorspace(),
|
||||
True,
|
||||
img.get_bits_per_sample(),
|
||||
max([width, self.img_w]),
|
||||
max([height, self.img_h]),
|
||||
)
|
||||
target.fill(0x0) # Transparent black
|
||||
|
||||
x = (target.get_width() - width) * self.pad_x
|
||||
y = (target.get_height() - height) * self.pad_y
|
||||
|
||||
img.composite(target, x, y, width, height, x, y, 1, 1, BILINEAR, 255)
|
||||
return target
|
||||
return img
|
||||
|
||||
|
||||
class PixmapManager:
|
||||
"""Manage a set of cached pixmaps, returns the default image
|
||||
if it can't find one or the missing image if that's available."""
|
||||
|
||||
missing_image = "image-missing"
|
||||
default_image = "application-default-icon"
|
||||
icon_theme = ICON_THEME
|
||||
theme_size = 32
|
||||
filters: List[type] = []
|
||||
pixmap_dir = None
|
||||
|
||||
def __init__(self, location="", **kwargs):
|
||||
self.location = location
|
||||
if self.pixmap_dir and not os.path.isabs(location):
|
||||
self.location = os.path.join(self.pixmap_dir, location)
|
||||
|
||||
self.loader_size = PixmapFilter.to_size(kwargs.pop("load_size", None))
|
||||
|
||||
# Add any instance specified filters first
|
||||
self._filters = []
|
||||
for item in kwargs.get("filters", []) + self.filters:
|
||||
if isinstance(item, PixmapFilter):
|
||||
self._filters.append(item)
|
||||
elif callable(item):
|
||||
# Now add any class specified filters with optional kwargs
|
||||
self._filters.append(item(**kwargs))
|
||||
|
||||
self.cache = {}
|
||||
self.get_pixmap(self.default_image)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""Get a pixmap of any kind"""
|
||||
return self.get_pixmap(*args, **kwargs)
|
||||
|
||||
def get_missing_image(self):
|
||||
"""Get a missing image when other images aren't found"""
|
||||
return self.get(self.missing_image)
|
||||
|
||||
@staticmethod
|
||||
def data_is_file(data):
|
||||
"""Test the file to see if it's a filename or not"""
|
||||
return isinstance(data, str) and "<svg" not in data
|
||||
|
||||
def get_pixmap(self, data, **kwargs):
|
||||
"""
|
||||
There are three types of images this might return.
|
||||
|
||||
1. A named gtk-image such as "gtk-stop"
|
||||
2. A file on the disk such as "/tmp/a.png"
|
||||
3. Data as either svg or binary png
|
||||
|
||||
All pixmaps are cached for multiple use.
|
||||
"""
|
||||
if "manager" not in kwargs:
|
||||
kwargs["manager"] = self
|
||||
|
||||
if not data:
|
||||
if not self.default_image:
|
||||
return None
|
||||
data = self.default_image
|
||||
|
||||
key = data[-30:] # bytes or string
|
||||
if not key in self.cache:
|
||||
# load the image from data or a filename/theme icon
|
||||
img = None
|
||||
try:
|
||||
if self.data_is_file(data):
|
||||
img = self.load_from_name(data)
|
||||
else:
|
||||
img = self.load_from_data(data)
|
||||
except PixmapLoadError as err:
|
||||
logging.warning(str(err))
|
||||
return self.get_missing_image()
|
||||
|
||||
if isinstance(img, Gtk.IconPaintable):
|
||||
# Temporary porting hack: rasterise iconpaintable to pixbuf
|
||||
# https://discourse.gnome.org/t/convert-symbolic-icon-to-gdktexture-gdkpixbuf/29324/3
|
||||
w = img.get_intrinsic_width()
|
||||
h = img.get_intrinsic_height()
|
||||
snapshot = Gtk.Snapshot()
|
||||
img.snapshot(snapshot, w, h)
|
||||
node = snapshot.to_node()
|
||||
surface = cairo.ImageSurface(cairo.Format.ARGB32, w, h)
|
||||
ctx = cairo.Context(surface)
|
||||
node.draw(ctx)
|
||||
img = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h)
|
||||
|
||||
if img is not None:
|
||||
self.cache[key] = self.apply_filters(img, **kwargs)
|
||||
|
||||
return self.cache[key]
|
||||
|
||||
def apply_filters(self, img, **kwargs):
|
||||
"""Apply all the filters to the given image"""
|
||||
for lens in self._filters:
|
||||
if lens.enabled:
|
||||
img = lens.filter(img, **kwargs)
|
||||
return img
|
||||
|
||||
def load_from_data(self, data):
|
||||
"""Load in memory picture file (jpeg etc)"""
|
||||
# This doesn't work yet, returns None *shrug*
|
||||
loader = GdkPixbuf.PixbufLoader()
|
||||
if self.loader_size:
|
||||
loader.set_size(*self.loader_size)
|
||||
try:
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
loader.write(data)
|
||||
loader.close()
|
||||
except GLib.GError as err:
|
||||
raise PixmapLoadError(f"Failed to load pixbuf from data: {err}")
|
||||
return loader.get_pixbuf()
|
||||
|
||||
def load_from_name(self, name):
|
||||
"""Load a pixbuf from a name, filename or theme icon name"""
|
||||
pixmap_path = self.pixmap_path(name)
|
||||
if os.path.exists(pixmap_path):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file(pixmap_path)
|
||||
except RuntimeError as msg:
|
||||
raise PixmapLoadError(f"Failed to load pixmap '{pixmap_path}', {msg}")
|
||||
elif (
|
||||
self.icon_theme and "/" not in name and "." not in name and "<" not in name
|
||||
):
|
||||
return self.theme_pixmap(name, size=self.theme_size)
|
||||
raise PixmapLoadError(f"Failed to find pixmap '{name}' in {self.location}")
|
||||
|
||||
def theme_pixmap(self, name, size=32):
|
||||
"""Internal user: get image from gnome theme"""
|
||||
size = size or 32
|
||||
if not self.icon_theme.has_icon(name):
|
||||
name = "image-missing"
|
||||
return self.icon_theme.lookup_icon(name, [], size, 1, Gtk.TextDirection.NONE, 0)
|
||||
|
||||
def pixmap_path(self, name):
|
||||
"""Returns the pixmap path based on stored location"""
|
||||
for filename in (
|
||||
name,
|
||||
os.path.join(self.location, f"{name}.svg"),
|
||||
os.path.join(self.location, f"{name}.png"),
|
||||
):
|
||||
if os.path.exists(filename) and os.path.isfile(filename):
|
||||
return name
|
||||
return os.path.join(self.location, name)
|
||||
78
extensions/botbox3000/deps/inkex/gui/tester.py
Normal file
78
extensions/botbox3000/deps/inkex/gui/tester.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright 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/>
|
||||
#
|
||||
"""
|
||||
Structures for consistant testing of Gtk GUI programs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
|
||||
class MainLoopProtection:
|
||||
"""
|
||||
This protection class provides a way to launch the Gtk mainloop in a test
|
||||
friendly way.
|
||||
|
||||
Exception handling hooks provide a way to see errors that happen
|
||||
inside the main loop, raising them back to the caller.
|
||||
A full timeout in seconds stops the gtk mainloop from operating
|
||||
beyond a set time, acting as a kill switch in the event something
|
||||
has gone horribly wrong.
|
||||
|
||||
Use:
|
||||
with MainLoopProtection(timeout=10s):
|
||||
app.run()
|
||||
"""
|
||||
|
||||
def __init__(self, timeout=10):
|
||||
self.timeout = timeout * 1000
|
||||
self._hooked = None
|
||||
self._old_excepthook = None
|
||||
|
||||
def __enter__(self):
|
||||
# replace sys.excepthook with our own and remember hooked raised error
|
||||
self._old_excepthook = sys.excepthook
|
||||
sys.excepthook = self.excepthook
|
||||
# Remove mainloop by force if it doesn't die within 10 seconds
|
||||
self._timeout = GLib.timeout_add(self.timeout, self.exit)
|
||||
|
||||
def __exit__(self, exc, value, traceback): # pragma: no cover
|
||||
"""Put the except handler back, cancel the timer and raise if needed"""
|
||||
if self._old_excepthook:
|
||||
sys.excepthook = self._old_excepthook
|
||||
# Remove the timeout, so we don't accidentally kill later mainloops
|
||||
if self._timeout:
|
||||
GLib.source_remove(self._timeout)
|
||||
# Raise an exception if one happened during the test run
|
||||
if self._hooked:
|
||||
exc, value, traceback = self._hooked
|
||||
if value and traceback:
|
||||
raise value.with_traceback(traceback)
|
||||
|
||||
def exit(self): # pragma: no cover
|
||||
"""Try to going to kill any running mainloop."""
|
||||
Gio.Application.get_default().quit()
|
||||
|
||||
def excepthook(self, ex_type, ex_value, traceback): # pragma: no cover
|
||||
"""Catch errors thrown by the Gtk mainloop"""
|
||||
self.exit()
|
||||
# Remember the exception data for raising inside the test context
|
||||
if ex_value is not None:
|
||||
self._hooked = [ex_type, ex_value, traceback]
|
||||
# Fallback and double print the exception (remove if double printing is problematic)
|
||||
return self._old_excepthook(ex_type, ex_value, traceback)
|
||||
38
extensions/botbox3000/deps/inkex/gui/window.py
Normal file
38
extensions/botbox3000/deps/inkex/gui/window.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright 2012-2022 Martin Owens <doctormo@geek-2.com>
|
||||
|
||||
"""
|
||||
Wraps Gtk.Window with something a little nicer.
|
||||
"""
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class Window:
|
||||
"""A very thin wrapper around Gtk.Window"""
|
||||
|
||||
name = None
|
||||
"""The name of the window to load from the ui file"""
|
||||
|
||||
def __init__(self, gapp):
|
||||
"""Create a new Window and load its Gtk.Window from a ui file"""
|
||||
self.gapp = gapp
|
||||
ui_file = gapp.get_ui_file(self.name)
|
||||
|
||||
# Set up the builder and load ui
|
||||
self.builder = Gtk.Builder(self)
|
||||
self.builder.set_translation_domain(gapp.app_name)
|
||||
self.builder.add_from_file(ui_file)
|
||||
self.widget = self.builder.get_object
|
||||
|
||||
# Find the window in the ui
|
||||
self.window = self.widget(self.name)
|
||||
if not self.window: # pragma: no cover
|
||||
raise KeyError(f"Missing window widget '{self.name}' from '{ui_file}'")
|
||||
|
||||
# Keep application alive
|
||||
self.window.set_application(self.gapp)
|
||||
|
||||
def close(self, widget=None):
|
||||
"""Closes the window. Can be connected to signals."""
|
||||
self.window.close()
|
||||
Reference in New Issue
Block a user