openprocess 0.11.0__tar.gz → 0.11.3__tar.gz
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.
- {openprocess-0.11.0/openprocess.egg-info → openprocess-0.11.3}/PKG-INFO +17 -12
- {openprocess-0.11.0 → openprocess-0.11.3}/README.md +16 -11
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/__init__.py +1 -1
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/cli.py +2 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/check.py +6 -1
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/convert.py +76 -8
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/canvas.py +10 -5
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/canvas.py +2 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/picker.py +113 -88
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/viewers.py +5 -2
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/app.py +74 -23
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/cpn_page.py +55 -25
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/derivation_view.py +3 -1
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/graph_view.py +46 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/model_page.py +21 -4
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/petri_page.py +5 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/sidebar.py +6 -4
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/tool_icons.py +27 -20
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/alignments.py +86 -7
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/inductive.py +133 -24
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/references.py +5 -2
- {openprocess-0.11.0 → openprocess-0.11.3/openprocess.egg-info}/PKG-INFO +17 -12
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_flow.py +20 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_flow_gui.py +47 -8
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_mining.py +71 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_studio_gui.py +94 -3
- {openprocess-0.11.0 → openprocess-0.11.3}/LICENSE +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/cpnpy/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/analysis/state_space.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/citation.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/box.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/science.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/explain.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/figures.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/history.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/library.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/provenance.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/record.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/report.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/reproducibility.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/runner.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/script.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/types.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/workflow.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/app.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/page.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/runs.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/summary.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/items.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/learn/answer_boxes.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/learn/mode.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/panning.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/connections.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/definition_view.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/documents.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/dotted_chart.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/instances.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/style.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/theme.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/io/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/answers.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/checks.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/computed.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/context.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/exam.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/importer.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/notation.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/pack.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/analysis.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/columns.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/token_replay.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/layout.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/log.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/playout.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/regions.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/stats.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/structure.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/xes.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/errors.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/parser.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/values.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/declarations.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/examples.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/net.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/plain.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/packages.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/binding.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/export.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/SOURCES.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/pyproject.toml +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/setup.cfg +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_arc_editing.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_citation.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_colorsets.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_columns.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_definitions.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_exercises_gui.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_hierarchy.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_history.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_io.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_learn.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_learn_gui.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_log_editing.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_ml.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_multiset.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_ocel.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_packages.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_regions.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_rename.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_report.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_simulation.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_snapshots.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_state_space.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_teaching.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_tidy.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_updates.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_wf_structure.py +0 -0
- {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.3
|
|
4
4
|
Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
|
|
5
5
|
Author: Sam Struthers
|
|
6
6
|
License: MIT
|
|
@@ -281,8 +281,9 @@ which space you were in.
|
|
|
281
281
|
state; it stays in the folder.
|
|
282
282
|
- **Flat lists, the same in both spaces.** The folder's own files first,
|
|
283
283
|
then each subfolder's under a caption naming it ("Week 5 / Part 1").
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
Click a caption to fold its files away or bring them back; the app
|
|
285
|
+
remembers which are folded, per folder. An empty folder shows in both
|
|
286
|
+
spaces until it has files.
|
|
286
287
|
- **Organise from the app**: right-click for **New Folder…**, **Rename…**,
|
|
287
288
|
**Show in Finder** and **Move to Bin** (recoverable from the Bin; never a
|
|
288
289
|
hard delete). **Drag files onto a subfolder** to move them on disk. An open
|
|
@@ -373,7 +374,9 @@ Nets drawn as in the lectures and the book:
|
|
|
373
374
|
- **Places** are circles with black tokens inside, drawn as dots up to 5 and
|
|
374
375
|
as a number above that.
|
|
375
376
|
- **Transitions** are boxes with their name inside. A **silent (τ)**
|
|
376
|
-
transition is a black bar
|
|
377
|
+
transition is a black bar: the **τ** tool (toolbar or palette) adds one
|
|
378
|
+
with a click, and the Element tab's *Silent* box turns an ordinary
|
|
379
|
+
transition into τ and back.
|
|
377
380
|
- **Arc weights** above 1 are written on the arc.
|
|
378
381
|
- **Names** go inside the places and transitions, which grow to fit them.
|
|
379
382
|
Tick **Names outside** in the toolbar to put them underneath instead; you
|
|
@@ -765,7 +768,10 @@ print(report.sound, report.findings)
|
|
|
765
768
|
|
|
766
769
|
[**docs/how-it-works.md**](docs/how-it-works.md) covers the architecture, the
|
|
767
770
|
CPN ML subset, binding search, timed nets, the state space, the file format
|
|
768
|
-
and the project layout.
|
|
771
|
+
and the project layout. [**docs/design.md**](docs/design.md) is the *why*:
|
|
772
|
+
the problem, the claim, the three ideas and the decisions that follow from
|
|
773
|
+
them. `tests/cross_check_inductive_miner.py` compares the built-in Inductive
|
|
774
|
+
Miner with PM4Py's on hundreds of logs and reports where they differ.
|
|
769
775
|
|
|
770
776
|
## Workflows
|
|
771
777
|
|
|
@@ -784,13 +790,12 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
784
790
|
**+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
|
|
785
791
|
and Save. The sidebar beside it lists the folder's analyses and, folded
|
|
786
792
|
under them, its logs.
|
|
787
|
-
- **+ Add box** (or a double-click on the canvas) opens every box,
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
first match.
|
|
793
|
+
- **+ Add box** (or a double-click on the canvas) opens every box, one
|
|
794
|
+
section per group (Input, Filter, Discover, Check, Compare, Output, then
|
|
795
|
+
Science, Predict, Coloured nets, Sweep and your own boxes), in a list that
|
|
796
|
+
scrolls. Under *Input*, **the folder's own files**: one click adds the box
|
|
797
|
+
that reads the file, already set. Type to search them all: "alpha" finds
|
|
798
|
+
the α-algorithm, "boarding" the log; Enter adds the first match.
|
|
794
799
|
- **Files flow into the canvas.** Drop a log, an OCEL 2.0 object-centric log
|
|
795
800
|
(`.jsonocel`), a PNML file, a .cpn or a ts.txt on the canvas and it becomes
|
|
796
801
|
its input box at that spot; an input
|
|
@@ -242,8 +242,9 @@ which space you were in.
|
|
|
242
242
|
state; it stays in the folder.
|
|
243
243
|
- **Flat lists, the same in both spaces.** The folder's own files first,
|
|
244
244
|
then each subfolder's under a caption naming it ("Week 5 / Part 1").
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
Click a caption to fold its files away or bring them back; the app
|
|
246
|
+
remembers which are folded, per folder. An empty folder shows in both
|
|
247
|
+
spaces until it has files.
|
|
247
248
|
- **Organise from the app**: right-click for **New Folder…**, **Rename…**,
|
|
248
249
|
**Show in Finder** and **Move to Bin** (recoverable from the Bin; never a
|
|
249
250
|
hard delete). **Drag files onto a subfolder** to move them on disk. An open
|
|
@@ -334,7 +335,9 @@ Nets drawn as in the lectures and the book:
|
|
|
334
335
|
- **Places** are circles with black tokens inside, drawn as dots up to 5 and
|
|
335
336
|
as a number above that.
|
|
336
337
|
- **Transitions** are boxes with their name inside. A **silent (τ)**
|
|
337
|
-
transition is a black bar
|
|
338
|
+
transition is a black bar: the **τ** tool (toolbar or palette) adds one
|
|
339
|
+
with a click, and the Element tab's *Silent* box turns an ordinary
|
|
340
|
+
transition into τ and back.
|
|
338
341
|
- **Arc weights** above 1 are written on the arc.
|
|
339
342
|
- **Names** go inside the places and transitions, which grow to fit them.
|
|
340
343
|
Tick **Names outside** in the toolbar to put them underneath instead; you
|
|
@@ -726,7 +729,10 @@ print(report.sound, report.findings)
|
|
|
726
729
|
|
|
727
730
|
[**docs/how-it-works.md**](docs/how-it-works.md) covers the architecture, the
|
|
728
731
|
CPN ML subset, binding search, timed nets, the state space, the file format
|
|
729
|
-
and the project layout.
|
|
732
|
+
and the project layout. [**docs/design.md**](docs/design.md) is the *why*:
|
|
733
|
+
the problem, the claim, the three ideas and the decisions that follow from
|
|
734
|
+
them. `tests/cross_check_inductive_miner.py` compares the built-in Inductive
|
|
735
|
+
Miner with PM4Py's on hundreds of logs and reports where they differ.
|
|
730
736
|
|
|
731
737
|
## Workflows
|
|
732
738
|
|
|
@@ -745,13 +751,12 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
745
751
|
**+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
|
|
746
752
|
and Save. The sidebar beside it lists the folder's analyses and, folded
|
|
747
753
|
under them, its logs.
|
|
748
|
-
- **+ Add box** (or a double-click on the canvas) opens every box,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
first match.
|
|
754
|
+
- **+ Add box** (or a double-click on the canvas) opens every box, one
|
|
755
|
+
section per group (Input, Filter, Discover, Check, Compare, Output, then
|
|
756
|
+
Science, Predict, Coloured nets, Sweep and your own boxes), in a list that
|
|
757
|
+
scrolls. Under *Input*, **the folder's own files**: one click adds the box
|
|
758
|
+
that reads the file, already set. Type to search them all: "alpha" finds
|
|
759
|
+
the α-algorithm, "boarding" the log; Enter adds the first match.
|
|
755
760
|
- **Files flow into the canvas.** Drop a log, an OCEL 2.0 object-centric log
|
|
756
761
|
(`.jsonocel`), a PNML file, a .cpn or a ts.txt on the canvas and it becomes
|
|
757
762
|
its input box at that spot; an input
|
|
@@ -48,7 +48,7 @@ from .sim.simulator import Simulator
|
|
|
48
48
|
#: The one place the version number is written (pyproject.toml, the update
|
|
49
49
|
#: check and packaging/build.py read it from here). A release is tagged
|
|
50
50
|
#: "v" + this, e.g. v0.2.0.
|
|
51
|
-
__version__ = "0.11.
|
|
51
|
+
__version__ = "0.11.3"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
|
|
@@ -249,6 +249,8 @@ def command_mine_conform(arguments: argparse.Namespace) -> int:
|
|
|
249
249
|
print(f"Generalisation {generalisation(replay):.4f}")
|
|
250
250
|
print(f"Simplicity {simplicity(net):.4f}")
|
|
251
251
|
print(f"Fitting cases {alignments.fitting_traces} / {alignments.trace_count}")
|
|
252
|
+
for note in alignments.notes:
|
|
253
|
+
print(f"Note: {note}")
|
|
252
254
|
return 0
|
|
253
255
|
|
|
254
256
|
|
|
@@ -40,6 +40,8 @@ def check_fit(model: PetriNet, log: EventLog,
|
|
|
40
40
|
alignments = align_log(model, simple)
|
|
41
41
|
fitness = alignments.average_fitness
|
|
42
42
|
flow.note(f"{alignments.fitting_traces} of {alignments.trace_count} cases align without a deviation")
|
|
43
|
+
for note in alignments.notes:
|
|
44
|
+
flow.note(note)
|
|
43
45
|
else:
|
|
44
46
|
fitness = replay.fitness
|
|
45
47
|
flow.note(f"{replay.fitting_traces} of {replay.trace_count} cases replay without missing or remaining tokens")
|
|
@@ -63,8 +65,11 @@ def alignments(model: PetriNet, log: EventLog) -> AlignmentResult:
|
|
|
63
65
|
"""Optimal alignments of every variant with the model (A* over the
|
|
64
66
|
synchronous product, standard cost function)."""
|
|
65
67
|
result = align_log(model, log.simple_log())
|
|
66
|
-
rows = [["⟨" + ", ".join(a.trace) + "⟩", a.count, a.cost, round(a.fitness, 4)]
|
|
68
|
+
rows = [["⟨" + ", ".join(a.trace) + "⟩", a.count, a.cost if a.complete else "?", round(a.fitness, 4)]
|
|
69
|
+
for a in result.alignments]
|
|
67
70
|
flow.show(Table("Alignments", ["variant", "cases", "cost", "fitness"], rows), "Alignment per variant")
|
|
71
|
+
for note in result.notes:
|
|
72
|
+
flow.note(note)
|
|
68
73
|
return result
|
|
69
74
|
|
|
70
75
|
|
|
@@ -15,6 +15,7 @@ class and the target (a class, or a dotted name).
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import importlib
|
|
18
|
+
import importlib.util
|
|
18
19
|
from typing import Any, Callable
|
|
19
20
|
|
|
20
21
|
from ..mining.log import KEY_NAME, KEY_RESOURCE, KEY_TIME, EventLog, SimpleLog
|
|
@@ -25,11 +26,49 @@ _converters: dict[tuple[str, str], Callable] = {}
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def _name(target) -> str:
|
|
29
|
+
"""The name a type is registered under: ``module.qualname`` for a class,
|
|
30
|
+
a dotted string as given. Nothing is imported here, so registering a
|
|
31
|
+
converter for ``"pandas.core.frame.DataFrame"`` costs nothing when pandas
|
|
32
|
+
is absent, and importing this module never imports pandas."""
|
|
28
33
|
if isinstance(target, str):
|
|
29
34
|
return target
|
|
30
35
|
return f"{target.__module__}.{target.__qualname__}"
|
|
31
36
|
|
|
32
37
|
|
|
38
|
+
def _resolve(dotted: str):
|
|
39
|
+
"""The class a dotted name points at, or None when it cannot be found.
|
|
40
|
+
|
|
41
|
+
Tries the longest module prefix first (``a.b.c.D`` as module ``a.b.c``,
|
|
42
|
+
attribute ``D``), then shorter ones, so both ``pandas.DataFrame`` and
|
|
43
|
+
``pandas.core.frame.DataFrame`` resolve to the same class. A name whose
|
|
44
|
+
top-level package is not installed resolves to None without importing
|
|
45
|
+
anything.
|
|
46
|
+
"""
|
|
47
|
+
parts = dotted.split(".")
|
|
48
|
+
if len(parts) < 2 or importlib.util.find_spec(parts[0]) is None:
|
|
49
|
+
return None
|
|
50
|
+
for split in range(len(parts) - 1, 0, -1):
|
|
51
|
+
try:
|
|
52
|
+
obj = importlib.import_module(".".join(parts[:split]))
|
|
53
|
+
except ImportError:
|
|
54
|
+
continue
|
|
55
|
+
for attribute in parts[split:]:
|
|
56
|
+
obj = getattr(obj, attribute, None)
|
|
57
|
+
if obj is None:
|
|
58
|
+
break
|
|
59
|
+
else:
|
|
60
|
+
if isinstance(obj, type):
|
|
61
|
+
return obj
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _canonical(name: str) -> str:
|
|
66
|
+
"""``name`` as the class it points at is defined (``module.qualname``),
|
|
67
|
+
or ``name`` itself when it points at nothing importable."""
|
|
68
|
+
resolved = _resolve(name)
|
|
69
|
+
return name if resolved is None else _name(resolved)
|
|
70
|
+
|
|
71
|
+
|
|
33
72
|
def register(source, target, function: Callable) -> None:
|
|
34
73
|
"""``function(value) -> converted`` for a value of class ``source``."""
|
|
35
74
|
_converters[(_name(source), _name(target))] = function
|
|
@@ -39,20 +78,49 @@ def _class_names(value) -> list[str]:
|
|
|
39
78
|
return [f"{cls.__module__}.{cls.__qualname__}" for cls in type(value).__mro__]
|
|
40
79
|
|
|
41
80
|
|
|
81
|
+
def _find(value: Any, target) -> Callable | None:
|
|
82
|
+
"""The registered converter from ``value``'s class (or a base class) to
|
|
83
|
+
``target``, or None.
|
|
84
|
+
|
|
85
|
+
A class may be named by where it is imported from (``"pandas.DataFrame"``)
|
|
86
|
+
or where it is defined (``"pandas.core.frame.DataFrame"``, which pandas
|
|
87
|
+
2 reports as the class's module, while pandas 3 reports ``"pandas"``).
|
|
88
|
+
Both the source and the target are matched by resolving the names to the
|
|
89
|
+
class. Only registered names in the same top-level package as the value
|
|
90
|
+
or the target are resolved, so looking up a pandas conversion imports
|
|
91
|
+
pandas and nothing else, and only once a conversion is asked for.
|
|
92
|
+
"""
|
|
93
|
+
wanted = _name(target)
|
|
94
|
+
wanted_names = {wanted, _canonical(wanted)}
|
|
95
|
+
target_package = wanted.split(".")[0]
|
|
96
|
+
value_names = set(_class_names(value))
|
|
97
|
+
value_package = type(value).__module__.split(".")[0]
|
|
98
|
+
|
|
99
|
+
def matches(registered: str, names: set[str], package: str) -> bool:
|
|
100
|
+
if registered in names:
|
|
101
|
+
return True
|
|
102
|
+
return registered.split(".")[0] == package and _canonical(registered) in names
|
|
103
|
+
|
|
104
|
+
for (source, registered), function in _converters.items():
|
|
105
|
+
if matches(source, value_names, value_package) and \
|
|
106
|
+
matches(registered, wanted_names, target_package):
|
|
107
|
+
return function
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
42
111
|
def can_convert(value: Any, target) -> bool:
|
|
43
|
-
return
|
|
112
|
+
return _name(target) in _class_names(value) or _find(value, target) is not None
|
|
44
113
|
|
|
45
114
|
|
|
46
115
|
def convert(value: Any, target):
|
|
47
116
|
"""Convert ``value`` to ``target`` (a class or a dotted name)."""
|
|
48
117
|
wanted = _name(target)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raise TypeError(f"No converter from {type(value).__name__} to {wanted}")
|
|
118
|
+
if wanted in _class_names(value):
|
|
119
|
+
return value
|
|
120
|
+
function = _find(value, target)
|
|
121
|
+
if function is None:
|
|
122
|
+
raise TypeError(f"No converter from {type(value).__name__} to {wanted}")
|
|
123
|
+
return function(value)
|
|
56
124
|
|
|
57
125
|
|
|
58
126
|
def converters() -> list[tuple[str, str]]:
|
|
@@ -407,6 +407,9 @@ class NetScene(QGraphicsScene):
|
|
|
407
407
|
if self.tool == "transition":
|
|
408
408
|
self._create_transition(position)
|
|
409
409
|
return
|
|
410
|
+
if self.tool == "silent":
|
|
411
|
+
self._create_transition(position, silent=True)
|
|
412
|
+
return
|
|
410
413
|
if self.tool == "arc":
|
|
411
414
|
self._arc_press(position)
|
|
412
415
|
return
|
|
@@ -728,10 +731,10 @@ class NetScene(QGraphicsScene):
|
|
|
728
731
|
self.model_changed.emit()
|
|
729
732
|
self.created.emit(place)
|
|
730
733
|
|
|
731
|
-
def _create_transition(self, position: QPointF) -> None:
|
|
734
|
+
def _create_transition(self, position: QPointF, silent: bool = False) -> None:
|
|
732
735
|
self.about_to_change.emit()
|
|
733
736
|
self._hold_views_still()
|
|
734
|
-
transition = self._new_transition(on_grid(position) if self.snap_to_grid else position)
|
|
737
|
+
transition = self._new_transition(on_grid(position) if self.snap_to_grid else position, silent)
|
|
735
738
|
self.rebuild()
|
|
736
739
|
self.model_changed.emit()
|
|
737
740
|
self.created.emit(transition)
|
|
@@ -748,12 +751,14 @@ class NetScene(QGraphicsScene):
|
|
|
748
751
|
self.page.places.append(place)
|
|
749
752
|
return place
|
|
750
753
|
|
|
751
|
-
def _new_transition(self, position: QPointF) -> Transition:
|
|
752
|
-
"""Add a transition centred on ``position`` to the model (no redraw).
|
|
754
|
+
def _new_transition(self, position: QPointF, silent: bool = False) -> Transition:
|
|
755
|
+
"""Add a transition centred on ``position`` to the model (no redraw).
|
|
756
|
+
A silent (τ) one has no name: it is drawn as a black bar."""
|
|
753
757
|
assert self.page is not None
|
|
754
758
|
prefix = "t" if self.plain else "T"
|
|
755
759
|
transition = Transition(
|
|
756
|
-
name=self._unique_name(prefix, [t.name for t in self.net.all_transitions()])
|
|
760
|
+
name="" if silent else self._unique_name(prefix, [t.name for t in self.net.all_transitions()]),
|
|
761
|
+
silent=silent,
|
|
757
762
|
)
|
|
758
763
|
transition.graphics.x, transition.graphics.y = to_model(position)
|
|
759
764
|
self.page.transitions.append(transition)
|
|
@@ -900,6 +900,8 @@ class WorkflowView(GraphView):
|
|
|
900
900
|
self.files_dropped.emit(paths, self.mapToScene(event.position().toPoint()))
|
|
901
901
|
|
|
902
902
|
def fit(self) -> None:
|
|
903
|
+
if self.fit_waits():
|
|
904
|
+
return
|
|
903
905
|
self.auto_fit = True
|
|
904
906
|
rect = self.graph.itemsBoundingRect()
|
|
905
907
|
if rect.isEmpty():
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
The Workflows page has no permanent box list. *+ Add box* in its header
|
|
4
4
|
(and a double-click on the canvas, and *Add box here…* in its menu) opens
|
|
5
|
-
this popover: a search field,
|
|
6
|
-
|
|
7
|
-
Predict, Coloured nets, Sweep
|
|
8
|
-
|
|
5
|
+
this popover: a search field, then every box, one section per group with
|
|
6
|
+
its name on a rule above it (Input, Filter, Discover, Check, Compare, Output,
|
|
7
|
+
then Science, Predict, Coloured nets, Sweep and a folder's own boxes), the
|
|
8
|
+
boxes of a group in three columns. The list scrolls when it is taller than
|
|
9
|
+
the screen leaves room for, so nothing is folded away and nothing is cut
|
|
10
|
+
off. Typing searches every box; Enter adds the first match.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from __future__ import annotations
|
|
@@ -14,19 +16,20 @@ from pathlib import Path
|
|
|
14
16
|
|
|
15
17
|
from PySide6.QtCore import QPoint, Qt, Signal
|
|
16
18
|
from PySide6.QtGui import QColor
|
|
17
|
-
from PySide6.QtWidgets import (QFrame, QGraphicsDropShadowEffect, QGridLayout, QLineEdit, QPushButton,
|
|
18
|
-
QWidget)
|
|
19
|
+
from PySide6.QtWidgets import (QFrame, QGraphicsDropShadowEffect, QGridLayout, QLineEdit, QPushButton, QScrollArea,
|
|
20
|
+
QSizePolicy, QVBoxLayout, QWidget)
|
|
19
21
|
|
|
20
|
-
from ...flow.library import
|
|
22
|
+
from ...flow.library import Library
|
|
21
23
|
from .. import theme
|
|
22
24
|
from ..studio import motion, style
|
|
23
25
|
from ..studio.widgets import hbox, label, vbox
|
|
24
26
|
from ..studio.workspace import display_name, file_kind
|
|
25
27
|
|
|
26
|
-
#:
|
|
27
|
-
CORE_GROUPS = GROUP_ORDER[:6]
|
|
28
|
+
#: How many boxes side by side in a group's section.
|
|
28
29
|
COLUMNS = 3
|
|
29
|
-
#:
|
|
30
|
+
#: The list scrolls beyond this height (the card stays a popover, not a page).
|
|
31
|
+
MAX_LIST_HEIGHT = 460
|
|
32
|
+
#: How many of the folder's files the Input section lists before "… more".
|
|
30
33
|
FILE_LIMIT = 6
|
|
31
34
|
#: The input box for each kind of file in the folder (see workspace.file_kind).
|
|
32
35
|
INPUT_BOXES = {"log": "open_log", "ocel": "open_ocel", "petri": "open_net", "cpn": "open_cpn",
|
|
@@ -87,7 +90,6 @@ class BoxPicker(QWidget):
|
|
|
87
90
|
self.card.setGraphicsEffect(shadow)
|
|
88
91
|
self.library = library
|
|
89
92
|
self.folder = folder
|
|
90
|
-
self.show_all = False
|
|
91
93
|
self.file_buttons: list[QPushButton] = []
|
|
92
94
|
self.search = QLineEdit()
|
|
93
95
|
self.search.setObjectName("boxSearch")
|
|
@@ -95,17 +97,24 @@ class BoxPicker(QWidget):
|
|
|
95
97
|
self.search.setClearButtonEnabled(True)
|
|
96
98
|
self.search.textChanged.connect(lambda _text: self.refill())
|
|
97
99
|
self.search.returnPressed.connect(self._choose_first)
|
|
100
|
+
# The sections, in a list that scrolls when there are more than fit.
|
|
98
101
|
self.groups_host = QWidget()
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
102
|
-
self.
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
105
|
-
self.
|
|
106
|
-
self.
|
|
107
|
-
self.
|
|
108
|
-
self.
|
|
102
|
+
self.groups_host.setObjectName("plain")
|
|
103
|
+
self.sections = QVBoxLayout(self.groups_host)
|
|
104
|
+
self.sections.setContentsMargins(0, 0, 8, 0) # room for the scrollbar
|
|
105
|
+
self.sections.setSpacing(14)
|
|
106
|
+
self.scroll = QScrollArea()
|
|
107
|
+
self.scroll.setObjectName("boxPickerList")
|
|
108
|
+
self.scroll.setWidgetResizable(True)
|
|
109
|
+
self.scroll.setFrameShape(QFrame.NoFrame)
|
|
110
|
+
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
111
|
+
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
112
|
+
self.scroll.setStyleSheet("QScrollArea#boxPickerList, QScrollArea#boxPickerList > QWidget > QWidget "
|
|
113
|
+
"{ background: transparent; }")
|
|
114
|
+
self.scroll.setWidget(self.groups_host)
|
|
115
|
+
self.footer = label("", "muted", wrap=True)
|
|
116
|
+
self.card.setLayout(vbox(self.search, self.scroll, self.footer, spacing=12, margins=(16, 16, 16, 16)))
|
|
117
|
+
self.card.setMinimumWidth(600)
|
|
109
118
|
self.setLayout(vbox(self.card, margins=(SHADOW_MARGIN,) * 4))
|
|
110
119
|
self.buttons: list[QPushButton] = []
|
|
111
120
|
self.refill()
|
|
@@ -114,7 +123,6 @@ class BoxPicker(QWidget):
|
|
|
114
123
|
def open_at(self, point: QPoint) -> None:
|
|
115
124
|
"""Show the popover with the card's top-left corner at ``point`` (global), kept on screen."""
|
|
116
125
|
self.search.clear()
|
|
117
|
-
self.show_all = False
|
|
118
126
|
self.refill()
|
|
119
127
|
self.adjustSize()
|
|
120
128
|
screen = self.screen().availableGeometry() if self.screen() else None
|
|
@@ -135,74 +143,105 @@ class BoxPicker(QWidget):
|
|
|
135
143
|
|
|
136
144
|
# -- filling -----------------------------------------------------------------------------
|
|
137
145
|
def shown_groups(self) -> dict[str, list]:
|
|
138
|
-
"""Group → boxes to list now: the
|
|
139
|
-
|
|
146
|
+
"""Group → boxes to list now: every group, in the library's order, or
|
|
147
|
+
what matches the search (a search looks everywhere)."""
|
|
140
148
|
wanted = self.search.text().strip().lower()
|
|
141
149
|
groups = {}
|
|
142
150
|
for group, specs in self.library.by_group().items():
|
|
143
|
-
if not wanted and not self.show_all and group not in CORE_GROUPS:
|
|
144
|
-
continue
|
|
145
151
|
shown = [s for s in specs if not wanted or _matches(s, group, wanted)]
|
|
146
152
|
if shown:
|
|
147
153
|
groups[group] = shown
|
|
148
154
|
return groups
|
|
149
155
|
|
|
150
156
|
def refill(self) -> None:
|
|
151
|
-
while self.
|
|
152
|
-
item = self.
|
|
157
|
+
while self.sections.count():
|
|
158
|
+
item = self.sections.takeAt(0)
|
|
153
159
|
widget = item.widget()
|
|
154
160
|
if widget is not None:
|
|
155
161
|
widget.hide() # gone now, not at the next event loop turn: no ghosts under the new rows
|
|
156
162
|
widget.setParent(None)
|
|
157
163
|
widget.deleteLater()
|
|
158
164
|
self.buttons, self.file_buttons = [], []
|
|
159
|
-
t = style.tokens()
|
|
160
165
|
groups = self.shown_groups()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
files, _more = self.shown_files()
|
|
167
|
+
|
|
168
|
+
def add(widget: QWidget) -> None:
|
|
169
|
+
self.sections.addWidget(widget)
|
|
170
|
+
widget.show() # now, not at the next event loop turn: the list is measured below
|
|
171
|
+
|
|
172
|
+
for group, specs in groups.items():
|
|
173
|
+
add(self._section(group, specs, with_files=group == "Input"))
|
|
174
|
+
if "Input" not in groups and files:
|
|
175
|
+
# A search that matches files but no box: still a section for them.
|
|
176
|
+
add(self._section("Input", [], with_files=True))
|
|
177
|
+
elif not groups:
|
|
178
|
+
add(label("No box matches. Try another word, or clear the search.", "muted", wrap=True))
|
|
179
|
+
for broken in self.library.broken:
|
|
180
|
+
note = label(f"{Path(broken.file).name}: {broken.reason}", "muted", wrap=True)
|
|
181
|
+
note.setStyleSheet(f"color: {style.STATUS['critical']};")
|
|
182
|
+
note.setToolTip("This box file could not be loaded; fix it and it reloads when saved")
|
|
183
|
+
add(note)
|
|
184
|
+
self.sections.addStretch(1)
|
|
185
|
+
self._refresh_footer()
|
|
186
|
+
# The list takes the height of its sections, up to the limit; past
|
|
187
|
+
# that it scrolls. (A scroll area's own size hint is a guess.)
|
|
188
|
+
self.groups_host.adjustSize()
|
|
189
|
+
self.scroll.setFixedHeight(min(self.groups_host.sizeHint().height() + 2, MAX_LIST_HEIGHT))
|
|
190
|
+
self.scroll.verticalScrollBar().setValue(0)
|
|
191
|
+
for layout in (self.card.layout(), self.layout()):
|
|
192
|
+
layout.activate() # the card follows the list's new height at once, not a turn later
|
|
193
|
+
self.adjustSize()
|
|
194
|
+
|
|
195
|
+
def _section(self, group: str, specs: list, with_files: bool = False) -> QWidget:
|
|
196
|
+
"""One group: its name on a rule, then its boxes in columns (and, for
|
|
197
|
+
Input, the folder's own files under a caption of their own)."""
|
|
198
|
+
t = style.tokens()
|
|
199
|
+
section = QWidget()
|
|
200
|
+
section.setObjectName("plain")
|
|
201
|
+
rows = QVBoxLayout(section)
|
|
202
|
+
rows.setContentsMargins(0, 0, 0, 0)
|
|
203
|
+
rows.setSpacing(4)
|
|
204
|
+
rows.addLayout(self._heading(group.upper()))
|
|
205
|
+
if specs:
|
|
206
|
+
grid = self._grid()
|
|
207
|
+
for index, spec in enumerate(specs):
|
|
169
208
|
choice = QPushButton(spec.name + (" (Yours)" if spec.custom else ""))
|
|
170
209
|
choice.setObjectName("boxChoice")
|
|
171
210
|
choice.setCursor(Qt.PointingHandCursor)
|
|
211
|
+
choice.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
172
212
|
tip = (spec.help or "").strip().split("\n\n")[0].replace("\n", " ")
|
|
173
213
|
if not spec.available:
|
|
174
214
|
tip += f"\n({spec.unavailable_reason})"
|
|
175
215
|
choice.setStyleSheet(f"color: {t.text_muted};")
|
|
176
216
|
choice.setToolTip(tip)
|
|
177
217
|
choice.clicked.connect(lambda _checked=False, box_id=spec.id: self._choose(box_id))
|
|
178
|
-
|
|
218
|
+
grid.addWidget(choice, index // COLUMNS, index % COLUMNS)
|
|
179
219
|
self.buttons.append(choice)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
rows.addStretch(1)
|
|
183
|
-
self.grid.addWidget(column, index // COLUMNS, index % COLUMNS, Qt.AlignTop)
|
|
184
|
-
files, _more = self.shown_files()
|
|
185
|
-
if "Input" not in groups and files:
|
|
186
|
-
# A search that matches files but no box: still a column for them.
|
|
187
|
-
column = QWidget()
|
|
188
|
-
column.setObjectName("plain")
|
|
189
|
-
rows = QVBoxLayout(column)
|
|
190
|
-
rows.setContentsMargins(0, 0, 0, 0)
|
|
191
|
-
rows.setSpacing(2)
|
|
192
|
-
rows.addWidget(label("INPUT", "sectionLabel"))
|
|
220
|
+
rows.addLayout(grid)
|
|
221
|
+
if with_files:
|
|
193
222
|
self._add_files(rows)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
return section
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _heading(text: str) -> "QHBoxLayout":
|
|
227
|
+
"""A section's name, with a thin rule running to the right edge."""
|
|
228
|
+
rule = QFrame()
|
|
229
|
+
rule.setFixedHeight(1)
|
|
230
|
+
rule.setStyleSheet(f"background: {style.tokens().border};")
|
|
231
|
+
rule.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
232
|
+
heading = hbox(label(text, "sectionLabel"), rule, spacing=10)
|
|
233
|
+
heading.setAlignment(rule, Qt.AlignVCenter)
|
|
234
|
+
return heading
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _grid() -> QGridLayout:
|
|
238
|
+
grid = QGridLayout()
|
|
239
|
+
grid.setContentsMargins(0, 2, 0, 0)
|
|
240
|
+
grid.setHorizontalSpacing(10)
|
|
241
|
+
grid.setVerticalSpacing(2)
|
|
242
|
+
for column in range(COLUMNS):
|
|
243
|
+
grid.setColumnStretch(column, 1)
|
|
244
|
+
return grid
|
|
206
245
|
|
|
207
246
|
def shown_files(self) -> tuple[list[tuple[str, str]], int]:
|
|
208
247
|
"""The folder's files to list under Input (those with an input box),
|
|
@@ -220,11 +259,12 @@ class BoxPicker(QWidget):
|
|
|
220
259
|
if not files:
|
|
221
260
|
return
|
|
222
261
|
from ..studio.app import _faded_icon # the sidebar's file icons (no cycle: app is loaded)
|
|
223
|
-
rows.addSpacing(
|
|
262
|
+
rows.addSpacing(8)
|
|
224
263
|
rows.addWidget(label("IN THIS FOLDER", "sectionLabel"))
|
|
225
264
|
names = [Path(relative).name for relative, _kind in files]
|
|
226
265
|
icons = {"log": "log", "ocel": "log", "cpn": "cpn", "ts": "ts", "petri": "model"}
|
|
227
|
-
|
|
266
|
+
grid = self._grid()
|
|
267
|
+
for index, (relative, kind) in enumerate(files):
|
|
228
268
|
name = Path(relative).name
|
|
229
269
|
text = display_name(name)
|
|
230
270
|
if names.count(name) > 1 and "/" in relative:
|
|
@@ -233,10 +273,12 @@ class BoxPicker(QWidget):
|
|
|
233
273
|
choice.setObjectName("boxChoice")
|
|
234
274
|
choice.setIcon(_faded_icon(icons.get(kind, "model")))
|
|
235
275
|
choice.setCursor(Qt.PointingHandCursor)
|
|
276
|
+
choice.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
236
277
|
choice.setToolTip(f"{relative}\nAdds an input box that reads this file")
|
|
237
278
|
choice.clicked.connect(lambda _checked=False, r=relative, k=kind: self._choose_file(k, r))
|
|
238
|
-
|
|
279
|
+
grid.addWidget(choice, index // COLUMNS, index % COLUMNS)
|
|
239
280
|
self.file_buttons.append(choice)
|
|
281
|
+
rows.addLayout(grid)
|
|
240
282
|
if more:
|
|
241
283
|
rows.addWidget(label(f"… {more} more (Choose… in Settings)", "muted"))
|
|
242
284
|
|
|
@@ -248,25 +290,12 @@ class BoxPicker(QWidget):
|
|
|
248
290
|
self.file_chosen.emit(box_id, relative)
|
|
249
291
|
|
|
250
292
|
def _refresh_footer(self) -> None:
|
|
251
|
-
searching = bool(self.search.text().strip())
|
|
252
|
-
rest = [g for g in self.library.by_group() if g not in CORE_GROUPS]
|
|
253
293
|
total = len(self.library)
|
|
254
|
-
|
|
255
|
-
self.
|
|
256
|
-
|
|
257
|
-
if not self.footer_visible:
|
|
258
|
-
return
|
|
259
|
-
if self.show_all:
|
|
260
|
-
self.more_text.setText(f"All {total} boxes.")
|
|
261
|
-
self.more_button.setText("Core groups only ◂")
|
|
294
|
+
shown = len(self.buttons)
|
|
295
|
+
if self.search.text().strip():
|
|
296
|
+
self.footer.setText(f"{shown} of {total} boxes match. Return adds the first.")
|
|
262
297
|
else:
|
|
263
|
-
|
|
264
|
-
self.more_text.setText(f"More: {names}.")
|
|
265
|
-
self.more_button.setText(f"Show all {total} ▸")
|
|
266
|
-
|
|
267
|
-
def _toggle_all(self) -> None:
|
|
268
|
-
self.show_all = not self.show_all
|
|
269
|
-
self.refill()
|
|
298
|
+
self.footer.setText(f"All {total} boxes, by group. Type to search them; Return adds the first match.")
|
|
270
299
|
|
|
271
300
|
# -- choosing ---------------------------------------------------------------------------
|
|
272
301
|
def _choose(self, box_id: str) -> None:
|
|
@@ -285,8 +314,4 @@ def _matches(spec, group: str, wanted: str) -> bool:
|
|
|
285
314
|
return all(word in haystack for word in wanted.split())
|
|
286
315
|
|
|
287
316
|
|
|
288
|
-
|
|
289
|
-
return "your own boxes" if group == "Yours" else group
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
__all__ = ["BoxPicker", "CORE_GROUPS", "folder_files", "input_box_for"]
|
|
317
|
+
__all__ = ["BoxPicker", "folder_files", "input_box_for"]
|