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,124 @@
|
|
|
1
|
+
"""The result of comparing two nets on behaviour (Compare nets…, and Check in
|
|
2
|
+
an exercise)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from html import escape
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import Qt
|
|
9
|
+
from PySide6.QtWidgets import QComboBox, QSizePolicy, QTableWidget, QVBoxLayout, QWidget
|
|
10
|
+
|
|
11
|
+
from .widgets import Verdict, button, hbox, label, status_for
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def trace_text(trace) -> str:
|
|
15
|
+
return "⟨" + ", ".join(trace) + "⟩"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# The result of comparing two nets
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
class NetComparisonView(QWidget):
|
|
22
|
+
"""Same behaviour or not, the shortest differing traces, and a label mapping.
|
|
23
|
+
|
|
24
|
+
``replay(trace, in_first)`` replays a trace on the first net (yours);
|
|
25
|
+
``recompare(mapping)`` compares again with labels of the first net
|
|
26
|
+
renamed to labels of the second.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, comparison, first: str = "yours", second: str = "the answer",
|
|
30
|
+
replay=None, recompare=None, soundness=None, parent=None) -> None:
|
|
31
|
+
super().__init__(parent)
|
|
32
|
+
layout = QVBoxLayout(self)
|
|
33
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
34
|
+
layout.setSpacing(8)
|
|
35
|
+
self.comparison = comparison
|
|
36
|
+
same = comparison.equivalent
|
|
37
|
+
scope = "" if comparison.exact else \
|
|
38
|
+
f" Traces of up to {comparison.max_length} steps were compared (the nets are " \
|
|
39
|
+
"unbounded or very large), so this is not a proof."
|
|
40
|
+
if same:
|
|
41
|
+
layout.addWidget(Verdict("Same behaviour", "good",
|
|
42
|
+
f"{first.capitalize()} and {second} allow exactly the "
|
|
43
|
+
"same complete traces." + scope))
|
|
44
|
+
else:
|
|
45
|
+
layout.addWidget(Verdict(f"Differs from {second}", "warning",
|
|
46
|
+
f"{first.capitalize()} and {second} do not allow the same "
|
|
47
|
+
"complete traces. A different model can still be a "
|
|
48
|
+
"correct reading of the question." + scope))
|
|
49
|
+
self.replay_buttons: list = []
|
|
50
|
+
|
|
51
|
+
def row(text: str, trace, in_first: bool) -> None:
|
|
52
|
+
line = hbox(spacing=6)
|
|
53
|
+
line.addWidget(label(f"<span>{text}</span>", "muted", wrap=True, selectable=True), 1)
|
|
54
|
+
if replay is not None:
|
|
55
|
+
show = button("Replay ▶", lambda _=False, t=trace, f=in_first: replay(t, f),
|
|
56
|
+
kind="ghost", tooltip="Fire this trace on "
|
|
57
|
+
f"{first} in the token game, as far as it goes")
|
|
58
|
+
show.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
59
|
+
self.replay_buttons.append(show)
|
|
60
|
+
line.addWidget(show, 0, Qt.AlignTop)
|
|
61
|
+
holder = QWidget()
|
|
62
|
+
holder.setLayout(line)
|
|
63
|
+
layout.addWidget(holder)
|
|
64
|
+
|
|
65
|
+
for trace in comparison.only_second:
|
|
66
|
+
row(f"{escape(second.capitalize())} allows <b>{escape(trace_text(trace))}</b>, "
|
|
67
|
+
f"{escape(first)} does not.", trace, False)
|
|
68
|
+
for trace in comparison.only_first:
|
|
69
|
+
row(f"{escape(first.capitalize())} allows <b>{escape(trace_text(trace))}</b>, "
|
|
70
|
+
f"{escape(second)} does not.", trace, True)
|
|
71
|
+
if comparison.labels_only_first or comparison.labels_only_second:
|
|
72
|
+
layout.addWidget(label(
|
|
73
|
+
"Labels only in " + first + ": " + (", ".join(comparison.labels_only_first)
|
|
74
|
+
or "–")
|
|
75
|
+
+ ". Only in " + second + ": " + (", ".join(comparison.labels_only_second)
|
|
76
|
+
or "–") + ".", "muted", wrap=True))
|
|
77
|
+
if recompare is not None and comparison.labels_only_first and \
|
|
78
|
+
comparison.labels_only_second:
|
|
79
|
+
layout.addWidget(self._mapping_table(comparison, first, second, recompare))
|
|
80
|
+
for note in comparison.notes:
|
|
81
|
+
layout.addWidget(label(note, "muted", wrap=True))
|
|
82
|
+
if soundness is not None:
|
|
83
|
+
verdict = soundness.sound
|
|
84
|
+
if not soundness.workflow.is_workflow_net:
|
|
85
|
+
layout.addWidget(Verdict("Not a WF-net", "info", "; ".join(
|
|
86
|
+
soundness.workflow.problems[:2]), definition="wf_net"))
|
|
87
|
+
else:
|
|
88
|
+
layout.addWidget(Verdict(
|
|
89
|
+
f"{first.capitalize()}: " + ("sound" if verdict else "not sound"
|
|
90
|
+
if verdict is False else "soundness undecided"),
|
|
91
|
+
status_for(verdict), soundness.findings[0] if soundness.findings else
|
|
92
|
+
"every case can complete properly and every transition can fire",
|
|
93
|
+
definition="sound"))
|
|
94
|
+
|
|
95
|
+
def _mapping_table(self, comparison, first: str, second: str, recompare) -> QWidget:
|
|
96
|
+
"""Match labels that differ (``register`` vs ``Register request``) by hand."""
|
|
97
|
+
host = QWidget()
|
|
98
|
+
layout = QVBoxLayout(host)
|
|
99
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
100
|
+
layout.addWidget(label(f"Match the labels of {first} to those of {second}:", wrap=True))
|
|
101
|
+
table = QTableWidget(len(comparison.labels_only_first), 2)
|
|
102
|
+
table.setHorizontalHeaderLabels([first.capitalize(), second.capitalize()])
|
|
103
|
+
table.verticalHeader().setVisible(False)
|
|
104
|
+
boxes = []
|
|
105
|
+
for row, mine in enumerate(comparison.labels_only_first):
|
|
106
|
+
from PySide6.QtWidgets import QTableWidgetItem
|
|
107
|
+
item = QTableWidgetItem(mine)
|
|
108
|
+
table.setItem(row, 0, item)
|
|
109
|
+
box = QComboBox()
|
|
110
|
+
box.addItem("(no match)", None)
|
|
111
|
+
for theirs in comparison.labels_only_second:
|
|
112
|
+
box.addItem(theirs, theirs)
|
|
113
|
+
table.setCellWidget(row, 1, box)
|
|
114
|
+
boxes.append((mine, box))
|
|
115
|
+
table.horizontalHeader().setStretchLastSection(True)
|
|
116
|
+
table.setMinimumHeight(min(60 + 30 * len(boxes), 220))
|
|
117
|
+
layout.addWidget(table)
|
|
118
|
+
self.mapping_boxes = boxes
|
|
119
|
+
layout.addWidget(button("Compare again", lambda: recompare(
|
|
120
|
+
{mine: box.currentData() for mine, box in boxes if box.currentData()}),
|
|
121
|
+
kind="primary"))
|
|
122
|
+
return host
|
|
123
|
+
|
|
124
|
+
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Notes: scratch paper that floats over whatever page is open.
|
|
2
|
+
|
|
3
|
+
Learn has its own Notes pane (see :mod:`openprocess.gui.learn.mode`); this is the
|
|
4
|
+
same idea for the rest of the app::
|
|
5
|
+
|
|
6
|
+
+--------------------------------------------------------------+
|
|
7
|
+
| the page: a log, a net, the welcome page … |
|
|
8
|
+
| |
|
|
9
|
+
| +- NOTES ---------------- ⤢ – + |
|
|
10
|
+
| | Work things out here … | |
|
|
11
|
+
| | | |
|
|
12
|
+
| +-------------------------------+ [− 100% + Fit]|
|
|
13
|
+
+--------------------------------------------------------------+
|
|
14
|
+
| Ready [✎ Notes] |
|
|
15
|
+
+--------------------------------------------------------------+
|
|
16
|
+
|
|
17
|
+
* The **✎ Notes** button lives in the status bar (the window puts it there),
|
|
18
|
+
so nothing floats over the page while the notes are folded away.
|
|
19
|
+
* Open, it is a card with the notes, in the bottom-left corner of the page
|
|
20
|
+
(the canvases keep their zoom bar in the bottom-right one); **⤢** makes it
|
|
21
|
+
larger and **–** folds it away. Drag its header to move it out of the
|
|
22
|
+
way, and the grip in its bottom-right corner to resize it.
|
|
23
|
+
|
|
24
|
+
The window decides where the text is kept (with the folder that is open, or
|
|
25
|
+
in the app's settings): :class:`NotesOverlay` only reports edits, a moment
|
|
26
|
+
after the last keystroke.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from PySide6.QtCore import QEvent, QObject, QPoint, QRect, Qt, QTimer, Signal
|
|
32
|
+
from PySide6.QtGui import QColor, QPainter, QPen
|
|
33
|
+
from PySide6.QtWidgets import (
|
|
34
|
+
QFrame, QGraphicsDropShadowEffect, QHBoxLayout, QPlainTextEdit, QToolButton, QVBoxLayout,
|
|
35
|
+
QWidget,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from . import style
|
|
39
|
+
from .widgets import label
|
|
40
|
+
|
|
41
|
+
#: How long after the last keystroke the notes are saved (milliseconds).
|
|
42
|
+
SAVE_DELAY = 400
|
|
43
|
+
#: Space between the overlay and the edges of the page.
|
|
44
|
+
MARGIN = 14
|
|
45
|
+
#: The card's size, open and enlarged (enlarged: at most this much of the page).
|
|
46
|
+
OPEN_SIZE = (380, 300)
|
|
47
|
+
EXPANDED_FRACTION = (0.6, 0.75)
|
|
48
|
+
#: The smallest the card can be made.
|
|
49
|
+
MIN_SIZE = (220, 140)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _tool(text: str, tooltip: str, slot) -> QToolButton:
|
|
53
|
+
tool = QToolButton()
|
|
54
|
+
tool.setObjectName("notesOverlayButton")
|
|
55
|
+
tool.setText(text)
|
|
56
|
+
tool.setToolTip(tooltip)
|
|
57
|
+
tool.setCursor(Qt.PointingHandCursor)
|
|
58
|
+
tool.setFocusPolicy(Qt.NoFocus)
|
|
59
|
+
tool.clicked.connect(slot)
|
|
60
|
+
return tool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _Header(QFrame):
|
|
64
|
+
"""The card's title row, which also moves the card when dragged."""
|
|
65
|
+
|
|
66
|
+
dragged = Signal(QPoint) # how far, since the last move
|
|
67
|
+
|
|
68
|
+
def __init__(self, cursor=Qt.OpenHandCursor) -> None:
|
|
69
|
+
super().__init__()
|
|
70
|
+
self._cursor = cursor
|
|
71
|
+
self.setCursor(cursor)
|
|
72
|
+
self._last: QPoint | None = None
|
|
73
|
+
|
|
74
|
+
def mousePressEvent(self, event) -> None: # noqa: N802
|
|
75
|
+
if event.button() == Qt.LeftButton:
|
|
76
|
+
self._last = event.globalPosition().toPoint()
|
|
77
|
+
if self._cursor == Qt.OpenHandCursor:
|
|
78
|
+
self.setCursor(Qt.ClosedHandCursor)
|
|
79
|
+
super().mousePressEvent(event)
|
|
80
|
+
|
|
81
|
+
def mouseMoveEvent(self, event) -> None: # noqa: N802
|
|
82
|
+
if self._last is not None:
|
|
83
|
+
now = event.globalPosition().toPoint()
|
|
84
|
+
self.dragged.emit(now - self._last)
|
|
85
|
+
self._last = now
|
|
86
|
+
super().mouseMoveEvent(event)
|
|
87
|
+
|
|
88
|
+
def mouseReleaseEvent(self, event) -> None: # noqa: N802
|
|
89
|
+
self._last = None
|
|
90
|
+
self.setCursor(self._cursor)
|
|
91
|
+
super().mouseReleaseEvent(event)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _Grip(_Header):
|
|
95
|
+
"""The corner that resizes the card, drawn as a few diagonal strokes."""
|
|
96
|
+
|
|
97
|
+
SIZE = 14
|
|
98
|
+
|
|
99
|
+
def __init__(self, card: QFrame) -> None:
|
|
100
|
+
super().__init__(Qt.SizeFDiagCursor)
|
|
101
|
+
self.setParent(card)
|
|
102
|
+
self.setObjectName("notesOverlayGrip")
|
|
103
|
+
self.setFixedSize(self.SIZE, self.SIZE)
|
|
104
|
+
self.setToolTip("Drag to resize")
|
|
105
|
+
card.installEventFilter(self)
|
|
106
|
+
|
|
107
|
+
def eventFilter(self, watched, event) -> bool: # noqa: N802
|
|
108
|
+
if event.type() == QEvent.Resize:
|
|
109
|
+
self.move(watched.width() - self.SIZE - 2, watched.height() - self.SIZE - 2)
|
|
110
|
+
self.raise_()
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def paintEvent(self, event) -> None: # noqa: N802
|
|
114
|
+
painter = QPainter(self)
|
|
115
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
116
|
+
painter.setPen(QPen(QColor(style.tokens().text_muted), 1.2))
|
|
117
|
+
s = self.SIZE - 3
|
|
118
|
+
for offset in (4, 8):
|
|
119
|
+
painter.drawLine(s, s - offset, s - offset, s)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class NotesOverlay(QObject):
|
|
123
|
+
"""The notes card, floating over ``host`` (see the module docstring)."""
|
|
124
|
+
|
|
125
|
+
#: The text, a moment after it was edited (see :meth:`flush`).
|
|
126
|
+
edited = Signal(str)
|
|
127
|
+
#: The card was opened or folded away (the ✎ Notes button follows it).
|
|
128
|
+
open_changed = Signal(bool)
|
|
129
|
+
|
|
130
|
+
def __init__(self, host: QWidget) -> None:
|
|
131
|
+
super().__init__(host)
|
|
132
|
+
self.host = host
|
|
133
|
+
self.expanded = False
|
|
134
|
+
#: Where the card was dragged to (its top-left corner); None: the corner.
|
|
135
|
+
self._moved_to: QPoint | None = None
|
|
136
|
+
#: The size the card was resized to with its grip; None: the usual size.
|
|
137
|
+
self._resized_to: tuple[int, int] | None = None
|
|
138
|
+
|
|
139
|
+
self.card = QFrame(host)
|
|
140
|
+
self.card.setObjectName("notesOverlay")
|
|
141
|
+
shadow = QGraphicsDropShadowEffect(self.card)
|
|
142
|
+
shadow.setBlurRadius(28)
|
|
143
|
+
shadow.setOffset(0, 6)
|
|
144
|
+
shadow.setColor(QColor(0, 0, 0, 70))
|
|
145
|
+
self.card.setGraphicsEffect(shadow)
|
|
146
|
+
column = QVBoxLayout(self.card)
|
|
147
|
+
column.setContentsMargins(12, 6, 8, 12)
|
|
148
|
+
column.setSpacing(4)
|
|
149
|
+
header = _Header()
|
|
150
|
+
header.dragged.connect(self._drag)
|
|
151
|
+
row = QHBoxLayout(header)
|
|
152
|
+
row.setContentsMargins(4, 0, 0, 0)
|
|
153
|
+
row.setSpacing(2)
|
|
154
|
+
row.addWidget(label("NOTES", "taskCaption"))
|
|
155
|
+
row.addStretch(1)
|
|
156
|
+
self.expand_button = _tool("⤢", "Larger", lambda: self.set_expanded(not self.expanded))
|
|
157
|
+
row.addWidget(self.expand_button)
|
|
158
|
+
row.addWidget(_tool("–", "Fold the notes away (they are kept)",
|
|
159
|
+
lambda: self.set_open(False)))
|
|
160
|
+
column.addWidget(header)
|
|
161
|
+
self.editor = QPlainTextEdit()
|
|
162
|
+
self.editor.setObjectName("notesEdit")
|
|
163
|
+
self.editor.setPlaceholderText("Jot things down while you work: markings, firing "
|
|
164
|
+
"sequences, traces to try, questions…")
|
|
165
|
+
column.addWidget(self.editor, 1)
|
|
166
|
+
grip = _Grip(self.card)
|
|
167
|
+
grip.dragged.connect(self._resize)
|
|
168
|
+
self.grip = grip
|
|
169
|
+
self._timer = QTimer(self)
|
|
170
|
+
self._timer.setSingleShot(True)
|
|
171
|
+
self._timer.setInterval(SAVE_DELAY)
|
|
172
|
+
self._timer.timeout.connect(self.flush)
|
|
173
|
+
self.editor.textChanged.connect(self._timer.start)
|
|
174
|
+
self.card.hide()
|
|
175
|
+
|
|
176
|
+
# Follow the page's size, and stay above pages added later.
|
|
177
|
+
host.installEventFilter(self)
|
|
178
|
+
self._place()
|
|
179
|
+
|
|
180
|
+
# -- the text ---------------------------------------------------------------------
|
|
181
|
+
def text(self) -> str:
|
|
182
|
+
return self.editor.toPlainText()
|
|
183
|
+
|
|
184
|
+
def set_text(self, text: str) -> None:
|
|
185
|
+
"""Show other notes (another folder's): not an edit, so nothing is saved."""
|
|
186
|
+
self.flush()
|
|
187
|
+
self.editor.blockSignals(True)
|
|
188
|
+
self.editor.setPlainText(text)
|
|
189
|
+
self.editor.blockSignals(False)
|
|
190
|
+
self._timer.stop()
|
|
191
|
+
|
|
192
|
+
def flush(self) -> None:
|
|
193
|
+
"""Report an edit not reported yet (before switching folders, or quitting)."""
|
|
194
|
+
if self._timer.isActive() or self.sender() is self._timer:
|
|
195
|
+
self._timer.stop()
|
|
196
|
+
self.edited.emit(self.text())
|
|
197
|
+
|
|
198
|
+
# -- open, folded, enlarged ---------------------------------------------------------------
|
|
199
|
+
@property
|
|
200
|
+
def is_open(self) -> bool:
|
|
201
|
+
return not self.card.isHidden()
|
|
202
|
+
|
|
203
|
+
def set_open(self, show: bool) -> None:
|
|
204
|
+
if show == self.is_open:
|
|
205
|
+
if show:
|
|
206
|
+
self.editor.setFocus()
|
|
207
|
+
return
|
|
208
|
+
self.card.setVisible(show)
|
|
209
|
+
self._place()
|
|
210
|
+
if show:
|
|
211
|
+
self.editor.setFocus()
|
|
212
|
+
else:
|
|
213
|
+
self.flush()
|
|
214
|
+
self.open_changed.emit(show)
|
|
215
|
+
|
|
216
|
+
def set_expanded(self, expanded: bool) -> None:
|
|
217
|
+
self.expanded = expanded
|
|
218
|
+
self._resized_to = None # ⤢ / ⤡ go back to a standard size
|
|
219
|
+
self.expand_button.setText("⤡" if expanded else "⤢")
|
|
220
|
+
self.expand_button.setToolTip("Smaller" if expanded else "Larger")
|
|
221
|
+
self._place()
|
|
222
|
+
|
|
223
|
+
# -- where it goes ---------------------------------------------------------------------------
|
|
224
|
+
def _card_size(self) -> tuple[int, int]:
|
|
225
|
+
width, height = self.host.width() - 2 * MARGIN, self.host.height() - 2 * MARGIN
|
|
226
|
+
if self._resized_to is not None:
|
|
227
|
+
wanted = self._resized_to
|
|
228
|
+
elif self.expanded:
|
|
229
|
+
wanted = (max(OPEN_SIZE[0], int(self.host.width() * EXPANDED_FRACTION[0])),
|
|
230
|
+
max(OPEN_SIZE[1], int(self.host.height() * EXPANDED_FRACTION[1])))
|
|
231
|
+
else:
|
|
232
|
+
wanted = OPEN_SIZE
|
|
233
|
+
return max(MIN_SIZE[0], min(wanted[0], width)), max(MIN_SIZE[1], min(wanted[1], height))
|
|
234
|
+
|
|
235
|
+
def _place(self) -> None:
|
|
236
|
+
host = self.host.rect()
|
|
237
|
+
if not self.is_open:
|
|
238
|
+
return
|
|
239
|
+
width, height = self._card_size()
|
|
240
|
+
if self._moved_to is None:
|
|
241
|
+
corner = QPoint(MARGIN, host.height() - height - MARGIN)
|
|
242
|
+
else:
|
|
243
|
+
corner = self._moved_to
|
|
244
|
+
self.card.setGeometry(self._inside(QRect(corner.x(), corner.y(), width, height)))
|
|
245
|
+
self.card.raise_()
|
|
246
|
+
|
|
247
|
+
def _inside(self, rect: QRect) -> QRect:
|
|
248
|
+
"""``rect`` moved (not resized) so that it stays on the page."""
|
|
249
|
+
host = self.host.rect()
|
|
250
|
+
x = min(max(rect.x(), 0), max(0, host.width() - rect.width()))
|
|
251
|
+
y = min(max(rect.y(), 0), max(0, host.height() - rect.height()))
|
|
252
|
+
return QRect(x, y, rect.width(), rect.height())
|
|
253
|
+
|
|
254
|
+
def _drag(self, delta: QPoint) -> None:
|
|
255
|
+
rect = self._inside(self.card.geometry().translated(delta))
|
|
256
|
+
self._moved_to = rect.topLeft()
|
|
257
|
+
self.card.setGeometry(rect)
|
|
258
|
+
|
|
259
|
+
def _resize(self, delta: QPoint) -> None:
|
|
260
|
+
"""The grip in the bottom-right corner: the top-left corner stays put."""
|
|
261
|
+
rect = self.card.geometry()
|
|
262
|
+
host = self.host.rect()
|
|
263
|
+
width = min(max(MIN_SIZE[0], rect.width() + delta.x()), host.width() - rect.x())
|
|
264
|
+
height = min(max(MIN_SIZE[1], rect.height() + delta.y()), host.height() - rect.y())
|
|
265
|
+
self._resized_to = (width, height)
|
|
266
|
+
self._moved_to = rect.topLeft()
|
|
267
|
+
self.card.setGeometry(rect.x(), rect.y(), width, height)
|
|
268
|
+
|
|
269
|
+
def eventFilter(self, watched, event) -> bool: # noqa: N802
|
|
270
|
+
if watched is self.host and event.type() in (QEvent.Resize, QEvent.ChildAdded):
|
|
271
|
+
QTimer.singleShot(0, self._place)
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
__all__ = ["NotesOverlay"]
|