digsim-logic-simulator 0.22.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 (107) hide show
  1. digsim/__init__.py +6 -0
  2. digsim/app/__main__.py +12 -0
  3. digsim/app/cli.py +68 -0
  4. digsim/app/gui/__init__.py +6 -0
  5. digsim/app/gui/_circuit_area.py +468 -0
  6. digsim/app/gui/_component_selection.py +154 -0
  7. digsim/app/gui/_main_window.py +163 -0
  8. digsim/app/gui/_top_bar.py +339 -0
  9. digsim/app/gui/_utils.py +26 -0
  10. digsim/app/gui/_warning_dialog.py +46 -0
  11. digsim/app/gui_objects/__init__.py +7 -0
  12. digsim/app/gui_objects/_bus_bit_object.py +94 -0
  13. digsim/app/gui_objects/_buzzer_object.py +97 -0
  14. digsim/app/gui_objects/_component_context_menu.py +79 -0
  15. digsim/app/gui_objects/_component_object.py +374 -0
  16. digsim/app/gui_objects/_component_port_item.py +63 -0
  17. digsim/app/gui_objects/_dip_switch_object.py +104 -0
  18. digsim/app/gui_objects/_gui_note_object.py +80 -0
  19. digsim/app/gui_objects/_gui_object_factory.py +80 -0
  20. digsim/app/gui_objects/_hexdigit_object.py +53 -0
  21. digsim/app/gui_objects/_image_objects.py +239 -0
  22. digsim/app/gui_objects/_label_object.py +97 -0
  23. digsim/app/gui_objects/_logic_analyzer_object.py +86 -0
  24. digsim/app/gui_objects/_seven_segment_object.py +131 -0
  25. digsim/app/gui_objects/_shortcut_objects.py +82 -0
  26. digsim/app/gui_objects/_yosys_object.py +32 -0
  27. digsim/app/gui_objects/images/AND.png +0 -0
  28. digsim/app/gui_objects/images/Analyzer.png +0 -0
  29. digsim/app/gui_objects/images/BUF.png +0 -0
  30. digsim/app/gui_objects/images/Buzzer.png +0 -0
  31. digsim/app/gui_objects/images/Clock.png +0 -0
  32. digsim/app/gui_objects/images/DFF.png +0 -0
  33. digsim/app/gui_objects/images/DIP_SWITCH.png +0 -0
  34. digsim/app/gui_objects/images/FlipFlop.png +0 -0
  35. digsim/app/gui_objects/images/IC.png +0 -0
  36. digsim/app/gui_objects/images/LED_OFF.png +0 -0
  37. digsim/app/gui_objects/images/LED_ON.png +0 -0
  38. digsim/app/gui_objects/images/MUX.png +0 -0
  39. digsim/app/gui_objects/images/NAND.png +0 -0
  40. digsim/app/gui_objects/images/NOR.png +0 -0
  41. digsim/app/gui_objects/images/NOT.png +0 -0
  42. digsim/app/gui_objects/images/ONE.png +0 -0
  43. digsim/app/gui_objects/images/OR.png +0 -0
  44. digsim/app/gui_objects/images/PB.png +0 -0
  45. digsim/app/gui_objects/images/Switch_OFF.png +0 -0
  46. digsim/app/gui_objects/images/Switch_ON.png +0 -0
  47. digsim/app/gui_objects/images/XNOR.png +0 -0
  48. digsim/app/gui_objects/images/XOR.png +0 -0
  49. digsim/app/gui_objects/images/YOSYS.png +0 -0
  50. digsim/app/gui_objects/images/ZERO.png +0 -0
  51. digsim/app/images/app_icon.png +0 -0
  52. digsim/app/model/__init__.py +6 -0
  53. digsim/app/model/_model.py +210 -0
  54. digsim/app/model/_model_components.py +162 -0
  55. digsim/app/model/_model_new_wire.py +57 -0
  56. digsim/app/model/_model_objects.py +155 -0
  57. digsim/app/model/_model_settings.py +35 -0
  58. digsim/app/model/_model_shortcuts.py +72 -0
  59. digsim/app/settings/__init__.py +8 -0
  60. digsim/app/settings/_component_settings.py +415 -0
  61. digsim/app/settings/_gui_settings.py +71 -0
  62. digsim/app/settings/_shortcut_dialog.py +39 -0
  63. digsim/circuit/__init__.py +7 -0
  64. digsim/circuit/_circuit.py +329 -0
  65. digsim/circuit/_waves_writer.py +61 -0
  66. digsim/circuit/components/__init__.py +26 -0
  67. digsim/circuit/components/_bus_bits.py +68 -0
  68. digsim/circuit/components/_button.py +44 -0
  69. digsim/circuit/components/_buzzer.py +45 -0
  70. digsim/circuit/components/_clock.py +54 -0
  71. digsim/circuit/components/_dip_switch.py +73 -0
  72. digsim/circuit/components/_flip_flops.py +99 -0
  73. digsim/circuit/components/_gates.py +246 -0
  74. digsim/circuit/components/_hexdigit.py +82 -0
  75. digsim/circuit/components/_ic.py +36 -0
  76. digsim/circuit/components/_label_wire.py +167 -0
  77. digsim/circuit/components/_led.py +18 -0
  78. digsim/circuit/components/_logic_analyzer.py +60 -0
  79. digsim/circuit/components/_mem64kbyte.py +42 -0
  80. digsim/circuit/components/_memstdout.py +37 -0
  81. digsim/circuit/components/_note.py +25 -0
  82. digsim/circuit/components/_on_off_switch.py +54 -0
  83. digsim/circuit/components/_seven_segment.py +28 -0
  84. digsim/circuit/components/_static_level.py +28 -0
  85. digsim/circuit/components/_static_value.py +44 -0
  86. digsim/circuit/components/_yosys_atoms.py +1353 -0
  87. digsim/circuit/components/_yosys_component.py +232 -0
  88. digsim/circuit/components/atoms/__init__.py +23 -0
  89. digsim/circuit/components/atoms/_component.py +280 -0
  90. digsim/circuit/components/atoms/_digsim_exception.py +8 -0
  91. digsim/circuit/components/atoms/_port.py +398 -0
  92. digsim/circuit/components/ic/74162.json +1331 -0
  93. digsim/circuit/components/ic/7448.json +834 -0
  94. digsim/storage_model/__init__.py +7 -0
  95. digsim/storage_model/_app.py +58 -0
  96. digsim/storage_model/_circuit.py +126 -0
  97. digsim/synth/__init__.py +6 -0
  98. digsim/synth/__main__.py +67 -0
  99. digsim/synth/_synthesis.py +156 -0
  100. digsim/utils/__init__.py +6 -0
  101. digsim/utils/_yosys_netlist.py +134 -0
  102. digsim_logic_simulator-0.22.0.dist-info/METADATA +140 -0
  103. digsim_logic_simulator-0.22.0.dist-info/RECORD +107 -0
  104. digsim_logic_simulator-0.22.0.dist-info/WHEEL +5 -0
  105. digsim_logic_simulator-0.22.0.dist-info/entry_points.txt +2 -0
  106. digsim_logic_simulator-0.22.0.dist-info/licenses/LICENSE.md +32 -0
  107. digsim_logic_simulator-0.22.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,210 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """An application model for a GUI simulated circuit"""
5
+
6
+ import queue
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from PySide6.QtCore import QThread, Signal
11
+
12
+ from digsim.app.gui_objects import ComponentObject
13
+ from digsim.circuit.components.atoms import Component
14
+ from digsim.storage_model import AppFileDataClass
15
+
16
+ from ._model_objects import ModelObjects
17
+ from ._model_settings import ModelSettings
18
+ from ._model_shortcuts import ModelShortcuts
19
+
20
+
21
+ class AppModel(QThread):
22
+ """The application model class for a GUI simulated circuit"""
23
+
24
+ sig_audio_start = Signal(bool)
25
+ sig_audio_notify = Signal(Component)
26
+ sig_control_notify = Signal()
27
+ sig_sim_time_notify = Signal(float)
28
+ sig_synchronize_gui = Signal()
29
+ sig_repaint = Signal()
30
+ sig_update_wires = Signal()
31
+ sig_delete_component = Signal(ComponentObject)
32
+ sig_delete_wires = Signal()
33
+ sig_error = Signal(str)
34
+ sig_warning_log = Signal(str, str)
35
+ sig_zoom_in_gui = Signal()
36
+ sig_zoom_out_gui = Signal()
37
+
38
+ def __init__(self):
39
+ super().__init__()
40
+ self._setup_model_components()
41
+ self._started = False
42
+ self._single_step = False
43
+ self._changed = False
44
+ self._gui_event_queue = queue.Queue()
45
+ self._multi_select = False
46
+
47
+ def _setup_model_components(self):
48
+ self._model_objects = ModelObjects(self)
49
+ self._model_shortcuts = ModelShortcuts(self)
50
+ self._model_settings = ModelSettings(self)
51
+
52
+ @property
53
+ def objects(self):
54
+ """return the model objects"""
55
+ return self._model_objects
56
+
57
+ @property
58
+ def shortcuts(self):
59
+ """return the model shortcuts"""
60
+ return self._model_shortcuts
61
+
62
+ @property
63
+ def settings(self):
64
+ """return the model settings"""
65
+ return self._model_settings
66
+
67
+ @property
68
+ def is_running(self):
69
+ """Return True if the simulation thread is running"""
70
+ return self._started
71
+
72
+ @property
73
+ def is_changed(self):
74
+ """Return True if there are changes in the model since last save"""
75
+ return self._changed
76
+
77
+ def _model_clear(self):
78
+ """Clear model"""
79
+ self.objects.clear()
80
+ self.shortcuts.clear()
81
+ self._changed = False
82
+
83
+ def model_init(self):
84
+ """(Re)initialize the model/circuit"""
85
+ self.objects.circuit.init()
86
+ self.objects.init()
87
+ self.sig_sim_time_notify.emit(0)
88
+
89
+ def model_start(self):
90
+ """Start model simulation thread"""
91
+ self.model_abort_wire()
92
+ self._started = True
93
+ self._single_step = False
94
+ self.start()
95
+ self.sig_control_notify.emit()
96
+
97
+ def model_single_step(self):
98
+ """Start model simulation thread"""
99
+ self._started = True
100
+ self._single_step = True
101
+ self.start()
102
+ self.sig_control_notify.emit()
103
+
104
+ def model_stop(self):
105
+ """Stop model simulation thread"""
106
+ self._started = False
107
+ self.wait()
108
+
109
+ def model_reset(self):
110
+ """Reset model simulation"""
111
+ if not self._started:
112
+ self.model_init()
113
+
114
+ def model_changed(self):
115
+ """Set changed to True, for example when gui has moved component"""
116
+ self._changed = True
117
+ self.sig_control_notify.emit()
118
+
119
+ def model_add_event(self, func):
120
+ """Add medel events (functions) from the GUI"""
121
+ self._gui_event_queue.put(func)
122
+
123
+ def model_abort_wire(self):
124
+ if self._model_objects.new_wire.ongoing():
125
+ self._model_objects.new_wire.abort()
126
+ self.sig_repaint.emit()
127
+
128
+ def run(self):
129
+ """Simulation thread run function"""
130
+ start_time = time.perf_counter()
131
+ next_tick = start_time
132
+ sim_tick_ms = 1000 / self._model_settings.get("update_frequency")
133
+ real_time = self._model_settings.get("real_time")
134
+ self.sig_audio_start.emit(True)
135
+ while self._started:
136
+ next_tick += sim_tick_ms / 1000
137
+
138
+ # Execute one GUI event at a time
139
+ if not self._gui_event_queue.empty():
140
+ gui_event_func = self._gui_event_queue.get()
141
+ gui_event_func()
142
+
143
+ single_step_stop = self.objects.circuit.run(
144
+ ms=sim_tick_ms, single_step=self._single_step
145
+ )
146
+ if single_step_stop:
147
+ self._started = False
148
+
149
+ self.objects.components.update_callback_objects()
150
+
151
+ if not self._single_step and real_time:
152
+ now = time.perf_counter()
153
+ sleep_time = next_tick - now
154
+ sleep_time = max(0.01, sleep_time)
155
+ else:
156
+ sleep_time = 0.01 # Sleep a little, to be able to handle event
157
+ time.sleep(sleep_time)
158
+
159
+ self.sig_sim_time_notify.emit(self.objects.circuit.time_ns / 1000000000)
160
+
161
+ self._single_step = False
162
+ self.sig_control_notify.emit()
163
+ self.sig_audio_start.emit(False)
164
+
165
+ def save_circuit(self, path):
166
+ """Save the circuit with GUI information"""
167
+ circuit_folder = str(Path(path).parent)
168
+ model_dataclass = self.objects.circuit_to_model(circuit_folder)
169
+ appfile_dataclass = AppFileDataClass(
170
+ circuit=model_dataclass.circuit,
171
+ gui=model_dataclass.gui,
172
+ shortcuts=self.shortcuts.to_dict(),
173
+ settings=self.settings.get_all(),
174
+ )
175
+ appfile_dataclass.save(path)
176
+ self._changed = False
177
+ self.sig_control_notify.emit()
178
+
179
+ def load_circuit(self, path):
180
+ """Load a circuit with GUI information"""
181
+ self._model_clear()
182
+ app_file_dc = AppFileDataClass.load(path)
183
+ circuit_folder = str(Path(path).parent)
184
+ if len(circuit_folder) == 0:
185
+ circuit_folder = "."
186
+ exception_str_list = self.objects.model_to_circuit(app_file_dc, circuit_folder)
187
+ self.shortcuts.from_dict(app_file_dc.shortcuts)
188
+ self.settings.from_dict(app_file_dc.settings)
189
+ self.model_init()
190
+ self._changed = False
191
+ self.objects.reset_undo_stack()
192
+ self.sig_synchronize_gui.emit()
193
+ self.sig_control_notify.emit()
194
+ if len(exception_str_list) > 0:
195
+ self.sig_warning_log.emit("Load Circuit Warning", "\n".join(exception_str_list))
196
+
197
+ def clear_circuit(self):
198
+ """Clear the circuit"""
199
+ self.objects.push_undo_state()
200
+ self._model_clear()
201
+ self.sig_synchronize_gui.emit()
202
+ self.sig_control_notify.emit()
203
+
204
+ def zoom_in(self):
205
+ """Send zoom in signal to GUI"""
206
+ self.sig_zoom_in_gui.emit()
207
+
208
+ def zoom_out(self):
209
+ """Send zoom out signal to GUI"""
210
+ self.sig_zoom_out_gui.emit()
@@ -0,0 +1,162 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """Handle component objects in the model"""
5
+
6
+ import digsim.circuit.components
7
+ from digsim.app.gui_objects import ComponentObject
8
+ from digsim.circuit.components import Buzzer
9
+ from digsim.circuit.components.atoms import CallbackComponent
10
+ from digsim.storage_model import GuiPositionDataClass
11
+
12
+
13
+ class ModelComponents:
14
+ """Class to handle the component objects in the model"""
15
+
16
+ @staticmethod
17
+ def is_component_object(obj):
18
+ """Test if this object is a component object"""
19
+ return isinstance(obj, ComponentObject)
20
+
21
+ def __init__(self, app_model, circuit):
22
+ self._app_model = app_model
23
+ self._circuit = circuit
24
+ self._component_objects = {}
25
+ self._component_callback_list = []
26
+
27
+ def clear(self):
28
+ """Clear components objects"""
29
+ self._component_objects = {}
30
+
31
+ def init(self):
32
+ """Initialize components objects"""
33
+ self._app_model.sig_repaint.emit()
34
+
35
+ def get_dict(self):
36
+ """Get component objects dict"""
37
+ return self._component_objects
38
+
39
+ def is_empty(self):
40
+ """Return True if there are component objects in the model"""
41
+ return len(self._component_objects) == 0
42
+
43
+ def get_top_zlevel(self):
44
+ """Get thehighest z level in the model"""
45
+ max_zlevel = None
46
+ for _, comp_object in self._component_objects.items():
47
+ max_zlevel = (
48
+ comp_object.zlevel if max_zlevel is None else (max(max_zlevel, comp_object.zlevel))
49
+ )
50
+ return max_zlevel
51
+
52
+ def component_moved(self):
53
+ """Call when component has moved to update state"""
54
+ self._app_model.objects.push_undo_state()
55
+ self._app_model.model_changed()
56
+
57
+ def bring_to_front(self, component_object):
58
+ """Make the component object the highest in the stack"""
59
+ max_zlevel = self.get_top_zlevel()
60
+ component_object.zlevel = max_zlevel + 1
61
+ self._app_model.model_changed()
62
+
63
+ def send_to_back(self, component_object):
64
+ """Make the component object the lowest in the stack"""
65
+ min_zlevel = None
66
+ for _, comp_object in self._component_objects.items():
67
+ min_zlevel = (
68
+ comp_object.zlevel if min_zlevel is None else (min(min_zlevel, comp_object.zlevel))
69
+ )
70
+ if min_zlevel == 0:
71
+ for _, comp_object in self._component_objects.items():
72
+ comp_object.zlevel = comp_object.zlevel + 1
73
+ component_object.zlevel = 0
74
+ else:
75
+ component_object.zlevel = min_zlevel - 1
76
+ self._app_model.model_changed()
77
+
78
+ def update_callback_objects(self):
79
+ """
80
+ Update the GUI for the components that have changed since the last call
81
+ """
82
+ if len(self._component_callback_list) == 0:
83
+ return
84
+ for comp in self._component_callback_list:
85
+ if isinstance(comp, Buzzer):
86
+ self._app_model.sig_audio_notify.emit(comp)
87
+ self._app_model.sig_repaint.emit()
88
+ self._component_callback_list = []
89
+
90
+ def _component_callback(self, component):
91
+ """Add component to callback list (if not available)"""
92
+ if component not in self._component_callback_list:
93
+ self._component_callback_list.append(component)
94
+
95
+ def _get_component_class(self, name):
96
+ return getattr(digsim.circuit.components, name)
97
+
98
+ def get_object_parameters(self, name):
99
+ """Get parameters for a component"""
100
+ return self._get_component_class(name).get_parameters()
101
+
102
+ def _add_object(self, component, xpos, ypos):
103
+ """Add component object in position"""
104
+ component_object_class = digsim.app.gui_objects.class_factory(type(component).__name__)
105
+ self._component_objects[component] = component_object_class(
106
+ self._app_model, component, xpos, ypos
107
+ )
108
+ if isinstance(component, CallbackComponent):
109
+ component.set_callback(self._component_callback)
110
+ return self._component_objects[component]
111
+
112
+ def add_object_by_name(self, name, pos, settings):
113
+ """Add component object from class name"""
114
+ self._app_model.objects.push_undo_state()
115
+ component_class = self._get_component_class(name)
116
+ component = component_class(self._circuit, **settings)
117
+ self._app_model.model_init()
118
+ component_object = self._add_object(component, pos.x(), pos.y())
119
+ self._app_model.model_changed()
120
+ return component_object
121
+
122
+ def get_object(self, component):
123
+ """Get component object (from component)"""
124
+ return self._component_objects[component]
125
+
126
+ def get_object_list(self):
127
+ """Get list of component objects"""
128
+ return list(self._component_objects.values())
129
+
130
+ def update_settings(self, component_object, settings):
131
+ """Update settings for a component"""
132
+ self._app_model.objects.push_undo_state()
133
+ component_object.component.update_settings(settings)
134
+ self._app_model.model_changed()
135
+ # Settings can change the component size
136
+ component_object.update_size()
137
+ self._app_model.sig_repaint.emit()
138
+
139
+ def update_sizes(self):
140
+ """Update all component sizes"""
141
+ for comp_object in self.get_object_list():
142
+ comp_object.update_size()
143
+
144
+ def delete(self, component_object):
145
+ """Delete a component object in the model"""
146
+ del self._component_objects[component_object.component]
147
+ self._circuit.delete_component(component_object.component)
148
+ self._app_model.sig_delete_component.emit(component_object)
149
+
150
+ def add_gui_positions(self, gui_dc_dict):
151
+ """Create model components from circuit_dict"""
152
+ for comp in self._circuit.get_toplevel_components():
153
+ gui_dc = gui_dc_dict.get(comp.name(), GuiPositionDataClass())
154
+ component_object = self._add_object(comp, gui_dc.x, gui_dc.y)
155
+ component_object.zlevel = gui_dc.z
156
+
157
+ def get_gui_dict(self):
158
+ """Return gui dict from component objects"""
159
+ gui_dict = {}
160
+ for comp, comp_object in self.get_dict().items():
161
+ gui_dict[comp.name()] = comp_object.to_gui_dataclass()
162
+ return gui_dict
@@ -0,0 +1,57 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """Handle new wire object in the model"""
5
+
6
+ from digsim.circuit.components.atoms import PortConnectionError
7
+
8
+
9
+ class NewWire:
10
+ """Class to handle functionality for making a new wire"""
11
+
12
+ def __init__(self, app_model):
13
+ self._app_model = app_model
14
+ self._start_port = None
15
+ self._end_pos = None
16
+
17
+ def start_port(self):
18
+ """Get start port for unfinished new wire object"""
19
+ return self._start_port
20
+
21
+ def end_pos(self):
22
+ """Get end point for unfinished new wire object"""
23
+ return self._end_pos
24
+
25
+ def start(self, component, portname):
26
+ """Start new wire object"""
27
+ if component.port(portname).can_add_wire():
28
+ self._start_port = component.port(portname)
29
+
30
+ def end(self, component, portname):
31
+ """End new wire object"""
32
+ end_port = component.port(portname)
33
+ if self._start_port.is_output() and end_port.is_input():
34
+ self._app_model.objects.push_undo_state()
35
+ self._start_port.wire = end_port
36
+ elif self._start_port.is_input() and end_port.is_output():
37
+ self._app_model.objects.push_undo_state()
38
+ end_port.wire = self._start_port
39
+ else:
40
+ raise PortConnectionError("Cannot connect to port of same type")
41
+ self._app_model.sig_update_wires.emit()
42
+ self._app_model.model_changed()
43
+ self._start_port = None
44
+ self._end_pos = None
45
+
46
+ def abort(self):
47
+ """Abort new wire object"""
48
+ self._start_port = None
49
+ self._end_pos = None
50
+
51
+ def set_end_pos(self, pos):
52
+ """Update end point for unfinished new wire object"""
53
+ self._end_pos = pos
54
+
55
+ def ongoing(self):
56
+ """Return True if an unfinished new wire object is active"""
57
+ return self._start_port is not None
@@ -0,0 +1,155 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """Handle objects in the model"""
5
+
6
+ from digsim.circuit import Circuit
7
+ from digsim.circuit.components.atoms import DigsimException
8
+ from digsim.storage_model import AppFileDataClass, ModelDataClass
9
+
10
+ from ._model_components import ModelComponents
11
+ from ._model_new_wire import NewWire
12
+
13
+
14
+ class ModelObjects:
15
+ """Class to handle objects in the model"""
16
+
17
+ def __init__(self, app_model):
18
+ self._app_model = app_model
19
+ self._circuit = Circuit(name="DigSimCircuit")
20
+ self._model_components = ModelComponents(app_model, self._circuit)
21
+ self._undo_stack = []
22
+ self._redo_stack = []
23
+ self._new_wire = NewWire(self._app_model)
24
+
25
+ @property
26
+ def circuit(self):
27
+ """return the model circuit"""
28
+ return self._circuit
29
+
30
+ @property
31
+ def components(self):
32
+ """return the model components"""
33
+ return self._model_components
34
+
35
+ @property
36
+ def new_wire(self):
37
+ """return the model components"""
38
+ return self._new_wire
39
+
40
+ def init(self):
41
+ """Initialize objects"""
42
+ self._model_components.init()
43
+
44
+ def clear(self):
45
+ """Clear components and wires"""
46
+ self._model_components.clear()
47
+ self._circuit.clear()
48
+
49
+ def get_list(self):
50
+ """Get list of all model objects"""
51
+ model_objects = self._model_components.get_object_list()
52
+ return model_objects
53
+
54
+ def get_selected(self):
55
+ """Get selected objects"""
56
+ return [obj for obj in self.get_list() if obj.selected]
57
+
58
+ def _delete(self, selected_objects):
59
+ for obj in selected_objects:
60
+ if ModelComponents.is_component_object(obj):
61
+ self._model_components.delete(obj)
62
+
63
+ def delete_selected(self):
64
+ """Delete selected object(s)"""
65
+ self.push_undo_state()
66
+ selected_objects = self._app_model.objects.get_selected()
67
+ if len(selected_objects) > 0:
68
+ self._delete(selected_objects)
69
+ self._app_model.model_changed()
70
+ self._app_model.sig_delete_wires.emit()
71
+
72
+ def model_to_circuit(self, model_dc, circuit_folder):
73
+ if isinstance(model_dc, AppFileDataClass):
74
+ # Loaded model
75
+ dc = ModelDataClass.from_app_file_dc(model_dc)
76
+ else:
77
+ dc = model_dc
78
+
79
+ try:
80
+ # Create circuit
81
+ exception_str_list = self.circuit.from_dataclass(
82
+ dc.circuit,
83
+ circuit_folder,
84
+ component_exceptions=False,
85
+ connect_exceptions=False,
86
+ )
87
+ # Add component positions
88
+ self.components.add_gui_positions(dc.gui)
89
+ except DigsimException as exc:
90
+ self.sig_error.emit(f"Circuit error: {str(exc)}")
91
+ return exception_str_list
92
+ return exception_str_list
93
+
94
+ def circuit_to_model(self, circuit_folder):
95
+ model_dc = ModelDataClass(
96
+ circuit=self.circuit.to_dataclass(circuit_folder), gui=self.components.get_gui_dict()
97
+ )
98
+ return model_dc
99
+
100
+ def _restore_state(self, model_dc):
101
+ self.clear()
102
+ exception_str_list = self.model_to_circuit(model_dc, None)
103
+ self._app_model.model_init()
104
+ self._app_model.model_changed()
105
+ if len(exception_str_list) > 0:
106
+ self.sig_warning_log.emit("Load Circuit Warning", "\n".join(exception_str_list))
107
+
108
+ def reset_undo_stack(self):
109
+ """Clear undo/redo stacks"""
110
+ self._undo_stack = []
111
+ self._redo_stack = []
112
+ self._app_model.sig_control_notify.emit()
113
+
114
+ def push_undo_state(self, clear_redo_stack=True):
115
+ """Push undo state to stack"""
116
+ self._undo_stack.append(self.circuit_to_model("/"))
117
+ if clear_redo_stack:
118
+ self._redo_stack = []
119
+ self._app_model.sig_control_notify.emit()
120
+
121
+ def drop_undo_state(self):
122
+ """Drop last undo state"""
123
+ if len(self._undo_stack) > 0:
124
+ self._undo_stack.pop()
125
+ self._app_model.sig_control_notify.emit()
126
+
127
+ def push_redo_state(self):
128
+ """Push redo state to stack"""
129
+ self._redo_stack.append(self.circuit_to_model("/"))
130
+
131
+ def undo(self):
132
+ """Undo to last saved state"""
133
+ if len(self._undo_stack) > 0:
134
+ self.push_redo_state()
135
+ model_dc = self._undo_stack.pop()
136
+ self._restore_state(model_dc)
137
+ self._app_model.sig_control_notify.emit()
138
+ self._app_model.sig_synchronize_gui.emit()
139
+
140
+ def redo(self):
141
+ """Undo to last saved state"""
142
+ if len(self._redo_stack) > 0:
143
+ self.push_undo_state(clear_redo_stack=False)
144
+ model_dc = self._redo_stack.pop()
145
+ self._restore_state(model_dc)
146
+ self._app_model.sig_control_notify.emit()
147
+ self._app_model.sig_synchronize_gui.emit()
148
+
149
+ def can_undo(self):
150
+ """Return true if the undo stack is not empty"""
151
+ return len(self._undo_stack) > 0
152
+
153
+ def can_redo(self):
154
+ """Return true if the undo stack is not empty"""
155
+ return len(self._redo_stack) > 0
@@ -0,0 +1,35 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """Handle settings in the model"""
5
+
6
+ from digsim.app.settings import GuiSettingsDialog
7
+
8
+
9
+ class ModelSettings:
10
+ """Class to handle settings in the model"""
11
+
12
+ def __init__(self, app_model):
13
+ self._app_model = app_model
14
+ self._settings = GuiSettingsDialog.default_settings()
15
+
16
+ def update(self, settings):
17
+ """Update model settings"""
18
+ self._settings.update(settings)
19
+ self._app_model.model_changed()
20
+ # Settings can change the component sizes
21
+ self._app_model.objects.components.update_sizes()
22
+ self._app_model.sig_repaint.emit()
23
+
24
+ def get_all(self):
25
+ """Return settings dict"""
26
+ return self._settings.copy()
27
+
28
+ def from_dict(self, circuit_dict):
29
+ """Get settings from circuit dict"""
30
+ for key, data in circuit_dict.items():
31
+ self._settings[key] = data
32
+
33
+ def get(self, key):
34
+ """Get model setting"""
35
+ return self._settings.get(key)
@@ -0,0 +1,72 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """Handle shortcuts in the model"""
5
+
6
+ from PySide6.QtCore import Qt
7
+
8
+
9
+ class ModelShortcuts:
10
+ """class to handle key shortcuts in the model"""
11
+
12
+ QT_KEY_TO_KEY = {
13
+ Qt.Key_0: "0",
14
+ Qt.Key_1: "1",
15
+ Qt.Key_2: "2",
16
+ Qt.Key_3: "3",
17
+ Qt.Key_4: "4",
18
+ Qt.Key_5: "5",
19
+ Qt.Key_6: "6",
20
+ Qt.Key_7: "7",
21
+ Qt.Key_8: "8",
22
+ Qt.Key_9: "9",
23
+ }
24
+
25
+ def __init__(self, app_model):
26
+ self._app_model = app_model
27
+ self._shortcut_component = {}
28
+
29
+ def clear(self):
30
+ """Clear shortcuts"""
31
+ self._shortcut_component = {}
32
+
33
+ def set_component(self, key, component):
34
+ """Set shortcut"""
35
+ self._shortcut_component[key] = component
36
+
37
+ def get_component(self, key):
38
+ """Get shortcut"""
39
+ return self._shortcut_component.get(key)
40
+
41
+ def press(self, qtkey):
42
+ """Handle shortcut keypress"""
43
+ key = self.QT_KEY_TO_KEY.get(qtkey)
44
+ if key is None:
45
+ return
46
+ component = self.get_component(key)
47
+ if component is not None:
48
+ self._app_model.model_add_event(component.onpress)
49
+
50
+ def release(self, qtkey):
51
+ """Handle shortcut keyrelease"""
52
+ key = self.QT_KEY_TO_KEY.get(qtkey)
53
+ if key is None:
54
+ return
55
+ component = self.get_component(key)
56
+ if component is not None:
57
+ self._app_model.model_add_event(component.onrelease)
58
+
59
+ def to_dict(self):
60
+ """Generate dict from shortcuts"""
61
+ shortcuts_dict = {}
62
+ for key, component in self._shortcut_component.items():
63
+ shortcuts_dict[key] = component.name()
64
+ return shortcuts_dict
65
+
66
+ def from_dict(self, shortcuts_dict):
67
+ """Generate shortcuts from dict"""
68
+ self.clear()
69
+ if shortcuts_dict is not None:
70
+ for key, component_name in shortcuts_dict.items():
71
+ component = self._app_model.objects.circuit.get_component(component_name)
72
+ self.set_component(key, component)