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,154 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Module for the component selection classes"""
|
|
5
|
+
|
|
6
|
+
from functools import partial
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QMimeData, QSize, Qt, QTimer
|
|
9
|
+
from PySide6.QtGui import QDrag, QPainter, QPixmap
|
|
10
|
+
from PySide6.QtWidgets import QFrame, QLabel, QPushButton, QVBoxLayout, QWidget
|
|
11
|
+
|
|
12
|
+
import digsim.app.gui_objects
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SelectableComponentWidget(QPushButton):
|
|
16
|
+
"""
|
|
17
|
+
The selectable component class,
|
|
18
|
+
this is the component widget than can be dragged into the circuit area.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, name, parent, circuit_area, display_name=None):
|
|
22
|
+
super().__init__(parent)
|
|
23
|
+
self._name = name
|
|
24
|
+
self._circuit_area = circuit_area
|
|
25
|
+
self._paint_class = digsim.app.gui_objects.class_factory(name)
|
|
26
|
+
if display_name is not None:
|
|
27
|
+
self._display_name = display_name
|
|
28
|
+
else:
|
|
29
|
+
self._display_name = name
|
|
30
|
+
|
|
31
|
+
def sizeHint(self):
|
|
32
|
+
"""QT event callback function"""
|
|
33
|
+
return QSize(80, 105)
|
|
34
|
+
|
|
35
|
+
def mousePressEvent(self, event):
|
|
36
|
+
"""QT event callback function"""
|
|
37
|
+
if event.buttons() == Qt.LeftButton:
|
|
38
|
+
drag = QDrag(self)
|
|
39
|
+
mime = QMimeData()
|
|
40
|
+
mime.setText(self._name)
|
|
41
|
+
drag.setMimeData(mime)
|
|
42
|
+
drag.setHotSpot(event.pos() - self.rect().topLeft())
|
|
43
|
+
# Create a square pixmap for dragging, using the widget's width for both dimensions.
|
|
44
|
+
# This is intentional to maintain a consistent visual representation during drag operations.
|
|
45
|
+
pixmap = QPixmap(self.size().width(), self.size().width())
|
|
46
|
+
self.render(pixmap)
|
|
47
|
+
drag.setPixmap(pixmap)
|
|
48
|
+
drag.exec_(Qt.CopyAction | Qt.MoveAction, Qt.CopyAction)
|
|
49
|
+
|
|
50
|
+
def mouseDoubleClickEvent(self, _):
|
|
51
|
+
"""QT event callback function"""
|
|
52
|
+
QTimer.singleShot(0, partial(self._circuit_area.add_component, self._name, None))
|
|
53
|
+
|
|
54
|
+
def paintEvent(self, event):
|
|
55
|
+
"""QT event callback function"""
|
|
56
|
+
if self._paint_class is None:
|
|
57
|
+
super().paintEvent(event)
|
|
58
|
+
else:
|
|
59
|
+
painter = QPainter(self)
|
|
60
|
+
self._paint_class.paint_selectable_component(painter, self.size(), self._display_name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HorizontalLine(QFrame):
|
|
64
|
+
"""
|
|
65
|
+
Horizontal line for the component selection
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, parent):
|
|
69
|
+
super().__init__(parent)
|
|
70
|
+
self.setFrameShape(QFrame.HLine)
|
|
71
|
+
self.setFrameShadow(QFrame.Sunken)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DescriptionText(QWidget):
|
|
75
|
+
"""
|
|
76
|
+
Text label for the component selection
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, parent, text):
|
|
80
|
+
super().__init__(parent)
|
|
81
|
+
self.setLayout(QVBoxLayout(self))
|
|
82
|
+
self.layout().addWidget(HorizontalLine(self))
|
|
83
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
84
|
+
self.layout().setSpacing(0)
|
|
85
|
+
label = QLabel(text)
|
|
86
|
+
label.setAlignment(Qt.AlignCenter)
|
|
87
|
+
self.layout().addWidget(label)
|
|
88
|
+
self.layout().addWidget(HorizontalLine(self))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ComponentSelection(QWidget):
|
|
92
|
+
"""
|
|
93
|
+
The component selection area,
|
|
94
|
+
these are the components than can be dragged into the circuit area.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, app_model, circuit_area, parent):
|
|
98
|
+
super().__init__(parent)
|
|
99
|
+
self.app_model = app_model
|
|
100
|
+
self.setLayout(QVBoxLayout(self))
|
|
101
|
+
self.layout().setContentsMargins(5, 5, 5, 5)
|
|
102
|
+
self.layout().setSpacing(5)
|
|
103
|
+
self.layout().addWidget(DescriptionText(self, "Input"))
|
|
104
|
+
self.layout().addWidget(SelectableComponentWidget("PushButton", self, circuit_area))
|
|
105
|
+
self.layout().addWidget(SelectableComponentWidget("OnOffSwitch", self, circuit_area))
|
|
106
|
+
self.layout().addWidget(
|
|
107
|
+
SelectableComponentWidget("DipSwitch", self, circuit_area, display_name="DIP switch")
|
|
108
|
+
)
|
|
109
|
+
self.layout().addWidget(SelectableComponentWidget("Clock", self, circuit_area))
|
|
110
|
+
self.layout().addWidget(SelectableComponentWidget("StaticValue", self, circuit_area))
|
|
111
|
+
self.layout().addWidget(DescriptionText(self, "Output"))
|
|
112
|
+
self.layout().addWidget(SelectableComponentWidget("Led", self, circuit_area))
|
|
113
|
+
self.layout().addWidget(
|
|
114
|
+
SelectableComponentWidget("HexDigit", self, circuit_area, display_name="Hex-digit")
|
|
115
|
+
)
|
|
116
|
+
self.layout().addWidget(
|
|
117
|
+
SelectableComponentWidget("SevenSegment", self, circuit_area, display_name="7-Seg")
|
|
118
|
+
)
|
|
119
|
+
self.layout().addWidget(SelectableComponentWidget("Buzzer", self, circuit_area))
|
|
120
|
+
self.layout().addWidget(
|
|
121
|
+
SelectableComponentWidget(
|
|
122
|
+
"LogicAnalyzer", self, circuit_area, display_name="Logic Analyzer"
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
self.layout().addWidget(DescriptionText(self, "Gates"))
|
|
126
|
+
self.layout().addWidget(SelectableComponentWidget("OR", self, circuit_area))
|
|
127
|
+
self.layout().addWidget(SelectableComponentWidget("AND", self, circuit_area))
|
|
128
|
+
self.layout().addWidget(SelectableComponentWidget("NOT", self, circuit_area))
|
|
129
|
+
self.layout().addWidget(SelectableComponentWidget("XOR", self, circuit_area))
|
|
130
|
+
self.layout().addWidget(SelectableComponentWidget("NAND", self, circuit_area))
|
|
131
|
+
self.layout().addWidget(SelectableComponentWidget("NOR", self, circuit_area))
|
|
132
|
+
self.layout().addWidget(SelectableComponentWidget("DFF", self, circuit_area))
|
|
133
|
+
self.layout().addWidget(SelectableComponentWidget("FlipFlop", self, circuit_area))
|
|
134
|
+
self.layout().addWidget(SelectableComponentWidget("MUX", self, circuit_area))
|
|
135
|
+
self.layout().addWidget(DescriptionText(self, "Bus / Wires"))
|
|
136
|
+
self.layout().addWidget(
|
|
137
|
+
SelectableComponentWidget("LabelWireIn", self, circuit_area, display_name="Wire Sink")
|
|
138
|
+
)
|
|
139
|
+
self.layout().addWidget(
|
|
140
|
+
SelectableComponentWidget(
|
|
141
|
+
"LabelWireOut", self, circuit_area, display_name="Wire Source"
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
self.layout().addWidget(SelectableComponentWidget("Bus2Wires", self, circuit_area))
|
|
145
|
+
self.layout().addWidget(SelectableComponentWidget("Wires2Bus", self, circuit_area))
|
|
146
|
+
self.layout().addWidget(DescriptionText(self, "IC / Verilog"))
|
|
147
|
+
self.layout().addWidget(
|
|
148
|
+
SelectableComponentWidget("IntegratedCircuit", self, circuit_area, display_name="IC")
|
|
149
|
+
)
|
|
150
|
+
self.layout().addWidget(
|
|
151
|
+
SelectableComponentWidget("YosysComponent", self, circuit_area, display_name="Yosys")
|
|
152
|
+
)
|
|
153
|
+
self.layout().addWidget(DescriptionText(self, "Other"))
|
|
154
|
+
self.layout().addWidget(SelectableComponentWidget("Note", self, circuit_area))
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""The main window and widgets of the digsim gui application"""
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from PySide6.QtGui import QKeySequence, QShortcut
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QHBoxLayout,
|
|
10
|
+
QMainWindow,
|
|
11
|
+
QMessageBox,
|
|
12
|
+
QScrollArea,
|
|
13
|
+
QSplitter,
|
|
14
|
+
QVBoxLayout,
|
|
15
|
+
QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ._circuit_area import CircuitArea
|
|
19
|
+
from ._component_selection import ComponentSelection
|
|
20
|
+
from ._top_bar import TopBar
|
|
21
|
+
from ._utils import are_you_sure_destroy_circuit
|
|
22
|
+
from ._warning_dialog import WarningDialog
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CircuitEditor(QSplitter):
|
|
26
|
+
"""
|
|
27
|
+
The circuit editor, the component selction widget and the circuit area widget.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, app_model, parent):
|
|
31
|
+
super().__init__(parent)
|
|
32
|
+
self._app_model = app_model
|
|
33
|
+
self._app_model.sig_control_notify.connect(self._control_notify)
|
|
34
|
+
|
|
35
|
+
self.setLayout(QHBoxLayout(self))
|
|
36
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
37
|
+
self.layout().setSpacing(0)
|
|
38
|
+
|
|
39
|
+
self._selection_area = QScrollArea(self)
|
|
40
|
+
self._selection_area.setFixedWidth(106)
|
|
41
|
+
self._selection_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
|
42
|
+
self._selection_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
43
|
+
|
|
44
|
+
self._circuit_area = CircuitArea(app_model, self)
|
|
45
|
+
selection_panel = ComponentSelection(app_model, self._circuit_area, self)
|
|
46
|
+
|
|
47
|
+
self.layout().addWidget(self._circuit_area)
|
|
48
|
+
self._selection_area.setWidget(selection_panel)
|
|
49
|
+
|
|
50
|
+
self.layout().addWidget(self._selection_area)
|
|
51
|
+
self.layout().setStretchFactor(self._selection_area, 0)
|
|
52
|
+
self._circuit_area.setFocus()
|
|
53
|
+
|
|
54
|
+
def _control_notify(self):
|
|
55
|
+
self._selection_area.setEnabled(not self._app_model.is_running)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def circuit_area(self):
|
|
59
|
+
"""Get the circuit area"""
|
|
60
|
+
return self._circuit_area
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CentralWidget(QWidget):
|
|
64
|
+
"""
|
|
65
|
+
The central widget with the top widget and circuit editor widget.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, app_model, parent):
|
|
69
|
+
super().__init__(parent)
|
|
70
|
+
self._app_model = app_model
|
|
71
|
+
|
|
72
|
+
self.setLayout(QVBoxLayout(self))
|
|
73
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
74
|
+
self.layout().setSpacing(0)
|
|
75
|
+
|
|
76
|
+
circuit_editor = CircuitEditor(app_model, self)
|
|
77
|
+
top_bar = TopBar(app_model, circuit_editor, self)
|
|
78
|
+
self.layout().addWidget(top_bar)
|
|
79
|
+
self.layout().setStretchFactor(top_bar, 0)
|
|
80
|
+
|
|
81
|
+
self.layout().addWidget(circuit_editor)
|
|
82
|
+
self.layout().setStretchFactor(circuit_editor, 1)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class MainWindow(QMainWindow):
|
|
86
|
+
"""
|
|
87
|
+
The main window for the applicaton.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, app_model, package_version):
|
|
91
|
+
super().__init__()
|
|
92
|
+
|
|
93
|
+
self._app_model = app_model
|
|
94
|
+
self.resize(1280, 720)
|
|
95
|
+
central_widget = CentralWidget(app_model, self)
|
|
96
|
+
self.setWindowTitle(f"DigSim - Interactive Digital Logic Simulator [v{package_version}]")
|
|
97
|
+
self.setCentralWidget(central_widget)
|
|
98
|
+
self.setAcceptDrops(True) # Needed to avoid "No drag target set."
|
|
99
|
+
self._app_model.sig_error.connect(self.error_dialog)
|
|
100
|
+
self._app_model.sig_warning_log.connect(self.warning_log_dialog)
|
|
101
|
+
|
|
102
|
+
QShortcut(QKeySequence("Ctrl+Z"), self, self._app_model.objects.undo)
|
|
103
|
+
QShortcut(QKeySequence("Ctrl+Y"), self, self._app_model.objects.redo)
|
|
104
|
+
QShortcut(QKeySequence("Ctrl++"), self, self._app_model.zoom_in)
|
|
105
|
+
QShortcut(QKeySequence("Ctrl+-"), self, self._app_model.zoom_out)
|
|
106
|
+
QShortcut(QKeySequence("Del"), self, self._app_model.objects.delete_selected)
|
|
107
|
+
|
|
108
|
+
def keyPressEvent(self, event):
|
|
109
|
+
"""QT event callback function"""
|
|
110
|
+
super().keyPressEvent(event)
|
|
111
|
+
if event.isAutoRepeat():
|
|
112
|
+
event.accept()
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if event.key() == Qt.Key_Space:
|
|
116
|
+
if self._app_model.is_running:
|
|
117
|
+
self._app_model.model_stop()
|
|
118
|
+
else:
|
|
119
|
+
self._app_model.model_start()
|
|
120
|
+
event.accept()
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if self._app_model.is_running:
|
|
124
|
+
self._app_model.shortcuts.press(event.key())
|
|
125
|
+
event.accept()
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if event.key() == Qt.Key_Escape:
|
|
129
|
+
self._app_model.model_abort_wire()
|
|
130
|
+
event.accept()
|
|
131
|
+
|
|
132
|
+
def keyReleaseEvent(self, event):
|
|
133
|
+
"""QT event callback function"""
|
|
134
|
+
super().keyReleaseEvent(event)
|
|
135
|
+
if event.isAutoRepeat():
|
|
136
|
+
event.accept()
|
|
137
|
+
return
|
|
138
|
+
if self._app_model.is_running:
|
|
139
|
+
self._app_model.shortcuts.release(event.key())
|
|
140
|
+
event.accept()
|
|
141
|
+
|
|
142
|
+
def _undo_shortcut(self):
|
|
143
|
+
self._app_model.objects.undo()
|
|
144
|
+
|
|
145
|
+
def error_dialog(self, error_message):
|
|
146
|
+
"""Execute Error dialog"""
|
|
147
|
+
QMessageBox.critical(self.parent(), "Error!", error_message, QMessageBox.Ok)
|
|
148
|
+
|
|
149
|
+
def warning_log_dialog(self, title, warning_message):
|
|
150
|
+
"""Execute warning log dialog"""
|
|
151
|
+
warning_dialog = WarningDialog(self, title, warning_message)
|
|
152
|
+
warning_dialog.exec_()
|
|
153
|
+
|
|
154
|
+
def closeEvent(self, event):
|
|
155
|
+
"""QT event callback function"""
|
|
156
|
+
if not self._app_model.is_changed or are_you_sure_destroy_circuit(
|
|
157
|
+
self.parent(), "Close Application"
|
|
158
|
+
):
|
|
159
|
+
self._app_model.model_stop()
|
|
160
|
+
self._app_model.wait()
|
|
161
|
+
super().closeEvent(event)
|
|
162
|
+
else:
|
|
163
|
+
event.ignore()
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""The top bar of the main window/gui application"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import qtawesome as qta
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import Qt, QTimer
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QCheckBox,
|
|
13
|
+
QFileDialog,
|
|
14
|
+
QFrame,
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QLabel,
|
|
17
|
+
QPushButton,
|
|
18
|
+
QStyle,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from digsim.app.settings import GuiSettingsDialog
|
|
22
|
+
|
|
23
|
+
from ._utils import are_you_sure_destroy_circuit
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SimControlWidget(QFrame):
|
|
27
|
+
"""
|
|
28
|
+
The widget for controlling the simulation in the top bar
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
CONTROL_WIDGET_WIDTH = 120
|
|
32
|
+
|
|
33
|
+
def __init__(self, app_model, parent):
|
|
34
|
+
super().__init__(parent)
|
|
35
|
+
self._time_s = 0
|
|
36
|
+
self._app_model = app_model
|
|
37
|
+
self.setLayout(QHBoxLayout(self))
|
|
38
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
39
|
+
self.layout().setSpacing(5)
|
|
40
|
+
self._start_button = QPushButton("Start Simulation", self)
|
|
41
|
+
self._start_button.clicked.connect(self._start_stop)
|
|
42
|
+
self._start_button.setMinimumWidth(self.CONTROL_WIDGET_WIDTH)
|
|
43
|
+
self.layout().addWidget(self._start_button)
|
|
44
|
+
self._single_step_button = QPushButton("Single Step", self)
|
|
45
|
+
self._single_step_button.clicked.connect(self._single_step)
|
|
46
|
+
self._single_step_button.setMinimumWidth(self.CONTROL_WIDGET_WIDTH)
|
|
47
|
+
self.layout().addWidget(self._single_step_button)
|
|
48
|
+
self._reset_button = QPushButton("Reset Simulation", self)
|
|
49
|
+
self._reset_button.clicked.connect(self._reset)
|
|
50
|
+
self._reset_button.setEnabled(False)
|
|
51
|
+
self._reset_button.setMinimumWidth(self.CONTROL_WIDGET_WIDTH)
|
|
52
|
+
self.layout().addWidget(self._reset_button)
|
|
53
|
+
self._app_model.sig_control_notify.connect(self._control_notify)
|
|
54
|
+
self._app_model.sig_sim_time_notify.connect(self._sim_time_notify)
|
|
55
|
+
|
|
56
|
+
def _start_stop(self):
|
|
57
|
+
"""Button action: Start/Stop"""
|
|
58
|
+
if not self._app_model.is_running:
|
|
59
|
+
self._start_button.setEnabled(False)
|
|
60
|
+
self._app_model.model_start()
|
|
61
|
+
else:
|
|
62
|
+
self._start_button.setEnabled(False)
|
|
63
|
+
self._app_model.model_stop()
|
|
64
|
+
|
|
65
|
+
def _single_step(self):
|
|
66
|
+
"""Button action: Reset"""
|
|
67
|
+
self._start_button.setEnabled(False)
|
|
68
|
+
self._single_step_button.setEnabled(False)
|
|
69
|
+
self._app_model.model_single_step()
|
|
70
|
+
|
|
71
|
+
def _reset(self):
|
|
72
|
+
"""Button action: Reset"""
|
|
73
|
+
self._app_model.model_reset()
|
|
74
|
+
|
|
75
|
+
def _control_notify(self):
|
|
76
|
+
if self._app_model.is_running:
|
|
77
|
+
self._start_button.setText("Stop Similation")
|
|
78
|
+
self._start_button.setEnabled(True)
|
|
79
|
+
self._single_step_button.setEnabled(False)
|
|
80
|
+
else:
|
|
81
|
+
self._start_button.setText("Start Similation")
|
|
82
|
+
self._start_button.setEnabled(True)
|
|
83
|
+
self._single_step_button.setEnabled(True)
|
|
84
|
+
if self._time_s > 0:
|
|
85
|
+
self._reset_button.setEnabled(True)
|
|
86
|
+
|
|
87
|
+
def _sim_time_notify(self, time_s):
|
|
88
|
+
self._time_s = time_s
|
|
89
|
+
if self._time_s and not self._app_model.is_running:
|
|
90
|
+
self._reset_button.setEnabled(True)
|
|
91
|
+
else:
|
|
92
|
+
self._reset_button.setEnabled(False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SimTimeWidget(QFrame):
|
|
96
|
+
"""
|
|
97
|
+
The widget showing simulation time in the opt bar
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, app_model, parent):
|
|
101
|
+
super().__init__(parent)
|
|
102
|
+
self.setFrameShape(QFrame.StyledPanel)
|
|
103
|
+
self.setFrameShadow(QFrame.Sunken)
|
|
104
|
+
self.setLayout(QHBoxLayout(self))
|
|
105
|
+
self.layout().setContentsMargins(5, 1, 5, 1)
|
|
106
|
+
self.layout().setSpacing(5)
|
|
107
|
+
self._app_model = app_model
|
|
108
|
+
sim_time_desc = QLabel("Simulation time:")
|
|
109
|
+
sim_time_desc.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
110
|
+
self.layout().addWidget(sim_time_desc)
|
|
111
|
+
self._sim_time = QLabel("0 s")
|
|
112
|
+
self._sim_time.setMinimumWidth(60)
|
|
113
|
+
self._sim_time.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
114
|
+
self.layout().addWidget(self._sim_time)
|
|
115
|
+
self._app_model.sig_sim_time_notify.connect(self._sim_time_notify)
|
|
116
|
+
|
|
117
|
+
def _sim_time_notify(self, time_s):
|
|
118
|
+
self._sim_time.setText(f"{time_s:.2f} s")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class VcdFilenameWidget(QFrame):
|
|
122
|
+
"""
|
|
123
|
+
The widget showing the vcd filename
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, parent):
|
|
127
|
+
super().__init__(parent)
|
|
128
|
+
self.setFrameShape(QFrame.StyledPanel)
|
|
129
|
+
self.setFrameShadow(QFrame.Sunken)
|
|
130
|
+
self.setLayout(QHBoxLayout(self))
|
|
131
|
+
self.layout().setContentsMargins(5, 1, 5, 1)
|
|
132
|
+
self.layout().setSpacing(5)
|
|
133
|
+
self._vcd_filename = QLabel("<vcd file>")
|
|
134
|
+
self._vcd_filename.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
135
|
+
self._vcd_filename.setMinimumWidth(200)
|
|
136
|
+
self.layout().addWidget(self._vcd_filename)
|
|
137
|
+
|
|
138
|
+
def set_filename(self, filename):
|
|
139
|
+
"""Set filename in VcdFilenameWdiget"""
|
|
140
|
+
self._vcd_filename.setText(filename)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class VcdControlWidget(QFrame):
|
|
144
|
+
"""
|
|
145
|
+
The widget for controlling the vcd output file
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, app_model, parent):
|
|
149
|
+
super().__init__(parent)
|
|
150
|
+
self._app_model = app_model
|
|
151
|
+
self._vcd_filename = None
|
|
152
|
+
self.setLayout(QHBoxLayout(self))
|
|
153
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
154
|
+
self.layout().setSpacing(5)
|
|
155
|
+
self._enable_vcd = QCheckBox("VCD output", self)
|
|
156
|
+
self._enable_vcd.stateChanged.connect(self._enable)
|
|
157
|
+
self._vcd_path = QPushButton("", self)
|
|
158
|
+
self._vcd_path.setIcon(self.style().standardIcon(QStyle.SP_FileIcon))
|
|
159
|
+
self._vcd_path.clicked.connect(self._change_vcd_file)
|
|
160
|
+
self._vcd_path.setToolTip("Select VCD output")
|
|
161
|
+
self._filename_widget = VcdFilenameWidget(self)
|
|
162
|
+
self.layout().addWidget(self._enable_vcd)
|
|
163
|
+
self.layout().addWidget(self._vcd_path)
|
|
164
|
+
self.layout().addWidget(self._filename_widget)
|
|
165
|
+
self._enable_buttons(False)
|
|
166
|
+
self._app_model.sig_control_notify.connect(self._control_notify)
|
|
167
|
+
|
|
168
|
+
def _enable_buttons(self, enable):
|
|
169
|
+
self._vcd_path.setEnabled(enable)
|
|
170
|
+
self._filename_widget.setEnabled(enable)
|
|
171
|
+
|
|
172
|
+
def _control_notify(self):
|
|
173
|
+
self._enable_vcd.setEnabled(not self._app_model.is_running)
|
|
174
|
+
self._enable_buttons(not self._app_model.is_running and self._enable_vcd.isChecked())
|
|
175
|
+
|
|
176
|
+
def _disable_vcd_selection(self):
|
|
177
|
+
self._enable_vcd.setChecked(False)
|
|
178
|
+
self._enable_buttons(False)
|
|
179
|
+
|
|
180
|
+
def _start_vcd(self, filename):
|
|
181
|
+
self._vcd_filename = filename
|
|
182
|
+
display_filename = Path(self._vcd_filename).name
|
|
183
|
+
self._filename_widget.set_filename(display_filename)
|
|
184
|
+
self._enable_buttons(True)
|
|
185
|
+
# Close old vcd (if any)
|
|
186
|
+
self._app_model.objects.circuit.vcd_close()
|
|
187
|
+
# Enable VCD in the model
|
|
188
|
+
self._app_model.objects.circuit.vcd(self._vcd_filename)
|
|
189
|
+
|
|
190
|
+
def _open_filedialog(self):
|
|
191
|
+
default_filename = self._vcd_filename
|
|
192
|
+
if default_filename is None:
|
|
193
|
+
default_filename = "vcd_file.vcd"
|
|
194
|
+
path = QFileDialog.getSaveFileName(
|
|
195
|
+
self, "VCD Output name", default_filename, "VCD Files (*.vcd);;All Files (*.*)"
|
|
196
|
+
)
|
|
197
|
+
if len(path[0]) == 0:
|
|
198
|
+
return None
|
|
199
|
+
return path[0]
|
|
200
|
+
|
|
201
|
+
def _change_vcd_file(self):
|
|
202
|
+
filename = self._open_filedialog()
|
|
203
|
+
if filename is not None:
|
|
204
|
+
self._start_vcd(filename)
|
|
205
|
+
|
|
206
|
+
def _enable(self, _):
|
|
207
|
+
is_checked = self._enable_vcd.isChecked()
|
|
208
|
+
if is_checked:
|
|
209
|
+
if self._vcd_filename is not None:
|
|
210
|
+
self._enable_buttons(True)
|
|
211
|
+
else:
|
|
212
|
+
filename = self._open_filedialog()
|
|
213
|
+
|
|
214
|
+
if filename is not None:
|
|
215
|
+
self._enable_buttons(True)
|
|
216
|
+
self._start_vcd(filename)
|
|
217
|
+
else:
|
|
218
|
+
QTimer.singleShot(0, self._disable_vcd_selection)
|
|
219
|
+
else:
|
|
220
|
+
self._enable_buttons(False)
|
|
221
|
+
# Disable VCD in the model
|
|
222
|
+
self._app_model.objects.circuit.vcd_close()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class LoadSaveWidget(QFrame):
|
|
226
|
+
"""
|
|
227
|
+
The widget with load/save buttons in the top bar
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(self, app_model, circuit_editor, parent):
|
|
231
|
+
super().__init__(parent)
|
|
232
|
+
self._app_model = app_model
|
|
233
|
+
self._circuit_editor = circuit_editor
|
|
234
|
+
self.setLayout(QHBoxLayout(self))
|
|
235
|
+
self.layout().setContentsMargins(0, 0, 0, 0)
|
|
236
|
+
self.layout().setSpacing(5)
|
|
237
|
+
self._zoom_in_button = QPushButton("", self)
|
|
238
|
+
self._zoom_in_button.setIcon(qta.icon("msc.zoom-in"))
|
|
239
|
+
self._zoom_in_button.clicked.connect(self._app_model.zoom_in)
|
|
240
|
+
self._zoom_out_button = QPushButton("", self)
|
|
241
|
+
self._zoom_out_button.setIcon(qta.icon("msc.zoom-out"))
|
|
242
|
+
self._zoom_out_button.clicked.connect(self._app_model.zoom_out)
|
|
243
|
+
self._zoom_out_button.setToolTip("Zoom out")
|
|
244
|
+
self.layout().addWidget(self._zoom_in_button)
|
|
245
|
+
self.layout().addWidget(self._zoom_out_button)
|
|
246
|
+
self._delete_button = QPushButton("", self)
|
|
247
|
+
self._delete_button.setIcon(qta.icon("mdi.delete-forever"))
|
|
248
|
+
self._delete_button.clicked.connect(self._app_model.objects.delete_selected)
|
|
249
|
+
self._delete_button.setToolTip("Delete")
|
|
250
|
+
self.layout().addWidget(self._delete_button)
|
|
251
|
+
self._undo_button = QPushButton("", self)
|
|
252
|
+
self._undo_button.setIcon(qta.icon("mdi.undo"))
|
|
253
|
+
self._undo_button.setToolTip("Undo")
|
|
254
|
+
self._undo_button.clicked.connect(self._app_model.objects.undo)
|
|
255
|
+
self.layout().addWidget(self._undo_button)
|
|
256
|
+
self._redo_button = QPushButton("", self)
|
|
257
|
+
self._redo_button.setIcon(qta.icon("mdi.redo"))
|
|
258
|
+
self._redo_button.clicked.connect(self._app_model.objects.redo)
|
|
259
|
+
self._redo_button.setToolTip("Redo")
|
|
260
|
+
self.layout().addWidget(self._redo_button)
|
|
261
|
+
self._settings_button = QPushButton("", self)
|
|
262
|
+
self._settings_button.setIcon(qta.icon("mdi.cog"))
|
|
263
|
+
self._settings_button.clicked.connect(self._settings_dialog)
|
|
264
|
+
self._settings_button.setToolTip("Settings")
|
|
265
|
+
self.layout().addWidget(self._settings_button)
|
|
266
|
+
self._load_button = QPushButton("Load Circuit", self)
|
|
267
|
+
self._load_button.clicked.connect(self._load)
|
|
268
|
+
self.layout().addWidget(self._load_button)
|
|
269
|
+
self._save_button = QPushButton("Save Circuit", self)
|
|
270
|
+
self._save_button.clicked.connect(self._save)
|
|
271
|
+
self.layout().addWidget(self._save_button)
|
|
272
|
+
self._clear_button = QPushButton("Clear Circuit", self)
|
|
273
|
+
self._clear_button.clicked.connect(self._clear)
|
|
274
|
+
self.layout().addWidget(self._clear_button)
|
|
275
|
+
self._app_model.sig_control_notify.connect(self._control_notify)
|
|
276
|
+
self._control_notify()
|
|
277
|
+
|
|
278
|
+
def _settings_dialog(self):
|
|
279
|
+
settings_dialog = GuiSettingsDialog(self, self._app_model)
|
|
280
|
+
settings_dialog.start()
|
|
281
|
+
|
|
282
|
+
def _load(self):
|
|
283
|
+
"""Button action: Load"""
|
|
284
|
+
if self._app_model.is_changed and not are_you_sure_destroy_circuit(
|
|
285
|
+
self.parent(), "Load circuit"
|
|
286
|
+
):
|
|
287
|
+
return
|
|
288
|
+
path = QFileDialog.getOpenFileName(
|
|
289
|
+
self, "Load Circuit", "", "Circuit Files (*.circuit);;All Files (*.*)"
|
|
290
|
+
)
|
|
291
|
+
if len(path[0]) == 0:
|
|
292
|
+
return
|
|
293
|
+
self._app_model.load_circuit(path[0])
|
|
294
|
+
|
|
295
|
+
def _save(self):
|
|
296
|
+
"""Button action: Save"""
|
|
297
|
+
path = QFileDialog.getSaveFileName(
|
|
298
|
+
self, "Save Circuit", "", "Circuit Files (*.circuit);;All Files (*.*)"
|
|
299
|
+
)
|
|
300
|
+
if len(path[0]) == 0:
|
|
301
|
+
return
|
|
302
|
+
self._app_model.save_circuit(path[0])
|
|
303
|
+
|
|
304
|
+
def _clear(self):
|
|
305
|
+
"""Button action: Save"""
|
|
306
|
+
self._app_model.clear_circuit()
|
|
307
|
+
|
|
308
|
+
def _control_notify(self):
|
|
309
|
+
if self._app_model.is_running:
|
|
310
|
+
self._load_button.setEnabled(False)
|
|
311
|
+
self._save_button.setEnabled(False)
|
|
312
|
+
self._clear_button.setEnabled(False)
|
|
313
|
+
self._delete_button.setEnabled(False)
|
|
314
|
+
self._undo_button.setEnabled(False)
|
|
315
|
+
self._redo_button.setEnabled(False)
|
|
316
|
+
self._settings_button.setEnabled(False)
|
|
317
|
+
else:
|
|
318
|
+
self._load_button.setEnabled(True)
|
|
319
|
+
self._save_button.setEnabled(self._app_model.is_changed)
|
|
320
|
+
self._clear_button.setEnabled(not self._app_model.objects.components.is_empty())
|
|
321
|
+
self._delete_button.setEnabled(self._circuit_editor.circuit_area.has_selection())
|
|
322
|
+
self._undo_button.setEnabled(self._app_model.objects.can_undo())
|
|
323
|
+
self._redo_button.setEnabled(self._app_model.objects.can_redo())
|
|
324
|
+
self._settings_button.setEnabled(True)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class TopBar(QFrame):
|
|
328
|
+
"""
|
|
329
|
+
The top widget with the control and status widgets
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def __init__(self, app_model, circuit_editor, parent):
|
|
333
|
+
super().__init__(parent)
|
|
334
|
+
self.setLayout(QHBoxLayout(self))
|
|
335
|
+
self.layout().addWidget(SimControlWidget(app_model, self))
|
|
336
|
+
self.layout().addWidget(SimTimeWidget(app_model, self))
|
|
337
|
+
self.layout().addStretch(1)
|
|
338
|
+
self.layout().addWidget(VcdControlWidget(app_model, self))
|
|
339
|
+
self.layout().addWidget(LoadSaveWidget(app_model, circuit_editor, self))
|
digsim/app/gui/_utils.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (c) Fredrik Andersson, 2023-2025
|
|
2
|
+
# All rights reserved
|
|
3
|
+
|
|
4
|
+
"""Module with utility functions shared in the GUI"""
|
|
5
|
+
|
|
6
|
+
from PySide6.QtWidgets import QMessageBox
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def warning_messagebox(parent, dialog_text, message_text):
|
|
10
|
+
"""Are you sure messagebox"""
|
|
11
|
+
result = QMessageBox.question(
|
|
12
|
+
parent,
|
|
13
|
+
dialog_text,
|
|
14
|
+
message_text,
|
|
15
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
16
|
+
)
|
|
17
|
+
return result == QMessageBox.Yes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def are_you_sure_destroy_circuit(parent, dialog_text):
|
|
21
|
+
"""Are you sure messagebox"""
|
|
22
|
+
return warning_messagebox(
|
|
23
|
+
parent,
|
|
24
|
+
dialog_text,
|
|
25
|
+
"Are you sure want to destroy the current circuit?",
|
|
26
|
+
)
|