triplot 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dscpanel/__init__.py +4 -0
- dscpanel/__main__.py +155 -0
- dscpanel/branding.py +142 -0
- dscpanel/core/__init__.py +0 -0
- dscpanel/core/arrange.py +143 -0
- dscpanel/core/chem.py +252 -0
- dscpanel/core/dtg.py +135 -0
- dscpanel/core/export.py +649 -0
- dscpanel/core/figure.py +171 -0
- dscpanel/core/labels.py +430 -0
- dscpanel/core/loader.py +202 -0
- dscpanel/core/log.py +147 -0
- dscpanel/core/measure.py +636 -0
- dscpanel/core/model.py +1993 -0
- dscpanel/core/molar.py +344 -0
- dscpanel/core/numbers.py +208 -0
- dscpanel/core/ops.py +161 -0
- dscpanel/core/presets.py +312 -0
- dscpanel/core/profile.py +15 -0
- dscpanel/core/session.py +667 -0
- dscpanel/core/shades.py +54 -0
- dscpanel/core/style.py +528 -0
- dscpanel/core/trios_analysis.py +636 -0
- dscpanel/core/trios_io.py +1311 -0
- dscpanel/core/undo.py +230 -0
- dscpanel/core/units.py +220 -0
- dscpanel/register.py +284 -0
- dscpanel/ui/__init__.py +0 -0
- dscpanel/ui/appearance.py +146 -0
- dscpanel/ui/colour.py +629 -0
- dscpanel/ui/dialogs.py +3639 -0
- dscpanel/ui/loading.py +95 -0
- dscpanel/ui/numbox.py +103 -0
- dscpanel/ui/outliner.py +818 -0
- dscpanel/ui/palette.py +193 -0
- dscpanel/ui/plot.py +8349 -0
- dscpanel/ui/settings.py +256 -0
- dscpanel/ui/window.py +4129 -0
- triplot-1.1.0.dist-info/METADATA +315 -0
- triplot-1.1.0.dist-info/RECORD +44 -0
- triplot-1.1.0.dist-info/WHEEL +5 -0
- triplot-1.1.0.dist-info/entry_points.txt +5 -0
- triplot-1.1.0.dist-info/licenses/LICENSE +22 -0
- triplot-1.1.0.dist-info/top_level.txt +1 -0
dscpanel/ui/colour.py
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"""The colour picker: a hue wheel, a value bar, typed numbers, a dropper.
|
|
2
|
+
|
|
3
|
+
Qt's own picker has a bar that runs to DARKER shades,
|
|
4
|
+
while the usual wish is a saturated colour made paler. Here the wheel is
|
|
5
|
+
hue by angle and saturation by radius with WHITE at the centre, so paler is
|
|
6
|
+
a move inwards, and darker is the bar beside it. Every number box takes a
|
|
7
|
+
sum ("255-20", `numbers.evaluate`); the hex field does not, because a sum
|
|
8
|
+
on a whole colour says nothing about which channel it means.
|
|
9
|
+
|
|
10
|
+
`get_colour` is the entry point, like `QColorDialog.getColor`: modal,
|
|
11
|
+
returning an invalid QColor when nothing was chosen. Like every pop-up here
|
|
12
|
+
it keeps its change however it is closed; Revert puts back the colour it
|
|
13
|
+
opened with.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal
|
|
20
|
+
from PySide6.QtGui import (QColor, QCursor, QGuiApplication, QImage,
|
|
21
|
+
QLinearGradient, QPainter, QPen)
|
|
22
|
+
from PySide6.QtWidgets import (QCheckBox, QDialog, QDialogButtonBox,
|
|
23
|
+
QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
|
24
|
+
QSizePolicy, QToolButton, QVBoxLayout, QWidget)
|
|
25
|
+
|
|
26
|
+
from ..core import shades
|
|
27
|
+
from .numbox import NumberBox, WholeBox
|
|
28
|
+
|
|
29
|
+
#: The DSC_Plotter template's line colours, in its order (`colors` in its
|
|
30
|
+
#: driver): what the colour picker's basic colours start with.
|
|
31
|
+
PLOTTER_COLOURS = ("#0000ff", "#008000", "#ff0000", "#ffa500", "#ff00ff",
|
|
32
|
+
"#d2691e", "#00008b", "#005000", "#00ffff", "#008080",
|
|
33
|
+
"#800080", "#8a2be2", "#808080", "#800000", "#9acd32",
|
|
34
|
+
"#000000")
|
|
35
|
+
#: And after them: the panel's own screen colours, Tableau's ten,
|
|
36
|
+
#: Okabe and Ito's colour-blind-safe set, and greys.
|
|
37
|
+
MORE_COLOURS = ("#6ea8ff", "#ffb04e", "#7fd08a", "#e07b7b", "#c79bef",
|
|
38
|
+
"#4fd0c8", "#d8d16a", "#f08ac0",
|
|
39
|
+
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
|
|
40
|
+
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
|
|
41
|
+
"#e69f00", "#56b4e9", "#009e73", "#f0e442", "#0072b2",
|
|
42
|
+
"#d55e00", "#cc79a7",
|
|
43
|
+
"#ffffff", "#e0e0e0", "#c0c0c0", "#a0a0a0", "#606060",
|
|
44
|
+
"#404040", "#202020")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _hsv_to_rgb(h, s, v):
|
|
48
|
+
"""Vectorised HSV (all 0..1) to RGB (0..1)."""
|
|
49
|
+
h6 = (np.asarray(h, dtype=float) * 6.0) % 6.0
|
|
50
|
+
i = np.floor(h6).astype(int) % 6
|
|
51
|
+
f = h6 - np.floor(h6)
|
|
52
|
+
v = np.broadcast_to(np.asarray(v, dtype=float), h6.shape)
|
|
53
|
+
s = np.broadcast_to(np.asarray(s, dtype=float), h6.shape)
|
|
54
|
+
p = v * (1.0 - s)
|
|
55
|
+
q = v * (1.0 - s * f)
|
|
56
|
+
t = v * (1.0 - s * (1.0 - f))
|
|
57
|
+
red = np.choose(i, [v, q, p, p, t, v])
|
|
58
|
+
green = np.choose(i, [t, v, v, q, p, p])
|
|
59
|
+
blue = np.choose(i, [p, p, t, v, v, q])
|
|
60
|
+
return red, green, blue
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def hsv_image(size, value):
|
|
64
|
+
"""The hue/saturation disc at `value`, `size` pixels square: hue by
|
|
65
|
+
angle (red to the right, counter-clockwise), saturation by radius,
|
|
66
|
+
transparent outside, antialiased at the rim."""
|
|
67
|
+
n = max(int(size), 2)
|
|
68
|
+
centre = (n - 1) / 2.0
|
|
69
|
+
radius = n / 2.0 - 1.0
|
|
70
|
+
ys, xs = np.mgrid[0:n, 0:n].astype(float)
|
|
71
|
+
dx, dy = xs - centre, centre - ys
|
|
72
|
+
r = np.hypot(dx, dy)
|
|
73
|
+
h = (np.degrees(np.arctan2(dy, dx)) % 360.0) / 360.0
|
|
74
|
+
s = np.clip(r / radius, 0.0, 1.0)
|
|
75
|
+
red, green, blue = _hsv_to_rgb(h, s, float(value))
|
|
76
|
+
alpha = np.clip(radius + 0.5 - r, 0.0, 1.0)
|
|
77
|
+
# Premultiplied, so the rim blends rather than fringes.
|
|
78
|
+
argb = np.empty((n, n, 4), dtype=np.uint8)
|
|
79
|
+
argb[..., 0] = np.round(blue * alpha * 255)
|
|
80
|
+
argb[..., 1] = np.round(green * alpha * 255)
|
|
81
|
+
argb[..., 2] = np.round(red * alpha * 255)
|
|
82
|
+
argb[..., 3] = np.round(alpha * 255)
|
|
83
|
+
data = argb.tobytes()
|
|
84
|
+
image = QImage(data, n, n, 4 * n, QImage.Format_ARGB32_Premultiplied)
|
|
85
|
+
return image.copy()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class HueWheel(QWidget):
|
|
89
|
+
"""Hue by angle, saturation by radius (white in the middle)."""
|
|
90
|
+
|
|
91
|
+
picked = Signal(float, float) # hue 0..1, saturation 0..1
|
|
92
|
+
|
|
93
|
+
def __init__(self, parent=None):
|
|
94
|
+
QWidget.__init__(self, parent)
|
|
95
|
+
self.setMinimumSize(200, 200)
|
|
96
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
97
|
+
self.h, self.s, self.v = 0.0, 0.0, 1.0
|
|
98
|
+
self._cache = None
|
|
99
|
+
self.setCursor(Qt.CrossCursor)
|
|
100
|
+
|
|
101
|
+
def sizeHint(self):
|
|
102
|
+
return QSize(230, 230)
|
|
103
|
+
|
|
104
|
+
def set_hsv(self, h, s, v):
|
|
105
|
+
self.h, self.s, self.v = h, s, v
|
|
106
|
+
self.update()
|
|
107
|
+
|
|
108
|
+
def _disc(self):
|
|
109
|
+
side = float(min(self.width(), self.height()))
|
|
110
|
+
return QRectF((self.width() - side) / 2.0,
|
|
111
|
+
(self.height() - side) / 2.0, side, side)
|
|
112
|
+
|
|
113
|
+
def point_of(self, h, s):
|
|
114
|
+
"""Where (h, s) is on the wheel, in widget pixels."""
|
|
115
|
+
disc = self._disc()
|
|
116
|
+
radius = disc.width() / 2.0 - 1.0
|
|
117
|
+
angle = h * 2.0 * math.pi
|
|
118
|
+
return QPointF(disc.center().x() + math.cos(angle) * s * radius,
|
|
119
|
+
disc.center().y() - math.sin(angle) * s * radius)
|
|
120
|
+
|
|
121
|
+
def paintEvent(self, _ev):
|
|
122
|
+
disc = self._disc()
|
|
123
|
+
side = int(disc.width())
|
|
124
|
+
key = (side, round(self.v * 255))
|
|
125
|
+
if self._cache is None or self._cache[0] != key:
|
|
126
|
+
self._cache = (key, hsv_image(side, self.v))
|
|
127
|
+
p = QPainter(self)
|
|
128
|
+
p.drawImage(disc.topLeft(), self._cache[1])
|
|
129
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
130
|
+
at = self.point_of(self.h, self.s)
|
|
131
|
+
dark = QColor(0, 0, 0)
|
|
132
|
+
light = QColor(255, 255, 255)
|
|
133
|
+
p.setBrush(Qt.NoBrush)
|
|
134
|
+
p.setPen(QPen(dark if self.v > 0.5 else light, 1.5))
|
|
135
|
+
p.drawEllipse(at, 6.0, 6.0)
|
|
136
|
+
p.setPen(QPen(light if self.v > 0.5 else dark, 1.0))
|
|
137
|
+
p.drawEllipse(at, 7.5, 7.5)
|
|
138
|
+
p.end()
|
|
139
|
+
|
|
140
|
+
def pick(self, pos):
|
|
141
|
+
disc = self._disc()
|
|
142
|
+
radius = max(disc.width() / 2.0 - 1.0, 1.0)
|
|
143
|
+
dx = pos.x() - disc.center().x()
|
|
144
|
+
dy = disc.center().y() - pos.y()
|
|
145
|
+
s = min(math.hypot(dx, dy) / radius, 1.0)
|
|
146
|
+
h = (math.degrees(math.atan2(dy, dx)) % 360.0) / 360.0
|
|
147
|
+
if s == 0.0:
|
|
148
|
+
h = self.h # the centre keeps its hue
|
|
149
|
+
self.h, self.s = h, s
|
|
150
|
+
self.update()
|
|
151
|
+
self.picked.emit(h, s)
|
|
152
|
+
|
|
153
|
+
def mousePressEvent(self, ev):
|
|
154
|
+
if ev.button() == Qt.LeftButton:
|
|
155
|
+
self.pick(ev.position())
|
|
156
|
+
|
|
157
|
+
def mouseMoveEvent(self, ev):
|
|
158
|
+
if ev.buttons() & Qt.LeftButton:
|
|
159
|
+
self.pick(ev.position())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ValueBar(QWidget):
|
|
163
|
+
"""The value (brightness): full at the top, black at the bottom."""
|
|
164
|
+
|
|
165
|
+
picked = Signal(float)
|
|
166
|
+
|
|
167
|
+
def __init__(self, parent=None):
|
|
168
|
+
QWidget.__init__(self, parent)
|
|
169
|
+
self.setFixedWidth(26)
|
|
170
|
+
self.setMinimumHeight(200)
|
|
171
|
+
self.h, self.s, self.v = 0.0, 0.0, 1.0
|
|
172
|
+
self.setCursor(Qt.CrossCursor)
|
|
173
|
+
|
|
174
|
+
def set_hsv(self, h, s, v):
|
|
175
|
+
self.h, self.s, self.v = h, s, v
|
|
176
|
+
self.update()
|
|
177
|
+
|
|
178
|
+
def _bar(self):
|
|
179
|
+
return QRectF(4, 6, self.width() - 8, self.height() - 12)
|
|
180
|
+
|
|
181
|
+
def paintEvent(self, _ev):
|
|
182
|
+
bar = self._bar()
|
|
183
|
+
p = QPainter(self)
|
|
184
|
+
gradient = QLinearGradient(bar.topLeft(), bar.bottomLeft())
|
|
185
|
+
gradient.setColorAt(0.0, QColor.fromHsvF(self.h, self.s, 1.0))
|
|
186
|
+
gradient.setColorAt(1.0, QColor(0, 0, 0))
|
|
187
|
+
p.fillRect(bar, gradient)
|
|
188
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
189
|
+
y = bar.top() + (1.0 - self.v) * bar.height()
|
|
190
|
+
p.setPen(QPen(self.palette().text().color(), 1.5))
|
|
191
|
+
p.drawLine(QPointF(0, y), QPointF(bar.left() + 5, y))
|
|
192
|
+
p.drawLine(QPointF(bar.right() - 5, y), QPointF(self.width(), y))
|
|
193
|
+
p.end()
|
|
194
|
+
|
|
195
|
+
def pick(self, pos):
|
|
196
|
+
bar = self._bar()
|
|
197
|
+
v = 1.0 - (pos.y() - bar.top()) / max(bar.height(), 1.0)
|
|
198
|
+
self.v = min(max(v, 0.0), 1.0)
|
|
199
|
+
self.update()
|
|
200
|
+
self.picked.emit(self.v)
|
|
201
|
+
|
|
202
|
+
def mousePressEvent(self, ev):
|
|
203
|
+
if ev.button() == Qt.LeftButton:
|
|
204
|
+
self.pick(ev.position())
|
|
205
|
+
|
|
206
|
+
def mouseMoveEvent(self, ev):
|
|
207
|
+
if ev.buttons() & Qt.LeftButton:
|
|
208
|
+
self.pick(ev.position())
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class Swatch(QToolButton):
|
|
212
|
+
"""One colour to click."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, colour, parent=None, size=18):
|
|
215
|
+
QToolButton.__init__(self, parent)
|
|
216
|
+
self.setFixedSize(size, size)
|
|
217
|
+
self.set_colour(colour)
|
|
218
|
+
|
|
219
|
+
def set_colour(self, colour):
|
|
220
|
+
self.colour = QColor(colour)
|
|
221
|
+
self.setToolTip(self.colour.name())
|
|
222
|
+
self.setStyleSheet("QToolButton {{ background: {}; border: 1px solid "
|
|
223
|
+
"#555; }}".format(self.colour.name()))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def screen_colour(pos=None):
|
|
227
|
+
"""The colour on screen under `pos` (global; the pointer's if None)."""
|
|
228
|
+
pos = QCursor.pos() if pos is None else pos
|
|
229
|
+
screen = QGuiApplication.screenAt(pos)
|
|
230
|
+
if screen is None:
|
|
231
|
+
return None
|
|
232
|
+
origin = screen.geometry().topLeft()
|
|
233
|
+
pixmap = screen.grabWindow(0, pos.x() - origin.x(), pos.y() - origin.y(),
|
|
234
|
+
1, 1)
|
|
235
|
+
image = pixmap.toImage()
|
|
236
|
+
if image.isNull():
|
|
237
|
+
return None
|
|
238
|
+
return QColor(image.pixel(0, 0))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class ColourDialog(QDialog):
|
|
242
|
+
"""The picker. `colour()` is the chosen one; `live(name)`, when given,
|
|
243
|
+
hears every change as it happens, so the figure follows the wheel."""
|
|
244
|
+
|
|
245
|
+
changed = Signal(str)
|
|
246
|
+
|
|
247
|
+
def __init__(self, initial, parent=None, title="Pick a colour",
|
|
248
|
+
live=None):
|
|
249
|
+
QDialog.__init__(self, parent)
|
|
250
|
+
self.setWindowTitle(title)
|
|
251
|
+
initial = QColor(initial)
|
|
252
|
+
self._initial = initial if initial.isValid() else QColor(255, 255,
|
|
253
|
+
255)
|
|
254
|
+
self._live = live
|
|
255
|
+
self._picking = None
|
|
256
|
+
self._busy = False
|
|
257
|
+
self._reverted = False
|
|
258
|
+
self.h = max(self._initial.hsvHueF(), 0.0)
|
|
259
|
+
self.s = self._initial.hsvSaturationF()
|
|
260
|
+
self.v = self._initial.valueF()
|
|
261
|
+
|
|
262
|
+
layout = QVBoxLayout(self)
|
|
263
|
+
top = QHBoxLayout()
|
|
264
|
+
layout.addLayout(top)
|
|
265
|
+
self.wheel = HueWheel(self)
|
|
266
|
+
self.wheel.setToolTip("Hue round the wheel, saturation outwards: "
|
|
267
|
+
"white in the middle, so paler is inwards.")
|
|
268
|
+
self.bar = ValueBar(self)
|
|
269
|
+
self.bar.setToolTip("Value: full at the top, black at the bottom.")
|
|
270
|
+
top.addWidget(self.wheel, 1)
|
|
271
|
+
top.addWidget(self.bar)
|
|
272
|
+
|
|
273
|
+
side = QVBoxLayout()
|
|
274
|
+
top.addLayout(side)
|
|
275
|
+
grid = QGridLayout()
|
|
276
|
+
side.addLayout(grid)
|
|
277
|
+
self.boxes = {}
|
|
278
|
+
rows = (("R", 255, ""), ("G", 255, ""), ("B", 255, ""),
|
|
279
|
+
("H", 359, "\u00b0"), ("S", 100, " %"), ("V", 100, " %"))
|
|
280
|
+
for row, (name, high, suffix) in enumerate(rows):
|
|
281
|
+
box = WholeBox(self)
|
|
282
|
+
box.setRange(0, high)
|
|
283
|
+
box.setWrapping(name == "H")
|
|
284
|
+
box.setSuffix(suffix)
|
|
285
|
+
box.setToolTip("A number or a sum: 255-20.")
|
|
286
|
+
grid.addWidget(QLabel(name), row, 0)
|
|
287
|
+
grid.addWidget(box, row, 1)
|
|
288
|
+
box.valueChanged.connect(
|
|
289
|
+
(lambda n: lambda _v: self._typed(n))(name))
|
|
290
|
+
self.boxes[name] = box
|
|
291
|
+
grid.addWidget(QLabel("Hex"), len(rows), 0)
|
|
292
|
+
self.hex = QLineEdit(self)
|
|
293
|
+
self.hex.setMaxLength(7)
|
|
294
|
+
self.hex.setToolTip("#rrggbb, rrggbb or #rgb.")
|
|
295
|
+
self.hex.editingFinished.connect(self._typed_hex)
|
|
296
|
+
grid.addWidget(self.hex, len(rows), 1)
|
|
297
|
+
|
|
298
|
+
self.dropper = QPushButton("Pick from screen", self)
|
|
299
|
+
self.dropper.setToolTip("Then click anywhere on the screen to take "
|
|
300
|
+
"its colour. Esc or the right button "
|
|
301
|
+
"stops.")
|
|
302
|
+
self.dropper.setAutoDefault(False)
|
|
303
|
+
self.dropper.clicked.connect(lambda _c=False: self.start_picking())
|
|
304
|
+
side.addWidget(self.dropper)
|
|
305
|
+
|
|
306
|
+
compare = QHBoxLayout()
|
|
307
|
+
self.before = Swatch(self._initial, self, 34)
|
|
308
|
+
self.before.setToolTip("As it was. Click to go back to it.")
|
|
309
|
+
self.before.clicked.connect(
|
|
310
|
+
lambda _c=False: self.set_colour(self._initial))
|
|
311
|
+
self.after = Swatch(self._initial, self, 34)
|
|
312
|
+
self.after.setToolTip("As it is now.")
|
|
313
|
+
compare.addWidget(QLabel("Was"))
|
|
314
|
+
compare.addWidget(self.before)
|
|
315
|
+
compare.addWidget(QLabel("Now"))
|
|
316
|
+
compare.addWidget(self.after)
|
|
317
|
+
compare.addStretch(1)
|
|
318
|
+
side.addLayout(compare)
|
|
319
|
+
side.addStretch(1)
|
|
320
|
+
|
|
321
|
+
swatches = QGridLayout()
|
|
322
|
+
swatches.setSpacing(3)
|
|
323
|
+
for order, name in enumerate(PLOTTER_COLOURS + MORE_COLOURS):
|
|
324
|
+
swatch = Swatch(name, self)
|
|
325
|
+
swatch.clicked.connect(
|
|
326
|
+
(lambda c: lambda _c=False: self.set_colour(c))(name))
|
|
327
|
+
swatches.addWidget(swatch, order // 16, order % 16)
|
|
328
|
+
layout.addLayout(swatches)
|
|
329
|
+
|
|
330
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok
|
|
331
|
+
| QDialogButtonBox.Cancel)
|
|
332
|
+
revert = buttons.button(QDialogButtonBox.Cancel)
|
|
333
|
+
revert.setText("Revert")
|
|
334
|
+
revert.setToolTip("Put back the colour it had and close. Closing "
|
|
335
|
+
"otherwise keeps the new one.")
|
|
336
|
+
for button in buttons.buttons():
|
|
337
|
+
button.setAutoDefault(False)
|
|
338
|
+
buttons.accepted.connect(self.accept)
|
|
339
|
+
buttons.rejected.connect(self.revert)
|
|
340
|
+
layout.addWidget(buttons)
|
|
341
|
+
|
|
342
|
+
self.wheel.picked.connect(self._wheel_picked)
|
|
343
|
+
self.bar.picked.connect(self._bar_picked)
|
|
344
|
+
self._show(tell=False)
|
|
345
|
+
|
|
346
|
+
# -------------------------------------------------------------- state
|
|
347
|
+
def colour(self):
|
|
348
|
+
return QColor.fromHsvF(self.h, self.s, self.v)
|
|
349
|
+
|
|
350
|
+
def set_colour(self, colour):
|
|
351
|
+
colour = QColor(colour)
|
|
352
|
+
if not colour.isValid():
|
|
353
|
+
return
|
|
354
|
+
hue = colour.hsvHueF()
|
|
355
|
+
# A grey has no hue: keep the one there was, so the wheel does not
|
|
356
|
+
# jump to red on the way through white.
|
|
357
|
+
if hue >= 0:
|
|
358
|
+
self.h = hue
|
|
359
|
+
self.s, self.v = colour.hsvSaturationF(), colour.valueF()
|
|
360
|
+
self._show()
|
|
361
|
+
|
|
362
|
+
def _show(self, tell=True):
|
|
363
|
+
"""Every widget from (h, s, v), and the colour to whoever listens."""
|
|
364
|
+
self._busy = True
|
|
365
|
+
try:
|
|
366
|
+
colour = self.colour()
|
|
367
|
+
self.wheel.set_hsv(self.h, self.s, self.v)
|
|
368
|
+
self.bar.set_hsv(self.h, self.s, self.v)
|
|
369
|
+
for name, value in (("R", colour.red()), ("G", colour.green()),
|
|
370
|
+
("B", colour.blue()),
|
|
371
|
+
("H", int(round(self.h * 360)) % 360),
|
|
372
|
+
("S", int(round(self.s * 100))),
|
|
373
|
+
("V", int(round(self.v * 100)))):
|
|
374
|
+
self.boxes[name].setValue(value)
|
|
375
|
+
self.hex.setText(colour.name())
|
|
376
|
+
self.hex.setStyleSheet("")
|
|
377
|
+
self.after.set_colour(colour)
|
|
378
|
+
finally:
|
|
379
|
+
self._busy = False
|
|
380
|
+
if tell:
|
|
381
|
+
name = self.colour().name()
|
|
382
|
+
self.changed.emit(name)
|
|
383
|
+
if self._live is not None:
|
|
384
|
+
self._live(name)
|
|
385
|
+
|
|
386
|
+
def _wheel_picked(self, h, s):
|
|
387
|
+
self.h, self.s = h, s
|
|
388
|
+
self._show()
|
|
389
|
+
|
|
390
|
+
def _bar_picked(self, v):
|
|
391
|
+
self.v = v
|
|
392
|
+
self._show()
|
|
393
|
+
|
|
394
|
+
def _typed(self, name):
|
|
395
|
+
if self._busy:
|
|
396
|
+
return
|
|
397
|
+
b = self.boxes
|
|
398
|
+
if name in "RGB":
|
|
399
|
+
self.set_colour(QColor(b["R"].value(), b["G"].value(),
|
|
400
|
+
b["B"].value()))
|
|
401
|
+
return
|
|
402
|
+
self.h = (b["H"].value() % 360) / 360.0
|
|
403
|
+
self.s = b["S"].value() / 100.0
|
|
404
|
+
self.v = b["V"].value() / 100.0
|
|
405
|
+
self._show()
|
|
406
|
+
|
|
407
|
+
def _typed_hex(self):
|
|
408
|
+
text = self.hex.text().strip()
|
|
409
|
+
if text and not text.startswith("#"):
|
|
410
|
+
text = "#" + text
|
|
411
|
+
colour = QColor(text)
|
|
412
|
+
if len(text) not in (4, 7) or not colour.isValid():
|
|
413
|
+
self.hex.setStyleSheet("border: 1px solid #d04040;")
|
|
414
|
+
return
|
|
415
|
+
if colour.name() != self.colour().name():
|
|
416
|
+
self.set_colour(colour)
|
|
417
|
+
|
|
418
|
+
# ------------------------------------------------------------ dropper
|
|
419
|
+
def start_picking(self):
|
|
420
|
+
"""The dropper: the colour under the pointer follows it until a
|
|
421
|
+
click takes it; Esc or the right button puts back what there was."""
|
|
422
|
+
self._picking = self.colour()
|
|
423
|
+
self.setMouseTracking(True)
|
|
424
|
+
self.grabMouse(Qt.CrossCursor)
|
|
425
|
+
self.grabKeyboard()
|
|
426
|
+
self.dropper.setText("Click to take the colour")
|
|
427
|
+
|
|
428
|
+
def stop_picking(self, keep):
|
|
429
|
+
before, self._picking = self._picking, None
|
|
430
|
+
self.releaseMouse()
|
|
431
|
+
self.releaseKeyboard()
|
|
432
|
+
self.setMouseTracking(False)
|
|
433
|
+
self.dropper.setText("Pick from screen")
|
|
434
|
+
if not keep and before is not None:
|
|
435
|
+
self.set_colour(before)
|
|
436
|
+
|
|
437
|
+
def picking(self):
|
|
438
|
+
return self._picking is not None
|
|
439
|
+
|
|
440
|
+
def mouseMoveEvent(self, ev):
|
|
441
|
+
if self._picking is not None:
|
|
442
|
+
found = screen_colour(ev.globalPosition().toPoint())
|
|
443
|
+
if found is not None:
|
|
444
|
+
self.set_colour(found)
|
|
445
|
+
return
|
|
446
|
+
QDialog.mouseMoveEvent(self, ev)
|
|
447
|
+
|
|
448
|
+
def mousePressEvent(self, ev):
|
|
449
|
+
if self._picking is not None:
|
|
450
|
+
if ev.button() == Qt.LeftButton:
|
|
451
|
+
found = screen_colour(ev.globalPosition().toPoint())
|
|
452
|
+
if found is not None:
|
|
453
|
+
self.set_colour(found)
|
|
454
|
+
self.stop_picking(True)
|
|
455
|
+
else:
|
|
456
|
+
self.stop_picking(False)
|
|
457
|
+
return
|
|
458
|
+
QDialog.mousePressEvent(self, ev)
|
|
459
|
+
|
|
460
|
+
def keyPressEvent(self, ev):
|
|
461
|
+
if self._picking is not None:
|
|
462
|
+
if ev.key() == Qt.Key_Escape:
|
|
463
|
+
self.stop_picking(False)
|
|
464
|
+
return
|
|
465
|
+
if ev.key() in (Qt.Key_Return, Qt.Key_Enter):
|
|
466
|
+
# Enter takes a typed number (the box has it already); it does
|
|
467
|
+
# not close the picker (`dialogs.enter_stays`, the same rule).
|
|
468
|
+
return
|
|
469
|
+
QDialog.keyPressEvent(self, ev)
|
|
470
|
+
|
|
471
|
+
# ------------------------------------------------------------ closing
|
|
472
|
+
def reject(self):
|
|
473
|
+
"""X or Esc keep the colour, as every pop-up does."""
|
|
474
|
+
if self._picking is not None:
|
|
475
|
+
self.stop_picking(False)
|
|
476
|
+
return
|
|
477
|
+
self.accept()
|
|
478
|
+
|
|
479
|
+
def revert(self):
|
|
480
|
+
self._reverted = True
|
|
481
|
+
self.set_colour(self._initial)
|
|
482
|
+
QDialog.reject(self)
|
|
483
|
+
|
|
484
|
+
def reverted(self):
|
|
485
|
+
return self._reverted
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def get_colour(initial, parent=None, title="Pick a colour", live=None):
|
|
489
|
+
"""Modal, like `QColorDialog.getColor`: the chosen colour, or an
|
|
490
|
+
invalid QColor if the picker was reverted. `live(name)` hears every
|
|
491
|
+
change while it is open (and the old colour again on Revert)."""
|
|
492
|
+
dialog = ColourDialog(initial, parent, title, live=live)
|
|
493
|
+
dialog.exec()
|
|
494
|
+
if dialog.reverted():
|
|
495
|
+
return QColor()
|
|
496
|
+
return dialog.colour()
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class GradientDialog(QDialog):
|
|
500
|
+
"""Shades of one colour on the selected curves, live (F3): a base
|
|
501
|
+
colour, how dark the darkest and how light the lightest, darkest at
|
|
502
|
+
the top of the stack unless reversed.
|
|
503
|
+
|
|
504
|
+
Built here and shown by the window (`MainWindow.colour_gradient`),
|
|
505
|
+
which makes the one undo step when it closes. Closing keeps the colours
|
|
506
|
+
however it is done; Revert puts the old ones back.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
def __init__(self, scans, parent=None, on_change=None):
|
|
510
|
+
QDialog.__init__(self, parent)
|
|
511
|
+
self.setWindowTitle("Colour gradient")
|
|
512
|
+
self.scans = shades.stack_order(scans)
|
|
513
|
+
self.before = [(scan, scan.colour) for scan in self.scans]
|
|
514
|
+
self._on_change = on_change
|
|
515
|
+
self._reverted = False
|
|
516
|
+
self.base = QColor(self.scans[0].colour if self.scans else "#1f77b4")
|
|
517
|
+
|
|
518
|
+
layout = QVBoxLayout(self)
|
|
519
|
+
form = QFormLayout()
|
|
520
|
+
layout.addLayout(form)
|
|
521
|
+
self.base_button = Swatch(self.base, self, 28)
|
|
522
|
+
self.base_button.setToolTip("The colour the shades are made of.")
|
|
523
|
+
self.base_button.clicked.connect(lambda _c=False: self.pick_base())
|
|
524
|
+
form.addRow("Colour", self.base_button)
|
|
525
|
+
self.darkest = NumberBox(self)
|
|
526
|
+
self.lightest = NumberBox(self)
|
|
527
|
+
for box, value, word in ((self.darkest, -40.0, "darkest"),
|
|
528
|
+
(self.lightest, 50.0, "lightest")):
|
|
529
|
+
box.setRange(-100.0, 100.0)
|
|
530
|
+
box.setDecimals(0)
|
|
531
|
+
box.setSingleStep(5.0)
|
|
532
|
+
box.setSuffix(" %")
|
|
533
|
+
box.setValue(value)
|
|
534
|
+
box.setToolTip("The {} curve: the colour mixed towards black "
|
|
535
|
+
"(below 0) or white (above 0) by this much."
|
|
536
|
+
.format(word))
|
|
537
|
+
box.valueChanged.connect(lambda _v: self.apply())
|
|
538
|
+
form.addRow("Darkest", self.darkest)
|
|
539
|
+
form.addRow("Lightest", self.lightest)
|
|
540
|
+
self.reverse = QCheckBox("Lightest at the top")
|
|
541
|
+
self.reverse.setToolTip("Darkest at the top of the stack unless "
|
|
542
|
+
"ticked.")
|
|
543
|
+
self.reverse.toggled.connect(lambda _on: self.apply())
|
|
544
|
+
form.addRow("", self.reverse)
|
|
545
|
+
self.preview = QHBoxLayout()
|
|
546
|
+
self.preview.setSpacing(2)
|
|
547
|
+
self._swatches = []
|
|
548
|
+
for _scan in self.scans:
|
|
549
|
+
swatch = Swatch(self.base, self, 18)
|
|
550
|
+
swatch.setEnabled(False)
|
|
551
|
+
self._swatches.append(swatch)
|
|
552
|
+
self.preview.addWidget(swatch)
|
|
553
|
+
self.preview.addStretch(1)
|
|
554
|
+
form.addRow("Shades", self.preview)
|
|
555
|
+
|
|
556
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok
|
|
557
|
+
| QDialogButtonBox.Cancel)
|
|
558
|
+
revert = buttons.button(QDialogButtonBox.Cancel)
|
|
559
|
+
revert.setText("Revert")
|
|
560
|
+
revert.setToolTip("Put back the colours the curves had and close. "
|
|
561
|
+
"Closing otherwise keeps the new ones.")
|
|
562
|
+
for button in buttons.buttons():
|
|
563
|
+
button.setAutoDefault(False)
|
|
564
|
+
buttons.accepted.connect(self.accept)
|
|
565
|
+
buttons.rejected.connect(self.revert)
|
|
566
|
+
layout.addWidget(buttons)
|
|
567
|
+
self.apply()
|
|
568
|
+
|
|
569
|
+
def colours(self):
|
|
570
|
+
"""The shades, top of the stack first."""
|
|
571
|
+
found = shades.shades(self.base.name(), len(self.scans),
|
|
572
|
+
self.darkest.value() / 100.0,
|
|
573
|
+
self.lightest.value() / 100.0)
|
|
574
|
+
return found[::-1] if self.reverse.isChecked() else found
|
|
575
|
+
|
|
576
|
+
def apply(self):
|
|
577
|
+
for scan, swatch, name in zip(self.scans, self._swatches,
|
|
578
|
+
self.colours()):
|
|
579
|
+
scan.colour = name
|
|
580
|
+
swatch.set_colour(name)
|
|
581
|
+
if self._on_change is not None:
|
|
582
|
+
self._on_change()
|
|
583
|
+
|
|
584
|
+
def pick_base(self):
|
|
585
|
+
was = QColor(self.base)
|
|
586
|
+
|
|
587
|
+
def live(name):
|
|
588
|
+
self.base = QColor(name)
|
|
589
|
+
self.base_button.set_colour(self.base)
|
|
590
|
+
self.apply()
|
|
591
|
+
|
|
592
|
+
chosen = get_colour(was, self, "Colour of the gradient", live=live)
|
|
593
|
+
live((chosen if chosen.isValid() else was).name())
|
|
594
|
+
|
|
595
|
+
def changes(self):
|
|
596
|
+
"""`[(scan, "colour", new)]`, the old colours put back first so the
|
|
597
|
+
window's undo step records them."""
|
|
598
|
+
found = []
|
|
599
|
+
for scan, old in self.before:
|
|
600
|
+
new = scan.colour
|
|
601
|
+
if new != old:
|
|
602
|
+
scan.colour = old
|
|
603
|
+
found.append((scan, "colour", new))
|
|
604
|
+
return found
|
|
605
|
+
|
|
606
|
+
def keyPressEvent(self, ev):
|
|
607
|
+
if ev.key() in (Qt.Key_Return, Qt.Key_Enter):
|
|
608
|
+
return # takes a typed number, stays open
|
|
609
|
+
QDialog.keyPressEvent(self, ev)
|
|
610
|
+
|
|
611
|
+
def reject(self):
|
|
612
|
+
"""X or Esc keep the colours, as every pop-up does."""
|
|
613
|
+
self.accept()
|
|
614
|
+
|
|
615
|
+
def revert(self):
|
|
616
|
+
self._reverted = True
|
|
617
|
+
for scan, old in self.before:
|
|
618
|
+
scan.colour = old
|
|
619
|
+
if self._on_change is not None:
|
|
620
|
+
self._on_change()
|
|
621
|
+
QDialog.reject(self)
|
|
622
|
+
|
|
623
|
+
def reverted(self):
|
|
624
|
+
return self._reverted
|
|
625
|
+
|
|
626
|
+
# Qt calls the handlers here by itself; an error in one is logged and
|
|
627
|
+
# survived rather than the end of the program (`core/log.py`).
|
|
628
|
+
from ..core import log as _log
|
|
629
|
+
_log.guard_classes(globals(), __name__)
|