"""Module to define a parent qtdesigner plugin class.
Please continue to name your qtdesigner plugin modules using the convention
modulename.py <--- defines the widget
modulename_qtplugin.py <--- imports this module + the widget
However, a majority of the builtin plugins are defined in qtplugins.py
adjacent to this module.
NOTE: PyDMDesignerPlugin is a valid plugin, so designer will try to pick it up
and instantiate it if you import it into another module's namespace. You
will need to avoid having it present in the global namespace of any
module that defines a qtplugin.
If you do not heed this warning, you will get a one-line traceback:
TypeError: __init__() takes exactly 3 arguments (1 given)
for each PyDMDesignerPlugin that Qt Designer tries to use. This will not
affect any of your widgets, but it will be annoying.
"""
import enum
import inspect
import logging
from typing import Dict, List, Optional, Type
import entrypoints
from qtpy import QtCore, QtDesigner, QtGui, QtWidgets
from .. import config
from ..qtdesigner import DesignerHooks
from .qtplugin_extensions import PyDMExtensionFactory
logger = logging.getLogger(__name__)
[docs]def qtplugin_factory(
cls: Type[QtWidgets.QWidget],
is_container: bool = False,
group: str = "PyDM Widgets",
extensions: Optional[List[Type]] = None,
icon: Optional[QtGui.QIcon] = None
) -> Type["PyDMDesignerPlugin"]:
"""
Helper function to create a generic PyDMDesignerPlugin class.
Parameters
----------
cls : QWidget subclass
The widget class.
is_container : bool, optional
True if this widget can contain other widgets (as in a Frame).
This will also affect whether or not the widget can be used as the
top-level widget of a display. Defaults to False.
extensions : list of extension classes, optional
Extension classes to use with the widget.
icon : QtGui.QIcon, optional
An icon to use with the widget in the designer. Consider using
the PyDM-provided fontawesome ``ifont`` here for simplicity.
"""
class Plugin(PyDMDesignerPlugin):
__doc__ = "PyDMDesigner Plugin for {}".format(cls.__name__)
def __init__(self):
super(Plugin, self).__init__(cls, is_container, group, extensions,
icon)
return Plugin
class PyDMDesignerPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
"""
Parent class to standardize how pydm plugins are accessed in qt designer.
All functions have default returns that can be overriden as necessary.
"""
def __init__(
self,
cls: Type[QtWidgets.QWidget],
is_container: bool = False,
group: str = "PyDM Widgets",
extensions: Optional[List[Type]] = None,
icon: Optional[QtGui.QIcon] = None,
):
"""
Set up the plugin using the class info in cls
:param cls: Class of the widget to use
:type cls: QWidget
"""
QtDesigner.QPyDesignerCustomWidgetPlugin.__init__(self)
self.initialized = False
self.is_container = is_container
self.cls = cls
self._group = group
if icon is None and QtWidgets.QApplication.instance() is not None:
get_designer_icon = getattr(cls, "get_designer_icon", None)
if get_designer_icon is not None:
icon = get_designer_icon()
if icon is None:
icon = QtGui.QIcon()
self._icon = icon
self.extensions = extensions
self.manager = None
def initialize(self, core):
"""
Override this function if you need special initialization instructions.
Make sure you don't neglect to set the self.initialized flag to True
after a successful initialization.
:param core: form editor interface to use in the initialization
:type core: QDesignerFormEditorInterface
"""
if self.initialized:
return
self.initialized = True
designer_hooks = DesignerHooks()
designer_hooks.form_editor = core
if self.extensions is not None and len(self.extensions) > 0:
self.manager = core.extensionManager()
if self.manager:
factory = PyDMExtensionFactory(parent=self.manager)
self.manager.registerExtensions(
factory,
'org.qt-project.Qt.Designer.TaskMenu') # Qt5
def isInitialized(self):
"""
Return True if initialize function has been called successfully.
"""
return self.initialized
def createWidget(self, parent):
"""
Instantiate a widget with the given parent.
:param parent: Parent widget of instantiated widget
:type parent: QWidget
"""
w = self.cls(parent=parent)
try:
setattr(w, "extensions", self.extensions)
w.init_for_designer()
except (AttributeError, NameError):
pass
return w
def name(self):
"""
Return the class name of the widget.
"""
return self.cls.__name__
def group(self):
"""
Return a common group name so all PyDM Widgets are together in
Qt Designer.
"""
return self._group
def toolTip(self):
"""
A short description to pop up on mouseover. If we leave this as an
empty string, we'll have no tooltip by default and can override this
on a case-by-case basis.
"""
return ""
def whatsThis(self):
"""
A longer description of the widget for Qt Designer. By default, this
is the entire class docstring.
"""
return ""
def isContainer(self):
"""
Return True if this widget can contain other widgets.
"""
return self.is_container
def icon(self):
"""
Return a QIcon to represent this widget in Qt Designer.
"""
return QtGui.QIcon(self._icon.pixmap(QtCore.QSize(32, 32)))
def domXml(self):
"""
XML Description of the widget's properties.
"""
return (
"<widget class=\"{0}\" name=\"{0}\">\n"
" <property name=\"toolTip\" >\n"
" <string>{1}</string>\n"
" </property>\n"
"</widget>\n"
).format(self.name(), self.toolTip())
def includeFile(self):
"""
Include the class module for the generated qt code
"""
return self.cls.__module__
def create_designer_widget_from_widget(
widget_cls: Type[QtWidgets.QWidget],
) -> Type[PyDMDesignerPlugin]:
"""
Get a designable widget class.
Accepts either user-provided :class:`PyDMDesignerPlugin` subclasses or
:class:`QWidget` subclasses.
In the case of :class:`QWidget` subclasses, designer-specific settings
may be specified on a class attribute. These arguments should match
what :func:`qtplugin_factory` expects.
"""
if not inspect.isclass(widget_cls):
raise ValueError(
f"Expected a class, got a {type(widget_cls).__name__}"
)
if issubclass(widget_cls, PyDMDesignerPlugin):
return widget_cls
if issubclass(widget_cls, QtWidgets.QWidget):
designer_kwargs = getattr(
widget_cls, "_qt_designer_", None
) or {}
return qtplugin_factory(widget_cls, **designer_kwargs)
raise ValueError(
f"Expected a PyDMDesignerPlugin or a QWidget subclass, "
f"got a {type(widget_cls).__name__}"
)
def get_widgets_from_entrypoints(
key: str = config.ENTRYPOINT_WIDGET
) -> Dict[str, Type[PyDMDesignerPlugin]]:
"""
Get all widgets from entrypoint definitions.
Parameters
----------
key : str, optional
The entrypoint key. Defaults to ``pydm.config.ENTRYPOINT_WIDGET``.
Returns
-------
widgets : dict
Dictionary of class name to ``PyDMDesignerPlugin`` subclass.
"""
widgets = {}
for entry in entrypoints.get_group_all(key):
logger.debug("Found widget from entrypoint: %s", entry.name)
try:
plugin_cls = entry.load()
except Exception as ex:
logger.exception(
"Failed to load %s entry %s: %s",
key, entry.name, ex
)
continue
try:
designer_widget = create_designer_widget_from_widget(
plugin_cls
)
except Exception:
logger.warning(
"Invalid widget class specified in entrypoint "
"%s: %s",
entry.name, plugin_cls
)
else:
widgets[entry.name] = designer_widget
return widgets