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
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""Design tokens and the stylesheet for the Studio window.
|
|
2
|
+
|
|
3
|
+
Every colour is named by *role* and has a light and a dark value, resolved
|
|
4
|
+
through :func:`tokens` at the moment it is needed, so the whole window
|
|
5
|
+
follows the system appearance.
|
|
6
|
+
|
|
7
|
+
Chart colours follow a validated data-visualisation palette:
|
|
8
|
+
|
|
9
|
+
* **categorical** -- eight hues in a *fixed order*, assigned to activities by
|
|
10
|
+
frequency rank (most frequent activity = slot 1). The order was checked for
|
|
11
|
+
colour-vision-deficiency separation between neighbours; a ninth activity
|
|
12
|
+
never gets a generated hue but folds into a neutral grey "Other". Colour
|
|
13
|
+
always follows the activity, so filtering never repaints the survivors.
|
|
14
|
+
* **sequential** -- one blue ramp, light to dark, for magnitudes (frequency
|
|
15
|
+
in the process map).
|
|
16
|
+
* **status** -- good / warning / critical, reserved for verdicts (sound,
|
|
17
|
+
fitting) and always shown with an icon and a word, never colour alone.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from PySide6.QtGui import QColor
|
|
25
|
+
|
|
26
|
+
from .. import theme
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Tokens:
|
|
31
|
+
page: str # window background behind content
|
|
32
|
+
sidebar: str # source list background
|
|
33
|
+
surface: str # cards, canvas paper
|
|
34
|
+
surface_alt: str # table stripes, hovered rows, inputs
|
|
35
|
+
border: str # hairlines
|
|
36
|
+
text: str
|
|
37
|
+
text_secondary: str
|
|
38
|
+
text_muted: str
|
|
39
|
+
accent: str
|
|
40
|
+
accent_soft: str
|
|
41
|
+
accent_text: str # text on accent fills
|
|
42
|
+
grid: str
|
|
43
|
+
axis: str
|
|
44
|
+
canvas: str
|
|
45
|
+
node_fill: str
|
|
46
|
+
node_stroke: str
|
|
47
|
+
silent_fill: str
|
|
48
|
+
selection: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
LIGHT = Tokens(
|
|
52
|
+
page="#f5f5f7", sidebar="#ebebee", surface="#ffffff", surface_alt="#f6f6f8",
|
|
53
|
+
border="#e1e1e6", text="#1d1d1f", text_secondary="#52514e", text_muted="#86868b",
|
|
54
|
+
accent="#0a84ff", accent_soft="#dcebff", accent_text="#ffffff",
|
|
55
|
+
grid="#e1e0d9", axis="#c3c2b7", canvas="#fcfcfb", node_fill="#ffffff",
|
|
56
|
+
node_stroke="#3a3a3c", silent_fill="#3a3a3c", selection="#0a84ff",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
DARK = Tokens(
|
|
60
|
+
page="#1c1c1e", sidebar="#242427", surface="#2a2a2d", surface_alt="#323236",
|
|
61
|
+
border="#3a3a3e", text="#f5f5f7", text_secondary="#c3c2b7", text_muted="#98989d",
|
|
62
|
+
accent="#0a84ff", accent_soft="#153a63", accent_text="#ffffff",
|
|
63
|
+
grid="#2c2c2a", axis="#383835", canvas="#1a1a19", node_fill="#2a2a2d",
|
|
64
|
+
node_stroke="#d1d1d6", silent_fill="#d1d1d6", selection="#409cff",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def tokens() -> Tokens:
|
|
69
|
+
return DARK if theme.is_dark() else LIGHT
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def qc(hex_value: str, alpha: float = 1.0) -> QColor:
|
|
73
|
+
colour = QColor(hex_value)
|
|
74
|
+
colour.setAlphaF(alpha)
|
|
75
|
+
return colour
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Data colours
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
CATEGORICAL_LIGHT = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100",
|
|
82
|
+
"#e87ba4", "#008300", "#4a3aa7", "#e34948"]
|
|
83
|
+
CATEGORICAL_DARK = ["#3987e5", "#d95926", "#199e70", "#c98500",
|
|
84
|
+
"#d55181", "#008300", "#9085e9", "#e66767"]
|
|
85
|
+
OTHER_LIGHT, OTHER_DARK = "#a8a7a0", "#6b6a66"
|
|
86
|
+
|
|
87
|
+
SEQUENTIAL_BLUE = ["#cde2fb", "#b7d3f6", "#9ec5f4", "#86b6ef", "#6da7ec", "#5598e7",
|
|
88
|
+
"#3987e5", "#2a78d6", "#256abf", "#1c5cab", "#184f95", "#104281", "#0d366b"]
|
|
89
|
+
|
|
90
|
+
STATUS = {"good": "#0ca30c", "warning": "#fab219", "serious": "#ec835a", "critical": "#d03b3b"}
|
|
91
|
+
STATUS_ICON = {"good": "✓", "warning": "!", "serious": "!", "critical": "✕", "unknown": "?",
|
|
92
|
+
"info": "i"}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def categorical(slot: int) -> str:
|
|
96
|
+
palette = CATEGORICAL_DARK if theme.is_dark() else CATEGORICAL_LIGHT
|
|
97
|
+
if 0 <= slot < len(palette):
|
|
98
|
+
return palette[slot]
|
|
99
|
+
return OTHER_DARK if theme.is_dark() else OTHER_LIGHT
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def sequential(fraction: float) -> str:
|
|
103
|
+
"""A step of the blue ramp for a magnitude in [0, 1]."""
|
|
104
|
+
fraction = min(max(fraction, 0.0), 1.0)
|
|
105
|
+
# Do not use the palest steps for real data: they vanish into the surface.
|
|
106
|
+
low, high = 1, len(SEQUENTIAL_BLUE) - 2
|
|
107
|
+
return SEQUENTIAL_BLUE[round(low + fraction * (high - low))]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def text_on(fill: str) -> str:
|
|
111
|
+
"""Ink colour that stays legible on ``fill``."""
|
|
112
|
+
colour = QColor(fill)
|
|
113
|
+
luminance = 0.2126 * colour.redF() + 0.7152 * colour.greenF() + 0.0722 * colour.blueF()
|
|
114
|
+
return "#0b0b0b" if luminance > 0.55 else "#ffffff"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ActivityColours:
|
|
118
|
+
"""Stable activity -> colour assignment for one log (by frequency rank)."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, activities_by_frequency: list[str]) -> None:
|
|
121
|
+
self.rank = {name: i for i, name in enumerate(activities_by_frequency)}
|
|
122
|
+
|
|
123
|
+
def slot(self, activity: str) -> int:
|
|
124
|
+
return self.rank.get(activity, 99)
|
|
125
|
+
|
|
126
|
+
def colour(self, activity: str) -> str:
|
|
127
|
+
return categorical(self.slot(activity))
|
|
128
|
+
|
|
129
|
+
def is_other(self, activity: str) -> bool:
|
|
130
|
+
return self.slot(activity) >= len(CATEGORICAL_LIGHT)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Stylesheet
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
RADIUS = 10
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _arrow_images(colour: str) -> dict[str, str]:
|
|
140
|
+
"""Small up/down chevrons (spin boxes, combo boxes) and the tick and dash of a
|
|
141
|
+
check box, as PNG files.
|
|
142
|
+
|
|
143
|
+
A stylesheet that styles a spin box's buttons must also supply the
|
|
144
|
+
arrows (Qt then stops drawing its own), and stylesheets can only take
|
|
145
|
+
images from files -- so they are painted once per colour into a cache
|
|
146
|
+
folder. Returns ``{"up": path, "down": path}`` with forward slashes.
|
|
147
|
+
"""
|
|
148
|
+
import tempfile
|
|
149
|
+
from pathlib import Path
|
|
150
|
+
from PySide6.QtCore import QPointF, Qt
|
|
151
|
+
from PySide6.QtGui import QImage, QPainter, QPen
|
|
152
|
+
folder = Path(tempfile.gettempdir()) / "openprocess-ui"
|
|
153
|
+
folder.mkdir(exist_ok=True)
|
|
154
|
+
paths = {}
|
|
155
|
+
for name, points in (("up", ((3, 10), (8, 5), (13, 10))),
|
|
156
|
+
("down", ((3, 6), (8, 11), (13, 6))),
|
|
157
|
+
("check", ((4, 8.5), (7, 11.5), (12.5, 5))),
|
|
158
|
+
("dash", ((4.5, 8), (11.5, 8)))):
|
|
159
|
+
path = folder / f"chevron-{name}-{colour.lstrip('#')}.png"
|
|
160
|
+
if not path.exists():
|
|
161
|
+
image = QImage(32, 32, QImage.Format_ARGB32)
|
|
162
|
+
image.fill(Qt.transparent)
|
|
163
|
+
painter = QPainter(image)
|
|
164
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
165
|
+
painter.scale(2, 2)
|
|
166
|
+
pen = QPen(QColor(colour), 1.8, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
|
|
167
|
+
painter.setPen(pen)
|
|
168
|
+
painter.drawPolyline([QPointF(x, y) for x, y in points])
|
|
169
|
+
painter.end()
|
|
170
|
+
image.save(str(path))
|
|
171
|
+
paths[name] = path.as_posix()
|
|
172
|
+
return paths
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def stylesheet() -> str:
|
|
176
|
+
t = tokens()
|
|
177
|
+
# Outlines of check boxes, radio buttons and slider handles: the text
|
|
178
|
+
# colour, faded -- visible on light and dark (the axis grey all but
|
|
179
|
+
# vanished on the dark surface).
|
|
180
|
+
outline = qc(t.text, 0.38).name(QColor.HexArgb)
|
|
181
|
+
track = qc(t.text, 0.16).name(QColor.HexArgb)
|
|
182
|
+
arrows = _arrow_images(t.text_secondary)
|
|
183
|
+
ticks = _arrow_images(t.accent_text) # white, on the accent fill
|
|
184
|
+
return f"""
|
|
185
|
+
QWidget {{ color: {t.text}; }}
|
|
186
|
+
QMainWindow, #studioRoot {{ background: {t.page}; }}
|
|
187
|
+
QDialog, QMessageBox {{ background: {t.page}; }}
|
|
188
|
+
QToolTip {{ background: {t.surface}; color: {t.text}; border: 1px solid {t.border};
|
|
189
|
+
padding: 6px 8px; border-radius: 8px; }}
|
|
190
|
+
|
|
191
|
+
/* ---- sidebar -------------------------------------------------------- */
|
|
192
|
+
#sidebar {{ background: {t.sidebar}; border-right: 1px solid {t.border}; }}
|
|
193
|
+
#sidebar QTreeWidget {{ background: transparent; border: none; outline: none; }}
|
|
194
|
+
#sidebar QTreeWidget::item {{ padding: 5px 28px 5px 6px; border-radius: 7px; margin: 1px 6px; }}
|
|
195
|
+
#sidebar QTreeWidget::item:hover {{ background: {qc(t.text, 0.06).name(QColor.HexArgb)}; }}
|
|
196
|
+
#sidebar QTreeWidget::item:selected {{ background: {t.accent}; color: {t.accent_text}; }}
|
|
197
|
+
#sidebarTitle {{ color: {t.text}; font-weight: 700; font-size: 15px; padding: 0 2px; }}
|
|
198
|
+
#sidebarCaption {{ color: {t.text_muted}; font-size: 10px; font-weight: 700;
|
|
199
|
+
letter-spacing: 0.8px; padding: 0 2px; }}
|
|
200
|
+
#sidebarMenuButton {{ background: transparent; border: none; border-radius: 6px;
|
|
201
|
+
color: {t.text_secondary}; font-size: 16px; padding: 0 6px 2px 6px; }}
|
|
202
|
+
#sidebarMenuButton:hover {{ background: {qc(t.text, 0.08).name(QColor.HexArgb)}; }}
|
|
203
|
+
#sidebarMenuButton::menu-indicator {{ image: none; width: 0; }}
|
|
204
|
+
#sidebarToggle {{ background: transparent; border: none; border-radius: 6px; padding: 4px; }}
|
|
205
|
+
#rail {{ background: {t.sidebar}; border-right: 1px solid {t.border}; }}
|
|
206
|
+
#sidebarToggle:hover {{ background: {qc(t.text, 0.08).name(QColor.HexArgb)}; }}
|
|
207
|
+
#sidebarFooter QPushButton {{ background: transparent; border: none; color: {t.text_secondary};
|
|
208
|
+
padding: 6px 8px; border-radius: 6px; text-align: left; }}
|
|
209
|
+
#sidebarFooter QPushButton:hover {{ background: {qc(t.text, 0.06).name(QColor.HexArgb)}; }}
|
|
210
|
+
|
|
211
|
+
/* ---- page chrome -------------------------------------------------- */
|
|
212
|
+
#pageHeader {{ background: {t.page}; }}
|
|
213
|
+
#pageTitle {{ font-size: 22px; font-weight: 700; }}
|
|
214
|
+
#pageSubtitle {{ color: {t.text_muted}; }}
|
|
215
|
+
#noticeBar {{ background: {qc(STATUS["warning"], 0.16).name(QColor.HexArgb)};
|
|
216
|
+
border-bottom: 1px solid {qc(STATUS["warning"], 0.45).name(QColor.HexArgb)}; }}
|
|
217
|
+
#noticeBar QLabel {{ color: {t.text}; }}
|
|
218
|
+
#updateBar {{ background: {t.accent_soft}; border-bottom: 1px solid {t.border}; }}
|
|
219
|
+
#updateBar QLabel {{ color: {t.text}; }}
|
|
220
|
+
#updateBarClose {{ background: transparent; border: none; border-radius: 6px;
|
|
221
|
+
color: {t.text_secondary}; padding: 3px 7px; }}
|
|
222
|
+
#updateBarClose:hover {{ background: {qc(t.text, 0.08).name(QColor.HexArgb)}; }}
|
|
223
|
+
#card {{ background: {t.surface}; border: 1px solid {t.border}; border-radius: {RADIUS}px; }}
|
|
224
|
+
#cardTitle {{ font-weight: 600; font-size: 13px; }}
|
|
225
|
+
/* A section inside a card (no second frame around it). */
|
|
226
|
+
#flatCard {{ background: transparent; border: none; }}
|
|
227
|
+
#cardCaption, #muted {{ color: {t.text_muted}; }}
|
|
228
|
+
#statLabel {{ color: {t.text_muted}; font-size: 12px; }}
|
|
229
|
+
#statValue {{ font-size: 22px; font-weight: 600; }}
|
|
230
|
+
#statSub {{ color: {t.text_muted}; font-size: 11px; }}
|
|
231
|
+
#sectionLabel {{ color: {t.text_muted}; font-size: 11px; font-weight: 700;
|
|
232
|
+
letter-spacing: 0.6px; }}
|
|
233
|
+
|
|
234
|
+
/* The canvas's "out of view — show it" hint (see panning.py). */
|
|
235
|
+
QPushButton#offscreenHint {{ background: {t.accent}; border: none; color: {t.accent_text};
|
|
236
|
+
border-radius: 13px; padding: 5px 14px; font-weight: 600; }}
|
|
237
|
+
QPushButton#offscreenHint:hover {{ background: {QColor(t.accent).lighter(110).name()}; }}
|
|
238
|
+
|
|
239
|
+
/* ---- exercise mode -------------------------------------------------------- */
|
|
240
|
+
#exerciseBar {{ background: {t.page}; border-bottom: 1px solid {t.border}; }}
|
|
241
|
+
#exerciseWhere {{ color: {t.text_secondary}; font-weight: 600; }}
|
|
242
|
+
#exercisePosition {{ color: {t.text_muted}; font-size: 12px; min-width: 34px;
|
|
243
|
+
qproperty-alignment: AlignCenter; }}
|
|
244
|
+
#exerciseBarButton {{ background: transparent; border: none; border-radius: 6px;
|
|
245
|
+
color: {t.text_secondary}; padding: 4px 9px; font-size: 13px; }}
|
|
246
|
+
#exerciseBarButton:hover {{ background: {qc(t.text, 0.08).name(QColor.HexArgb)};
|
|
247
|
+
color: {t.text}; }}
|
|
248
|
+
#exerciseBarButton:disabled {{ color: {qc(t.text, 0.22).name(QColor.HexArgb)}; }}
|
|
249
|
+
#exerciseBarButton::menu-indicator {{ image: none; width: 0; }}
|
|
250
|
+
QPushButton#exerciseExit {{ padding: 4px 14px; }}
|
|
251
|
+
#progressDot {{ border-radius: 6px; border: 1.5px solid {qc(t.text, 0.28).name(QColor.HexArgb)};
|
|
252
|
+
background: transparent; padding: 0; }}
|
|
253
|
+
#progressDot[state="started"] {{ border-color: {STATUS["warning"]};
|
|
254
|
+
background: {qc(STATUS["warning"], 0.35).name(QColor.HexArgb)}; }}
|
|
255
|
+
#progressDot[state="done"] {{ border-color: {STATUS["good"]}; background: {STATUS["good"]}; }}
|
|
256
|
+
#progressDot[current="true"] {{ border: 2px solid {t.accent}; }}
|
|
257
|
+
#exerciseHome, #worksheet {{ background: {t.page}; }}
|
|
258
|
+
#materials {{ background: {t.page}; }}
|
|
259
|
+
#sheetChapter {{ color: {t.accent}; font-size: 11px; font-weight: 700;
|
|
260
|
+
letter-spacing: 0.8px; }}
|
|
261
|
+
#sheetTitle {{ font-size: 24px; font-weight: 700; }}
|
|
262
|
+
#sheetProblem {{ color: {t.text}; background: {qc(STATUS["warning"], 0.16).name(QColor.HexArgb)};
|
|
263
|
+
border-radius: 8px; padding: 8px 10px; }}
|
|
264
|
+
QLabel#markdown {{ font-size: 14px; line-height: 150%; }}
|
|
265
|
+
#notesPanel {{ background: {t.page}; border-top: 1px solid {t.border}; }}
|
|
266
|
+
QPlainTextEdit#notesEdit {{ font-size: 14px; padding: 8px 10px; }}
|
|
267
|
+
#notesOverlay {{ background: {t.surface}; border: 1px solid {t.border}; border-radius: 12px; }}
|
|
268
|
+
#notesOverlayButton {{ background: transparent; border: none; border-radius: 6px;
|
|
269
|
+
padding: 2px 7px; color: {t.text_secondary}; font-size: 14px; }}
|
|
270
|
+
#notesOverlayButton:hover {{ background: {qc(t.text, 0.08).name(QColor.HexArgb)}; color: {t.text}; }}
|
|
271
|
+
QToolButton#notesStatusButton {{ background: {t.surface}; border: 1px solid {t.border};
|
|
272
|
+
border-radius: 7px; padding: 2px 10px; margin: 2px 6px; color: {t.text_secondary}; }}
|
|
273
|
+
QToolButton#notesStatusButton:hover {{ color: {t.text}; }}
|
|
274
|
+
QToolButton#notesStatusButton:checked {{ background: {t.accent_soft}; color: {t.text};
|
|
275
|
+
border-color: {qc(t.accent, 0.45).name(QColor.HexArgb)}; }}
|
|
276
|
+
#exerciseBarButton:checked {{ background: {t.accent_soft}; color: {t.text}; }}
|
|
277
|
+
#taskCard {{ background: {t.surface}; border: 1px solid {t.border}; border-radius: 12px; }}
|
|
278
|
+
#taskCard[state="good"] {{ border-color: {qc(STATUS["good"], 0.55).name(QColor.HexArgb)}; }}
|
|
279
|
+
#taskCard[state="critical"] {{ border-color: {qc(STATUS["critical"], 0.45).name(QColor.HexArgb)}; }}
|
|
280
|
+
#taskCard[state="warning"] {{ border-color: {qc(STATUS["warning"], 0.7).name(QColor.HexArgb)}; }}
|
|
281
|
+
#taskCaption {{ color: {t.text_muted}; font-size: 10px; font-weight: 700;
|
|
282
|
+
letter-spacing: 0.8px; }}
|
|
283
|
+
#statusChip {{ font-size: 11px; font-weight: 600; border-radius: 9px; padding: 2px 8px; }}
|
|
284
|
+
#statusChip[state="good"] {{ color: {STATUS["good"]};
|
|
285
|
+
background: {qc(STATUS["good"], 0.13).name(QColor.HexArgb)}; }}
|
|
286
|
+
#statusChip[state="warning"] {{ color: {t.text};
|
|
287
|
+
background: {qc(STATUS["warning"], 0.25).name(QColor.HexArgb)}; }}
|
|
288
|
+
#statusChip[state="critical"] {{ color: {STATUS["critical"]};
|
|
289
|
+
background: {qc(STATUS["critical"], 0.12).name(QColor.HexArgb)}; }}
|
|
290
|
+
#feedback {{ border-radius: 8px; padding: 7px 10px;
|
|
291
|
+
background: {qc(t.text, 0.05).name(QColor.HexArgb)}; }}
|
|
292
|
+
#feedback[state="good"] {{ background: {qc(STATUS["good"], 0.12).name(QColor.HexArgb)}; }}
|
|
293
|
+
#feedback[state="warning"] {{ background: {qc(STATUS["warning"], 0.2).name(QColor.HexArgb)}; }}
|
|
294
|
+
#feedback[state="critical"] {{ background: {qc(STATUS["critical"], 0.1).name(QColor.HexArgb)}; }}
|
|
295
|
+
#taskNote {{ background: {qc(t.accent, 0.07).name(QColor.HexArgb)};
|
|
296
|
+
border-left: 3px solid {qc(t.accent, 0.55).name(QColor.HexArgb)}; border-radius: 6px; }}
|
|
297
|
+
#taskNote QLabel {{ background: transparent; }}
|
|
298
|
+
#answerReading {{ color: {t.text_muted}; font-size: 12px; }}
|
|
299
|
+
#answerReading[ok="false"] {{ color: {STATUS["critical"]}; }}
|
|
300
|
+
QLineEdit#answerLine {{ padding: 6px 9px; font-size: 14px; }}
|
|
301
|
+
#choiceOption {{ border: 1px solid {t.border}; border-radius: 8px; background: {t.surface}; }}
|
|
302
|
+
#choiceOption:hover {{ border-color: {t.accent}; }}
|
|
303
|
+
#choiceOption QLabel {{ background: transparent; }}
|
|
304
|
+
QPushButton#choicePill {{ min-width: 56px; padding: 5px 16px; border-radius: 12px; }}
|
|
305
|
+
QPushButton#choicePill:checked {{ background: {t.accent}; border-color: {t.accent};
|
|
306
|
+
color: {t.accent_text}; font-weight: 600; }}
|
|
307
|
+
QToolButton#footprintCell {{ background: {t.surface_alt}; border: 1px solid {t.border};
|
|
308
|
+
border-radius: 6px; font-size: 15px; }}
|
|
309
|
+
QToolButton#footprintCell:hover {{ border-color: {t.accent}; }}
|
|
310
|
+
QToolButton#footprintCell:focus {{ border: 1.5px solid {t.accent}; }}
|
|
311
|
+
QToolButton#footprintCell[wrong="true"] {{ border: 2px solid {STATUS["critical"]};
|
|
312
|
+
background: {qc(STATUS["critical"], 0.08).name(QColor.HexArgb)}; }}
|
|
313
|
+
#footprintHeading {{ color: {t.text_secondary}; font-weight: 600; padding: 0 4px; }}
|
|
314
|
+
QLineEdit#gridCell {{ background: {t.surface_alt}; border: 1px solid {t.border};
|
|
315
|
+
border-radius: 6px; font-size: 14px; padding: 2px 4px; }}
|
|
316
|
+
QLineEdit#gridCell:focus {{ border: 1.5px solid {t.accent}; }}
|
|
317
|
+
QLineEdit#gridCell[wrong="true"] {{ border: 2px solid {STATUS["critical"]};
|
|
318
|
+
background: {qc(STATUS["critical"], 0.08).name(QColor.HexArgb)}; }}
|
|
319
|
+
#tupleName {{ color: {t.text_secondary}; font-weight: 600; font-size: 14px; min-width: 34px; }}
|
|
320
|
+
#tupleMark {{ font-weight: 700; font-size: 14px; }}
|
|
321
|
+
#tupleMark[state="good"] {{ color: {STATUS["good"]}; }}
|
|
322
|
+
#tupleMark[state="warning"] {{ color: {STATUS["warning"]}; }}
|
|
323
|
+
#tupleMark[state="critical"] {{ color: {STATUS["critical"]}; }}
|
|
324
|
+
QListWidget#rankingList {{ background: {t.surface_alt}; border: 1px solid {t.border};
|
|
325
|
+
border-radius: 8px; font-size: 14px; padding: 4px; }}
|
|
326
|
+
QListWidget#rankingList::item {{ padding: 5px 8px; border-radius: 6px; }}
|
|
327
|
+
QListWidget#rankingList::item:selected {{ background: {t.accent_soft}; color: {t.text}; }}
|
|
328
|
+
#examClock {{ color: {t.text_secondary}; font-weight: 600; font-variant-numeric: tabular-nums;
|
|
329
|
+
padding: 2px 10px; border-radius: 9px; background: {qc(t.text, 0.06).name(QColor.HexArgb)}; }}
|
|
330
|
+
#examClock[state="soon"] {{ color: {t.text};
|
|
331
|
+
background: {qc(STATUS["warning"], 0.3).name(QColor.HexArgb)}; }}
|
|
332
|
+
#examClock[state="over"] {{ color: {STATUS["critical"]};
|
|
333
|
+
background: {qc(STATUS["critical"], 0.12).name(QColor.HexArgb)}; }}
|
|
334
|
+
#exerciseRow {{ background: {t.surface}; border: 1px solid {t.border}; border-radius: 10px; }}
|
|
335
|
+
#exerciseRow:hover {{ border-color: {t.accent}; }}
|
|
336
|
+
#exerciseRow QLabel {{ background: transparent; }}
|
|
337
|
+
#rowTitle {{ font-weight: 600; font-size: 14px; }}
|
|
338
|
+
#rowChevron {{ color: {t.text_muted}; font-size: 18px; }}
|
|
339
|
+
#rowMark {{ color: {t.text_muted}; font-size: 15px; }}
|
|
340
|
+
#rowMark[state="done"] {{ color: {STATUS["good"]}; font-weight: 700; }}
|
|
341
|
+
#rowMark[state="started"] {{ color: {STATUS["warning"]}; }}
|
|
342
|
+
QTextBrowser#guideBrowser {{ border: none; border-radius: 0; padding: 18px 26px; }}
|
|
343
|
+
QTextBrowser#plainBrowser {{ background: transparent; border: none; padding: 0; }}
|
|
344
|
+
/* A result an exercise hides: a soft inset with its Reveal button. */
|
|
345
|
+
#revealRow {{ background: {qc(t.text, 0.045).name(QColor.HexArgb)};
|
|
346
|
+
border: 1px dashed {qc(t.text, 0.18).name(QColor.HexArgb)};
|
|
347
|
+
border-radius: 9px; }}
|
|
348
|
+
#revealRow QLabel {{ background: transparent; }}
|
|
349
|
+
#revealTitle {{ font-weight: 600; }}
|
|
350
|
+
|
|
351
|
+
/* ---- segmented control ---------------------------------------------- */
|
|
352
|
+
#segmented {{ background: {qc(t.text, 0.07).name(QColor.HexArgb)}; border-radius: 8px; }}
|
|
353
|
+
#segmented QPushButton {{ background: transparent; border: none; border-radius: 6px;
|
|
354
|
+
padding: 5px 11px; color: {t.text_secondary}; font-weight: 500; }}
|
|
355
|
+
#segmented QPushButton:hover {{ color: {t.text}; }}
|
|
356
|
+
#segmented QPushButton:checked {{ background: {t.surface}; color: {t.text};
|
|
357
|
+
border: 1px solid {t.border}; }}
|
|
358
|
+
#segmented[compact="true"] QPushButton {{ padding: 4px 8px; font-size: 12px; }}
|
|
359
|
+
|
|
360
|
+
/* ---- buttons ------------------------------------------------------ */
|
|
361
|
+
QPushButton {{ background: {t.surface}; border: 1px solid {t.border}; border-radius: 7px;
|
|
362
|
+
padding: 6px 14px; }}
|
|
363
|
+
QPushButton:hover {{ background: {t.surface_alt}; }}
|
|
364
|
+
QPushButton:disabled {{ color: {t.text_muted}; }}
|
|
365
|
+
QPushButton#primary {{ background: {t.accent}; border-color: {t.accent};
|
|
366
|
+
color: {t.accent_text}; font-weight: 600; }}
|
|
367
|
+
QPushButton#primary:hover {{ background: {QColor(t.accent).lighter(110).name()}; }}
|
|
368
|
+
QPushButton#primary:disabled {{ background: {t.border}; border-color: {t.border};
|
|
369
|
+
color: {t.text_muted}; }}
|
|
370
|
+
QPushButton#ghost {{ background: transparent; border: none; color: {t.accent};
|
|
371
|
+
padding: 4px 6px; }}
|
|
372
|
+
QPushButton#ghost:hover {{ text-decoration: underline; }}
|
|
373
|
+
QToolButton#canvasTool {{ background: {t.surface}; border: 1px solid {t.border};
|
|
374
|
+
border-radius: 7px; padding: 4px 9px; }}
|
|
375
|
+
QToolButton#canvasTool:hover {{ background: {t.surface_alt}; }}
|
|
376
|
+
QToolButton#canvasTool::menu-indicator {{ image: url("{arrows['down']}");
|
|
377
|
+
subcontrol-position: right center; width: 9px; height: 9px; right: 6px; }}
|
|
378
|
+
QToolButton#canvasTool[popupMode="2"] {{ padding-right: 20px; }}
|
|
379
|
+
QToolButton#canvasTool:checked {{ background: {t.accent}; color: {t.accent_text};
|
|
380
|
+
border-color: {t.accent}; }}
|
|
381
|
+
|
|
382
|
+
/* ---- inputs ------------------------------------------------------- */
|
|
383
|
+
QTextBrowser, QTextEdit {{ background: {t.surface}; border: 1px solid {t.border};
|
|
384
|
+
border-radius: 10px; padding: 6px; }}
|
|
385
|
+
QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QPlainTextEdit {{
|
|
386
|
+
background: {t.surface}; border: 1px solid {t.border}; border-radius: 7px;
|
|
387
|
+
padding: 4px 8px; selection-background-color: {t.accent}; }}
|
|
388
|
+
QComboBox:focus, QLineEdit:focus, QPlainTextEdit:focus, QSpinBox:focus,
|
|
389
|
+
QDoubleSpinBox:focus {{ border-color: {t.accent}; }}
|
|
390
|
+
QSpinBox, QDoubleSpinBox {{ padding-right: 20px; }}
|
|
391
|
+
QSpinBox::up-button, QSpinBox::down-button,
|
|
392
|
+
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {{ subcontrol-origin: border;
|
|
393
|
+
width: 18px; border: none; border-radius: 4px; margin: 2px 3px;
|
|
394
|
+
background: transparent; }}
|
|
395
|
+
QSpinBox::up-button:hover, QSpinBox::down-button:hover,
|
|
396
|
+
QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover {{
|
|
397
|
+
background: {qc(t.text, 0.08).name(QColor.HexArgb)}; }}
|
|
398
|
+
QSpinBox::up-button, QDoubleSpinBox::up-button {{ subcontrol-position: top right;
|
|
399
|
+
margin-bottom: 0; }}
|
|
400
|
+
QSpinBox::down-button, QDoubleSpinBox::down-button {{ subcontrol-position: bottom right;
|
|
401
|
+
margin-top: 0; }}
|
|
402
|
+
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {{ image: url("{arrows['up']}");
|
|
403
|
+
width: 10px; height: 10px; }}
|
|
404
|
+
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {{ image: url("{arrows['down']}");
|
|
405
|
+
width: 10px; height: 10px; }}
|
|
406
|
+
QSpinBox::up-arrow:disabled, QSpinBox::down-arrow:disabled,
|
|
407
|
+
QDoubleSpinBox::up-arrow:disabled, QDoubleSpinBox::down-arrow:disabled {{ image: none; }}
|
|
408
|
+
/* combobox-popup: 0 -- a list that drops down below the box (as on the
|
|
409
|
+
web), which follows the stylesheet; the menu-like one paints a square
|
|
410
|
+
panel of its own around the rounded list. */
|
|
411
|
+
QComboBox {{ padding-right: 22px; combobox-popup: 0; }}
|
|
412
|
+
QComboBox::drop-down {{ border: none; width: 20px; }}
|
|
413
|
+
QComboBox::down-arrow {{ image: url("{arrows['down']}"); width: 10px; height: 10px; }}
|
|
414
|
+
/* The list that drops down: rounded like the menus (widgets.round_menus
|
|
415
|
+
makes its window see-through, so the corners show). */
|
|
416
|
+
QComboBoxPrivateContainer {{ background: transparent; border: none; }}
|
|
417
|
+
QComboBox QAbstractItemView {{ background: {t.surface}; border: 1px solid {t.border};
|
|
418
|
+
border-radius: 10px; padding: 5px; outline: none;
|
|
419
|
+
selection-background-color: {t.accent}; selection-color: {t.accent_text}; }}
|
|
420
|
+
QComboBox QAbstractItemView::item {{ padding: 6px 12px; border-radius: 6px;
|
|
421
|
+
min-height: 18px; }}
|
|
422
|
+
QComboBox QAbstractItemView::item:selected, QComboBox QAbstractItemView::item:hover {{
|
|
423
|
+
background: {t.accent}; color: {t.accent_text}; }}
|
|
424
|
+
QSlider::groove:horizontal {{ height: 4px; background: {track}; border-radius: 2px; }}
|
|
425
|
+
QSlider::sub-page:horizontal {{ background: {t.accent}; border-radius: 2px; }}
|
|
426
|
+
QSlider::handle:horizontal {{ background: {t.surface}; border: 1px solid {outline};
|
|
427
|
+
width: 16px; height: 16px; margin: -7px 0; border-radius: 8px; }}
|
|
428
|
+
QRadioButton, QCheckBox {{ spacing: 8px; }}
|
|
429
|
+
/* Check boxes (also in lists, e.g. a chart's legend) and radio buttons:
|
|
430
|
+
rounded, filled with the accent colour when on. */
|
|
431
|
+
QCheckBox::indicator, QAbstractItemView::indicator {{ width: 14px; height: 14px;
|
|
432
|
+
border: 1px solid {outline}; border-radius: 4px; background: {t.surface}; }}
|
|
433
|
+
QCheckBox::indicator:hover, QAbstractItemView::indicator:hover {{
|
|
434
|
+
border-color: {t.accent}; }}
|
|
435
|
+
QCheckBox::indicator:checked, QAbstractItemView::indicator:checked {{
|
|
436
|
+
background: {t.accent}; border-color: {t.accent}; image: url("{ticks['check']}"); }}
|
|
437
|
+
QCheckBox::indicator:indeterminate, QAbstractItemView::indicator:indeterminate {{
|
|
438
|
+
background: {t.accent}; border-color: {t.accent}; image: url("{ticks['dash']}"); }}
|
|
439
|
+
QCheckBox::indicator:disabled {{ background: {t.surface_alt}; border-color: {t.border}; }}
|
|
440
|
+
QRadioButton::indicator {{ width: 14px; height: 14px; border: 1px solid {outline};
|
|
441
|
+
border-radius: 8px; background: {t.surface}; }}
|
|
442
|
+
QRadioButton::indicator:hover {{ border-color: {t.accent}; }}
|
|
443
|
+
QRadioButton::indicator:checked {{ width: 6px; height: 6px; border: 5px solid {t.accent};
|
|
444
|
+
background: {t.surface}; }}
|
|
445
|
+
|
|
446
|
+
/* Framed sections (the Filter dialog's): rounded like the cards. */
|
|
447
|
+
QGroupBox {{ border: 1px solid {t.border}; border-radius: 10px; margin-top: 14px;
|
|
448
|
+
padding: 10px 8px 8px 8px; background: {t.surface}; }}
|
|
449
|
+
QGroupBox::title {{ subcontrol-origin: margin; subcontrol-position: top left;
|
|
450
|
+
left: 4px; padding: 0 4px; color: {t.text}; font-weight: 600; }}
|
|
451
|
+
QGroupBox::indicator {{ width: 14px; height: 14px; border: 1px solid {outline};
|
|
452
|
+
border-radius: 4px; background: {t.surface}; }}
|
|
453
|
+
QGroupBox::indicator:checked {{ background: {t.accent}; border-color: {t.accent};
|
|
454
|
+
image: url("{ticks['check']}"); }}
|
|
455
|
+
|
|
456
|
+
/* ---- tables and lists --------------------------------------------- */
|
|
457
|
+
QTableView, QTreeView, QListView {{ background: {t.surface}; border: none;
|
|
458
|
+
alternate-background-color: {t.surface_alt}; gridline-color: {t.border};
|
|
459
|
+
selection-background-color: {t.accent_soft}; selection-color: {t.text}; outline: none; }}
|
|
460
|
+
QHeaderView::section {{ background: {t.surface}; color: {t.text_muted}; border: none;
|
|
461
|
+
border-bottom: 1px solid {t.border}; padding: 6px 8px; font-weight: 600;
|
|
462
|
+
font-size: 11px; }}
|
|
463
|
+
QTableCornerButton::section {{ background: {t.surface}; border: none; }}
|
|
464
|
+
/* The Workflows page's box list: rows like the sidebar's, group names as captions. */
|
|
465
|
+
#boxList {{ background: transparent; }}
|
|
466
|
+
#boxList::item {{ padding: 4px 6px; border-radius: 6px; margin: 0 2px; }}
|
|
467
|
+
#boxList::item:hover {{ background: {qc(t.text, 0.06).name(QColor.HexArgb)}; }}
|
|
468
|
+
#boxList::item:selected {{ background: {t.accent_soft}; color: {t.text}; }}
|
|
469
|
+
#boxList::item:has-children {{ padding-top: 10px; background: transparent; }}
|
|
470
|
+
/* A list on its own in a dialog (Compare logs, the Filter dialog's
|
|
471
|
+
activities) gets a rounded frame, like a text field. */
|
|
472
|
+
QDialog QListWidget, QDialog QListView {{ border: 1px solid {t.border};
|
|
473
|
+
border-radius: 8px; padding: 4px; }}
|
|
474
|
+
|
|
475
|
+
/* ---- scroll bars: thin overlays ------------------------------------- */
|
|
476
|
+
QScrollArea {{ background: transparent; border: none; }}
|
|
477
|
+
QScrollArea > QWidget > QWidget {{ background: transparent; }}
|
|
478
|
+
QScrollBar:vertical, QScrollBar:horizontal {{ background: transparent;
|
|
479
|
+
width: 10px; height: 10px; margin: 2px; }}
|
|
480
|
+
QScrollBar::handle {{ background: {qc(t.text, 0.22).name(QColor.HexArgb)};
|
|
481
|
+
border-radius: 4px; min-height: 30px; min-width: 30px; }}
|
|
482
|
+
QScrollBar::add-line, QScrollBar::sub-line, QScrollBar::add-page, QScrollBar::sub-page {{
|
|
483
|
+
background: none; border: none; height: 0; width: 0; }}
|
|
484
|
+
|
|
485
|
+
QSplitter::handle {{ background: {t.border}; }}
|
|
486
|
+
/* The Workflows page: gaps between cards, lit under the mouse so they read as handles. */
|
|
487
|
+
WorkflowPage QSplitter::handle {{ background: transparent; }}
|
|
488
|
+
WorkflowPage QSplitter::handle:hover {{ background: {qc(t.accent, 0.35).name(QColor.HexArgb)};
|
|
489
|
+
margin: 6px 5px; border-radius: 2px; }}
|
|
490
|
+
QGraphicsView {{ background: {t.canvas}; border: none; }}
|
|
491
|
+
QStatusBar {{ background: {t.page}; color: {t.text_muted}; border-top: 1px solid {t.border}; }}
|
|
492
|
+
/* Pop-up menus: rounded like the cards (see widgets.round_menus, which
|
|
493
|
+
gives them the see-through window the corners need). */
|
|
494
|
+
QMenu {{ background: {t.surface}; border: 1px solid {t.border}; border-radius: 10px;
|
|
495
|
+
padding: 5px; }}
|
|
496
|
+
QMenu::item {{ padding: 6px 24px 6px 12px; border-radius: 6px; margin: 1px 0; }}
|
|
497
|
+
QMenu::item:selected {{ background: {t.accent}; color: {t.accent_text}; }}
|
|
498
|
+
QMenu::item:disabled {{ color: {t.text_muted}; }}
|
|
499
|
+
QMenu::separator {{ height: 1px; background: {t.border}; margin: 5px 8px; }}
|
|
500
|
+
QProgressBar {{ background: {t.border}; border: none; border-radius: 2px; max-height: 4px; }}
|
|
501
|
+
QProgressBar::chunk {{ background: {t.accent}; border-radius: 2px; }}
|
|
502
|
+
QTabWidget::pane {{ border: none; }}
|
|
503
|
+
"""
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Icons and mouse cursors for the drawing tools.
|
|
2
|
+
|
|
3
|
+
Each tool gets a picture of what it does -- a pointing hand for *Select*, a
|
|
4
|
+
circle for *Place*, a square for *Transition*, an arrow for *Arc* -- on its
|
|
5
|
+
button and as the mouse cursor over the canvas, so the active tool is
|
|
6
|
+
obvious wherever you look. Painted in code: the app ships no image files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import QPointF, QRectF, Qt
|
|
12
|
+
from PySide6.QtGui import QColor, QCursor, QIcon, QPainter, QPainterPath, QPen, QPixmap
|
|
13
|
+
|
|
14
|
+
from . import style
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _hand(painter: QPainter, size: float, colour: QColor) -> None:
|
|
18
|
+
"""A pointing hand (index finger up), drawn in a ``size`` square."""
|
|
19
|
+
s = size / 24.0
|
|
20
|
+
path = QPainterPath()
|
|
21
|
+
# Index finger.
|
|
22
|
+
path.addRoundedRect(QRectF(8.2 * s, 2.0 * s, 3.6 * s, 11.5 * s), 1.8 * s, 1.8 * s)
|
|
23
|
+
# Palm with the folded fingers.
|
|
24
|
+
path.addRoundedRect(QRectF(6.0 * s, 10.0 * s, 12.5 * s, 11.5 * s), 3.2 * s, 3.2 * s)
|
|
25
|
+
# Thumb.
|
|
26
|
+
path.addRoundedRect(QRectF(3.2 * s, 11.6 * s, 4.6 * s, 3.4 * s), 1.6 * s, 1.6 * s)
|
|
27
|
+
painter.setPen(QPen(colour, 1.4 * s))
|
|
28
|
+
painter.setBrush(QColor(255, 255, 255))
|
|
29
|
+
painter.drawPath(path.simplified())
|
|
30
|
+
painter.setPen(QPen(colour, 1.0 * s))
|
|
31
|
+
for x in (11.8, 14.8): # knuckle creases
|
|
32
|
+
painter.drawLine(QPointF(x * s, 10.8 * s), QPointF(x * s, 14.0 * s))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _shape(painter: QPainter, kind: str, size: float, colour: QColor) -> None:
|
|
36
|
+
s = size / 24.0
|
|
37
|
+
pen = QPen(colour, 2.0 * s)
|
|
38
|
+
pen.setCapStyle(Qt.RoundCap)
|
|
39
|
+
pen.setJoinStyle(Qt.RoundJoin)
|
|
40
|
+
painter.setPen(pen)
|
|
41
|
+
painter.setBrush(Qt.NoBrush)
|
|
42
|
+
if kind == "place":
|
|
43
|
+
painter.drawEllipse(QRectF(4 * s, 4 * s, 16 * s, 16 * s))
|
|
44
|
+
painter.setBrush(colour)
|
|
45
|
+
painter.setPen(Qt.NoPen)
|
|
46
|
+
painter.drawEllipse(QPointF(12 * s, 12 * s), 2.4 * s, 2.4 * s)
|
|
47
|
+
elif kind == "transition":
|
|
48
|
+
painter.drawRect(QRectF(5 * s, 4 * s, 14 * s, 16 * s))
|
|
49
|
+
elif kind == "arc":
|
|
50
|
+
painter.drawLine(QPointF(4 * s, 19 * s), QPointF(18 * s, 6 * s))
|
|
51
|
+
head = QPainterPath(QPointF(20 * s, 4 * s))
|
|
52
|
+
head.lineTo(QPointF(12.5 * s, 6.2 * s))
|
|
53
|
+
head.lineTo(QPointF(17.8 * s, 11.5 * s))
|
|
54
|
+
head.closeSubpath()
|
|
55
|
+
painter.setBrush(colour)
|
|
56
|
+
painter.drawPath(head)
|
|
57
|
+
elif kind == "select":
|
|
58
|
+
_hand(painter, size, colour)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tool_icon(kind: str) -> QIcon:
|
|
62
|
+
"""The button icon for a tool (``select``, ``place``, ``transition``, ``arc``)."""
|
|
63
|
+
pixmap = QPixmap(48, 48)
|
|
64
|
+
pixmap.setDevicePixelRatio(2.0)
|
|
65
|
+
pixmap.fill(Qt.transparent)
|
|
66
|
+
painter = QPainter(pixmap)
|
|
67
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
68
|
+
_shape(painter, kind, 24, QColor(style.tokens().text))
|
|
69
|
+
painter.end()
|
|
70
|
+
return QIcon(pixmap)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def tool_cursor(kind: str) -> QCursor:
|
|
74
|
+
"""The canvas cursor for a tool: a crosshair with the tool's shape next to
|
|
75
|
+
it (the plain pointer for Select)."""
|
|
76
|
+
if kind == "select":
|
|
77
|
+
return QCursor(Qt.ArrowCursor)
|
|
78
|
+
pixmap = QPixmap(64, 64)
|
|
79
|
+
pixmap.setDevicePixelRatio(2.0)
|
|
80
|
+
pixmap.fill(Qt.transparent)
|
|
81
|
+
painter = QPainter(pixmap)
|
|
82
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
83
|
+
black = QColor(20, 20, 20)
|
|
84
|
+
for width, colour in ((3.0, QColor(255, 255, 255)), (1.2, black)): # outlined cross
|
|
85
|
+
painter.setPen(QPen(colour, width))
|
|
86
|
+
painter.drawLine(QPointF(8, 1), QPointF(8, 15))
|
|
87
|
+
painter.drawLine(QPointF(1, 8), QPointF(15, 8))
|
|
88
|
+
painter.translate(13, 13)
|
|
89
|
+
_shape(painter, kind, 16, QColor(style.tokens().accent))
|
|
90
|
+
painter.end()
|
|
91
|
+
return QCursor(pixmap, 8, 8)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def sidebar_icon() -> QIcon:
|
|
95
|
+
"""A window with a pane on the left: the button that shows or hides the sidebar."""
|
|
96
|
+
pixmap = QPixmap(40, 40)
|
|
97
|
+
pixmap.setDevicePixelRatio(2.0)
|
|
98
|
+
pixmap.fill(Qt.transparent)
|
|
99
|
+
painter = QPainter(pixmap)
|
|
100
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
101
|
+
colour = QColor(style.tokens().text_secondary)
|
|
102
|
+
pen = QPen(colour, 1.5)
|
|
103
|
+
pen.setJoinStyle(Qt.RoundJoin)
|
|
104
|
+
frame = QRectF(2.5, 4.5, 15, 11)
|
|
105
|
+
painter.setPen(Qt.NoPen)
|
|
106
|
+
painter.setBrush(QColor(colour.red(), colour.green(), colour.blue(), 60))
|
|
107
|
+
painter.drawRoundedRect(QRectF(frame.x(), frame.y(), 5.5, frame.height()), 2.5, 2.5)
|
|
108
|
+
painter.setPen(pen)
|
|
109
|
+
painter.setBrush(Qt.NoBrush)
|
|
110
|
+
painter.drawRoundedRect(frame, 2.5, 2.5)
|
|
111
|
+
painter.drawLine(QPointF(8, frame.top()), QPointF(8, frame.bottom()))
|
|
112
|
+
painter.end()
|
|
113
|
+
return QIcon(pixmap)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def question_icon() -> QIcon:
|
|
117
|
+
"""A sheet with a question mark: the button that brings back an exercise's question."""
|
|
118
|
+
pixmap = QPixmap(40, 40)
|
|
119
|
+
pixmap.setDevicePixelRatio(2.0)
|
|
120
|
+
pixmap.fill(Qt.transparent)
|
|
121
|
+
painter = QPainter(pixmap)
|
|
122
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
123
|
+
colour = QColor(style.tokens().text_secondary)
|
|
124
|
+
pen = QPen(colour, 1.5)
|
|
125
|
+
pen.setJoinStyle(Qt.RoundJoin)
|
|
126
|
+
painter.setPen(pen)
|
|
127
|
+
painter.drawRoundedRect(QRectF(4.5, 2.5, 11, 15), 2.5, 2.5)
|
|
128
|
+
font = painter.font()
|
|
129
|
+
font.setBold(True)
|
|
130
|
+
font.setPixelSize(10)
|
|
131
|
+
painter.setFont(font)
|
|
132
|
+
painter.drawText(QRectF(4.5, 2.5, 11, 15), Qt.AlignCenter, "?")
|
|
133
|
+
painter.end()
|
|
134
|
+
return QIcon(pixmap)
|