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/loading.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Reading files without freezing the window.
|
|
2
|
+
|
|
3
|
+
Measured on real files: 1.09 s for a 16 MB `.tri` (seven segments,
|
|
4
|
+
13 200 points each, eighteen stored analyses) and 0.51 s for an 11 MB one. A
|
|
5
|
+
drop of five files is therefore several seconds, and several seconds of a
|
|
6
|
+
frozen window is a program that looks broken.
|
|
7
|
+
|
|
8
|
+
So every read runs on a thread pool and the window is told as each one
|
|
9
|
+
arrives. Two things this deliberately does NOT do: it does not parse on the
|
|
10
|
+
GUI thread "just for one small file" (the small file is the one that turns
|
|
11
|
+
out to be 40 MB), and it does not hand a half-built sample to the window
|
|
12
|
+
(the reader either returns a `Sample` or raises, and the failure carries the
|
|
13
|
+
file's name so a bad file in a drop of five costs one line and not the drop).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
|
|
17
|
+
|
|
18
|
+
from ..core import loader
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _JobSignals(QObject):
|
|
22
|
+
done = Signal(object, str, str) # sample, path, error
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Job(QRunnable):
|
|
26
|
+
def __init__(self, path, signals):
|
|
27
|
+
QRunnable.__init__(self)
|
|
28
|
+
self.path = str(path)
|
|
29
|
+
self.signals = signals
|
|
30
|
+
|
|
31
|
+
def run(self):
|
|
32
|
+
try:
|
|
33
|
+
sample = loader.read_sample(self.path)
|
|
34
|
+
except Exception as exc: # the reader raises many kinds
|
|
35
|
+
self.signals.done.emit(None, self.path, str(exc))
|
|
36
|
+
return
|
|
37
|
+
self.signals.done.emit(sample, self.path, "")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Loader(QObject):
|
|
41
|
+
"""Reads files in the background and reports each one as it lands."""
|
|
42
|
+
|
|
43
|
+
loaded = Signal(object) # Sample
|
|
44
|
+
failed = Signal(str, str) # path, message
|
|
45
|
+
progress = Signal(int, int) # done, total
|
|
46
|
+
finished = Signal()
|
|
47
|
+
|
|
48
|
+
def __init__(self, parent=None):
|
|
49
|
+
QObject.__init__(self, parent)
|
|
50
|
+
self._pool = QThreadPool(self)
|
|
51
|
+
# Two at a time. The reader is numpy-heavy and mostly holds the GIL,
|
|
52
|
+
# so more threads would not read faster, and a bounded pool keeps a
|
|
53
|
+
# drop of twenty files from starting twenty reads at once.
|
|
54
|
+
self._pool.setMaxThreadCount(2)
|
|
55
|
+
self._signals = _JobSignals()
|
|
56
|
+
self._signals.done.connect(self._one_done)
|
|
57
|
+
self._pending = 0
|
|
58
|
+
self._total = 0
|
|
59
|
+
|
|
60
|
+
def busy(self):
|
|
61
|
+
return self._pending > 0
|
|
62
|
+
|
|
63
|
+
def load(self, paths):
|
|
64
|
+
"""Queue every readable path. Returns how many were queued."""
|
|
65
|
+
queued = 0
|
|
66
|
+
for path in paths or ():
|
|
67
|
+
if not loader.looks_readable(path):
|
|
68
|
+
continue
|
|
69
|
+
self._pending += 1
|
|
70
|
+
self._total += 1
|
|
71
|
+
self._pool.start(_Job(path, self._signals))
|
|
72
|
+
queued += 1
|
|
73
|
+
if queued:
|
|
74
|
+
self.progress.emit(self._total - self._pending, self._total)
|
|
75
|
+
return queued
|
|
76
|
+
|
|
77
|
+
def wait(self, timeout_ms=30000):
|
|
78
|
+
"""For tests: block until the queue is empty."""
|
|
79
|
+
return self._pool.waitForDone(timeout_ms)
|
|
80
|
+
|
|
81
|
+
def _one_done(self, sample, path, error):
|
|
82
|
+
self._pending = max(0, self._pending - 1)
|
|
83
|
+
if sample is None:
|
|
84
|
+
self.failed.emit(path, error)
|
|
85
|
+
else:
|
|
86
|
+
self.loaded.emit(sample)
|
|
87
|
+
self.progress.emit(self._total - self._pending, self._total)
|
|
88
|
+
if not self._pending:
|
|
89
|
+
self._total = 0
|
|
90
|
+
self.finished.emit()
|
|
91
|
+
|
|
92
|
+
# Qt calls the handlers here by itself; an error in one is logged and
|
|
93
|
+
# survived rather than the end of the program (`core/log.py`).
|
|
94
|
+
from ..core import log as _log
|
|
95
|
+
_log.guard_classes(globals(), __name__)
|
dscpanel/ui/numbox.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Number boxes that take a sum as well as a number.
|
|
2
|
+
|
|
3
|
+
"255-20" in a box is 235. Every number box of the program is one of these
|
|
4
|
+
(`NumberBox`, `WholeBox`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QLocale
|
|
8
|
+
from PySide6.QtGui import QValidator
|
|
9
|
+
from PySide6.QtWidgets import QDoubleSpinBox, QSpinBox
|
|
10
|
+
|
|
11
|
+
from ..core import numbers
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Sums(object):
|
|
15
|
+
"""A spin box that takes a SUM as well as a number: "255-20" is 235
|
|
16
|
+
worked out on Enter or when the box is left.
|
|
17
|
+
`core.numbers.evaluate`: + - * / and brackets, a comma is a decimal
|
|
18
|
+
point. A sum past the range is clamped to it."""
|
|
19
|
+
|
|
20
|
+
def _setup(self):
|
|
21
|
+
locale = QLocale(QLocale.C)
|
|
22
|
+
locale.setNumberOptions(QLocale.OmitGroupSeparator)
|
|
23
|
+
self.setLocale(locale)
|
|
24
|
+
self.setKeyboardTracking(False)
|
|
25
|
+
|
|
26
|
+
def _core(self, text):
|
|
27
|
+
"""The typed text without the box's prefix and suffix."""
|
|
28
|
+
text = str(text).strip()
|
|
29
|
+
suffix, prefix = self.suffix().strip(), self.prefix().strip()
|
|
30
|
+
if suffix and text.endswith(suffix):
|
|
31
|
+
text = text[:-len(suffix)]
|
|
32
|
+
if prefix and text.startswith(prefix):
|
|
33
|
+
text = text[len(prefix):]
|
|
34
|
+
return text.strip()
|
|
35
|
+
|
|
36
|
+
def _sum(self, text):
|
|
37
|
+
"""The value of a typed sum, clamped; None if it is not one."""
|
|
38
|
+
core = self._core(text)
|
|
39
|
+
if not numbers.is_sum(core):
|
|
40
|
+
return None
|
|
41
|
+
value = numbers.evaluate(core)
|
|
42
|
+
if value is None:
|
|
43
|
+
return None
|
|
44
|
+
return min(max(value, self.minimum()), self.maximum())
|
|
45
|
+
|
|
46
|
+
def validate(self, text, pos):
|
|
47
|
+
core = self._core(text)
|
|
48
|
+
if numbers.is_sum(core):
|
|
49
|
+
# Still being typed ("255-") is fine; Enter works it out.
|
|
50
|
+
state = (QValidator.Acceptable
|
|
51
|
+
if numbers.evaluate(core) is not None
|
|
52
|
+
else QValidator.Intermediate)
|
|
53
|
+
return state, text, pos
|
|
54
|
+
state, _fixed, position = self._base.validate(
|
|
55
|
+
self, self._normalise(text), pos)
|
|
56
|
+
return state, text, position
|
|
57
|
+
|
|
58
|
+
def valueFromText(self, text):
|
|
59
|
+
value = self._sum(text)
|
|
60
|
+
if value is not None:
|
|
61
|
+
return self._cast(value)
|
|
62
|
+
return self._base.valueFromText(self, self._normalise(text))
|
|
63
|
+
|
|
64
|
+
def fixup(self, text):
|
|
65
|
+
value = self._sum(text)
|
|
66
|
+
if value is not None:
|
|
67
|
+
return self.textFromValue(self._cast(value))
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _normalise(text):
|
|
72
|
+
return str(text).strip().replace(",", ".")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NumberBox(_Sums, QDoubleSpinBox):
|
|
76
|
+
"""A spin box that reads "1.5" and "1,5" alike, takes a sum, and
|
|
77
|
+
commits on Enter."""
|
|
78
|
+
|
|
79
|
+
_base = QDoubleSpinBox
|
|
80
|
+
_cast = float
|
|
81
|
+
|
|
82
|
+
def __init__(self, parent=None):
|
|
83
|
+
QDoubleSpinBox.__init__(self, parent)
|
|
84
|
+
self._setup()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class WholeBox(_Sums, QSpinBox):
|
|
88
|
+
"""`NumberBox` for whole numbers: a sum is rounded."""
|
|
89
|
+
|
|
90
|
+
_base = QSpinBox
|
|
91
|
+
|
|
92
|
+
def __init__(self, parent=None):
|
|
93
|
+
QSpinBox.__init__(self, parent)
|
|
94
|
+
self._setup()
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _cast(value):
|
|
98
|
+
return int(round(value))
|
|
99
|
+
|
|
100
|
+
# Qt calls the handlers here by itself; an error in one is logged and
|
|
101
|
+
# survived rather than the end of the program (`core/log.py`).
|
|
102
|
+
from ..core import log as _log
|
|
103
|
+
_log.guard_classes(globals(), __name__)
|