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.
Files changed (230) hide show
  1. {openprocess-0.11.0/openprocess.egg-info → openprocess-0.11.3}/PKG-INFO +17 -12
  2. {openprocess-0.11.0 → openprocess-0.11.3}/README.md +16 -11
  3. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/__init__.py +1 -1
  4. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/cli.py +2 -0
  5. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/check.py +6 -1
  6. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/convert.py +76 -8
  7. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/canvas.py +10 -5
  8. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/canvas.py +2 -0
  9. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/picker.py +113 -88
  10. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/viewers.py +5 -2
  11. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/app.py +74 -23
  12. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/cpn_page.py +55 -25
  13. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/derivation_view.py +3 -1
  14. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/graph_view.py +46 -0
  15. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/model_page.py +21 -4
  16. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/petri_page.py +5 -0
  17. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/sidebar.py +6 -4
  18. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/tool_icons.py +27 -20
  19. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/alignments.py +86 -7
  20. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/inductive.py +133 -24
  21. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/references.py +5 -2
  22. {openprocess-0.11.0 → openprocess-0.11.3/openprocess.egg-info}/PKG-INFO +17 -12
  23. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_flow.py +20 -0
  24. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_flow_gui.py +47 -8
  25. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_mining.py +71 -0
  26. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_studio_gui.py +94 -3
  27. {openprocess-0.11.0 → openprocess-0.11.3}/LICENSE +0 -0
  28. {openprocess-0.11.0 → openprocess-0.11.3}/cpnpy/__init__.py +0 -0
  29. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/analysis/__init__.py +0 -0
  30. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/analysis/state_space.py +0 -0
  31. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/analysis/state_space_process.py +0 -0
  32. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/citation.py +0 -0
  33. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  34. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  35. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  36. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  37. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  38. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  39. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  40. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  41. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  42. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  43. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  44. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  45. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  46. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  47. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  48. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  49. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  50. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  51. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  52. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/exercises/pack.md +0 -0
  53. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/__init__.py +0 -0
  54. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/box.py +0 -0
  55. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/__init__.py +0 -0
  56. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/compare.py +0 -0
  57. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/cpn.py +0 -0
  58. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/discover.py +0 -0
  59. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/filter.py +0 -0
  60. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/input.py +0 -0
  61. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/output.py +0 -0
  62. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/predict.py +0 -0
  63. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/science.py +0 -0
  64. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/boxes/sweeps.py +0 -0
  65. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/datasets.py +0 -0
  66. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/explain.py +0 -0
  67. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/figures.py +0 -0
  68. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/history.py +0 -0
  69. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/library.py +0 -0
  70. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/provenance.py +0 -0
  71. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/record.py +0 -0
  72. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/report.py +0 -0
  73. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/reproducibility.py +0 -0
  74. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/runner.py +0 -0
  75. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/script.py +0 -0
  76. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/snapshots.py +0 -0
  77. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/sweep.py +0 -0
  78. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/types.py +0 -0
  79. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/flow/workflow.py +0 -0
  80. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/__init__.py +0 -0
  81. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/app.py +0 -0
  82. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/arc_editing.py +0 -0
  83. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/__init__.py +0 -0
  84. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/page.py +0 -0
  85. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/runs.py +0 -0
  86. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/stepper.py +0 -0
  87. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/summary.py +0 -0
  88. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/flow/templates.py +0 -0
  89. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/items.py +0 -0
  90. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/learn/answer_boxes.py +0 -0
  91. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/learn/concealment.py +0 -0
  92. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/learn/mode.py +0 -0
  93. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/panning.py +0 -0
  94. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/resources/openprocess-icon.png +0 -0
  95. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/__init__.py +0 -0
  96. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/__main__.py +0 -0
  97. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/charts.py +0 -0
  98. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/cite.py +0 -0
  99. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/command_palette.py +0 -0
  100. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/compare_page.py +0 -0
  101. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/connections.py +0 -0
  102. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/definition_view.py +0 -0
  103. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/documents.py +0 -0
  104. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/dotted_chart.py +0 -0
  105. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/file_dialogs.py +0 -0
  106. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/filter_dialog.py +0 -0
  107. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/graph_builders.py +0 -0
  108. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/instances.py +0 -0
  109. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/log_editor.py +0 -0
  110. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/log_page.py +0 -0
  111. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/markdown_view.py +0 -0
  112. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/mathtext.py +0 -0
  113. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/ml_highlighter.py +0 -0
  114. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/motion.py +0 -0
  115. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/net_comparison.py +0 -0
  116. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/notes_overlay.py +0 -0
  117. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/provenance.py +0 -0
  118. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/regions_view.py +0 -0
  119. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/spaces.py +0 -0
  120. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/style.py +0 -0
  121. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/updates.py +0 -0
  122. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/widgets.py +0 -0
  123. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/workers.py +0 -0
  124. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/studio/workspace.py +0 -0
  125. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/theme.py +0 -0
  126. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/gui/tidy.py +0 -0
  127. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/io/__init__.py +0 -0
  128. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/io/cpn_reader.py +0 -0
  129. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/io/cpn_writer.py +0 -0
  130. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/__init__.py +0 -0
  131. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/answers.py +0 -0
  132. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/checks.py +0 -0
  133. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/computed.py +0 -0
  134. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/context.py +0 -0
  135. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/exam.py +0 -0
  136. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/exercise-packs.md +0 -0
  137. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/importer.py +0 -0
  138. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/notation.py +0 -0
  139. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/pack.py +0 -0
  140. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/learn/sheet.py +0 -0
  141. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/__init__.py +0 -0
  142. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/analysis.py +0 -0
  143. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/columns.py +0 -0
  144. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/compare_nets.py +0 -0
  145. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/__init__.py +0 -0
  146. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/quality.py +0 -0
  147. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/conformance/token_replay.py +0 -0
  148. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/csv_import.py +0 -0
  149. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/definitions.py +0 -0
  150. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/dfg.py +0 -0
  151. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/__init__.py +0 -0
  152. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/alpha.py +0 -0
  153. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/heuristics.py +0 -0
  154. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/discovery/state_regions.py +0 -0
  155. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/filtering.py +0 -0
  156. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/footprint.py +0 -0
  157. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/invariants.py +0 -0
  158. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/layout.py +0 -0
  159. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/log.py +0 -0
  160. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/ocel.py +0 -0
  161. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/petrinet.py +0 -0
  162. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/playout.py +0 -0
  163. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/pm4py_bridge.py +0 -0
  164. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/pnml.py +0 -0
  165. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/processtree.py +0 -0
  166. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/regions.py +0 -0
  167. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/stats.py +0 -0
  168. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/structure.py +0 -0
  169. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/transition_system.py +0 -0
  170. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/mining/xes.py +0 -0
  171. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/__init__.py +0 -0
  172. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/ast_nodes.py +0 -0
  173. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/builtins.py +0 -0
  174. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/colorsets.py +0 -0
  175. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/errors.py +0 -0
  176. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/evaluator.py +0 -0
  177. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/lexer.py +0 -0
  178. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/multiset.py +0 -0
  179. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/parser.py +0 -0
  180. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/ml/values.py +0 -0
  181. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/__init__.py +0 -0
  182. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/declarations.py +0 -0
  183. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/examples.py +0 -0
  184. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/net.py +0 -0
  185. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/model/plain.py +0 -0
  186. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/packages.py +0 -0
  187. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/__init__.py +0 -0
  188. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/binding.py +0 -0
  189. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/export.py +0 -0
  190. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/sim/simulator.py +0 -0
  191. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/__init__.py +0 -0
  192. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/answers.py +0 -0
  193. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/checks.py +0 -0
  194. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/pack.py +0 -0
  195. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess/teaching/sheet.py +0 -0
  196. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/SOURCES.txt +0 -0
  197. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/dependency_links.txt +0 -0
  198. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/entry_points.txt +0 -0
  199. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/requires.txt +0 -0
  200. {openprocess-0.11.0 → openprocess-0.11.3}/openprocess.egg-info/top_level.txt +0 -0
  201. {openprocess-0.11.0 → openprocess-0.11.3}/pyproject.toml +0 -0
  202. {openprocess-0.11.0 → openprocess-0.11.3}/setup.cfg +0 -0
  203. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_arc_editing.py +0 -0
  204. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_citation.py +0 -0
  205. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_colorsets.py +0 -0
  206. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_columns.py +0 -0
  207. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_cpn_course_model.py +0 -0
  208. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_definitions.py +0 -0
  209. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_exercises_gui.py +0 -0
  210. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_hierarchy.py +0 -0
  211. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_history.py +0 -0
  212. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_io.py +0 -0
  213. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_learn.py +0 -0
  214. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_learn_gui.py +0 -0
  215. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_log_editing.py +0 -0
  216. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_ml.py +0 -0
  217. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_multiset.py +0 -0
  218. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_ocel.py +0 -0
  219. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_packages.py +0 -0
  220. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_regions.py +0 -0
  221. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_rename.py +0 -0
  222. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_report.py +0 -0
  223. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_simulation.py +0 -0
  224. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_snapshots.py +0 -0
  225. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_state_space.py +0 -0
  226. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_teaching.py +0 -0
  227. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_tidy.py +0 -0
  228. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_updates.py +0 -0
  229. {openprocess-0.11.0 → openprocess-0.11.3}/tests/test_wf_structure.py +0 -0
  230. {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.0
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
- Nothing to unfold except LOGS. An empty folder shows in both spaces until
285
- it has files.
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, by group:
788
- the six core groups side by side (Input, Filter, Discover, Check, Compare,
789
- Output) and one line for the rest (Science, Predict, Coloured nets, Sweep,
790
- your own boxes) with *Show all*. Under *Input*, **the folder's own files**:
791
- one click adds the box that reads the file, already set. Type to search
792
- them all: "alpha" finds the α-algorithm, "boarding" the log; Enter adds the
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
- Nothing to unfold except LOGS. An empty folder shows in both spaces until
246
- it has files.
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, by group:
749
- the six core groups side by side (Input, Filter, Discover, Check, Compare,
750
- Output) and one line for the rest (Science, Predict, Coloured nets, Sweep,
751
- your own boxes) with *Show all*. Under *Input*, **the folder's own files**:
752
- one click adds the box that reads the file, already set. Type to search
753
- them all: "alpha" finds the α-algorithm, "boarding" the log; Enter adds the
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.0"
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)] for a in result.alignments]
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 any((name, _name(target)) in _converters for name in _class_names(value))
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
- for name in _class_names(value):
50
- if name == wanted:
51
- return value
52
- function = _converters.get((name, wanted))
53
- if function is not None:
54
- return function(value)
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, the six core groups (Input, Filter, Discover,
6
- Check, Compare, Output) side by side, and one line for the rest (Science,
7
- Predict, Coloured nets, Sweep, a folder's own boxes) with *Show all*.
8
- Typing searches every box, core or not. Enter adds the first match.
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, QVBoxLayout,
18
- QWidget)
19
+ from PySide6.QtWidgets import (QFrame, QGraphicsDropShadowEffect, QGridLayout, QLineEdit, QPushButton, QScrollArea,
20
+ QSizePolicy, QVBoxLayout, QWidget)
19
21
 
20
- from ...flow.library import GROUP_ORDER, Library
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
- #: The groups shown before *Show all*: what most workflows are made of.
27
- CORE_GROUPS = GROUP_ORDER[:6]
28
+ #: How many boxes side by side in a group's section.
28
29
  COLUMNS = 3
29
- #: How many of the folder's files the Input group lists before "… more".
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.grid = QGridLayout(self.groups_host)
100
- self.grid.setContentsMargins(0, 0, 0, 0)
101
- self.grid.setHorizontalSpacing(14)
102
- self.grid.setVerticalSpacing(10)
103
- self.more_text = label("", "muted", wrap=True)
104
- self.more_button = QPushButton()
105
- self.more_button.clicked.connect(self._toggle_all)
106
- self.footer = hbox(self.more_text, None, self.more_button)
107
- self.card.setLayout(vbox(self.search, self.groups_host, self.footer, spacing=12, margins=(16, 16, 16, 16)))
108
- self.card.setMinimumWidth(560)
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 core groups, every group when
139
- *Show all* is on or a search is typed (a search looks everywhere)."""
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.grid.count():
152
- item = self.grid.takeAt(0)
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
- for index, (group, specs) in enumerate(groups.items()):
162
- column = QWidget()
163
- column.setObjectName("plain")
164
- rows = QVBoxLayout(column)
165
- rows.setContentsMargins(0, 0, 0, 0)
166
- rows.setSpacing(2)
167
- rows.addWidget(label(group.upper(), "sectionLabel"))
168
- for spec in specs:
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
- rows.addWidget(choice)
218
+ grid.addWidget(choice, index // COLUMNS, index % COLUMNS)
179
219
  self.buttons.append(choice)
180
- if group == "Input":
181
- self._add_files(rows)
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
- rows.addStretch(1)
195
- self.grid.addWidget(column, len(groups) // COLUMNS, len(groups) % COLUMNS, Qt.AlignTop)
196
- elif not groups:
197
- self.grid.addWidget(label("No box matches. Try another word, or clear the search.", "muted", wrap=True),
198
- 0, 0, 1, COLUMNS)
199
- for broken in self.library.broken if (self.show_all or self.search.text().strip()) else []:
200
- note = label(f"{Path(broken.file).name}: {broken.reason}", "muted", wrap=True)
201
- note.setStyleSheet(f"color: {style.STATUS['critical']};")
202
- note.setToolTip("This box file could not be loaded; fix it and it reloads when saved")
203
- self.grid.addWidget(note, self.grid.rowCount(), 0, 1, COLUMNS)
204
- self._refresh_footer()
205
- self.adjustSize()
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(6)
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
- for relative, kind in files:
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
- rows.addWidget(choice)
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
- self.footer_visible = bool(rest) and not searching
255
- self.more_text.setVisible(self.footer_visible)
256
- self.more_button.setVisible(self.footer_visible)
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
- names = ", ".join(_plain(g) for g in rest)
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
- def _plain(group: str) -> str:
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"]