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.
Files changed (173) hide show
  1. cpnpy/__init__.py +46 -0
  2. openprocess/__init__.py +57 -0
  3. openprocess/analysis/__init__.py +0 -0
  4. openprocess/analysis/state_space.py +521 -0
  5. openprocess/analysis/state_space_process.py +251 -0
  6. openprocess/cli.py +742 -0
  7. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
  8. openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
  9. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
  10. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
  11. openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
  12. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
  13. openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
  14. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
  15. openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
  16. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
  17. openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
  18. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
  19. openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
  20. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
  21. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
  22. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
  23. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
  24. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
  25. openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
  26. openprocess/exercises/pack.md +14 -0
  27. openprocess/flow/__init__.py +50 -0
  28. openprocess/flow/box.py +466 -0
  29. openprocess/flow/boxes/__init__.py +7 -0
  30. openprocess/flow/boxes/check.py +119 -0
  31. openprocess/flow/boxes/compare.py +16 -0
  32. openprocess/flow/boxes/cpn.py +53 -0
  33. openprocess/flow/boxes/discover.py +124 -0
  34. openprocess/flow/boxes/filter.py +80 -0
  35. openprocess/flow/boxes/input.py +124 -0
  36. openprocess/flow/boxes/output.py +52 -0
  37. openprocess/flow/boxes/predict.py +186 -0
  38. openprocess/flow/boxes/science.py +159 -0
  39. openprocess/flow/boxes/sweeps.py +18 -0
  40. openprocess/flow/convert.py +187 -0
  41. openprocess/flow/datasets.py +198 -0
  42. openprocess/flow/explain.py +115 -0
  43. openprocess/flow/library.py +222 -0
  44. openprocess/flow/record.py +385 -0
  45. openprocess/flow/runner.py +357 -0
  46. openprocess/flow/sweep.py +92 -0
  47. openprocess/flow/types.py +290 -0
  48. openprocess/flow/workflow.py +628 -0
  49. openprocess/gui/__init__.py +0 -0
  50. openprocess/gui/app.py +90 -0
  51. openprocess/gui/arc_editing.py +295 -0
  52. openprocess/gui/canvas.py +1414 -0
  53. openprocess/gui/flow/__init__.py +8 -0
  54. openprocess/gui/flow/canvas.py +854 -0
  55. openprocess/gui/flow/page.py +972 -0
  56. openprocess/gui/flow/templates.py +131 -0
  57. openprocess/gui/flow/viewers.py +665 -0
  58. openprocess/gui/items.py +1275 -0
  59. openprocess/gui/learn/answer_boxes.py +978 -0
  60. openprocess/gui/learn/concealment.py +91 -0
  61. openprocess/gui/learn/mode.py +1181 -0
  62. openprocess/gui/panning.py +241 -0
  63. openprocess/gui/resources/openprocess-icon.png +0 -0
  64. openprocess/gui/studio/__init__.py +1 -0
  65. openprocess/gui/studio/__main__.py +3 -0
  66. openprocess/gui/studio/app.py +4031 -0
  67. openprocess/gui/studio/charts.py +115 -0
  68. openprocess/gui/studio/compare_page.py +487 -0
  69. openprocess/gui/studio/cpn_page.py +1858 -0
  70. openprocess/gui/studio/definition_view.py +284 -0
  71. openprocess/gui/studio/derivation_view.py +421 -0
  72. openprocess/gui/studio/documents.py +152 -0
  73. openprocess/gui/studio/dotted_chart.py +1401 -0
  74. openprocess/gui/studio/file_dialogs.py +143 -0
  75. openprocess/gui/studio/filter_dialog.py +247 -0
  76. openprocess/gui/studio/graph_builders.py +176 -0
  77. openprocess/gui/studio/graph_view.py +682 -0
  78. openprocess/gui/studio/instances.py +413 -0
  79. openprocess/gui/studio/log_editor.py +675 -0
  80. openprocess/gui/studio/log_page.py +800 -0
  81. openprocess/gui/studio/markdown_view.py +127 -0
  82. openprocess/gui/studio/mathtext.py +260 -0
  83. openprocess/gui/studio/ml_highlighter.py +75 -0
  84. openprocess/gui/studio/model_page.py +760 -0
  85. openprocess/gui/studio/net_comparison.py +124 -0
  86. openprocess/gui/studio/notes_overlay.py +275 -0
  87. openprocess/gui/studio/petri_page.py +844 -0
  88. openprocess/gui/studio/regions_view.py +502 -0
  89. openprocess/gui/studio/sidebar.py +149 -0
  90. openprocess/gui/studio/style.py +503 -0
  91. openprocess/gui/studio/tool_icons.py +134 -0
  92. openprocess/gui/studio/updates.py +439 -0
  93. openprocess/gui/studio/widgets.py +899 -0
  94. openprocess/gui/studio/workers.py +60 -0
  95. openprocess/gui/studio/workspace.py +447 -0
  96. openprocess/gui/theme.py +394 -0
  97. openprocess/gui/tidy.py +86 -0
  98. openprocess/io/__init__.py +0 -0
  99. openprocess/io/cpn_reader.py +389 -0
  100. openprocess/io/cpn_writer.py +357 -0
  101. openprocess/learn/__init__.py +23 -0
  102. openprocess/learn/answers.py +188 -0
  103. openprocess/learn/checks.py +953 -0
  104. openprocess/learn/computed.py +1180 -0
  105. openprocess/learn/context.py +145 -0
  106. openprocess/learn/exam.py +169 -0
  107. openprocess/learn/exercise-packs.md +325 -0
  108. openprocess/learn/importer.py +216 -0
  109. openprocess/learn/notation.py +474 -0
  110. openprocess/learn/pack.py +511 -0
  111. openprocess/learn/sheet.py +296 -0
  112. openprocess/mining/__init__.py +73 -0
  113. openprocess/mining/analysis.py +689 -0
  114. openprocess/mining/columns.py +282 -0
  115. openprocess/mining/compare_nets.py +246 -0
  116. openprocess/mining/conformance/__init__.py +0 -0
  117. openprocess/mining/conformance/alignments.py +263 -0
  118. openprocess/mining/conformance/quality.py +145 -0
  119. openprocess/mining/conformance/token_replay.py +252 -0
  120. openprocess/mining/csv_import.py +222 -0
  121. openprocess/mining/definitions.py +584 -0
  122. openprocess/mining/dfg.py +187 -0
  123. openprocess/mining/discovery/__init__.py +0 -0
  124. openprocess/mining/discovery/alpha.py +168 -0
  125. openprocess/mining/discovery/heuristics.py +332 -0
  126. openprocess/mining/discovery/inductive.py +477 -0
  127. openprocess/mining/discovery/state_regions.py +62 -0
  128. openprocess/mining/filtering.py +237 -0
  129. openprocess/mining/footprint.py +183 -0
  130. openprocess/mining/invariants.py +191 -0
  131. openprocess/mining/layout.py +279 -0
  132. openprocess/mining/log.py +364 -0
  133. openprocess/mining/petrinet.py +354 -0
  134. openprocess/mining/playout.py +75 -0
  135. openprocess/mining/pm4py_bridge.py +82 -0
  136. openprocess/mining/pnml.py +223 -0
  137. openprocess/mining/processtree.py +216 -0
  138. openprocess/mining/regions.py +476 -0
  139. openprocess/mining/stats.py +160 -0
  140. openprocess/mining/structure.py +374 -0
  141. openprocess/mining/transition_system.py +409 -0
  142. openprocess/mining/xes.py +399 -0
  143. openprocess/ml/__init__.py +0 -0
  144. openprocess/ml/ast_nodes.py +332 -0
  145. openprocess/ml/builtins.py +364 -0
  146. openprocess/ml/colorsets.py +522 -0
  147. openprocess/ml/errors.py +60 -0
  148. openprocess/ml/evaluator.py +754 -0
  149. openprocess/ml/lexer.py +277 -0
  150. openprocess/ml/multiset.py +417 -0
  151. openprocess/ml/parser.py +737 -0
  152. openprocess/ml/values.py +319 -0
  153. openprocess/model/__init__.py +0 -0
  154. openprocess/model/declarations.py +617 -0
  155. openprocess/model/examples.py +98 -0
  156. openprocess/model/net.py +701 -0
  157. openprocess/model/plain.py +192 -0
  158. openprocess/references.py +280 -0
  159. openprocess/sim/__init__.py +0 -0
  160. openprocess/sim/binding.py +620 -0
  161. openprocess/sim/export.py +66 -0
  162. openprocess/sim/simulator.py +315 -0
  163. openprocess/teaching/__init__.py +4 -0
  164. openprocess/teaching/answers.py +4 -0
  165. openprocess/teaching/checks.py +5 -0
  166. openprocess/teaching/pack.py +4 -0
  167. openprocess/teaching/sheet.py +4 -0
  168. openprocess-0.7.0.dist-info/METADATA +927 -0
  169. openprocess-0.7.0.dist-info/RECORD +173 -0
  170. openprocess-0.7.0.dist-info/WHEEL +5 -0
  171. openprocess-0.7.0.dist-info/entry_points.txt +6 -0
  172. openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
  173. openprocess-0.7.0.dist-info/top_level.txt +2 -0
@@ -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
+ """
@@ -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