openprocess 0.7.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.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- openprocess-0.7.0.dist-info/top_level.txt +2 -0
openprocess/gui/theme.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Visual theme: one place that decides what the application looks like.
|
|
2
|
+
|
|
3
|
+
Why a theme module
|
|
4
|
+
------------------
|
|
5
|
+
Colours, fonts and corner radii were previously scattered through the canvas
|
|
6
|
+
items and the panels. Collecting them here means the look can be adjusted in
|
|
7
|
+
one file, and -- more importantly -- it makes **dark mode** possible: every
|
|
8
|
+
colour is resolved through :func:`palette`, which returns a different mapping
|
|
9
|
+
depending on the system appearance.
|
|
10
|
+
|
|
11
|
+
The palette follows macOS system conventions (the same greys and the same
|
|
12
|
+
accent blue AppKit uses), so the window sits comfortably next to native apps
|
|
13
|
+
rather than looking like a ported X11 program.
|
|
14
|
+
|
|
15
|
+
Nothing here imports from the rest of the GUI, so it can be used from any
|
|
16
|
+
module without circular imports.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
from PySide6.QtCore import Qt
|
|
24
|
+
from PySide6.QtGui import QColor, QFont, QFontDatabase
|
|
25
|
+
from PySide6.QtWidgets import QApplication
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Palette
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Palette:
|
|
33
|
+
"""Every colour the application uses, named by role rather than by hue.
|
|
34
|
+
|
|
35
|
+
Naming by role (``surface``, ``muted``, ``accent``) rather than by colour
|
|
36
|
+
(``light_grey``, ``blue``) is what lets the same code render correctly in
|
|
37
|
+
both appearances: the dark palette swaps the values, not the names.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Chrome
|
|
41
|
+
window: QColor # the window background behind panels
|
|
42
|
+
surface: QColor # cards, list backgrounds, the canvas paper
|
|
43
|
+
surface_raised: QColor # hovered rows, input fields
|
|
44
|
+
border: QColor # hairlines and separators
|
|
45
|
+
border_strong: QColor # node outlines
|
|
46
|
+
|
|
47
|
+
# Type
|
|
48
|
+
text: QColor # primary text
|
|
49
|
+
text_muted: QColor # secondary text, colour set annotations
|
|
50
|
+
|
|
51
|
+
# Semantics
|
|
52
|
+
accent: QColor # selection, focus, primary buttons
|
|
53
|
+
accent_soft: QColor # selection fills
|
|
54
|
+
success: QColor # enabled transitions
|
|
55
|
+
success_soft: QColor
|
|
56
|
+
danger: QColor # compile errors
|
|
57
|
+
inscription: QColor # arc expressions, initial markings
|
|
58
|
+
marking: QColor # live token contents
|
|
59
|
+
|
|
60
|
+
# Canvas
|
|
61
|
+
canvas: QColor
|
|
62
|
+
grid: QColor
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# The values match the Studio design tokens (``openprocess.gui.studio.style``), so
|
|
66
|
+
# the CPN canvas and the process-mining canvases look like one application.
|
|
67
|
+
_LIGHT = Palette(
|
|
68
|
+
window=QColor("#F5F5F7"),
|
|
69
|
+
surface=QColor("#FFFFFF"),
|
|
70
|
+
surface_raised=QColor("#F6F6F8"),
|
|
71
|
+
border=QColor("#E1E1E6"),
|
|
72
|
+
border_strong=QColor("#3A3A3C"),
|
|
73
|
+
text=QColor("#1D1D1F"),
|
|
74
|
+
text_muted=QColor("#86868B"),
|
|
75
|
+
accent=QColor("#0A84FF"),
|
|
76
|
+
accent_soft=QColor("#DCEBFF"),
|
|
77
|
+
success=QColor("#2E9E4F"),
|
|
78
|
+
success_soft=QColor("#DFF5E4"),
|
|
79
|
+
danger=QColor("#D03B3B"),
|
|
80
|
+
inscription=QColor("#2468C0"),
|
|
81
|
+
marking=QColor("#B0430F"),
|
|
82
|
+
canvas=QColor("#FCFCFB"),
|
|
83
|
+
grid=QColor("#E1E0D9"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_DARK = Palette(
|
|
87
|
+
window=QColor("#1C1C1E"),
|
|
88
|
+
surface=QColor("#2A2A2D"),
|
|
89
|
+
surface_raised=QColor("#323236"),
|
|
90
|
+
border=QColor("#3A3A3E"),
|
|
91
|
+
border_strong=QColor("#D1D1D6"),
|
|
92
|
+
text=QColor("#F5F5F7"),
|
|
93
|
+
text_muted=QColor("#98989D"),
|
|
94
|
+
accent=QColor("#0A84FF"),
|
|
95
|
+
accent_soft=QColor("#153A63"),
|
|
96
|
+
success=QColor("#30D158"),
|
|
97
|
+
success_soft=QColor("#1E4028"),
|
|
98
|
+
danger=QColor("#FF6B61"),
|
|
99
|
+
inscription=QColor("#6DAAF0"),
|
|
100
|
+
marking=QColor("#FF9F6B"),
|
|
101
|
+
canvas=QColor("#1A1A19"),
|
|
102
|
+
grid=QColor("#2C2C2A"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_dark() -> bool:
|
|
107
|
+
"""Is the system (or the application palette) currently dark?
|
|
108
|
+
|
|
109
|
+
Qt 6.5 exposes the system colour scheme directly. On older builds we fall
|
|
110
|
+
back to measuring the lightness of the window colour, which is reliable
|
|
111
|
+
enough for the binary decision we need.
|
|
112
|
+
"""
|
|
113
|
+
application = QApplication.instance()
|
|
114
|
+
if application is None:
|
|
115
|
+
return False
|
|
116
|
+
hints = application.styleHints()
|
|
117
|
+
scheme = getattr(hints, "colorScheme", None)
|
|
118
|
+
if scheme is not None:
|
|
119
|
+
try:
|
|
120
|
+
return scheme() == Qt.ColorScheme.Dark
|
|
121
|
+
except Exception: # pragma: no cover - older Qt without the enum
|
|
122
|
+
pass
|
|
123
|
+
return application.palette().window().color().lightness() < 128
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def palette() -> Palette:
|
|
127
|
+
"""The palette matching the current appearance."""
|
|
128
|
+
return _DARK if is_dark() else _LIGHT
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Typography
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
def _logical_dpi() -> float:
|
|
135
|
+
"""Logical dots per inch: 72 on macOS, usually 96 on Windows and Linux."""
|
|
136
|
+
from PySide6.QtGui import QGuiApplication
|
|
137
|
+
screen = QGuiApplication.primaryScreen() if QGuiApplication.instance() else None
|
|
138
|
+
if screen is not None and screen.logicalDotsPerInchY() > 0:
|
|
139
|
+
return screen.logicalDotsPerInchY()
|
|
140
|
+
import sys
|
|
141
|
+
return 72.0 if sys.platform == "darwin" else 96.0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def set_px(font: QFont, pixels: float) -> QFont:
|
|
145
|
+
"""Size ``font`` in logical pixels -- the unit of the whole layout.
|
|
146
|
+
|
|
147
|
+
Every size in the application (fonts, paddings, node sizes) is meant in
|
|
148
|
+
logical pixels. Qt font sizes are in points, and a point is one pixel on
|
|
149
|
+
macOS (72 dpi) but 1⅓ pixels on Windows and most Linux desktops (96 dpi),
|
|
150
|
+
where a "13 pt" font would come out a third too big and overflow boxes
|
|
151
|
+
sized for it. Converting through the logical DPI makes the text the same
|
|
152
|
+
size relative to everything else on every platform. (Fractional sizes
|
|
153
|
+
are kept, which ``QFont.setPixelSize`` would round away.)
|
|
154
|
+
"""
|
|
155
|
+
font.setPointSizeF(max(1.0, pixels) * 72.0 / _logical_dpi())
|
|
156
|
+
return font
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def px(font: QFont) -> float:
|
|
160
|
+
"""The size of ``font`` in logical pixels (see :func:`set_px`)."""
|
|
161
|
+
if font.pixelSize() > 0:
|
|
162
|
+
return float(font.pixelSize())
|
|
163
|
+
return font.pointSizeF() * _logical_dpi() / 72.0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def ui_font(size: float = 13, weight: QFont.Weight = QFont.Normal) -> QFont:
|
|
167
|
+
"""The system UI font, ``size`` logical pixels high.
|
|
168
|
+
|
|
169
|
+
``QFontDatabase.systemFont`` returns SF Pro on macOS, Segoe UI on
|
|
170
|
+
Windows and the desktop's font on Linux, so the window looks native
|
|
171
|
+
rather than like a generic Qt application.
|
|
172
|
+
"""
|
|
173
|
+
font = QFontDatabase.systemFont(QFontDatabase.GeneralFont)
|
|
174
|
+
set_px(font, size)
|
|
175
|
+
font.setWeight(weight)
|
|
176
|
+
return font
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def mono_font(size: int = 12) -> QFont:
|
|
180
|
+
"""The system monospace font, for inscriptions and markings.
|
|
181
|
+
|
|
182
|
+
Inscriptions are code, and code in a proportional font is harder to scan --
|
|
183
|
+
particularly the backquote in ``2`x``, which is nearly invisible otherwise.
|
|
184
|
+
"""
|
|
185
|
+
font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
|
186
|
+
set_px(font, size)
|
|
187
|
+
return font
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Metrics
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
#: Corner radius for cards, buttons and transition nodes.
|
|
194
|
+
RADIUS = 8
|
|
195
|
+
#: Standard gap between widgets.
|
|
196
|
+
GAP = 10
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Stylesheet
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
def stylesheet() -> str:
|
|
203
|
+
"""The application-wide Qt stylesheet.
|
|
204
|
+
|
|
205
|
+
Qt stylesheets are CSS-like but not CSS: only a documented subset of
|
|
206
|
+
properties works, and a rule that mentions an unsupported property is
|
|
207
|
+
dropped whole. Everything below is restricted to that subset, and the
|
|
208
|
+
layout-critical parts (padding, radii) are chosen to give the roomy,
|
|
209
|
+
low-contrast feel of a current macOS application: hairline separators
|
|
210
|
+
instead of bevels, generous padding, and colour used only where it means
|
|
211
|
+
something.
|
|
212
|
+
"""
|
|
213
|
+
p = palette()
|
|
214
|
+
|
|
215
|
+
def rgb(colour: QColor) -> str:
|
|
216
|
+
return f"rgb({colour.red()},{colour.green()},{colour.blue()})"
|
|
217
|
+
|
|
218
|
+
return f"""
|
|
219
|
+
/* ---- base ------------------------------------------------------- */
|
|
220
|
+
QMainWindow, QWidget {{
|
|
221
|
+
background: {rgb(p.window)};
|
|
222
|
+
color: {rgb(p.text)};
|
|
223
|
+
}}
|
|
224
|
+
|
|
225
|
+
/* ---- docks: flat cards, hairline separators, quiet titles -------- */
|
|
226
|
+
QDockWidget {{
|
|
227
|
+
titlebar-close-icon: none;
|
|
228
|
+
titlebar-normal-icon: none;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
color: {rgb(p.text_muted)};
|
|
231
|
+
}}
|
|
232
|
+
QDockWidget::title {{
|
|
233
|
+
background: {rgb(p.window)};
|
|
234
|
+
padding: 9px 12px;
|
|
235
|
+
border-bottom: 1px solid {rgb(p.border)};
|
|
236
|
+
text-align: left;
|
|
237
|
+
}}
|
|
238
|
+
QDockWidget > QWidget {{
|
|
239
|
+
background: {rgb(p.window)};
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
/* ---- toolbar: flat, segmented ------------------------------------ */
|
|
243
|
+
QToolBar {{
|
|
244
|
+
background: {rgb(p.window)};
|
|
245
|
+
border: none;
|
|
246
|
+
border-bottom: 1px solid {rgb(p.border)};
|
|
247
|
+
padding: 6px 10px;
|
|
248
|
+
spacing: 4px;
|
|
249
|
+
}}
|
|
250
|
+
QToolBar QToolButton {{
|
|
251
|
+
background: transparent;
|
|
252
|
+
border: 1px solid transparent;
|
|
253
|
+
border-radius: {RADIUS - 2}px;
|
|
254
|
+
padding: 5px 12px;
|
|
255
|
+
color: {rgb(p.text)};
|
|
256
|
+
}}
|
|
257
|
+
QToolBar QToolButton:hover {{
|
|
258
|
+
background: {rgb(p.surface_raised)};
|
|
259
|
+
}}
|
|
260
|
+
QToolBar QToolButton:checked {{
|
|
261
|
+
background: {rgb(p.accent)};
|
|
262
|
+
color: white;
|
|
263
|
+
}}
|
|
264
|
+
QToolBar::separator {{
|
|
265
|
+
background: {rgb(p.border)};
|
|
266
|
+
width: 1px;
|
|
267
|
+
margin: 4px 8px;
|
|
268
|
+
}}
|
|
269
|
+
|
|
270
|
+
/* ---- buttons ----------------------------------------------------- */
|
|
271
|
+
QPushButton {{
|
|
272
|
+
background: {rgb(p.surface)};
|
|
273
|
+
border: 1px solid {rgb(p.border)};
|
|
274
|
+
border-radius: {RADIUS - 2}px;
|
|
275
|
+
padding: 6px 14px;
|
|
276
|
+
color: {rgb(p.text)};
|
|
277
|
+
}}
|
|
278
|
+
QPushButton:hover {{ background: {rgb(p.surface_raised)}; }}
|
|
279
|
+
QPushButton:pressed {{ background: {rgb(p.accent_soft)}; }}
|
|
280
|
+
QPushButton:default {{
|
|
281
|
+
background: {rgb(p.accent)};
|
|
282
|
+
border-color: {rgb(p.accent)};
|
|
283
|
+
color: white;
|
|
284
|
+
}}
|
|
285
|
+
|
|
286
|
+
/* ---- inputs ------------------------------------------------------ */
|
|
287
|
+
QLineEdit, QPlainTextEdit, QComboBox, QSpinBox {{
|
|
288
|
+
background: {rgb(p.surface)};
|
|
289
|
+
border: 1px solid {rgb(p.border)};
|
|
290
|
+
border-radius: {RADIUS - 2}px;
|
|
291
|
+
padding: 5px 8px;
|
|
292
|
+
selection-background-color: {rgb(p.accent)};
|
|
293
|
+
selection-color: white;
|
|
294
|
+
}}
|
|
295
|
+
QLineEdit:focus, QPlainTextEdit:focus, QComboBox:focus, QSpinBox:focus {{
|
|
296
|
+
border-color: {rgb(p.accent)};
|
|
297
|
+
}}
|
|
298
|
+
QComboBox::drop-down {{ border: none; width: 18px; }}
|
|
299
|
+
/* Fusion draws spin box arrows as bare glyphs on top of our flat field,
|
|
300
|
+
which renders as a stray dash. Give them a proper hit area instead. */
|
|
301
|
+
QSpinBox::up-button, QSpinBox::down-button {{
|
|
302
|
+
subcontrol-origin: border;
|
|
303
|
+
width: 16px;
|
|
304
|
+
border: none;
|
|
305
|
+
border-left: 1px solid {rgb(p.border)};
|
|
306
|
+
background: {rgb(p.surface_raised)};
|
|
307
|
+
}}
|
|
308
|
+
QSpinBox::up-button {{ subcontrol-position: top right; border-top-right-radius: {RADIUS - 2}px; }}
|
|
309
|
+
QSpinBox::down-button {{ subcontrol-position: bottom right; border-bottom-right-radius: {RADIUS - 2}px; }}
|
|
310
|
+
QSpinBox::up-arrow, QSpinBox::down-arrow {{ width: 7px; height: 7px; }}
|
|
311
|
+
|
|
312
|
+
/* ---- lists ------------------------------------------------------- */
|
|
313
|
+
QListWidget {{
|
|
314
|
+
background: {rgb(p.surface)};
|
|
315
|
+
border: 1px solid {rgb(p.border)};
|
|
316
|
+
border-radius: {RADIUS}px;
|
|
317
|
+
padding: 4px;
|
|
318
|
+
outline: none;
|
|
319
|
+
}}
|
|
320
|
+
QListWidget::item {{
|
|
321
|
+
padding: 5px 8px;
|
|
322
|
+
border-radius: {RADIUS - 3}px;
|
|
323
|
+
}}
|
|
324
|
+
QListWidget::item:hover {{ background: {rgb(p.surface_raised)}; }}
|
|
325
|
+
QListWidget::item:selected {{
|
|
326
|
+
background: {rgb(p.accent)};
|
|
327
|
+
color: white;
|
|
328
|
+
}}
|
|
329
|
+
|
|
330
|
+
/* ---- tabs: pill segmented control -------------------------------- */
|
|
331
|
+
QTabWidget::pane {{ border: none; }}
|
|
332
|
+
QTabBar {{ qproperty-drawBase: 0; }}
|
|
333
|
+
QTabBar::tab {{
|
|
334
|
+
background: transparent;
|
|
335
|
+
color: {rgb(p.text_muted)};
|
|
336
|
+
padding: 6px 14px;
|
|
337
|
+
margin: 6px 2px;
|
|
338
|
+
border-radius: {RADIUS - 2}px;
|
|
339
|
+
}}
|
|
340
|
+
QTabBar::tab:hover {{ background: {rgb(p.surface_raised)}; }}
|
|
341
|
+
QTabBar::tab:selected {{
|
|
342
|
+
background: {rgb(p.surface)};
|
|
343
|
+
color: {rgb(p.text)};
|
|
344
|
+
border: 1px solid {rgb(p.border)};
|
|
345
|
+
}}
|
|
346
|
+
|
|
347
|
+
/* ---- group boxes: label above a hairline card -------------------- */
|
|
348
|
+
QGroupBox {{
|
|
349
|
+
background: {rgb(p.surface)};
|
|
350
|
+
border: 1px solid {rgb(p.border)};
|
|
351
|
+
border-radius: {RADIUS}px;
|
|
352
|
+
margin-top: 18px;
|
|
353
|
+
padding: 8px;
|
|
354
|
+
}}
|
|
355
|
+
QGroupBox::title {{
|
|
356
|
+
subcontrol-origin: margin;
|
|
357
|
+
subcontrol-position: top left;
|
|
358
|
+
left: 2px;
|
|
359
|
+
padding: 0 2px;
|
|
360
|
+
color: {rgb(p.text_muted)};
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
}}
|
|
363
|
+
|
|
364
|
+
/* ---- scrollbars: thin, no arrows --------------------------------- */
|
|
365
|
+
QScrollBar:vertical, QScrollBar:horizontal {{
|
|
366
|
+
background: transparent;
|
|
367
|
+
width: 11px; height: 11px;
|
|
368
|
+
margin: 2px;
|
|
369
|
+
}}
|
|
370
|
+
QScrollBar::handle {{
|
|
371
|
+
background: {rgb(p.border)};
|
|
372
|
+
border-radius: 5px;
|
|
373
|
+
min-height: 28px; min-width: 28px;
|
|
374
|
+
}}
|
|
375
|
+
QScrollBar::handle:hover {{ background: {rgb(p.text_muted)}; }}
|
|
376
|
+
QScrollBar::add-line, QScrollBar::sub-line,
|
|
377
|
+
QScrollBar::add-page, QScrollBar::sub-page {{
|
|
378
|
+
background: none; border: none; height: 0; width: 0;
|
|
379
|
+
}}
|
|
380
|
+
|
|
381
|
+
/* ---- misc -------------------------------------------------------- */
|
|
382
|
+
QGraphicsView {{
|
|
383
|
+
background: {rgb(p.canvas)};
|
|
384
|
+
border: none;
|
|
385
|
+
}}
|
|
386
|
+
QStatusBar {{
|
|
387
|
+
background: {rgb(p.window)};
|
|
388
|
+
border-top: 1px solid {rgb(p.border)};
|
|
389
|
+
color: {rgb(p.text_muted)};
|
|
390
|
+
}}
|
|
391
|
+
QStatusBar::item {{ border: none; }}
|
|
392
|
+
QMenuBar {{ background: {rgb(p.window)}; }}
|
|
393
|
+
QSplitter::handle {{ background: {rgb(p.border)}; }}
|
|
394
|
+
"""
|
openprocess/gui/tidy.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Making a page of a net neat: snap it to the grid, keeping its layout.
|
|
2
|
+
|
|
3
|
+
:func:`snap_page` puts every node's centre and every arc bend on the canvas's
|
|
4
|
+
dot grid, and drops bends that no longer turn a corner. It works on the model
|
|
5
|
+
(a :class:`~openprocess.model.net.Page`), in model coordinates -- the canvas's with
|
|
6
|
+
``y`` flipped (see :func:`.items.to_scene`) -- so the editor only has to take
|
|
7
|
+
an undo snapshot first and redraw after.
|
|
8
|
+
|
|
9
|
+
(An automatic left-to-right layout was tried too, and taken out again: on a
|
|
10
|
+
model drawn by hand it threw away the modeller's layout for a worse one.)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from ..model.net import Page
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _round(value: float, step: float) -> float:
|
|
19
|
+
return round(value / step) * step
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _straight(a: tuple[float, float], b: tuple[float, float], c: tuple[float, float]) -> bool:
|
|
23
|
+
"""Is ``b`` on the straight line from ``a`` to ``c`` (so not a real corner)?"""
|
|
24
|
+
cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
25
|
+
length = max(abs(c[0] - a[0]) + abs(c[1] - a[1]), 1e-9)
|
|
26
|
+
return abs(cross) / length < 1.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _clean_bends(start: tuple[float, float], bends: list[tuple[float, float]],
|
|
30
|
+
end: tuple[float, float]) -> list[tuple[float, float]]:
|
|
31
|
+
"""Without repeated points, points on a node's centre, and bends that do
|
|
32
|
+
not turn (all of which snapping can produce)."""
|
|
33
|
+
points = [start]
|
|
34
|
+
for bend in bends:
|
|
35
|
+
if bend != points[-1] and bend != end:
|
|
36
|
+
points.append(bend)
|
|
37
|
+
points.append(end)
|
|
38
|
+
changed = True
|
|
39
|
+
while changed and len(points) > 2:
|
|
40
|
+
changed = False
|
|
41
|
+
for index in range(1, len(points) - 1):
|
|
42
|
+
if _straight(points[index - 1], points[index], points[index + 1]):
|
|
43
|
+
del points[index]
|
|
44
|
+
changed = True
|
|
45
|
+
break
|
|
46
|
+
return points[1:-1]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _centres(page: Page) -> dict[str, tuple[float, float]]:
|
|
50
|
+
return {node.id: (node.graphics.x, node.graphics.y)
|
|
51
|
+
for node in [*page.places, *page.transitions]}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _midpoint(centres, arc) -> tuple[float, float] | None:
|
|
55
|
+
if arc.transition_id not in centres or arc.place_id not in centres:
|
|
56
|
+
return None
|
|
57
|
+
(ax, ay), (bx, by) = centres[arc.transition_id], centres[arc.place_id]
|
|
58
|
+
return (ax + bx) / 2, (ay + by) / 2
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def snap_page(page: Page, step: float) -> int:
|
|
62
|
+
"""Put every node and arc bend of ``page`` on the grid. Returns how many
|
|
63
|
+
nodes moved. An inscription the modeller placed keeps its place beside
|
|
64
|
+
its arc (it is stored as an absolute position, so it moves along)."""
|
|
65
|
+
before = _centres(page)
|
|
66
|
+
middles = {id(arc): _midpoint(before, arc) for arc in page.arcs}
|
|
67
|
+
moved = 0
|
|
68
|
+
for node in [*page.places, *page.transitions]:
|
|
69
|
+
x, y = _round(node.graphics.x, step), _round(node.graphics.y, step)
|
|
70
|
+
if (x, y) != (node.graphics.x, node.graphics.y):
|
|
71
|
+
moved += 1
|
|
72
|
+
node.graphics.x, node.graphics.y = x, y
|
|
73
|
+
centres = _centres(page)
|
|
74
|
+
for arc in page.arcs:
|
|
75
|
+
if arc.transition_id not in centres or arc.place_id not in centres:
|
|
76
|
+
continue
|
|
77
|
+
bends = [(_round(x, step), _round(y, step)) for x, y in arc.bendpoints]
|
|
78
|
+
# Bends run from the transition to the place (see openprocess.model.net.Arc).
|
|
79
|
+
arc.bendpoints = _clean_bends(centres[arc.transition_id], bends,
|
|
80
|
+
centres[arc.place_id])
|
|
81
|
+
old, new = middles.get(id(arc)), _midpoint(centres, arc)
|
|
82
|
+
label = arc.graphics.label_offsets.get("annot")
|
|
83
|
+
if label is not None and old is not None and new is not None:
|
|
84
|
+
arc.graphics.label_offsets["annot"] = (label[0] + new[0] - old[0],
|
|
85
|
+
label[1] + new[1] - old[1])
|
|
86
|
+
return moved
|
|
File without changes
|