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,46 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2024
2
+ # All rights reserved
3
+
4
+ """A warning dialog"""
5
+
6
+ from PySide6.QtCore import QSize, Qt
7
+ from PySide6.QtWidgets import (
8
+ QDialog,
9
+ QDialogButtonBox,
10
+ QFrame,
11
+ QHBoxLayout,
12
+ QLabel,
13
+ QStyle,
14
+ QVBoxLayout,
15
+ )
16
+
17
+
18
+ class WarningDialog(QDialog):
19
+ """WarningDialog"""
20
+
21
+ def __init__(self, parent, title, warning_message):
22
+ super().__init__(parent)
23
+ self.setWindowTitle(title)
24
+ self.setLayout(QVBoxLayout(self))
25
+ self.setFocusPolicy(Qt.StrongFocus)
26
+ self.setStyleSheet("QLabel{font-size: 18pt;}")
27
+ frame = QFrame(parent)
28
+ frame.setLayout(QHBoxLayout(frame))
29
+ frame.setFrameShape(QFrame.StyledPanel)
30
+ frame.setFrameShadow(QFrame.Sunken)
31
+ label = QLabel()
32
+ label.setAlignment(Qt.AlignTop | Qt.AlignLeft)
33
+ label.setText(warning_message)
34
+ icon = self.style().standardIcon(QStyle.SP_MessageBoxWarning)
35
+ button = QLabel()
36
+ button.setPixmap(icon.pixmap(QSize(32, 32)))
37
+ frame.layout().addWidget(button)
38
+ frame.layout().setStretchFactor(label, 0)
39
+ frame.layout().addWidget(label)
40
+ frame.layout().setStretchFactor(label, 1)
41
+ buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
42
+ buttonBox.accepted.connect(self.accept)
43
+ self.layout().addWidget(frame)
44
+ self.layout().setStretchFactor(frame, 1)
45
+ self.layout().addWidget(buttonBox)
46
+ self.layout().setStretchFactor(buttonBox, 0)
@@ -0,0 +1,7 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """All classes within digsim.app.gui_object namespace"""
5
+
6
+ from ._component_object import ComponentObject # noqa: F401
7
+ from ._gui_object_factory import class_factory # noqa: F401
@@ -0,0 +1,94 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """A hexdigit component placed in the GUI"""
5
+
6
+ from PySide6.QtCore import QPoint, QSize, Qt
7
+ from PySide6.QtGui import QPen
8
+
9
+ from ._component_object import ComponentObject
10
+
11
+
12
+ class BusBitsObject(ComponentObject):
13
+ """The class for a bus/bit component placed in the GUI"""
14
+
15
+ WIRE_LENGTH_COMPONENT = 5
16
+ WIRE_LENGTH_SELECTABLE = 15
17
+ PORT_DISTANCE = 10
18
+
19
+ def __init__(self, app_model, component, xpos, ypos):
20
+ super().__init__(app_model, component, xpos, ypos, port_distance=self.PORT_DISTANCE)
21
+ bus_w, _ = self.get_string_metrics("bus[31:0]")
22
+ self.width = 2 * self.inport_x_pos() + 2 * bus_w + abs(2 * self.WIRE_LENGTH_COMPONENT)
23
+ self.update_ports()
24
+
25
+ @classmethod
26
+ def _paint_bus_bit(cls, painter, pos, size, wire_length, bit_wires_y):
27
+ center_pos = QPoint(size.width() / 2, size.height() / 2)
28
+ pen = QPen()
29
+ pen.setWidth(4)
30
+ pen.setColor(Qt.black)
31
+ painter.setPen(pen)
32
+ line_height = size.height() - cls.BORDER_TO_PORT
33
+ painter.drawLine(
34
+ pos.x() + center_pos.x(),
35
+ pos.y() + center_pos.y() - line_height / 2,
36
+ pos.x() + center_pos.x(),
37
+ pos.y() + center_pos.y() + line_height / 2,
38
+ )
39
+ painter.drawLine(
40
+ pos.x() + center_pos.x() - wire_length,
41
+ pos.y() + center_pos.y(),
42
+ pos.x() + center_pos.x(),
43
+ pos.y() + center_pos.y(),
44
+ )
45
+ pen.setWidth(2)
46
+ painter.setPen(pen)
47
+ for wire_y in bit_wires_y:
48
+ painter.drawLine(
49
+ pos.x() + center_pos.x() + wire_length, wire_y, pos.x() + center_pos.x(), wire_y
50
+ )
51
+
52
+ def _portlist(self):
53
+ return self.component.outports()
54
+
55
+ def paint_component(self, painter):
56
+ self.paint_component_base(painter)
57
+ bit_wires_y = []
58
+ for port in self._portlist():
59
+ bit_wires_y.append(self.get_port_pos(port.name()).y())
60
+ self._paint_bus_bit(
61
+ painter,
62
+ self.object_pos,
63
+ self.rect(),
64
+ self.WIRE_LENGTH_COMPONENT,
65
+ bit_wires_y,
66
+ )
67
+
68
+ @classmethod
69
+ def paint_selectable_component(cls, painter, size, name):
70
+ image_size = QSize(size.width(), size.width())
71
+ bit_wires_y = [
72
+ size.width() / 2 - 3 * size.width() / 12,
73
+ size.width() / 2 - 1 * size.width() / 12,
74
+ size.width() / 2 + 1 * size.width() / 12,
75
+ size.width() / 2 + 3 * size.width() / 12,
76
+ ]
77
+ cls._paint_bus_bit(
78
+ painter,
79
+ QPoint(0, 0),
80
+ image_size,
81
+ cls.WIRE_LENGTH_SELECTABLE,
82
+ bit_wires_y,
83
+ )
84
+ cls.paint_selectable_component_name(painter, QPoint(0, 0), size, name)
85
+
86
+
87
+ class BitsBusObject(BusBitsObject):
88
+ """The class for a bit/bit component placed in the GUI"""
89
+
90
+ WIRE_LENGTH_COMPONENT = -5
91
+ WIRE_LENGTH_SELECTABLE = -15
92
+
93
+ def _portlist(self):
94
+ return self.component.inports()
@@ -0,0 +1,97 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """A buzzer component object"""
5
+
6
+ from math import pi, sin
7
+ from struct import pack
8
+
9
+ from PySide6.QtCore import QByteArray, QIODevice
10
+ from PySide6.QtMultimedia import QAudioFormat, QAudioSink, QMediaDevices
11
+
12
+ from ._image_objects import ImageObjectWithActiveRect
13
+
14
+
15
+ class _AudioOutput(QIODevice):
16
+ def __init__(self, audio_format, tone_hz):
17
+ super().__init__()
18
+
19
+ self.pos = 0
20
+ self.audio_buffer = QByteArray()
21
+ self.audio_buffer.clear()
22
+
23
+ factor = 2 * pi * tone_hz / audio_format.sampleRate()
24
+ channel_bytes = audio_format.bytesPerSample()
25
+ length = audio_format.sampleRate() * audio_format.channelCount() * channel_bytes
26
+
27
+ sample_index = 0
28
+ while length > 0:
29
+ x = 32767 * sin((sample_index % audio_format.sampleRate()) * factor)
30
+ packed = pack("<h", int(x))
31
+ for _ in range(audio_format.channelCount()):
32
+ self.audio_buffer.append(packed)
33
+ length -= channel_bytes
34
+ sample_index += 1
35
+ self.open(QIODevice.ReadOnly)
36
+
37
+ def readData(self, maxlen):
38
+ """Read function"""
39
+ data = QByteArray()
40
+ total = 0
41
+ while maxlen > total:
42
+ chunk = min(self.audio_buffer.size() - self.pos, maxlen - total)
43
+ data.append(self.audio_buffer.mid(self.pos, chunk))
44
+ self.pos = (self.pos + chunk) % self.audio_buffer.size()
45
+ total += chunk
46
+
47
+ return data.data()
48
+
49
+ def writeData(self, _):
50
+ """Write function"""
51
+ return 0
52
+
53
+ def bytesAvailable(self):
54
+ """Byteas available function"""
55
+ return self.audio_buffer.size() + super().bytesAvailable()
56
+
57
+
58
+ class BuzzerObject(ImageObjectWithActiveRect):
59
+ """The class for Buzzer component placed in the GUI"""
60
+
61
+ IMAGE_FILENAME = "images/Buzzer.png"
62
+ DATA_SAMPLE_RATE_HZ = 44100
63
+
64
+ def __init__(self, app_model, component, xpos, ypos):
65
+ super().__init__(app_model, component, xpos, ypos)
66
+ self.audio_sink = None
67
+ self._app_model.sig_audio_start.connect(self._audio_start)
68
+ self._app_model.sig_audio_notify.connect(self._audio_notify)
69
+ self.device = QMediaDevices.defaultAudioOutput()
70
+ self.audio_format = QAudioFormat()
71
+ self.audio_format.setSampleRate(self.DATA_SAMPLE_RATE_HZ)
72
+ self.audio_format.setChannelCount(1)
73
+ self.audio_format.setSampleFormat(QAudioFormat.Int16)
74
+ self.audio_output = _AudioOutput(self.audio_format, self.component.tone_frequency())
75
+
76
+ def _audio_start(self, enable):
77
+ if enable:
78
+ self._play(self.component.active)
79
+ else:
80
+ self._play(False)
81
+
82
+ def _audio_notify(self, component):
83
+ if component == self.component:
84
+ self._play(component.active)
85
+
86
+ def _play(self, active):
87
+ if self.device is None:
88
+ return
89
+ if active:
90
+ if self.audio_sink is None:
91
+ self.audio_sink = QAudioSink(self.device, self.audio_format)
92
+ self.audio_sink.setVolume(0.1)
93
+ self.audio_sink.start(self.audio_output)
94
+ else:
95
+ if self.audio_sink is not None:
96
+ self.audio_sink.stop()
97
+ self.audio_sink = None
@@ -0,0 +1,79 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """A component context menu"""
5
+
6
+ from PySide6.QtGui import QAction
7
+ from PySide6.QtWidgets import QMenu
8
+
9
+ from digsim.app.settings import ComponentSettingsDialog
10
+
11
+
12
+ class ComponentContextMenu(QMenu):
13
+ """The component contextmenu class"""
14
+
15
+ def __init__(self, parent, app_model, component_object):
16
+ super().__init__(parent)
17
+ self._parent = parent
18
+ self._app_model = app_model
19
+ self._component_object = component_object
20
+ self._component = self._component_object.component
21
+ self._reconfigurable_parameters = {}
22
+ # Title
23
+ titleAction = QAction(self._component.display_name(), self)
24
+ titleAction.setEnabled(False)
25
+ self.addAction(titleAction)
26
+ self.addSeparator()
27
+ # Settings
28
+ self._add_settings()
29
+ self._component_object.add_context_menu_action(self, parent)
30
+ self.addSeparator()
31
+ # Bring to front / Send to back
32
+ raiseAction = QAction("Bring to front", self)
33
+ self.addAction(raiseAction)
34
+ raiseAction.triggered.connect(self._raise)
35
+ lowerAction = QAction("Send to back", self)
36
+ self.addAction(lowerAction)
37
+ lowerAction.triggered.connect(self._lower)
38
+ self.addSeparator()
39
+ # Delete
40
+ deleteAction = QAction("Delete", self)
41
+ self.addAction(deleteAction)
42
+ deleteAction.triggered.connect(self._delete)
43
+ self._menu_action = None
44
+
45
+ def _add_settings(self):
46
+ self._reconfigurable_parameters = self._component.get_reconfigurable_parameters()
47
+ if len(self._reconfigurable_parameters) > 0:
48
+ settingsAction = QAction("Settings", self)
49
+ self.addAction(settingsAction)
50
+ settingsAction.triggered.connect(self._settings)
51
+
52
+ def create(self, position):
53
+ """Create context menu for component"""
54
+ self._menu_action = self.exec_(position)
55
+
56
+ def get_action(self):
57
+ """Get context menu action"""
58
+ return self._menu_action.text() if self._menu_action is not None else ""
59
+
60
+ def _delete(self):
61
+ self._component_object.setSelected(True)
62
+ self._app_model.objects.delete_selected()
63
+
64
+ def _raise(self):
65
+ self._app_model.objects.components.bring_to_front(self._component_object)
66
+
67
+ def _lower(self):
68
+ self._app_model.objects.components.send_to_back(self._component_object)
69
+
70
+ def _settings(self):
71
+ """Start the settings dialog for reconfiguration"""
72
+ ok, settings = ComponentSettingsDialog.start(
73
+ self._parent,
74
+ self._app_model,
75
+ self._component.name(),
76
+ self._reconfigurable_parameters,
77
+ )
78
+ if ok:
79
+ self._app_model.objects.components.update_settings(self._component_object, settings)
@@ -0,0 +1,374 @@
1
+ # Copyright (c) Fredrik Andersson, 2023-2025
2
+ # All rights reserved
3
+
4
+ """A component placed in the GUI"""
5
+
6
+ import abc
7
+
8
+ from PySide6.QtCore import QPoint, QRect, QSize, Qt
9
+ from PySide6.QtGui import QFont, QFontMetrics, QPen
10
+ from PySide6.QtWidgets import QGraphicsItem, QGraphicsRectItem
11
+
12
+ from digsim.storage_model import GuiPositionDataClass
13
+
14
+ from ._component_context_menu import ComponentContextMenu
15
+ from ._component_port_item import PortGraphicsItem
16
+
17
+
18
+ class ComponentObject(QGraphicsRectItem):
19
+ """A component graphics item, a 'clickable' item with a custom paintEvent"""
20
+
21
+ DEFAULT_WIDTH = 80
22
+ DEFAULT_HEIGHT = 80
23
+ RECT_TO_BORDER = 5
24
+ BORDER_TO_PORT = 25
25
+ PORT_SIDE = 8
26
+ DEFAULT_PORT_TO_PORT_DISTANCE = 20
27
+
28
+ _COMPONENT_NAME_FONT = QFont("Arial", 10)
29
+ _PORT_NAME_FONT = QFont("Arial", 8)
30
+ _SELECTABLE_COMPONENT_NAME_FONT = QFont("Arial", 8)
31
+
32
+ def __init__(
33
+ self, app_model, component, xpos, ypos, port_distance=DEFAULT_PORT_TO_PORT_DISTANCE
34
+ ):
35
+ super().__init__(QRect(xpos, ypos, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT))
36
+ self._app_model = app_model
37
+ self._component = component
38
+ self._port_distance = port_distance
39
+
40
+ # Signals
41
+ self._app_model.sig_control_notify.connect(self._control_notify)
42
+
43
+ # Class variables
44
+ self._port_dict = {}
45
+ self._parent_widget = None
46
+ self._mouse_press_pos = None
47
+ self._moved = False
48
+ self._paint_port_names = True
49
+ self._save_pos = self.rect().topLeft()
50
+
51
+ # Create ports
52
+ self.create_ports()
53
+
54
+ # Qt Flags
55
+ self.setFlag(QGraphicsItem.ItemIsMovable, True)
56
+ self.setFlag(QGraphicsItem.ItemIsSelectable, True)
57
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
58
+ self.setAcceptHoverEvents(True)
59
+
60
+ def _control_notify(self):
61
+ if self._app_model.is_running:
62
+ self.setFlag(QGraphicsItem.ItemIsMovable, False)
63
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, False)
64
+ else:
65
+ self.setFlag(QGraphicsItem.ItemIsMovable, True)
66
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
67
+
68
+ def itemChange(self, change, value):
69
+ """QT event callback function"""
70
+ if change == QGraphicsItem.ItemPositionHasChanged:
71
+ self._moved = True
72
+ elif change == QGraphicsItem.ItemSelectedChange:
73
+ if self._app_model.is_running:
74
+ value = 0
75
+ elif change == QGraphicsItem.ItemSelectedHasChanged:
76
+ self.repaint()
77
+ return super().itemChange(change, value)
78
+
79
+ def hoverEnterEvent(self, _):
80
+ """QT event callback function"""
81
+ if self._app_model.is_running and self.component.has_action:
82
+ self.setCursor(Qt.PointingHandCursor)
83
+
84
+ def hoverLeaveEvent(self, _):
85
+ """QT event callback function"""
86
+ self.setCursor(Qt.ArrowCursor)
87
+
88
+ def hoverMoveEvent(self, event):
89
+ """QT event callback function"""
90
+ self.mouse_position(event.scenePos())
91
+ super().hoverMoveEvent(event)
92
+
93
+ def mousePressEvent(self, event):
94
+ """QT event callback function"""
95
+ super().mousePressEvent(event)
96
+ if event.button() == Qt.LeftButton:
97
+ if self._app_model.is_running:
98
+ self._app_model.model_add_event(self.component.onpress)
99
+ else:
100
+ self._mouse_press_pos = event.screenPos()
101
+ self.setCursor(Qt.ClosedHandCursor)
102
+
103
+ def mouseReleaseEvent(self, event):
104
+ """QT event callback function"""
105
+ super().mouseReleaseEvent(event)
106
+ if event.button() == Qt.LeftButton:
107
+ if self._app_model.is_running:
108
+ self._app_model.model_add_event(self.component.onrelease)
109
+ else:
110
+ self.setCursor(Qt.ArrowCursor)
111
+ if event.screenPos() != self._mouse_press_pos:
112
+ # Move completed, set model to changed
113
+ self._app_model.objects.components.component_moved()
114
+ position = self.rect().topLeft() + self.pos()
115
+ self._save_pos = position.toPoint()
116
+ else:
117
+ if not self._app_model.objects.new_wire.ongoing():
118
+ self.single_click_action()
119
+ self._mouse_press_pos = None
120
+
121
+ def contextMenuEvent(self, event):
122
+ """QT event callback function"""
123
+ if self._app_model.is_running:
124
+ return
125
+ if self._app_model.objects.new_wire.ongoing():
126
+ self._app_model.objects.new_wire.abort()
127
+ context_menu = ComponentContextMenu(self._parent_widget, self._app_model, self)
128
+ context_menu.create(event.screenPos())
129
+
130
+ def paint(self, painter, option, widget=None):
131
+ """QT function"""
132
+ self.paint_component(painter)
133
+ if self._paint_port_names:
134
+ self.paint_portnames(painter)
135
+
136
+ @property
137
+ def component(self):
138
+ """Get component"""
139
+ return self._component
140
+
141
+ def paint_port_names(self, enable):
142
+ """Enable/Disable paint port names"""
143
+ self._paint_port_names = enable
144
+
145
+ def has_moved(self):
146
+ """True if the component has moved since last call"""
147
+ moved = self._moved
148
+ self._moved = False
149
+ return moved
150
+
151
+ def create_ports(self):
152
+ """Create ports for component object"""
153
+ for port in self._component.ports:
154
+ self._port_dict[port] = {}
155
+ if port.width == 1:
156
+ self._port_dict[port]["name"] = port.name()
157
+ else:
158
+ self._port_dict[port]["name"] = f"{port.name()}[{port.width - 1}:0]"
159
+ item = PortGraphicsItem(self._app_model, self, port)
160
+ self._port_dict[port]["item"] = item
161
+ self.update_ports()
162
+
163
+ def update_ports(self):
164
+ """Update port positions for the placed component"""
165
+
166
+ # Make sure all ports are fit in the hieght of the component
167
+ max_ports = max(len(self._component.inports()), len(self._component.outports()))
168
+ if max_ports > 1:
169
+ min_height = (max_ports - 1) * self._port_distance + 2 * self.BORDER_TO_PORT
170
+ self.height = max(self.height, min_height)
171
+
172
+ # Make sure port names fit in the width of the component
173
+ max_inport_name = ""
174
+ max_outport_name = ""
175
+ for port in self.component.ports:
176
+ port_name = self._port_dict[port]["name"]
177
+ if port.is_input() and len(port_name) > len(max_inport_name):
178
+ max_inport_name = port_name
179
+ elif not port.is_input() and len(port_name) > len(max_outport_name):
180
+ max_outport_name = port_name
181
+ max_port_names = f"{max_inport_name} {max_outport_name}"
182
+ max_port_names_w, _ = self.get_string_metrics(max_port_names)
183
+ self.width = max(2 * self.inport_x_pos() + max_port_names_w, self.width)
184
+
185
+ # Create port rects
186
+ self._set_port_rects(self._component.inports(), -self.RECT_TO_BORDER)
187
+ self._set_port_rects(
188
+ self._component.outports(), self.RECT_TO_BORDER + self.width - self.PORT_SIDE - 1
189
+ )
190
+
191
+ def get_port_item(self, port):
192
+ """Get port item"""
193
+ return self._port_dict[port]["item"]
194
+
195
+ def set_port_rect(self, port, rect):
196
+ """Set port rect"""
197
+ self.get_port_item(port).setRect(rect)
198
+
199
+ def _set_port_rects(self, ports, xpos):
200
+ if len(ports) == 1:
201
+ rect = QRect(
202
+ self.object_pos.x() + xpos,
203
+ self.object_pos.y() + self.height / 2 - self.PORT_SIDE / 2,
204
+ self.PORT_SIDE,
205
+ self.PORT_SIDE,
206
+ )
207
+ self.get_port_item(ports[0]).setRect(rect)
208
+ elif len(ports) > 1:
209
+ top_to_port = (self.height - self._port_distance * (len(ports) - 1)) / 2
210
+ for idx, port in enumerate(ports):
211
+ rect = QRect(
212
+ self.object_pos.x() + xpos,
213
+ self.object_pos.y()
214
+ + top_to_port
215
+ + idx * self._port_distance
216
+ - self.PORT_SIDE / 2,
217
+ self.PORT_SIDE,
218
+ self.PORT_SIDE,
219
+ )
220
+ self.get_port_item(port).setRect(rect)
221
+
222
+ def set_parent_widget(self, parent):
223
+ """Set the parent"""
224
+ self._parent_widget = parent
225
+
226
+ @property
227
+ def selected(self):
228
+ """Get the selected variable for the current object"""
229
+ return self.isSelected()
230
+
231
+ def repaint(self):
232
+ """Update GUI for this component object"""
233
+ self._app_model.sig_repaint.emit()
234
+
235
+ def get_string_metrics(self, port_str, font=QFont("Arial", 8)):
236
+ """Get the port display name (including bits if available)"""
237
+ fm = QFontMetrics(font)
238
+ str_pixels_w = fm.horizontalAdvance(port_str)
239
+ str_pixels_h = fm.height()
240
+ return str_pixels_w, str_pixels_h
241
+
242
+ def paint_component_name(self, painter):
243
+ """Paint the component name"""
244
+ painter.setFont(self._COMPONENT_NAME_FONT)
245
+ fm = QFontMetrics(self._COMPONENT_NAME_FONT)
246
+ display_name_str = self._component.display_name()
247
+ str_pixels_w = fm.horizontalAdvance(display_name_str)
248
+ str_pixels_h = fm.height()
249
+ painter.drawText(
250
+ self.rect().x() + self.rect().width() / 2 - str_pixels_w / 2,
251
+ self.rect().y() + str_pixels_h,
252
+ display_name_str,
253
+ )
254
+
255
+ def paint_component_base(self, painter, color=Qt.gray):
256
+ """Paint component base rect"""
257
+ comp_rect = self.rect()
258
+ pen = QPen()
259
+ if self.selected:
260
+ pen.setWidth(4)
261
+ else:
262
+ pen.setWidth(1)
263
+ pen.setColor(Qt.black)
264
+ painter.setPen(pen)
265
+ painter.setBrush(Qt.SolidPattern)
266
+ painter.setBrush(color)
267
+ painter.drawRoundedRect(comp_rect, 5, 5)
268
+
269
+ @abc.abstractmethod
270
+ def paint_component(self, painter):
271
+ """Paint component"""
272
+
273
+ @classmethod
274
+ @abc.abstractmethod
275
+ def paint_selectable_component(cls, painter, size, name):
276
+ """
277
+ Paint selectable component
278
+ use width x width (square) to paint component image
279
+ """
280
+
281
+ @classmethod
282
+ def paint_selectable_component_name(cls, painter, point, size, name):
283
+ """Paint the name for the selectable component"""
284
+ painter.setFont(cls._SELECTABLE_COMPONENT_NAME_FONT)
285
+ fm = QFontMetrics(cls._SELECTABLE_COMPONENT_NAME_FONT)
286
+ str_pixels_w = fm.horizontalAdvance(name)
287
+ str_pixels_h = fm.height()
288
+ painter.setPen(Qt.black)
289
+ painter.drawText(
290
+ point.x() + size.width() / 2 - str_pixels_w / 2,
291
+ point.y() + size.height() - str_pixels_h,
292
+ name,
293
+ )
294
+
295
+ def inport_x_pos(self):
296
+ """Get the X position left of the input port"""
297
+ return 1.5 * self.PORT_SIDE
298
+
299
+ def paint_portnames(self, painter, color=Qt.black):
300
+ """Paint component ports"""
301
+ painter.setPen(color)
302
+ painter.setFont(self._PORT_NAME_FONT)
303
+ for port in self._component.ports:
304
+ rect = self.get_port_item(port).rect()
305
+ port_str = self._port_dict[port]["name"]
306
+ str_pixels_w, str_pixels_h = self.get_string_metrics(port_str, self._PORT_NAME_FONT)
307
+ text_y = rect.y() + str_pixels_h - self.PORT_SIDE / 2
308
+ if rect.x() < self.object_pos.x():
309
+ text_pos = QPoint(rect.x() + self.inport_x_pos(), text_y)
310
+ else:
311
+ text_pos = QPoint(rect.x() - str_pixels_w - self.PORT_SIDE / 2, text_y)
312
+ painter.drawText(text_pos, port_str)
313
+
314
+ def get_port_pos(self, portname):
315
+ """Get component port pos"""
316
+ port = self.component.port(portname)
317
+ return self.get_port_item(port).rect().center()
318
+
319
+ def mouse_position(self, pos):
320
+ """Component function: called for mouse position update"""
321
+
322
+ def single_click_action(self):
323
+ """Component function:: called for mouse single click"""
324
+
325
+ def add_context_menu_action(self, menu, parent):
326
+ """Component function:: called when context menu is created"""
327
+
328
+ def update_size(self):
329
+ """Component function: called when the component can update its size"""
330
+
331
+ @property
332
+ def size(self):
333
+ """Get size"""
334
+ return QSize(self.width, self.height)
335
+
336
+ @property
337
+ def object_pos(self):
338
+ """Get position"""
339
+ return self.rect().topLeft()
340
+
341
+ @property
342
+ def width(self):
343
+ """Get width"""
344
+ return self.rect().width()
345
+
346
+ @width.setter
347
+ def width(self, width):
348
+ """Set width"""
349
+ self.setRect(self.rect().x(), self.rect().y(), width, self.rect().height())
350
+
351
+ @property
352
+ def height(self):
353
+ """Get height"""
354
+ return self.rect().height()
355
+
356
+ @height.setter
357
+ def height(self, height):
358
+ """Set height"""
359
+ self.setRect(self.rect().x(), self.rect().y(), self.rect().width(), height)
360
+
361
+ @property
362
+ def zlevel(self):
363
+ """Get zlevel"""
364
+ return self.zValue()
365
+
366
+ @zlevel.setter
367
+ def zlevel(self, level):
368
+ """Set zlevel"""
369
+ self.setZValue(level)
370
+
371
+ def to_gui_dataclass(self):
372
+ return GuiPositionDataClass(
373
+ x=int(self._save_pos.x()), y=int(self._save_pos.y()), z=int(self.zlevel)
374
+ )