File: pen.py

package info (click to toggle)
python-pyqtgraph 0.13.7-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,068 kB
  • sloc: python: 54,043; makefile: 129; ansic: 40; sh: 2
file content (238 lines) | stat: -rw-r--r-- 9,600 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import re
from contextlib import ExitStack

from ... import functions as fn
from ...Qt import QtCore, QtWidgets
from ...SignalProxy import SignalProxy
from ...widgets.PenPreviewLabel import PenPreviewLabel
from . import GroupParameterItem, WidgetParameterItem
from .basetypes import GroupParameter, Parameter, ParameterItem
from .qtenum import QtEnumParameter


class PenParameterItem(GroupParameterItem):
    def __init__(self, param, depth):
        self.defaultBtn = self.makeDefaultButton()
        super().__init__(param, depth)
        self.itemWidget = QtWidgets.QWidget()
        layout = QtWidgets.QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(2)

        self.penLabel = PenPreviewLabel(param)
        for child in self.penLabel, self.defaultBtn:
            layout.addWidget(child)
        self.itemWidget.setLayout(layout)

    def optsChanged(self, param, opts):
        if "enabled" in opts or "readonly" in opts:
            self.updateDefaultBtn()

    def treeWidgetChanged(self):
        ParameterItem.treeWidgetChanged(self)
        tw = self.treeWidget()
        if tw is None:
            return
        tw.setItemWidget(self, 1, self.itemWidget)

    defaultClicked = WidgetParameterItem.defaultClicked
    makeDefaultButton = WidgetParameterItem.makeDefaultButton

    def valueChanged(self, param, val):
        self.updateDefaultBtn()

    def updateDefaultBtn(self):
        self.defaultBtn.setEnabled(
            not self.param.valueIsDefault()
            and self.param.opts["enabled"]
            and self.param.writable()
        )


def cap_first(s: str):
    if not s:
        return s
    return s[0].upper() + s[1:]


class PenParameter(GroupParameter):
    """
    Controls the appearance of a QPen value.

    When `saveState` is called, the value is encoded as (color, width, style, capStyle, joinStyle, cosmetic)

    ============== ========================================================
    **Options:**
    color          pen color, can be any argument accepted by :func:`~pyqtgraph.mkColor` (defaults to black)
    width          integer width >= 0 (defaults to 1)
    style          String version of QPenStyle enum, i.e. 'SolidLine' (default), 'DashLine', etc.
    capStyle       String version of QPenCapStyle enum, i.e. 'SquareCap' (default), 'RoundCap', etc.
    joinStyle      String version of QPenJoinStyle enum, i.e. 'BevelJoin' (default), 'RoundJoin', etc.
    cosmetic       Boolean, whether or not the pen is cosmetic (defaults to True)
    ============== ========================================================
    """
    itemClass = PenParameterItem

    def __init__(self, **opts):
        self.pen = fn.mkPen(**opts)
        children = self._makeChildren(self.pen)
        if 'children' in opts:
            raise KeyError('Cannot set "children" argument in Pen Parameter opts')
        super().__init__(**opts, children=list(children))
        self.valChangingProxy = SignalProxy(
            self.sigValueChanging,
            delay=1.0,
            slot=self._childrenFinishedChanging,
            threadSafe=False,
        )

    def _childrenFinishedChanging(self, paramAndValue):
        self.setValue(self.pen)

    def setDefault(self, val, **kwargs):
        pen = self._interpretValue(val)
        with self.treeChangeBlocker():
            # Block changes until all are finalized
            for opt in self.names:
                # Booleans have different naming convention
                if isinstance(self[opt], bool):
                    attrName = f'is{opt.title()}'
                else:
                    attrName = opt
                self.child(opt).setDefault(getattr(pen, attrName)(), **kwargs)
            out = super().setDefault(val, **kwargs)
        return out

    def saveState(self, filter=None):
        state = super().saveState(filter)
        opts = state.pop('children')
        state['value'] = tuple(o['value'] for o in opts.values())
        if 'default' not in state:
            state['default'] = state['value']  # TODO remove this after January 2025
        return state

    def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True):
        return super().restoreState(state, recursive=False, addChildren=False, removeChildren=False, blockSignals=blockSignals)

    def _interpretValue(self, v):
        return self.mkPen(v)

    def setValue(self, value, blockSignal=None):
        if not fn.eq(value, self.pen):
            value = self.mkPen(value)
            self.updateFromPen(self, value)
        return super().setValue(self.pen, blockSignal)

    def applyOptsToPen(self, **opts):
        # Transform opts into a value for the current pen
        paramNames = set(opts).intersection(self.names)
        # Value should be overridden by opts
        with self.treeChangeBlocker():
            if 'value' in opts:
                pen = self.mkPen(opts.pop('value'))
                if not fn.eq(pen, self.pen):
                    self.updateFromPen(self, pen)
            penOpts = {}
            for kk in paramNames:
                penOpts[kk] = opts[kk]
                self[kk] = opts[kk]
        return penOpts

    def setOpts(self, **opts):
        # Transform opts into a value
        if self.applyOptsToPen(**opts):
            self.setValue(self.pen)
        return super().setOpts(**opts)

    def mkPen(self, *args, **kwargs):
        """Thin wrapper around fn.mkPen which accepts the serialized state from saveState"""
        if len(args) == 1 and isinstance(args[0], tuple) and len(args[0]) == len(self.childs):
            opts = dict(zip(self.names, args[0]))
            self.applyOptsToPen(**opts)
            args = (self.pen,)
            kwargs = {}
        return fn.mkPen(*args, **kwargs)

    def _makeChildren(self, boundPen=None):
        cs = QtCore.Qt.PenCapStyle
        js = QtCore.Qt.PenJoinStyle
        ps = QtCore.Qt.PenStyle
        param = Parameter.create(
            name='Params', type='group', children=[
                dict(name='color', type='color', value='k'),
                dict(name='width', value=1, type='int', limits=[0, None]),
                QtEnumParameter(ps, name='style', value='SolidLine'),
                QtEnumParameter(cs, name='capStyle'),
                QtEnumParameter(js, name='joinStyle'),
                dict(name='cosmetic', type='bool', value=True)
            ]
            )

        optsPen = boundPen or fn.mkPen()
        for p in param:
            name = p.name()
            # Qt naming scheme uses isXxx for booleans
            if p.type() == 'bool':
                attrName = f'is{name.title()}'
            else:
                attrName = name
            default = getattr(optsPen, attrName)()
            replace = r'\1 \2'
            name = re.sub(r'(\w)([A-Z])', replace, name)
            name = name.title().strip()
            p.setOpts(title=name, default=default)

        if boundPen is not None:
            self.updateFromPen(param, boundPen)
            for p in param:
                setName = f'set{cap_first(p.name())}'
                # Instead, set the parameter which will signal the old setter
                setattr(boundPen, setName, p.setValue)
                newSetter = self.penPropertySetter
                # Edge case: color picker uses a dialog with user interaction, so wait until full change there
                if p.type() != 'color':
                    p.sigValueChanging.connect(newSetter)
                # Force children to emulate self's value instead of being part of a tree like normal
                try:
                    p.sigValueChanged.disconnect(p._emitValueChanged)
                except RuntimeError:
                    # workaround https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2487
                    # that affects PySide 6.5.3
                    # Since the child param was freshly created by us, there can only have been one slot
                    # connected. So we can just disconnect all the slots without specifying which one.
                    assert p.receivers(QtCore.SIGNAL("sigValueChanged(PyObject,PyObject)")) == 1
                    p.sigValueChanged.disconnect()
                # Some widgets (e.g. checkbox, combobox) don't emit 'changing' signals, so tie to 'changed' as well
                p.sigValueChanged.connect(newSetter)

        return param

    def penPropertySetter(self, p, value):
        boundPen = self.pen
        setName = f'set{cap_first(p.name())}'
        # boundPen.setName has been monkey-patched
        # so we get the original setter from the class
        getattr(boundPen.__class__, setName)(boundPen, value)
        self.sigValueChanging.emit(self, boundPen)

    @staticmethod
    def updateFromPen(param, pen):
        """
        Applies settings from a pen to either a Parameter or dict. The Parameter or dict must already
        be populated with the relevant keys that can be found in `PenSelectorDialog.mkParam`.
        """
        stack = ExitStack()
        if isinstance(param, Parameter):
            names = param.names
            # Block changes until all are finalized
            stack.enter_context(param.treeChangeBlocker())
        else:
            names = param
        for opt in names:
            # Booleans have different naming convention
            if isinstance(param[opt], bool):
                attrName = f'is{opt.title()}'
            else:
                attrName = opt
            param[opt] = getattr(pen, attrName)()
        stack.close()