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.
- digsim/__init__.py +6 -0
- digsim/app/__main__.py +12 -0
- digsim/app/cli.py +68 -0
- digsim/app/gui/__init__.py +6 -0
- digsim/app/gui/_circuit_area.py +468 -0
- digsim/app/gui/_component_selection.py +154 -0
- digsim/app/gui/_main_window.py +163 -0
- digsim/app/gui/_top_bar.py +339 -0
- digsim/app/gui/_utils.py +26 -0
- digsim/app/gui/_warning_dialog.py +46 -0
- digsim/app/gui_objects/__init__.py +7 -0
- digsim/app/gui_objects/_bus_bit_object.py +94 -0
- digsim/app/gui_objects/_buzzer_object.py +97 -0
- digsim/app/gui_objects/_component_context_menu.py +79 -0
- digsim/app/gui_objects/_component_object.py +374 -0
- digsim/app/gui_objects/_component_port_item.py +63 -0
- digsim/app/gui_objects/_dip_switch_object.py +104 -0
- digsim/app/gui_objects/_gui_note_object.py +80 -0
- digsim/app/gui_objects/_gui_object_factory.py +80 -0
- digsim/app/gui_objects/_hexdigit_object.py +53 -0
- digsim/app/gui_objects/_image_objects.py +239 -0
- digsim/app/gui_objects/_label_object.py +97 -0
- digsim/app/gui_objects/_logic_analyzer_object.py +86 -0
- digsim/app/gui_objects/_seven_segment_object.py +131 -0
- digsim/app/gui_objects/_shortcut_objects.py +82 -0
- digsim/app/gui_objects/_yosys_object.py +32 -0
- digsim/app/gui_objects/images/AND.png +0 -0
- digsim/app/gui_objects/images/Analyzer.png +0 -0
- digsim/app/gui_objects/images/BUF.png +0 -0
- digsim/app/gui_objects/images/Buzzer.png +0 -0
- digsim/app/gui_objects/images/Clock.png +0 -0
- digsim/app/gui_objects/images/DFF.png +0 -0
- digsim/app/gui_objects/images/DIP_SWITCH.png +0 -0
- digsim/app/gui_objects/images/FlipFlop.png +0 -0
- digsim/app/gui_objects/images/IC.png +0 -0
- digsim/app/gui_objects/images/LED_OFF.png +0 -0
- digsim/app/gui_objects/images/LED_ON.png +0 -0
- digsim/app/gui_objects/images/MUX.png +0 -0
- digsim/app/gui_objects/images/NAND.png +0 -0
- digsim/app/gui_objects/images/NOR.png +0 -0
- digsim/app/gui_objects/images/NOT.png +0 -0
- digsim/app/gui_objects/images/ONE.png +0 -0
- digsim/app/gui_objects/images/OR.png +0 -0
- digsim/app/gui_objects/images/PB.png +0 -0
- digsim/app/gui_objects/images/Switch_OFF.png +0 -0
- digsim/app/gui_objects/images/Switch_ON.png +0 -0
- digsim/app/gui_objects/images/XNOR.png +0 -0
- digsim/app/gui_objects/images/XOR.png +0 -0
- digsim/app/gui_objects/images/YOSYS.png +0 -0
- digsim/app/gui_objects/images/ZERO.png +0 -0
- digsim/app/images/app_icon.png +0 -0
- digsim/app/model/__init__.py +6 -0
- digsim/app/model/_model.py +210 -0
- digsim/app/model/_model_components.py +162 -0
- digsim/app/model/_model_new_wire.py +57 -0
- digsim/app/model/_model_objects.py +155 -0
- digsim/app/model/_model_settings.py +35 -0
- digsim/app/model/_model_shortcuts.py +72 -0
- digsim/app/settings/__init__.py +8 -0
- digsim/app/settings/_component_settings.py +415 -0
- digsim/app/settings/_gui_settings.py +71 -0
- digsim/app/settings/_shortcut_dialog.py +39 -0
- digsim/circuit/__init__.py +7 -0
- digsim/circuit/_circuit.py +329 -0
- digsim/circuit/_waves_writer.py +61 -0
- digsim/circuit/components/__init__.py +26 -0
- digsim/circuit/components/_bus_bits.py +68 -0
- digsim/circuit/components/_button.py +44 -0
- digsim/circuit/components/_buzzer.py +45 -0
- digsim/circuit/components/_clock.py +54 -0
- digsim/circuit/components/_dip_switch.py +73 -0
- digsim/circuit/components/_flip_flops.py +99 -0
- digsim/circuit/components/_gates.py +246 -0
- digsim/circuit/components/_hexdigit.py +82 -0
- digsim/circuit/components/_ic.py +36 -0
- digsim/circuit/components/_label_wire.py +167 -0
- digsim/circuit/components/_led.py +18 -0
- digsim/circuit/components/_logic_analyzer.py +60 -0
- digsim/circuit/components/_mem64kbyte.py +42 -0
- digsim/circuit/components/_memstdout.py +37 -0
- digsim/circuit/components/_note.py +25 -0
- digsim/circuit/components/_on_off_switch.py +54 -0
- digsim/circuit/components/_seven_segment.py +28 -0
- digsim/circuit/components/_static_level.py +28 -0
- digsim/circuit/components/_static_value.py +44 -0
- digsim/circuit/components/_yosys_atoms.py +1353 -0
- digsim/circuit/components/_yosys_component.py +232 -0
- digsim/circuit/components/atoms/__init__.py +23 -0
- digsim/circuit/components/atoms/_component.py +280 -0
- digsim/circuit/components/atoms/_digsim_exception.py +8 -0
- digsim/circuit/components/atoms/_port.py +398 -0
- digsim/circuit/components/ic/74162.json +1331 -0
- digsim/circuit/components/ic/7448.json +834 -0
- digsim/storage_model/__init__.py +7 -0
- digsim/storage_model/_app.py +58 -0
- digsim/storage_model/_circuit.py +126 -0
- digsim/synth/__init__.py +6 -0
- digsim/synth/__main__.py +67 -0
- digsim/synth/_synthesis.py +156 -0
- digsim/utils/__init__.py +6 -0
- digsim/utils/_yosys_netlist.py +134 -0
- digsim_logic_simulator-0.22.0.dist-info/METADATA +140 -0
- digsim_logic_simulator-0.22.0.dist-info/RECORD +107 -0
- digsim_logic_simulator-0.22.0.dist-info/WHEEL +5 -0
- digsim_logic_simulator-0.22.0.dist-info/entry_points.txt +2 -0
- digsim_logic_simulator-0.22.0.dist-info/licenses/LICENSE.md +32 -0
- 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,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
|
+
)
|