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
digsim/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """All classes within digsim namespace"""
5
+
6
+ from .circuit import Circuit # noqa: F401
digsim/app/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """The main class module of the digsim.app namespace"""
5
+
6
+ import sys
7
+
8
+ from .cli import main
9
+
10
+
11
+ if __name__ == "__main__":
12
+ sys.exit(main())
digsim/app/cli.py ADDED
@@ -0,0 +1,68 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """The main class module of the digsim.app namespace"""
5
+
6
+ import argparse
7
+ import importlib
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from PySide6.QtCore import Qt
12
+ from PySide6.QtGui import QIcon, QPainter, QPixmap
13
+ from PySide6.QtWidgets import QApplication
14
+
15
+ from digsim.app.gui import MainWindow
16
+ from digsim.app.model import AppModel
17
+
18
+
19
+ PACKAGE_NAME = "digsim-logic-simulator"
20
+
21
+
22
+ def _create_app_icon(image_path: Path) -> QIcon:
23
+ image_pixmap = QPixmap(image_path)
24
+ size = max(image_pixmap.size().height(), image_pixmap.size().width())
25
+ icon_pixmap = QPixmap(size, size)
26
+ icon_pixmap.fill(Qt.transparent)
27
+ painter = QPainter(icon_pixmap)
28
+ painter.drawPixmap(
29
+ (icon_pixmap.size().width() - image_pixmap.size().width()) // 2,
30
+ (icon_pixmap.size().height() - image_pixmap.size().height()) // 2,
31
+ image_pixmap,
32
+ )
33
+ painter.end()
34
+ return QIcon(icon_pixmap)
35
+
36
+
37
+ def _start(args, package_version):
38
+ app = QApplication(sys.argv)
39
+ main_path = Path(__file__).parent
40
+ image_path = main_path / "images/app_icon.png"
41
+ icon = _create_app_icon(image_path)
42
+ app.setWindowIcon(icon)
43
+
44
+ app_model = AppModel()
45
+ window = MainWindow(app_model, package_version)
46
+ window.show()
47
+
48
+ if args.load is not None:
49
+ app_model.load_circuit(args.load)
50
+
51
+ return app.exec()
52
+
53
+
54
+ def main():
55
+ parser = argparse.ArgumentParser()
56
+ parser.add_argument(
57
+ "--version", "-v", action="store_true", help="Print the version of digsim.app"
58
+ )
59
+ parser.add_argument("--load", "-l", help="The circuit to load when starting the application")
60
+ args = parser.parse_args()
61
+
62
+ package_version = importlib.metadata.version(PACKAGE_NAME)
63
+
64
+ if args.version:
65
+ print(f"DigSim '{PACKAGE_NAME}' [v{package_version}]")
66
+ return 0
67
+ else:
68
+ return _start(args, package_version)
@@ -0,0 +1,6 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """All classes within digsim.app.gui namespace"""
5
+
6
+ from ._main_window import MainWindow # noqa: F401
@@ -0,0 +1,468 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """The circuit area and component widget"""
5
+
6
+ from functools import partial
7
+
8
+ from PySide6.QtCore import QPoint, QPointF, QRect, QRectF, Qt, QTimer
9
+ from PySide6.QtGui import QBrush, QColor, QPainterPath, QPen
10
+ from PySide6.QtWidgets import (
11
+ QGraphicsItem,
12
+ QGraphicsPathItem,
13
+ QGraphicsRectItem,
14
+ QGraphicsScene,
15
+ QGraphicsView,
16
+ )
17
+
18
+ from digsim.app.settings import ComponentSettingsDialog
19
+
20
+
21
+ class WirePartGraphicsItem(QGraphicsRectItem):
22
+ """A part of a wire graphcis item"""
23
+
24
+ CLOSE_TO_WIRE_MARGIN = 10
25
+
26
+ def __init__(self, app_model, src_port, parent, point_pair):
27
+ src, dst = point_pair
28
+ x_low, x_high = (src.x(), dst.x()) if src.x() < dst.x() else (dst.x(), src.x())
29
+ y_low, y_high = (src.y(), dst.y()) if src.y() < dst.y() else (dst.y(), src.y())
30
+ rect = QRectF(
31
+ x_low - self.CLOSE_TO_WIRE_MARGIN,
32
+ y_low - self.CLOSE_TO_WIRE_MARGIN,
33
+ x_high - x_low + 2 * self.CLOSE_TO_WIRE_MARGIN,
34
+ y_high - y_low + 2 * self.CLOSE_TO_WIRE_MARGIN,
35
+ )
36
+ self._app_model = app_model
37
+ self._src_port = src_port
38
+ self._parent = parent
39
+ self._start = QPointF(x_low, y_low)
40
+ self._end = QPointF(x_high, y_high)
41
+ self._selected = False
42
+ super().__init__(rect, parent)
43
+ self.setFlag(QGraphicsItem.ItemIsSelectable, True)
44
+
45
+ def wire_selected(self, selected):
46
+ """Set Wire Selected"""
47
+ self._selected = selected
48
+
49
+ def itemChange(self, change, value):
50
+ """QT event callback function"""
51
+ if change == QGraphicsItem.ItemSelectedHasChanged:
52
+ self._parent.select(self.isSelected())
53
+ return super().itemChange(change, value)
54
+
55
+ def _get_wire_color(self, port_value, bus_width):
56
+ if port_value == "X":
57
+ return Qt.red
58
+ if port_value == 0:
59
+ return Qt.darkGray
60
+
61
+ max_value = 2**bus_width - 1
62
+ # Start with dark gray
63
+ green = 128
64
+ # Calculate the green component, ranging from 128 to 255
65
+ green += int(127 * port_value / max_value)
66
+ return QColor(128, green, 128)
67
+
68
+ def paint(self, painter, option, widget=None):
69
+ """QT function"""
70
+ pen = QPen(Qt.darkGray)
71
+ bus_width = self._src_port.width
72
+ if bus_width > 1:
73
+ pen.setWidth(4)
74
+ else:
75
+ pen.setWidth(2)
76
+
77
+ if not self._app_model.is_running and self._selected:
78
+ pen.setColor(Qt.black)
79
+ elif self._app_model.settings.get("color_wires"):
80
+ port_value = self._src_port.value
81
+ pen.setColor(self._get_wire_color(port_value, bus_width))
82
+
83
+ painter.setPen(pen)
84
+ painter.drawLine(self._start, self._end)
85
+
86
+
87
+ class WireGraphicsItem(QGraphicsPathItem):
88
+ """A wire graphics item"""
89
+
90
+ WIRE_TO_COMPONENT_DIST = 5
91
+ X_OFFSET = 10
92
+
93
+ def __init__(self, app_model, connection, src_port_item, dst_port_item):
94
+ super().__init__()
95
+ self._app_model = app_model
96
+ self._src_port, self._dst_port = connection
97
+ self._src_port_item = src_port_item
98
+ self._dst_port_item = dst_port_item
99
+ self._part_items = []
100
+ self.setZValue(-1)
101
+ self.update_wire()
102
+ self._selected = False
103
+
104
+ def disconnect(self):
105
+ """Disconnect (delete) the wire"""
106
+ self._src_port.disconnect(self._dst_port)
107
+
108
+ def select(self, selected):
109
+ """Select all parts of the wiregrapgicsitem"""
110
+ for item in self._part_items:
111
+ item.wire_selected(selected)
112
+ if selected:
113
+ self.setZValue(self._app_model.objects.components.get_top_zlevel() + 1)
114
+ else:
115
+ self.setZValue(-1)
116
+ self._selected = selected
117
+
118
+ def is_selected(self):
119
+ """Is the wire selected"""
120
+ return self._selected
121
+
122
+ @classmethod
123
+ def create_points(cls, source, dest, rect):
124
+ """Create a wire path"""
125
+ points = []
126
+ points.append(source)
127
+
128
+ if source.x() < dest.x():
129
+ half_dist_x = (dest.x() - source.x()) / 2
130
+ points.append(QPointF(source.x() + half_dist_x, source.y()))
131
+ points.append(QPointF(source.x() + half_dist_x, dest.y()))
132
+ else:
133
+ half_dist_y = (source.y() + dest.y()) / 2
134
+ if dest.y() < source.y():
135
+ comp_top = rect.y() - cls.WIRE_TO_COMPONENT_DIST
136
+ y_mid = min(comp_top, half_dist_y)
137
+ else:
138
+ comp_bottom = rect.y() + rect.height() + cls.WIRE_TO_COMPONENT_DIST
139
+ y_mid = max(comp_bottom, half_dist_y)
140
+
141
+ points.append(QPointF(source.x() + cls.X_OFFSET, source.y()))
142
+ points.append(QPointF(source.x() + cls.X_OFFSET, y_mid))
143
+ points.append(QPointF(dest.x() - cls.X_OFFSET, y_mid))
144
+ points.append(QPointF(dest.x() - cls.X_OFFSET, dest.y()))
145
+
146
+ points.append(dest)
147
+ return points
148
+
149
+ @classmethod
150
+ def create_path(cls, source, dest, rect):
151
+ """Create a wire path"""
152
+ path = QPainterPath()
153
+ points = cls.create_points(source, dest, rect)
154
+ path.moveTo(points[0])
155
+ for point in points[1:]:
156
+ path.lineTo(point)
157
+ return path
158
+
159
+ def update_wire(self):
160
+ """Update the wire path"""
161
+ source = self._src_port_item.portPos()
162
+ dest = self._dst_port_item.portPos()
163
+ rect = self._src_port_item.portParentRect()
164
+ points = self.create_points(source, dest, rect)
165
+ self._part_items = []
166
+ for idx, p1 in enumerate(points[0:-1]):
167
+ p2 = points[idx + 1]
168
+ item = WirePartGraphicsItem(self._app_model, self._src_port, self, (p1, p2))
169
+ self._part_items.append(item)
170
+
171
+
172
+ class NewWireGraphicsItem(QGraphicsPathItem):
173
+ """A new wire graphics item"""
174
+
175
+ def __init__(self, app_model):
176
+ super().__init__()
177
+ self._app_model = app_model
178
+
179
+ def paint(self, painter, option, widget=None):
180
+ """QT function"""
181
+ end_pos = None
182
+ start_port = self._app_model.objects.new_wire.start_port()
183
+ end_pos = self._app_model.objects.new_wire.end_pos()
184
+ pen = QPen(Qt.darkGray)
185
+ if start_port is None or end_pos is None:
186
+ return
187
+ if start_port.width > 1:
188
+ pen.setWidth(4)
189
+ else:
190
+ pen.setWidth(2)
191
+ component_object = self._app_model.objects.components.get_object(start_port.parent())
192
+ start_pos = component_object.get_port_pos(start_port.name()) + component_object.pos()
193
+ if start_port.is_output():
194
+ path = WireGraphicsItem.create_path(start_pos, end_pos, component_object.rect())
195
+ else:
196
+ path = WireGraphicsItem.create_path(end_pos, start_pos, component_object.rect())
197
+ self.setPath(path)
198
+ self.setPen(pen)
199
+ super().paint(painter, option, widget)
200
+
201
+
202
+ class _CircuitAreaScene(QGraphicsScene):
203
+ """The circuit area graphics scene"""
204
+
205
+ def __init__(self, app_model, view):
206
+ super().__init__()
207
+ self._app_model = app_model
208
+ self._view = view
209
+ self._app_model.sig_repaint.connect(self._repaint)
210
+ self._app_model.sig_synchronize_gui.connect(self._synchronize_gui)
211
+ self._app_model.sig_update_wires.connect(self._update_wires)
212
+ self._app_model.sig_delete_component.connect(self._delete_component)
213
+ self._app_model.sig_delete_wires.connect(self._delete_wires)
214
+ self._wire_items = []
215
+ self._select_start_pos = None
216
+ self._selection_rect_item = None
217
+ self._synchronize_gui()
218
+
219
+ def _repaint(self):
220
+ self.update()
221
+
222
+ def mousePressEvent(self, event):
223
+ """QT event callback function"""
224
+ super().mousePressEvent(event)
225
+ pos = event.scenePos()
226
+ items = self.items(pos)
227
+ if len(items) == 0:
228
+ self._select_start_pos = pos
229
+ self._repaint()
230
+
231
+ def mouseMoveEvent(self, event):
232
+ """QT event callback function"""
233
+ super().mouseMoveEvent(event)
234
+ pos = event.scenePos()
235
+ if self._select_start_pos is not None:
236
+ path = QPainterPath()
237
+ rect = QRect(
238
+ self._select_start_pos.x(),
239
+ self._select_start_pos.y(),
240
+ pos.x() - self._select_start_pos.x(),
241
+ pos.y() - self._select_start_pos.y(),
242
+ )
243
+ self._selection_rect_item.setRect(rect)
244
+ self._selection_rect_item.setVisible(True)
245
+ path.addRect(rect)
246
+ self.setSelectionArea(path)
247
+ self._repaint()
248
+
249
+ # Make sure to loop through complete list to clear has_moved()
250
+ has_moved_component = False
251
+ for component_object in self._app_model.objects.components.get_object_list():
252
+ if component_object.has_moved():
253
+ has_moved_component = True
254
+ if has_moved_component:
255
+ self._app_model.sig_update_wires.emit()
256
+
257
+ def mouseReleaseEvent(self, event):
258
+ """QT event callback function"""
259
+ super().mouseReleaseEvent(event)
260
+ self._select_start_pos = None
261
+ self._selection_rect_item.setVisible(False)
262
+ self._repaint()
263
+
264
+ def _delete_component(self, component_object):
265
+ self.removeItem(component_object)
266
+ self._update_wires()
267
+
268
+ def _delete_wires(self):
269
+ change = False
270
+ for item in self._wire_items:
271
+ if item.is_selected():
272
+ item.disconnect()
273
+ change = True
274
+ if change:
275
+ self._update_wires()
276
+ self._app_model.model_changed()
277
+
278
+ def _update_wires(self):
279
+ for item in self._wire_items:
280
+ self.removeItem(item)
281
+
282
+ self._wire_items = []
283
+ component_objects = self._app_model.objects.components.get_object_list()
284
+ for src_comp_item in component_objects:
285
+ for src_port in src_comp_item.component.outports():
286
+ for dst_port in src_port.wired_ports:
287
+ dst_comp_item = self._app_model.objects.components.get_object(
288
+ dst_port.parent()
289
+ )
290
+ src_port_item = src_comp_item.get_port_item(src_port)
291
+ dst_port_item = dst_comp_item.get_port_item(dst_port)
292
+ item = WireGraphicsItem(
293
+ self._app_model, (src_port, dst_port), src_port_item, dst_port_item
294
+ )
295
+ self.addItem(item)
296
+ self._wire_items.append(item)
297
+
298
+ def add_scene_component(self, component_object, update_wires=False):
299
+ """Add component to scene"""
300
+ self.addItem(component_object)
301
+ component_object.set_parent_widget(self._view)
302
+ if update_wires:
303
+ self._update_wires()
304
+
305
+ def _synchronize_gui(self):
306
+ """Remove everything from scene"""
307
+ self.clear()
308
+ self._wire_items = []
309
+ # Add new wire item
310
+ self.addItem(NewWireGraphicsItem(self._app_model))
311
+ # Add selection rect
312
+ self._selection_rect_item = QGraphicsRectItem()
313
+ self._selection_rect_item.setPen(Qt.DashLine)
314
+ self._selection_rect_item.setBrush(Qt.Dense7Pattern)
315
+ self.addItem(self._selection_rect_item)
316
+ # Add component
317
+ for component_object in self._app_model.objects.components.get_object_list():
318
+ self.add_scene_component(component_object)
319
+ # Update (add) wires
320
+ self._update_wires()
321
+
322
+
323
+ class CircuitArea(QGraphicsView):
324
+ """The circuit area graphics view"""
325
+
326
+ ZOOM_IN_FACTOR = 1.25
327
+ ZOOM_OUT_FACTOR = 0.8
328
+
329
+ def __init__(self, app_model, parent):
330
+ super().__init__(parent=parent)
331
+ self._app_model = app_model
332
+ self._app_model.sig_zoom_in_gui.connect(self._zoom_in)
333
+ self._app_model.sig_zoom_out_gui.connect(self._zoom_out)
334
+ self._scene = _CircuitAreaScene(app_model, self)
335
+ self._wheel_zoom_mode = False
336
+ self.setScene(self._scene)
337
+ self.setBackgroundBrush(QBrush(Qt.lightGray))
338
+ self.setAcceptDrops(True)
339
+ self.setTransformationAnchor(QGraphicsView.NoAnchor)
340
+ self._mouse_pos = QPoint(0, 0)
341
+
342
+ def has_selection(self):
343
+ """Return true if items are selected"""
344
+ return len(self._scene.selectedItems()) > 0
345
+
346
+ def _zoom_in(self):
347
+ self.scale(self.ZOOM_IN_FACTOR, self.ZOOM_IN_FACTOR)
348
+
349
+ def _zoom_out(self):
350
+ self.scale(self.ZOOM_OUT_FACTOR, self.ZOOM_OUT_FACTOR)
351
+
352
+ def _repaint(self):
353
+ """Make scene repaint for component update"""
354
+ self._app_model.sig_repaint.emit()
355
+
356
+ def keyPressEvent(self, event):
357
+ """QT event callback function"""
358
+ super().keyPressEvent(event)
359
+ if event.isAutoRepeat():
360
+ return
361
+ if event.key() == Qt.Key_Shift:
362
+ self.setCursor(Qt.OpenHandCursor)
363
+ self.setDragMode(QGraphicsView.ScrollHandDrag)
364
+ event.accept()
365
+ elif event.key() == Qt.Key_Control:
366
+ self.setCursor(Qt.ArrowCursor)
367
+ self._wheel_zoom_mode = True
368
+ event.accept()
369
+
370
+ def keyReleaseEvent(self, event):
371
+ """QT event callback function"""
372
+ super().keyReleaseEvent(event)
373
+ if event.isAutoRepeat():
374
+ return
375
+ if event.key() == Qt.Key_Shift:
376
+ self.setCursor(Qt.ArrowCursor)
377
+ self.setDragMode(QGraphicsView.NoDrag)
378
+ event.accept()
379
+ elif event.key() == Qt.Key_Control:
380
+ self.setCursor(Qt.ArrowCursor)
381
+ self._wheel_zoom_mode = False
382
+ event.accept()
383
+
384
+ def dragEnterEvent(self, event):
385
+ """QT event callback function"""
386
+ event.accept()
387
+
388
+ def dragLeaveEvent(self, event):
389
+ """QT event callback function"""
390
+ event.accept()
391
+
392
+ def dragMoveEvent(self, event):
393
+ """QT event callback function"""
394
+ event.accept()
395
+
396
+ def dropEvent(self, event):
397
+ """QT event callback function"""
398
+ event.setDropAction(Qt.IgnoreAction)
399
+ event.accept()
400
+ self.setFocus()
401
+
402
+ scene_pos = self.mapToScene(event.pos())
403
+
404
+ component_name = event.mimeData().text()
405
+ QTimer.singleShot(0, partial(self.add_component, component_name, scene_pos))
406
+
407
+ def enterEvent(self, event):
408
+ """QT event callback function"""
409
+ self.setFocus()
410
+ event.accept()
411
+
412
+ def mousePressEvent(self, event):
413
+ """QT event callback function"""
414
+ super().mousePressEvent(event)
415
+
416
+ def mouseReleaseEvent(self, event):
417
+ """QT event callback function"""
418
+ super().mouseReleaseEvent(event)
419
+
420
+ def mouseMoveEvent(self, event):
421
+ """QT event callback function"""
422
+ super().mouseMoveEvent(event)
423
+ self._mouse_pos = event.pos()
424
+ # Draw unfinished wire
425
+ if self._app_model.objects.new_wire.ongoing():
426
+ scene_pos = self.mapToScene(event.pos())
427
+ self._app_model.objects.new_wire.set_end_pos(scene_pos)
428
+ self._repaint()
429
+
430
+ def wheelEvent(self, event):
431
+ """QT event callback function"""
432
+ # Zoom on wheel
433
+ if not self._wheel_zoom_mode:
434
+ super().wheelEvent(event)
435
+ return
436
+ if event.angleDelta().y() > 0:
437
+ before = self.mapToScene(self._mouse_pos)
438
+ self._zoom_in()
439
+ after = self.mapToScene(self._mouse_pos)
440
+ translation = after - before
441
+ self.translate(translation.x(), translation.y())
442
+ elif event.angleDelta().y() < 0:
443
+ before = self.mapToScene(self._mouse_pos)
444
+ self._zoom_out()
445
+ after = self.mapToScene(self._mouse_pos)
446
+ translation = after - before
447
+ self.translate(translation.x(), translation.y())
448
+ event.accept()
449
+
450
+ def add_component(self, name, position):
451
+ """
452
+ Add component to circuit area
453
+ Used be drag'n'drop into Circuit area or double click in SelectableComponentWidget
454
+ """
455
+ if position is None:
456
+ position = self.mapToScene(0, 0) + QPoint(100, 100)
457
+
458
+ component_parameters = self._app_model.objects.components.get_object_parameters(name)
459
+ ok, settings = ComponentSettingsDialog.start(
460
+ self, self._app_model, name, component_parameters
461
+ )
462
+ if settings.get("name") is not None:
463
+ name = settings["name"]
464
+ if ok:
465
+ component_object = self._app_model.objects.components.add_object_by_name(
466
+ name, position, settings
467
+ )
468
+ self._scene.add_scene_component(component_object, True)