Source code for madgui.util.qt

"""
Miscellaneous utilities for programming with the Qt framework.
"""

__all__ = [
    'notifyEvent',
    'eventFilter',
    'EventFilter',
    'present',
    'monospace',
    'bold',
    'load_ui',
    'load_icon_resource',
    'SingleWindow',
    'Queued',
]

import functools
from importlib_resources import path as resource_filename, open_binary

from PyQt5.QtCore import QEvent, QObject, QTimer
from PyQt5.QtGui import QFont, QFontDatabase, QIcon, QPixmap
from PyQt5 import uic

from madgui.util.collections import Bool
from madgui.util.misc import cachedproperty, memoize


[docs]def notifyEvent(widget, name, handler): """Connect the handler function to be called when the widget receives an event specified by the given name.""" # This is implemented using `EventFilter`. The alternative would be to # - either overwrite the xxxEvent method on the object, or # - for closeEvent specifically, set the WA_DeleteOnClose attribute and # connect to the QWidget.destroyed signal. # However, either method should be avoided due to their disadvantages: # - overwriting methods on the object makes it impossible to cleanly # remove a handler, and may creates unneeded long-lasting reference # cycles on the python side, deepens the call-stack for handlers, # and only works with some events. For example the ``event()`` method # seems to be unimpressed by attempts to change it on the object. # - WA_DeleteOnClose alters the lifetime of the parent widget and can # lead to unforseen side-effects, and is of course only available for # a single event type (Close) anyway # Setting a parent is required to prevent the filter from being garbage # collected immediately: return eventFilter(widget, { name: lambda obj, evt: obj is widget and handler() and False, }, parent=widget)
[docs]def eventFilter(object, events, parent=None): """ Subscribe to events from ``object`` and dispatch via ``events`` lookup table. Callbacks are invoked with two parameters ``(object, event)`` and should return a false-ish value. A true-ish value will stop the event from being processed further. Example usage: >>> self.event_filter = eventFilter(window, { ... 'WindowActivate': self._on_window_activate, ... 'Close': self._on_window_close)) ... }) (Note that it is important to store the reference to the event filter somewhere - otherwise it may be garbage collected.) See ``QEvent`` for possible events. """ filter = EventFilter(events, parent) object.installEventFilter(filter) return filter
[docs]class EventFilter(QObject): """Implements an event filter from a lookup table. It is preferred to use the :func:`eventFilter` function rather than instanciating this class directly.""" def __init__(self, events, parent=None): super().__init__(parent) self.event_table = { getattr(QEvent, k): v for k, v in events.items() }
[docs] def eventFilter(self, object, event): dispatch = self.event_table.get(event.type()) return bool(dispatch and dispatch(object, event))
[docs] def uninstall(self): self.parent().removeEventFilter(self)
[docs]def present(window, raise_=False): """Activate window and bring to front.""" window.show() window.activateWindow() if raise_: window.raise_()
[docs]def monospace(): """Return a fixed-space ``QFont``.""" return QFontDatabase.systemFont(QFontDatabase.FixedFont)
[docs]def bold(): """Return a bold ``QFont``.""" font = QFont() font.setBold(True) return font
[docs]def load_ui(widget, package, filename): """ Initialize widget from ``.uic`` file loaded from the given package. This function is for loading GUIs that were developed using the qt-designer rapid development tool which creates ``.uic`` description files. These can be saved in the same package alongside the corresponding python code. Now, in the class that implements the widget, use this function as follows:: class MyWidget(QWidget): def __init__(self): super().__init__() load_ui(self, __package__, 'mywidget.uic') """ with open_binary(package, filename) as f: uic.loadUi(f, widget)
[docs]def load_icon_resource(module, name, format='XPM'): """Load an icon distributed with the given python package. Returns a ``QPixmap``.""" with resource_filename(module, name) as filename: return QIcon(QPixmap(str(filename), format))
class Property: """Internal class for cached properties. Should be simplified and rewritten. Currently only used as base class for ``SingleWindow``. Do not use for new code.""" def __init__(self, construct): self.construct = construct self.holds_value = Bool(False) # porcelain @classmethod def factory(cls, func): return cachedproperty(functools.wraps(func)( lambda self: cls(func.__get__(self)))) def create(self): if self._has: self._update() else: self._new() return self.val def destroy(self): if self._has: self._del() def toggle(self): if self._has: self._del() else: self._new() def _new(self): val = self.construct() self._set(val) return val def _update(self): pass @property def _has(self): return hasattr(self, '_val') def _get(self): return self._val def _set(self, val): self._val = val self.holds_value.set(True) def _del(self): del self._val self.holds_value.set(False) # use lambdas to enable overriding the _get/_set/_del methods # without having to redefine the 'val' property val = property(lambda self: self._get(), lambda self, val: self._set(val), lambda self: self._del())
[docs]class SingleWindow(Property): """ Decorator for widget constructor methods. It manages the lifetime of the widget to ensure that only one is active at the same time. """ def _del(self): self.val.close() def _closed(self): if self.holds_value(): super()._del() def _new(self): window = super()._new() present(window.window()) notifyEvent(window, 'Close', self._closed) return window def _update(self): present(self.val.window())
[docs]class Queued: """ A queued trigger. Calling the trigger will invoke the handler function in another mainloop iteration. Calling the trigger multiple times before the handler was invoked (e.g. within the same mainloop iteration) will result in only a *single* handler invocation! This can only be used with at least a ``QCoreApplication`` instanciated. """ def __init__(self, func): self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(func) def __call__(self): """Schedule the handler invocation for another mainloop iteration.""" self.timer.start()
[docs] @classmethod def method(cls, func): """Decorator for a queued method, i.e. a method that when called, actually runs at a later time.""" return property(memoize(functools.wraps(func)( lambda self: cls(func.__get__(self)))))