""" NOTE: The required `Qt` module has changed to use the one that vendorized. Remember to change to relative import when updating this. """ import re import logging from collections import OrderedDict as odict from Qt import QtCore, QtWidgets, QtGui import qtawesome __version__ = "0.5.2" _log = logging.getLogger(__name__) _type = type # used as argument try: # Python 2 _basestring = basestring except NameError: _basestring = str class QArgumentParser(QtWidgets.QWidget): """User interface arguments Arguments: arguments (list, optional): Instances of QArgument description (str, optional): Long-form text of what this parser is for storage (QSettings, optional): Persistence to disk, providing value() and setValue() methods """ changed = QtCore.Signal(QtCore.QObject) # A QArgument def __init__(self, arguments=None, description=None, storage=None, parent=None): super(QArgumentParser, self).__init__(parent) self.setAttribute(QtCore.Qt.WA_StyledBackground) # Create internal settings if storage is True: storage = QtCore.QSettings( QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, __name__, "QArgparse", ) if storage is not None: _log.info("Storing settings @ %s" % storage.fileName()) arguments = arguments or [] assert hasattr(arguments, "__iter__"), "arguments must be iterable" assert isinstance(storage, (type(None), QtCore.QSettings)), ( "storage must be of type QSettings" ) layout = QtWidgets.QGridLayout(self) layout.setRowStretch(999, 1) if description: layout.addWidget(QtWidgets.QLabel(description), 0, 0, 1, 2) self._row = 1 self._storage = storage self._arguments = odict() self._desciption = description for arg in arguments or []: self._addArgument(arg) self.setStyleSheet(style) def setDescription(self, text): self._desciption.setText(text) def addArgument(self, name, type=None, default=None, **kwargs): # Infer type from default if type is None and default is not None: type = _type(default) # Default to string type = type or str Argument = { None: String, int: Integer, float: Float, bool: Boolean, str: String, list: Enum, tuple: Enum, }.get(type, type) arg = Argument(name, default=default, **kwargs) self._addArgument(arg) return arg def _addArgument(self, arg): if arg["name"] in self._arguments: raise ValueError("Duplicate argument '%s'" % arg["name"]) if self._storage is not None: default = self._storage.value(arg["name"]) if default: if isinstance(arg, Boolean): default = bool({ None: QtCore.Qt.Unchecked, 0: QtCore.Qt.Unchecked, 1: QtCore.Qt.Checked, 2: QtCore.Qt.Checked, "0": QtCore.Qt.Unchecked, "1": QtCore.Qt.Checked, "2": QtCore.Qt.Checked, # May be stored as string, if used with IniFormat "false": QtCore.Qt.Unchecked, "true": QtCore.Qt.Checked, }.get(default)) arg["default"] = default arg.changed.connect(lambda: self.on_changed(arg)) label = ( QtWidgets.QLabel(arg["label"]) if arg.label else QtWidgets.QLabel() ) widget = arg.create() icon = qtawesome.icon("fa.refresh", color="white") reset = QtWidgets.QPushButton(icon, "") # default reset.setToolTip("Reset") reset.setProperty("type", "reset") reset.clicked.connect(lambda: self.on_reset(arg)) # Shown on edit reset.hide() for widget in (label, widget): widget.setToolTip(arg["help"]) widget.setObjectName(arg["name"]) # useful in CSS widget.setProperty("type", type(arg).__name__) widget.setAttribute(QtCore.Qt.WA_StyledBackground) widget.setEnabled(arg["enabled"]) # Align label on top of row if widget is over two times heiger height = (lambda w: w.sizeHint().height()) label_on_top = height(label) * 2 < height(widget) alignment = (QtCore.Qt.AlignTop,) if label_on_top else () layout = self.layout() layout.addWidget(label, self._row, 0, *alignment) layout.addWidget(widget, self._row, 1) layout.addWidget(reset, self._row, 2, *alignment) layout.setColumnStretch(1, 1) def on_changed(*_): reset.setVisible(arg["edited"]) arg.changed.connect(on_changed) self._row += 1 self._arguments[arg["name"]] = arg def clear(self): assert self._storage, "Cannot clear without persistent storage" self._storage.clear() _log.info("Clearing settings @ %s" % self._storage.fileName()) def find(self, name): return self._arguments[name] def on_reset(self, arg): arg.write(arg["default"]) def on_changed(self, arg): arg["edited"] = arg.read() != arg["default"] self.changed.emit(arg) # Optional PEP08 syntax add_argument = addArgument class QArgument(QtCore.QObject): """Base class of argument user interface """ changed = QtCore.Signal() # Provide a left-hand side label for this argument label = True # For defining default value for each argument type default = None def __init__(self, name, default=None, **kwargs): super(QArgument, self).__init__(kwargs.pop("parent", None)) kwargs["name"] = name kwargs["label"] = kwargs.get("label", camel_to_title(name)) kwargs["default"] = self.default if default is None else default kwargs["help"] = kwargs.get("help", "") kwargs["read"] = kwargs.get("read") kwargs["write"] = kwargs.get("write") kwargs["enabled"] = bool(kwargs.get("enabled", True)) kwargs["edited"] = False self._data = kwargs def __str__(self): return self["name"] def __repr__(self): return "%s(\"%s\")" % (type(self).__name__, self["name"]) def __getitem__(self, key): return self._data[key] def __setitem__(self, key, value): self._data[key] = value def __eq__(self, other): if isinstance(other, _basestring): return self["name"] == other return super(QArgument, self).__eq__(other) def __ne__(self, other): return not self.__eq__(other) def create(self): return QtWidgets.QWidget() def read(self): return self._read() def write(self, value): self._write(value) self.changed.emit() class Boolean(QArgument): """Boolean type user interface Presented by `QtWidgets.QCheckBox`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (bool, optional): Argument's default value, default None enabled (bool, optional): Whether to enable this widget, default True """ def create(self): widget = QtWidgets.QCheckBox() widget.clicked.connect(self.changed.emit) if isinstance(self, Tristate): self._read = lambda: widget.checkState() state = { 0: QtCore.Qt.Unchecked, 1: QtCore.Qt.PartiallyChecked, 2: QtCore.Qt.Checked, "1": QtCore.Qt.PartiallyChecked, "0": QtCore.Qt.Unchecked, "2": QtCore.Qt.Checked, } else: self._read = lambda: bool(widget.checkState()) state = { None: QtCore.Qt.Unchecked, 0: QtCore.Qt.Unchecked, 1: QtCore.Qt.Checked, 2: QtCore.Qt.Checked, "0": QtCore.Qt.Unchecked, "1": QtCore.Qt.Checked, "2": QtCore.Qt.Checked, # May be stored as string, if used with QSettings(..IniFormat) "false": QtCore.Qt.Unchecked, "true": QtCore.Qt.Checked, } self._write = lambda value: widget.setCheckState(state[value]) widget.clicked.connect(self.changed.emit) if self["default"] is not None: self._write(self["default"]) return widget def read(self): return self._read() class Tristate(QArgument): """Not implemented""" class Number(QArgument): """Base class of numeric type user interface""" default = 0 def create(self): if isinstance(self, Float): widget = QtWidgets.QDoubleSpinBox() widget.setMinimum(self._data.get("min", 0.0)) widget.setMaximum(self._data.get("max", 99.99)) else: widget = QtWidgets.QSpinBox() widget.setMinimum(self._data.get("min", 0)) widget.setMaximum(self._data.get("max", 99)) widget.editingFinished.connect(self.changed.emit) self._read = lambda: widget.value() self._write = lambda value: widget.setValue(value) if self["default"] != self.default: self._write(self["default"]) return widget class Integer(Number): """Integer type user interface A subclass of `qargparse.Number`, presented by `QtWidgets.QSpinBox`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (int, optional): Argument's default value, default 0 min (int, optional): Argument's minimum value, default 0 max (int, optional): Argument's maximum value, default 99 enabled (bool, optional): Whether to enable this widget, default True """ class Float(Number): """Float type user interface A subclass of `qargparse.Number`, presented by `QtWidgets.QDoubleSpinBox`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (float, optional): Argument's default value, default 0.0 min (float, optional): Argument's minimum value, default 0.0 max (float, optional): Argument's maximum value, default 99.99 enabled (bool, optional): Whether to enable this widget, default True """ class Range(Number): """Range type user interface A subclass of `qargparse.Number`, not production ready. """ class Double3(QArgument): """Double3 type user interface Presented by three `QtWidgets.QLineEdit` widget with `QDoubleValidator` installed. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (tuple or list, optional): Default (0, 0, 0). enabled (bool, optional): Whether to enable this widget, default True """ default = (0, 0, 0) def create(self): widget = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) x, y, z = (self.child_arg(layout, i) for i in range(3)) self._read = lambda: ( float(x.text()), float(y.text()), float(z.text())) self._write = lambda value: [ w.setText(str(float(v))) for w, v in zip([x, y, z], value)] if self["default"] != self.default: self._write(self["default"]) return widget def child_arg(self, layout, index): widget = QtWidgets.QLineEdit() widget.setValidator(QtGui.QDoubleValidator()) default = str(float(self["default"][index])) widget.setText(default) def focusOutEvent(event): if not widget.text(): widget.setText(default) # Ensure value exists for `_read` QtWidgets.QLineEdit.focusOutEvent(widget, event) widget.focusOutEvent = focusOutEvent widget.editingFinished.connect(self.changed.emit) widget.returnPressed.connect(widget.editingFinished.emit) layout.addWidget(widget) return widget class String(QArgument): """String type user interface Presented by `QtWidgets.QLineEdit`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (str, optional): Argument's default value, default None placeholder (str, optional): Placeholder message for the widget enabled (bool, optional): Whether to enable this widget, default True """ def __init__(self, *args, **kwargs): super(String, self).__init__(*args, **kwargs) self._previous = None def create(self): widget = QtWidgets.QLineEdit() widget.editingFinished.connect(self.onEditingFinished) widget.returnPressed.connect(widget.editingFinished.emit) self._read = lambda: widget.text() self._write = lambda value: widget.setText(value) if isinstance(self, Info): widget.setReadOnly(True) widget.setPlaceholderText(self._data.get("placeholder", "")) if self["default"] is not None: self._write(self["default"]) self._previous = self["default"] return widget def onEditingFinished(self): current = self._read() if current != self._previous: self.changed.emit() self._previous = current class Info(String): """String type user interface but read-only A subclass of `qargparse.String`, presented by `QtWidgets.QLineEdit`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (str, optional): Argument's default value, default None enabled (bool, optional): Whether to enable this widget, default True """ class Color(String): """Color type user interface A subclass of `qargparse.String`, not production ready. """ class Button(QArgument): """Button type user interface Presented by `QtWidgets.QPushButton`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (bool, optional): Argument's default value, default None enabled (bool, optional): Whether to enable this widget, default True """ label = False def create(self): widget = QtWidgets.QPushButton(self["label"]) widget.clicked.connect(self.changed.emit) state = [ QtCore.Qt.Unchecked, QtCore.Qt.Checked, ] if isinstance(self, Toggle): widget.setCheckable(True) if hasattr(widget, "isChecked"): self._read = lambda: state[int(widget.isChecked())] self._write = ( lambda value: widget.setChecked(value) ) else: self._read = lambda: widget.checkState() self._write = ( lambda value: widget.setCheckState(state[int(value)]) ) else: self._read = lambda: "clicked" self._write = lambda value: None if self["default"] is not None: self._write(self["default"]) return widget class Toggle(Button): """Checkable `Button` type user interface Presented by `QtWidgets.QPushButton`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument default (bool, optional): Argument's default value, default None enabled (bool, optional): Whether to enable this widget, default True """ class InfoList(QArgument): """String list type user interface Presented by `QtWidgets.QListView`, not production ready. """ def __init__(self, name, **kwargs): kwargs["default"] = kwargs.pop("default", ["Empty"]) super(InfoList, self).__init__(name, **kwargs) def create(self): class Model(QtCore.QStringListModel): def data(self, index, role): return super(Model, self).data(index, role) model = QtCore.QStringListModel(self["default"]) widget = QtWidgets.QListView() widget.setModel(model) widget.setEditTriggers(widget.NoEditTriggers) self._read = lambda: model.stringList() self._write = lambda value: model.setStringList(value) return widget class Choice(QArgument): """Argument user interface for selecting one from list Presented by `QtWidgets.QListView`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument items (list, optional): List of strings for select, default `["Empty"]` default (str, optional): Default item in `items`, use first of `items` if not given. enabled (bool, optional): Whether to enable this widget, default True """ def __init__(self, name, **kwargs): kwargs["items"] = kwargs.get("items", ["Empty"]) kwargs["default"] = kwargs.pop("default", kwargs["items"][0]) super(Choice, self).__init__(name, **kwargs) def index(self, value): """Return numerical equivalent to self.read()""" return self["items"].index(value) def create(self): def on_changed(selected, deselected): try: selected = selected.indexes()[0] except IndexError: # At least one item must be selected at all times selected = deselected.indexes()[0] value = selected.data(QtCore.Qt.DisplayRole) set_current(value) self.changed.emit() def set_current(current): options = model.stringList() if current == "Empty": index = 0 else: for index, member in enumerate(options): if member == current: break else: raise ValueError( "%s not a member of %s" % (current, options) ) qindex = model.index(index, 0, QtCore.QModelIndex()) smodel.setCurrentIndex(qindex, smodel.ClearAndSelect) self["current"] = options[index] def reset(items, default=None): items = items or ["Empty"] model.setStringList(items) set_current(default or items[0]) model = QtCore.QStringListModel() widget = QtWidgets.QListView() widget.setModel(model) widget.setEditTriggers(widget.NoEditTriggers) widget.setSelectionMode(widget.SingleSelection) smodel = widget.selectionModel() smodel.selectionChanged.connect(on_changed) self._read = lambda: self["current"] self._write = lambda value: set_current(value) self.reset = reset reset(self["items"], self["default"]) return widget class Separator(QArgument): """Visual separator Example: item1 item2 ------------ item3 item4 """ def create(self): widget = QtWidgets.QWidget() self._read = lambda: None self._write = lambda value: None return widget class Enum(QArgument): """Argument user interface for selecting one from dropdown list Presented by `QtWidgets.QComboBox`. Arguments: name (str): The name of argument label (str, optional): Display name, convert from `name` if not given help (str, optional): Tool tip message of this argument items (list, optional): List of strings for select, default `[]` default (int, optional): Index of default item, use first of `items` if not given. enabled (bool, optional): Whether to enable this widget, default True """ def __init__(self, name, **kwargs): kwargs["default"] = kwargs.pop("default", 0) kwargs["items"] = kwargs.get("items", []) assert isinstance(kwargs["items"], (tuple, list)), ( "items must be list" ) super(Enum, self).__init__(name, **kwargs) def create(self): widget = QtWidgets.QComboBox() widget.addItems(self["items"]) widget.currentIndexChanged.connect( lambda index: self.changed.emit()) self._read = lambda: widget.currentText() self._write = lambda value: widget.setCurrentIndex(value) if self["default"] is not None: self._write(self["default"]) return widget style = """\ QWidget { /* Explicitly specify a size, to account for automatic HDPi */ font-size: 11px; } *[type="Button"] { text-align:left; } *[type="Info"] { background: transparent; border: none; } QLabel[type="Separator"] { min-height: 20px; text-decoration: underline; } QPushButton[type="reset"] { max-width: 11px; max-height: 11px; } """ def camelToTitle(text): """Convert camelCase `text` to Title Case Example: >>> camelToTitle("mixedCase") "Mixed Case" >>> camelToTitle("myName") "My Name" >>> camelToTitle("you") "You" >>> camelToTitle("You") "You" >>> camelToTitle("This is That") "This Is That" """ return re.sub( r"((?<=[a-z])[A-Z]|(?