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,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))
@@ -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
+ )