pySimBlocks 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. examples/advanced/hardware/block_diagram.py +162 -0
  2. examples/advanced/hardware/camera.py +93 -0
  3. examples/advanced/hardware/gui_emio.py +231 -0
  4. examples/advanced/hardware/lmi.py +69 -0
  5. examples/advanced/hardware/main.py +57 -0
  6. examples/advanced/hardware/parameters.py +54 -0
  7. examples/advanced/qp_problem/params.py +10 -0
  8. examples/advanced/qp_problem/run.py +14 -0
  9. examples/advanced/sofa/gui/finger/Finger.py +108 -0
  10. examples/advanced/sofa/gui/finger/FingerController.py +53 -0
  11. examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
  12. examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
  13. examples/advanced/sofa/python/Finger.py +107 -0
  14. examples/advanced/sofa/python/FingerController.py +48 -0
  15. examples/advanced/sofa/python/closedloop.py +64 -0
  16. examples/advanced/sofa/python/openloop.py +58 -0
  17. examples/basics/algebraic_function/g.py +7 -0
  18. examples/basics/algebraic_function/run.py +14 -0
  19. examples/basics/dc_motor/gui/run.py +14 -0
  20. examples/basics/dc_motor/multiple_order.py +91 -0
  21. examples/basics/dc_motor/openloop.py +62 -0
  22. examples/basics/dc_motor/pi.py +99 -0
  23. examples/basics/dc_motor/pid.py +118 -0
  24. examples/basics/dc_motor/pid_block.py +82 -0
  25. examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
  26. examples/basics/external/parameters.py +4 -0
  27. examples/basics/external/run.py +14 -0
  28. examples/basics/file_sources/temp.py +16 -0
  29. examples/basics/observer.py +76 -0
  30. examples/basics/state_feedback/gui/parameters.py +9 -0
  31. examples/basics/state_feedback/gui/run.py +14 -0
  32. examples/basics/state_feedback/state_feedback.py +102 -0
  33. examples/basics/state_space/linear/gui/run.py +14 -0
  34. examples/basics/state_space/linear/ss_block.py +89 -0
  35. examples/basics/state_space/linear/ss_manual.py +105 -0
  36. examples/basics/state_space/non_linear/nlss.py +47 -0
  37. examples/basics/state_space/non_linear/run.py +14 -0
  38. examples/basics/state_space/polytope/params.py +39 -0
  39. examples/basics/state_space/polytope/weights.py +12 -0
  40. examples/comparison/simulink/convert.py +29 -0
  41. examples/comparison/simulink/openloop.py +127 -0
  42. examples/comparison/simulink/pi.py +148 -0
  43. examples/comparison/simulink/pid.py +154 -0
  44. examples/comparison/simulink/pid_backward.py +155 -0
  45. examples/quick_start/filter.py +35 -0
  46. examples/quick_start/gui/run.py +14 -0
  47. examples/tutorials/tutorial_1_python/main.py +99 -0
  48. examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
  49. examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
  50. pySimBlocks/__init__.py +35 -0
  51. pySimBlocks/blocks/__init__.py +68 -0
  52. pySimBlocks/blocks/controllers/__init__.py +27 -0
  53. pySimBlocks/blocks/controllers/pid.py +250 -0
  54. pySimBlocks/blocks/controllers/state_feedback.py +141 -0
  55. pySimBlocks/blocks/interfaces/__init__.py +27 -0
  56. pySimBlocks/blocks/interfaces/external_input.py +107 -0
  57. pySimBlocks/blocks/interfaces/external_output.py +113 -0
  58. pySimBlocks/blocks/observers/__init__.py +23 -0
  59. pySimBlocks/blocks/observers/luenberger.py +175 -0
  60. pySimBlocks/blocks/operators/__init__.py +47 -0
  61. pySimBlocks/blocks/operators/algebraic_function.py +258 -0
  62. pySimBlocks/blocks/operators/dead_zone.py +186 -0
  63. pySimBlocks/blocks/operators/delay.py +254 -0
  64. pySimBlocks/blocks/operators/demux.py +123 -0
  65. pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
  66. pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
  67. pySimBlocks/blocks/operators/gain.py +294 -0
  68. pySimBlocks/blocks/operators/mux.py +131 -0
  69. pySimBlocks/blocks/operators/product.py +194 -0
  70. pySimBlocks/blocks/operators/rate_limiter.py +214 -0
  71. pySimBlocks/blocks/operators/saturation.py +178 -0
  72. pySimBlocks/blocks/operators/sum.py +188 -0
  73. pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
  74. pySimBlocks/blocks/optimizers/__init__.py +23 -0
  75. pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
  76. pySimBlocks/blocks/sources/__init__.py +37 -0
  77. pySimBlocks/blocks/sources/chirp.py +168 -0
  78. pySimBlocks/blocks/sources/constant.py +82 -0
  79. pySimBlocks/blocks/sources/file_source.py +294 -0
  80. pySimBlocks/blocks/sources/function_source.py +197 -0
  81. pySimBlocks/blocks/sources/ramp.py +99 -0
  82. pySimBlocks/blocks/sources/sinusoidal.py +113 -0
  83. pySimBlocks/blocks/sources/step.py +138 -0
  84. pySimBlocks/blocks/sources/white_noise.py +107 -0
  85. pySimBlocks/blocks/systems/__init__.py +27 -0
  86. pySimBlocks/blocks/systems/linear_state_space.py +180 -0
  87. pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
  88. pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
  89. pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
  90. pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
  91. pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
  92. pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
  93. pySimBlocks/cli.py +127 -0
  94. pySimBlocks/core/__init__.py +32 -0
  95. pySimBlocks/core/block.py +167 -0
  96. pySimBlocks/core/block_source.py +94 -0
  97. pySimBlocks/core/config.py +90 -0
  98. pySimBlocks/core/fixed_time_manager.py +46 -0
  99. pySimBlocks/core/model.py +250 -0
  100. pySimBlocks/core/scheduler.py +30 -0
  101. pySimBlocks/core/simulator.py +329 -0
  102. pySimBlocks/core/task.py +73 -0
  103. pySimBlocks/docs/blocks/controllers/pid.md +78 -0
  104. pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
  105. pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
  106. pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
  107. pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
  108. pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
  109. pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
  110. pySimBlocks/docs/blocks/operators/delay.md +76 -0
  111. pySimBlocks/docs/blocks/operators/demux.md +64 -0
  112. pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
  113. pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
  114. pySimBlocks/docs/blocks/operators/gain.md +97 -0
  115. pySimBlocks/docs/blocks/operators/mux.md +64 -0
  116. pySimBlocks/docs/blocks/operators/product.md +51 -0
  117. pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
  118. pySimBlocks/docs/blocks/operators/saturation.md +62 -0
  119. pySimBlocks/docs/blocks/operators/sum.md +49 -0
  120. pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
  121. pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
  122. pySimBlocks/docs/blocks/sources/chirp.md +102 -0
  123. pySimBlocks/docs/blocks/sources/constant.md +53 -0
  124. pySimBlocks/docs/blocks/sources/file_source.md +51 -0
  125. pySimBlocks/docs/blocks/sources/function_source.md +52 -0
  126. pySimBlocks/docs/blocks/sources/ramp.md +58 -0
  127. pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
  128. pySimBlocks/docs/blocks/sources/step.md +63 -0
  129. pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
  130. pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
  131. pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
  132. pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
  133. pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
  134. pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
  135. pySimBlocks/gui/__init__.py +20 -0
  136. pySimBlocks/gui/addons/__init__.py +19 -0
  137. pySimBlocks/gui/addons/sofa/__init__.py +19 -0
  138. pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
  139. pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
  140. pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
  141. pySimBlocks/gui/blocks/block_meta.py +408 -0
  142. pySimBlocks/gui/blocks/controllers/pid.py +116 -0
  143. pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
  144. pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
  145. pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
  146. pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
  147. pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
  148. pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
  149. pySimBlocks/gui/blocks/operators/delay.py +79 -0
  150. pySimBlocks/gui/blocks/operators/demux.py +98 -0
  151. pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
  152. pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
  153. pySimBlocks/gui/blocks/operators/gain.py +79 -0
  154. pySimBlocks/gui/blocks/operators/mux.py +100 -0
  155. pySimBlocks/gui/blocks/operators/product.py +109 -0
  156. pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
  157. pySimBlocks/gui/blocks/operators/saturation.py +72 -0
  158. pySimBlocks/gui/blocks/operators/sum.py +91 -0
  159. pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
  160. pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
  161. pySimBlocks/gui/blocks/parameter_meta.py +34 -0
  162. pySimBlocks/gui/blocks/port_meta.py +31 -0
  163. pySimBlocks/gui/blocks/sources/chirp.py +146 -0
  164. pySimBlocks/gui/blocks/sources/constant.py +63 -0
  165. pySimBlocks/gui/blocks/sources/file_source.py +196 -0
  166. pySimBlocks/gui/blocks/sources/function_source.py +202 -0
  167. pySimBlocks/gui/blocks/sources/ramp.py +73 -0
  168. pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
  169. pySimBlocks/gui/blocks/sources/step.py +83 -0
  170. pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
  171. pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
  172. pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
  173. pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
  174. pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
  175. pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
  176. pySimBlocks/gui/dialogs/__init__.py +19 -0
  177. pySimBlocks/gui/dialogs/block_dialog.py +125 -0
  178. pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
  179. pySimBlocks/gui/dialogs/help_dialog.py +39 -0
  180. pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
  181. pySimBlocks/gui/dialogs/settings/plots.py +187 -0
  182. pySimBlocks/gui/dialogs/settings/project.py +146 -0
  183. pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
  184. pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
  185. pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
  186. pySimBlocks/gui/editor.py +49 -0
  187. pySimBlocks/gui/graphics/__init__.py +29 -0
  188. pySimBlocks/gui/graphics/block_item.py +289 -0
  189. pySimBlocks/gui/graphics/connection_item.py +266 -0
  190. pySimBlocks/gui/graphics/port_item.py +153 -0
  191. pySimBlocks/gui/graphics/theme.py +119 -0
  192. pySimBlocks/gui/main_window.py +162 -0
  193. pySimBlocks/gui/models/__init__.py +31 -0
  194. pySimBlocks/gui/models/block_instance.py +114 -0
  195. pySimBlocks/gui/models/connection_instance.py +43 -0
  196. pySimBlocks/gui/models/port_instance.py +52 -0
  197. pySimBlocks/gui/models/project_simulation_params.py +50 -0
  198. pySimBlocks/gui/models/project_state.py +114 -0
  199. pySimBlocks/gui/project_controller.py +302 -0
  200. pySimBlocks/gui/services/__init__.py +20 -0
  201. pySimBlocks/gui/services/project_loader.py +381 -0
  202. pySimBlocks/gui/services/project_saver.py +68 -0
  203. pySimBlocks/gui/services/simulation_runner.py +66 -0
  204. pySimBlocks/gui/services/yaml_tools.py +261 -0
  205. pySimBlocks/gui/widgets/__init__.py +19 -0
  206. pySimBlocks/gui/widgets/block_list.py +131 -0
  207. pySimBlocks/gui/widgets/diagram_view.py +312 -0
  208. pySimBlocks/gui/widgets/toolbar_view.py +161 -0
  209. pySimBlocks/project/__init__.py +35 -0
  210. pySimBlocks/project/build_model.py +92 -0
  211. pySimBlocks/project/generate_run_script.py +109 -0
  212. pySimBlocks/project/generate_sofa_controller.py +215 -0
  213. pySimBlocks/project/load_project_config.py +197 -0
  214. pySimBlocks/project/load_simulation_config.py +166 -0
  215. pySimBlocks/project/load_simulator.py +45 -0
  216. pySimBlocks/project/plot_from_config.py +174 -0
  217. pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
  218. pySimBlocks/real_time/__init__.py +23 -0
  219. pySimBlocks/real_time/real_time_runner.py +148 -0
  220. pySimBlocks/tools/__init__.py +19 -0
  221. pySimBlocks/tools/blocks_registry.py +141 -0
  222. pySimBlocks/tools/generate_blocks_index.py +124 -0
  223. pysimblocks-0.1.0.dist-info/METADATA +180 -0
  224. pysimblocks-0.1.0.dist-info/RECORD +262 -0
  225. pysimblocks-0.1.0.dist-info/WHEEL +5 -0
  226. pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
  227. pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
  228. pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
  229. tests/blocks/controllers/test_pid.py +143 -0
  230. tests/blocks/controllers/test_state_feedback.py +193 -0
  231. tests/blocks/observers/test_luenberger.py +225 -0
  232. tests/blocks/operators/test_algebraic_function.py +230 -0
  233. tests/blocks/operators/test_dead_zone.py +106 -0
  234. tests/blocks/operators/test_delay.py +167 -0
  235. tests/blocks/operators/test_demux.py +111 -0
  236. tests/blocks/operators/test_discrete_derivator.py +160 -0
  237. tests/blocks/operators/test_discrete_integrator.py +149 -0
  238. tests/blocks/operators/test_gain.py +182 -0
  239. tests/blocks/operators/test_mux.py +101 -0
  240. tests/blocks/operators/test_product.py +171 -0
  241. tests/blocks/operators/test_rate_limiter.py +166 -0
  242. tests/blocks/operators/test_saturation.py +157 -0
  243. tests/blocks/operators/test_sum.py +183 -0
  244. tests/blocks/operators/test_zero_order_hold.py +106 -0
  245. tests/blocks/sources/test_chirp.py +157 -0
  246. tests/blocks/sources/test_constant.py +55 -0
  247. tests/blocks/sources/test_file_source.py +185 -0
  248. tests/blocks/sources/test_function_source.py +135 -0
  249. tests/blocks/sources/test_ramp.py +110 -0
  250. tests/blocks/sources/test_sinusoidal.py +113 -0
  251. tests/blocks/sources/test_step.py +76 -0
  252. tests/blocks/sources/test_white_noise.py +108 -0
  253. tests/blocks/systems/test_linear_state_space.py +135 -0
  254. tests/blocks/systems/test_polytopic_state_space.py +133 -0
  255. tests/core/test_core_errors_and_determinism.py +82 -0
  256. tests/core/test_core_multirate_tasks.py +88 -0
  257. tests/core/test_core_two_phase_and_propagation.py +132 -0
  258. tests/gui/main_window_open_test.py +162 -0
  259. tests/gui/test_block_meta_param_cache.py +16 -0
  260. tests/gui/test_file_source_meta.py +17 -0
  261. tests/gui/test_function_source_meta.py +28 -0
  262. tests/regression/test_simulink_dc_motor.py +217 -0
@@ -0,0 +1,88 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from PySide6.QtWidgets import (
22
+ QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget
23
+ )
24
+
25
+ from pySimBlocks.gui.dialogs.settings.project import ProjectSettingsWidget
26
+ from pySimBlocks.gui.dialogs.settings.simulation import SimulationSettingsWidget
27
+ from pySimBlocks.gui.dialogs.settings.plots import PlotSettingsWidget
28
+ from pySimBlocks.gui.models.project_state import ProjectState
29
+ from pySimBlocks.gui.project_controller import ProjectController
30
+
31
+
32
+ class SettingsDialog(QDialog):
33
+ def __init__(self, project_state: ProjectState, project_controller: ProjectController, parent=None):
34
+ super().__init__(parent)
35
+ self.setWindowTitle("Settings")
36
+ self.setMinimumWidth(500)
37
+
38
+ layout = QVBoxLayout(self)
39
+
40
+ # ---------------- Tabs ----------------
41
+ self.tabs = QTabWidget()
42
+ self.project_tab = ProjectSettingsWidget(project_state, project_controller, self)
43
+ self.simulation_tab = SimulationSettingsWidget(project_state, project_controller)
44
+ self.plots_tab = PlotSettingsWidget(project_state, project_controller)
45
+
46
+ self.tabs.addTab(self.project_tab, "Project")
47
+ self.tabs.addTab(self.simulation_tab, "Simulation")
48
+ self.tabs.addTab(self.plots_tab, "Plots")
49
+
50
+ self.tabs.currentChanged.connect(self._on_tab_changed)
51
+
52
+ layout.addWidget(self.tabs)
53
+
54
+ # ---------------- Buttons ----------------
55
+ buttons = QHBoxLayout()
56
+ buttons.addStretch()
57
+
58
+ ok_btn = QPushButton("Ok")
59
+ ok_btn.setDefault(True)
60
+ ok_btn.clicked.connect(self.ok)
61
+
62
+ apply_btn = QPushButton("Apply")
63
+ apply_btn.clicked.connect(self.apply)
64
+
65
+ buttons.addWidget(ok_btn)
66
+ buttons.addWidget(apply_btn)
67
+
68
+ layout.addLayout(buttons)
69
+
70
+ def ok(self):
71
+ self.apply()
72
+ self.accept()
73
+
74
+ def apply(self):
75
+ if not self.project_tab.apply():
76
+ return
77
+ self.simulation_tab.apply()
78
+
79
+ def refresh_tabs_from_project(self):
80
+ self.simulation_tab.refresh_from_project()
81
+ self.plots_tab.refresh_from_project()
82
+
83
+ def _on_tab_changed(self, index):
84
+ #save curr widget
85
+ widget = self.tabs.widget(index)
86
+
87
+ if hasattr(widget, "refresh_from_project"):
88
+ widget.refresh_from_project()
@@ -0,0 +1,92 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from PySide6.QtWidgets import (
22
+ QDialog, QLabel, QPushButton,
23
+ QHBoxLayout, QVBoxLayout, QStyle
24
+ )
25
+ from PySide6.QtCore import Qt
26
+
27
+
28
+ class UnsavedChangesDialog(QDialog):
29
+ SAVE = 1
30
+ DISCARD = 2
31
+ CANCEL = 3
32
+
33
+ def __init__(self, action_name: str, parent=None):
34
+ super().__init__(parent)
35
+
36
+ self.setWindowTitle("Unsaved changes")
37
+ self.setModal(True)
38
+
39
+ self.setWindowFlags(
40
+ Qt.Dialog |
41
+ Qt.WindowTitleHint |
42
+ Qt.WindowCloseButtonHint
43
+ )
44
+
45
+ # --- Icon (same as QMessageBox warning) ---
46
+ icon_label = QLabel()
47
+ icon = self.style().standardIcon(QStyle.SP_MessageBoxWarning)
48
+ icon_label.setPixmap(icon.pixmap(64, 64))
49
+ icon_label.setAlignment(Qt.AlignTop)
50
+
51
+ # --- Text ---
52
+ text_label = QLabel(
53
+ "<b>The project has unsaved changes. "
54
+ f"Do you want to save your changes before {action_name}?"
55
+ )
56
+ text_label.setWordWrap(True)
57
+
58
+ text_layout = QVBoxLayout()
59
+ text_layout.addWidget(text_label)
60
+
61
+ content_layout = QHBoxLayout()
62
+ content_layout.addWidget(icon_label)
63
+ content_layout.addLayout(text_layout)
64
+
65
+ # --- Buttons (order EXACT) ---
66
+ yes_btn = QPushButton("Yes")
67
+ no_btn = QPushButton("No")
68
+ cancel_btn = QPushButton("Cancel")
69
+
70
+ yes_btn.clicked.connect(lambda: self.done(self.SAVE))
71
+ no_btn.clicked.connect(lambda: self.done(self.DISCARD))
72
+ cancel_btn.clicked.connect(lambda: self.done(self.CANCEL))
73
+
74
+ buttons_layout = QHBoxLayout()
75
+ buttons_layout.addStretch()
76
+ buttons_layout.addWidget(yes_btn)
77
+ buttons_layout.addWidget(no_btn)
78
+ buttons_layout.addWidget(cancel_btn)
79
+
80
+ # --- Main layout ---
81
+ layout = QVBoxLayout(self)
82
+ layout.addLayout(content_layout)
83
+ layout.addSpacing(12)
84
+ layout.addLayout(buttons_layout)
85
+
86
+ # ❌ Esc does nothing
87
+ def reject(self):
88
+ pass
89
+
90
+ # ❌ Close button (X) does nothing
91
+ def closeEvent(self, event):
92
+ event.ignore()
@@ -0,0 +1,49 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ import sys
22
+ import os
23
+ from pathlib import Path
24
+ from PySide6.QtWidgets import QApplication
25
+ from pySimBlocks.gui.main_window import MainWindow
26
+
27
+ # ============================================================
28
+ # Entry point
29
+ # ============================================================
30
+ def main():
31
+ if len(sys.argv) > 1:
32
+ project_dir = os.path.abspath(sys.argv[1])
33
+ else:
34
+ project_dir = os.getcwd()
35
+ project_path = Path(project_dir).resolve()
36
+ run_app(project_path)
37
+
38
+
39
+ def run_app(project_path: Path):
40
+ app = QApplication(sys.argv)
41
+ window = MainWindow(project_path)
42
+ app.aboutToQuit.connect(window.cleanup)
43
+ window.resize(1100, 600)
44
+ window.show()
45
+ sys.exit(app.exec())
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -0,0 +1,29 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from pySimBlocks.gui.graphics.block_item import BlockItem
22
+ from pySimBlocks.gui.graphics.connection_item import ConnectionItem
23
+ from pySimBlocks.gui.graphics.port_item import PortItem
24
+
25
+ __all__ = [
26
+ "BlockItem",
27
+ "ConnectionItem",
28
+ "PortItem"
29
+ ]
@@ -0,0 +1,289 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from typing import TYPE_CHECKING
22
+
23
+ from PySide6.QtCore import QPoint, QPointF, QRectF, Qt
24
+ from PySide6.QtGui import QPainterPath, QPen
25
+ from PySide6.QtWidgets import QGraphicsItem, QGraphicsRectItem, QStyle
26
+
27
+ from pySimBlocks.gui.dialogs.block_dialog import BlockDialog
28
+ from pySimBlocks.gui.graphics.port_item import PortItem
29
+
30
+ if TYPE_CHECKING:
31
+ from pySimBlocks.gui.models.block_instance import BlockInstance
32
+ from pySimBlocks.gui.widgets.diagram_view import DiagramView
33
+
34
+
35
+ class BlockItem(QGraphicsRectItem):
36
+ WIDTH = 120
37
+ HEIGHT = 60
38
+ MIN_WIDTH = 40
39
+ MIN_HEIGHT = 30
40
+ GRID_DX = 5
41
+ GRID_DY = 5
42
+ SELECTION_HANDLE_SIZE = 8
43
+ SELECTION_HANDLE_HIT_SIZE = 16
44
+
45
+ def __init__(self,
46
+ instance: "BlockInstance",
47
+ pos: QPointF | QPoint,
48
+ view: "DiagramView",
49
+ layout: dict | None = None,
50
+ ):
51
+ layout = layout or {}
52
+ width = layout.get("width", self.WIDTH)
53
+ height = layout.get("height", self.HEIGHT)
54
+ width = float(width) if isinstance(width, (int, float)) else float(self.WIDTH)
55
+ height = float(height) if isinstance(height, (int, float)) else float(self.HEIGHT)
56
+ width = max(float(self.MIN_WIDTH), width)
57
+ height = max(float(self.MIN_HEIGHT), height)
58
+ super().__init__(0, 0, width, height)
59
+ self.view = view
60
+ self.instance = instance
61
+ self.orientation = layout.get("orientation", "normal")
62
+ self._resize_handle: str | None = None
63
+ self._resize_start_mouse: QPointF | None = None
64
+ self._resize_start_pos: QPointF | None = None
65
+ self._resize_start_width = self.WIDTH
66
+ self._resize_start_height = self.HEIGHT
67
+
68
+ self.setPos(pos)
69
+ self.setFlag(QGraphicsRectItem.ItemIsMovable)
70
+ self.setFlag(QGraphicsRectItem.ItemIsSelectable)
71
+ self.setFlag(QGraphicsRectItem.ItemSendsScenePositionChanges)
72
+
73
+ # Ports
74
+ self.port_items: list[PortItem] = []
75
+ for port in self.instance.ports:
76
+ item = PortItem(port, self)
77
+ self.port_items.append(item)
78
+ self._layout_ports()
79
+
80
+
81
+ # --------------------------------------------------------------------------
82
+ # Public Methods
83
+ # --------------------------------------------------------------------------
84
+ def get_port_item(self, name:str) -> PortItem | None:
85
+ for port in self.port_items:
86
+ if port.instance.name == name:
87
+ return port
88
+
89
+ # --------------------------------------------------------------
90
+ def refresh_ports(self):
91
+
92
+ for item in list(self.port_items):
93
+ if item.instance not in self.instance.ports:
94
+ self.scene().removeItem(item)
95
+ self.port_items.remove(item)
96
+
97
+ displayed_ports = {item.instance for item in self.port_items}
98
+
99
+ for port in self.instance.ports:
100
+ if port not in displayed_ports:
101
+ item = PortItem(port, self)
102
+ self.port_items.append(item)
103
+
104
+ self._layout_ports()
105
+ for item in self.port_items:
106
+ item.update_display_as()
107
+ self.view.on_block_ports_refreshed(self)
108
+
109
+ # --------------------------------------------------------------
110
+ def toggle_orientation(self):
111
+ self.orientation = "flipped" if self.orientation == "normal" else "normal"
112
+
113
+ self._layout_ports()
114
+ self.view.on_block_moved(self)
115
+ self.update()
116
+
117
+ # --------------------------------------------------------------------------
118
+ # Visual Methods
119
+ # --------------------------------------------------------------------------
120
+ def boundingRect(self) -> QRectF:
121
+ half = self.SELECTION_HANDLE_HIT_SIZE / 2
122
+ return self.rect().adjusted(-half, -half, half, half)
123
+
124
+ # --------------------------------------------------------------
125
+ def shape(self) -> QPainterPath:
126
+ path = QPainterPath()
127
+ path.addRect(self.rect())
128
+ for rect in self._handle_hit_rects().values():
129
+ path.addRect(rect)
130
+ return path
131
+
132
+ # --------------------------------------------------------------
133
+ def paint(self, painter, option, widget=None):
134
+ t = self.view.theme
135
+ selected = bool(option.state & QStyle.State_Selected)
136
+
137
+ if selected:
138
+ painter.setBrush(t.block_bg_selected)
139
+ painter.setPen(QPen(t.block_border_selected, 3))
140
+ else:
141
+ painter.setBrush(t.block_bg)
142
+ painter.setPen(QPen(t.block_border, 3))
143
+
144
+ painter.drawRect(self.rect())
145
+ if selected:
146
+ painter.setPen(t.text_selected)
147
+ else:
148
+ painter.setPen(t.text)
149
+ painter.drawText(self.rect(), Qt.AlignCenter, self.instance.name)
150
+
151
+ if selected:
152
+ half = self.SELECTION_HANDLE_SIZE / 2
153
+ r = self.rect()
154
+ corners = [
155
+ (r.left(), r.top()),
156
+ (r.right(), r.top()),
157
+ (r.left(), r.bottom()),
158
+ (r.right(), r.bottom()),
159
+ ]
160
+
161
+ painter.setPen(QPen(t.block_border_selected, 1))
162
+ painter.setBrush(t.text_selected)
163
+ for x, y in corners:
164
+ painter.drawRect(x - half, y - half, self.SELECTION_HANDLE_SIZE, self.SELECTION_HANDLE_SIZE)
165
+
166
+ # --------------------------------------------------------------------------
167
+ # Event Methods
168
+ # --------------------------------------------------------------------------
169
+ def mousePressEvent(self, event):
170
+ if self.isSelected():
171
+ handle = self._handle_at(event.pos())
172
+ if handle is not None:
173
+ self._resize_handle = handle
174
+ self._resize_start_mouse = event.scenePos()
175
+ self._resize_start_pos = self.pos()
176
+ self._resize_start_width = self.rect().width()
177
+ self._resize_start_height = self.rect().height()
178
+ event.accept()
179
+ return
180
+
181
+ super().mousePressEvent(event)
182
+
183
+ # --------------------------------------------------------------
184
+ def mouseMoveEvent(self, event):
185
+ if self._resize_handle and self._resize_start_mouse and self._resize_start_pos:
186
+ delta = event.scenePos() - self._resize_start_mouse
187
+ dx = round(delta.x() / self.GRID_DX) * self.GRID_DX
188
+ dy = round(delta.y() / self.GRID_DY) * self.GRID_DY
189
+
190
+ start_x = self._resize_start_pos.x()
191
+ start_y = self._resize_start_pos.y()
192
+ start_w = self._resize_start_width
193
+ start_h = self._resize_start_height
194
+
195
+ if self._resize_handle in ("tl", "bl"): # drag left edge
196
+ new_x = min(start_x + dx, start_x + start_w - self.MIN_WIDTH)
197
+ new_w = max(self.MIN_WIDTH, (start_x + start_w) - new_x)
198
+ else: # drag right edge
199
+ new_x = start_x
200
+ new_w = max(self.MIN_WIDTH, start_w + dx)
201
+
202
+ if self._resize_handle in ("tl", "tr"): # drag top edge
203
+ new_y = min(start_y + dy, start_y + start_h - self.MIN_HEIGHT)
204
+ new_h = max(self.MIN_HEIGHT, (start_y + start_h) - new_y)
205
+ else: # drag bottom edge
206
+ new_y = start_y
207
+ new_h = max(self.MIN_HEIGHT, start_h + dy)
208
+
209
+ self.setPos(QPointF(new_x, new_y))
210
+ self.setRect(0, 0, new_w, new_h)
211
+ self._layout_ports()
212
+ self.view.on_block_moved(self)
213
+ self.update()
214
+ event.accept()
215
+ return
216
+
217
+ super().mouseMoveEvent(event)
218
+
219
+ # --------------------------------------------------------------
220
+ def mouseReleaseEvent(self, event):
221
+ self._resize_handle = None
222
+ self._resize_start_mouse = None
223
+ self._resize_start_pos = None
224
+ super().mouseReleaseEvent(event)
225
+
226
+ # --------------------------------------------------------------
227
+ def mouseDoubleClickEvent(self, event):
228
+ dialog = BlockDialog(self, readonly=False)
229
+ dialog.exec()
230
+ self.update()
231
+ event.accept()
232
+
233
+ # --------------------------------------------------------------
234
+ def itemChange(self, change, value):
235
+ if change == QGraphicsItem.ItemPositionChange and self.scene():
236
+ x = round(value.x() / self.GRID_DX) * self.GRID_DX
237
+ y = round(value.y() / self.GRID_DY) * self.GRID_DY
238
+ return QPointF(x, y)
239
+
240
+ if change == QGraphicsItem.ItemPositionHasChanged:
241
+ self.view.on_block_moved(self)
242
+
243
+ return super().itemChange(change, value)
244
+
245
+ # --------------------------------------------------------------------------
246
+ # Private Methods
247
+ # --------------------------------------------------------------------------
248
+ def _handle_hit_rects(self) -> dict[str, QRectF]:
249
+ half = self.SELECTION_HANDLE_HIT_SIZE / 2
250
+ r = self.rect()
251
+ return {
252
+ "tl": QRectF(r.left() - half, r.top() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
253
+ "tr": QRectF(r.right() - half, r.top() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
254
+ "bl": QRectF(r.left() - half, r.bottom() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
255
+ "br": QRectF(r.right() - half, r.bottom() - half, self.SELECTION_HANDLE_HIT_SIZE, self.SELECTION_HANDLE_HIT_SIZE),
256
+ }
257
+
258
+ # --------------------------------------------------------------
259
+ def _handle_at(self, local_pos: QPointF) -> str | None:
260
+ for name, rect in self._handle_hit_rects().items():
261
+ if rect.contains(local_pos):
262
+ return name
263
+ return None
264
+
265
+ # --------------------------------------------------------------
266
+ def _layout_ports(self):
267
+ inputs = [p for p in self.port_items if p.is_input]
268
+ outputs = [p for p in self.port_items if not p.is_input]
269
+
270
+ flipped = self.orientation == "flipped"
271
+ width = self.rect().width()
272
+
273
+ if not flipped:
274
+ self._layout_side(inputs, x=0)
275
+ self._layout_side(outputs, x=width)
276
+ else:
277
+ self._layout_side(inputs, x=width)
278
+ self._layout_side(outputs, x=0)
279
+
280
+ # --------------------------------------------------------------
281
+ def _layout_side(self, ports, x):
282
+ if not ports:
283
+ return
284
+
285
+ step = self.rect().height() / (len(ports) + 1)
286
+
287
+ for i, port in enumerate(ports, start=1):
288
+ port.setPos(x, i * step)
289
+ port.update_label_position()