Skip to content
Snippets Groups Projects
Commit 52bcfa23 authored by Johannes Kung's avatar Johannes Kung
Browse files

Implemented formatting/parsing and module actions

parent 10060300
No related branches found
No related tags found
1 merge request!45General gui module
Pipeline #134493 failed
from typing import Optional
from qtpy.QtCore import Qt from qtpy.QtCore import Qt
from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QComboBox, QComboBox,
QDialog, QDialog,
...@@ -12,65 +11,97 @@ from qtpy.QtWidgets import ( ...@@ -12,65 +11,97 @@ from qtpy.QtWidgets import (
) )
from simudator.core import Module from simudator.core import Module
from simudator.gui.formatting import format_to_str, parse_str
from simudator.gui.module_graphics_item.module_widget import ModuleWidget
class ModuleStateDialog(QDialog): class ModuleStateDialog(QDialog):
accepted = pyqtSignal(str, str, str, name='okSignal') """
A dialog for letting the user edit the state of a processor module.
Parameters
----------
module : Module
The module to edit the state of.
module_widget : ModuleWidget
The corresponding module widget for the module. Used mainly for
formatting and parsing the values of the state variables of the module.
parent : QWidget | None
Optional parent widget of this dialog.
flags : Qt.WindowFlags | Qt.WindowType
Optional window flags for the window of the dialog.
"""
state_edited = pyqtSignal()
def __init__( def __init__(
self, self,
module: Module, module: Module,
parent: Optional['QWidget'] = None, module_widget: ModuleWidget,
parent: QWidget | None = None,
flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(), flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
) -> None: ) -> None:
super().__init__(parent, flags) super().__init__(parent, flags)
self.module = module self._module = module
states = module.get_gui_state() self._state_vars = module_widget.get_state_vars()
# Remove the 'name' state of the module from the drop down menu so
# that the user cannot e.g. edit or add a breakpoint of a module
states.pop('name')
# Set up the drop down menu for selecting a state of the module # Set up the drop down menu for selecting a state variable of the module
# to perform an action to self._state_var_widget = QComboBox()
self.stateSelectWidget = QComboBox() self._state_var_widget.addItems(self._state_vars.keys())
self.stateSelectWidget.addItems(states.keys())
self.stateSelectWidget.activated.connect(self.updateState)
# Set up a text field for entering the value used for the e.g. editing # Set up a text field for displaying and editing the value of the
# the selected state # selected state variable
self.valueWidget = QLineEdit() self._value_widget = QLineEdit()
# Set up buttons # Set up buttons
self.okButton = QPushButton('OK') self._accept_button = QPushButton('OK')
self.okButton.clicked.connect(self.signalOK) self._accept_button.clicked.connect(self.edit_state)
self.cancelButton = QPushButton('Cancel') self._cancel_button = QPushButton('Cancel')
self.cancelButton.clicked.connect(self.close) self._cancel_button.clicked.connect(self.close)
# Set up the layout of the widget # Set up the layout of the widget
self.HBoxLayout = QHBoxLayout() self._layout = QHBoxLayout()
self.HBoxLayout.addWidget(self.stateSelectWidget, 0) self._layout.addWidget(self._state_var_widget, 0)
self.HBoxLayout.addWidget(self.valueWidget, 1) self._layout.addWidget(self._value_widget, 1)
self.HBoxLayout.addWidget(self.okButton, 2) self._layout.addWidget(self._accept_button, 2)
self.HBoxLayout.addWidget(self.cancelButton, 3) self._layout.addWidget(self._cancel_button, 3)
self.setLayout(self.HBoxLayout) self.setLayout(self._layout)
# Show the widget # Show the widget
self.updateState() self.select_state_var()
self.show() self.exec()
def updateState(self) -> None: @Slot()
selectedState = self.stateSelectWidget.currentText() def select_state_var(self) -> None:
value = self.module.get_gui_state()[selectedState] """
self.valueWidget.setText(str(value)) Select a state variable of the module to edit.
"""
def signalOK(self) -> None: selected_state = self._state_var_widget.currentText()
module_name = self.module.get_state()['name'] format_info = self._state_vars[selected_state]
selectedState = self.stateSelectWidget.currentText() value = format_to_str(format_info, self._module.get_state()[selected_state])
enteredValue = self.valueWidget.text() self._value_widget.setText(value)
self.accepted.emit(module_name, selectedState, enteredValue)
self.close() @Slot()
def edit_state(self) -> None:
def close(self) -> bool: """
return super().close() Parse the user input and edit the state of the module associated with
this dialog.
This triggers the `state_edited` signal if the parsing is
successful. Else, the user is given an error message.
"""
state = self._module.get_state()
selected_state_var = self._state_var_widget.currentText()
try:
value = parse_str(
self._state_vars[selected_state_var], self._value_widget.text()
)
state[selected_state_var] = value
self._module.set_state(state)
self.state_edited.emit()
self.close()
except ValueError as e:
# TODO: error handling
pass
import math
from enum import Enum, auto
from typing import Any
class Format(Enum):
"""
An Enum class containing the different valid formats for formatting and
parsing values of different data types.
"""
Str = (auto(),)
Int = (auto(),)
Bin = (auto(),)
Hex = (auto(),)
Decimal = (auto(),)
DecimalSigned = (auto(),)
class FormatInfo:
"""
A class for neatly keeping data about the supported formats and selected
format for correctly displaying and parsing values of different data types.
Parameters
----------
supported_format : list[Format]
List of formats that are supported for some variable.
selected_format : Format
The format that should be used on initialization.
**kwargs
Arguments passed to `format_to_str` and `parse_str`. Required by some
formats.
Attributes
----------
supported_formats : list[Format]
List of format that are supported.
selected_format : Format
The selected format to use when formatting/parsing. Should be one of
the supported formats.
"""
NUMERICAL_FORMATS = (
Format.Int,
Format.Bin,
Format.Hex,
Format.Decimal,
Format.DecimalSigned,
)
"""
List of standard numerical formats for handling numbers. It is possible to
switch between these formats easily as they all require the same
keyword arguments.
"""
__slots__ = ("selected_format", "supported_formats", "kwargs")
def __init__(
self, supported_formats: list[Format], selected_format: Format, **kwargs
) -> None:
self.selected_format = selected_format
self.supported_formats = supported_formats
self.kwargs = kwargs
def format_to_str(format_info: FormatInfo, value: Any) -> str:
"""
Create a formatted string from a given value.
Parameters
----------
format_info : FormatInfo
A class containing information about what format to use.
value : Any
An arbitrary value that is to be formatted to a string.
Returns
-------
The input value formatted to a string according to the input variable
format.
"""
match format_info.selected_format:
case Format.Int:
pass
case Format.Str:
pass
case Format.Bin:
return f"{value:0{format_info.kwargs["bit_length"]}b}"
case Format.Hex:
return f"{value:0{math.ceil(format_info.kwargs["bit_length"]/4)}x}"
case Format.Decimal:
pass
case Format.DecimalSigned:
pass
def parse_str(format_info: FormatInfo, value: str) -> Any:
"""
Parse an input string formatted using the given format.
Parameters
----------
format_info : FormatInfo
An instance containing information about the format used on the string.
value : str
A formatted string to parse into a value.
Returns
-------
The parsed value from the formatted string. The data type of the value
depends on the format.
"""
match format_info.selected_format:
case Format.Int:
return int(value)
case Format.Hex:
return int(value, base=16)
...@@ -2,7 +2,6 @@ import ast ...@@ -2,7 +2,6 @@ import ast
import sys import sys
from qtpy import QtCore, QtWidgets from qtpy import QtCore, QtWidgets
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot from qtpy.QtCore import Slot
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QAction, QAction,
...@@ -19,6 +18,7 @@ from simudator.gui.cpu_graphics_scene import CpuGraphicsScene ...@@ -19,6 +18,7 @@ from simudator.gui.cpu_graphics_scene import CpuGraphicsScene
from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog
from simudator.gui.menu_bar import MainMenuBar from simudator.gui.menu_bar import MainMenuBar
from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem
from simudator.gui.module_graphics_item.module_widget import ModuleWidget
from simudator.gui.pipeline import PipeLine from simudator.gui.pipeline import PipeLine
from simudator.gui.processor_handler import ProcessorHandler from simudator.gui.processor_handler import ProcessorHandler
from simudator.gui.signal_viewer import SignalViewer from simudator.gui.signal_viewer import SignalViewer
...@@ -182,12 +182,6 @@ class GUI(QMainWindow): ...@@ -182,12 +182,6 @@ class GUI(QMainWindow):
if self._breakpoint_window is not None: if self._breakpoint_window is not None:
self._breakpoint_window.update() self._breakpoint_window.update()
"""
@Slot is used to explicitly mark a python method as a Qt slot
and specify a C++ signature for it, which is used most commonly
in order to select a particular overload.
"""
@Slot(str, str, str) @Slot(str, str, str)
def editModuleState(self, module_name, state, value) -> None: def editModuleState(self, module_name, state, value) -> None:
""" """
...@@ -297,6 +291,20 @@ class GUI(QMainWindow): ...@@ -297,6 +291,20 @@ class GUI(QMainWindow):
self.connectModuleActions(item.getActionSignals()) self.connectModuleActions(item.getActionSignals())
self._processor_handler.changed.connect(item.update) self._processor_handler.changed.connect(item.update)
def add_module_widget(self, widget: ModuleWidget) -> None:
"""
Add a module widget to the processor scene and connect its QT signals
and slots.
Parameters
----------
widget : ModuleWidget
The module widget to add to the processor scene.
"""
self._processor_handler.changed.connect(widget.update)
self._graphics_scene.addItem(widget)
widget.state_changed.connect(self._processor_handler.handle_module_change)
def add_all_signals(self) -> None: def add_all_signals(self) -> None:
""" """
Add visual representations of all processor signals between all modules Add visual representations of all processor signals between all modules
......
from enum import Enum, auto
from PyQt5.QtWidgets import QAction
from simudator.core.module import Module
from simudator.gui.dialogs.module_state_dialog import ModuleStateDialog
from simudator.gui.module_graphics_item.module_widget import ModuleWidget
class ActionType(Enum):
"""
An Enum class containing the different valid actions for interacting with
module widgets.
"""
EditState = (auto(),)
def edit_state_action(module: Module, module_widget: ModuleWidget) -> QAction:
"""
Create a QAction for opening a state-edit dialog for a module widget.
The dialog triggers the state_changed signal of the module widget if the
user accepts the dialog.
Parameters
----------
module : Module
The module that should be edited when triggering the created edit action.
module_widget : ModuleWidget
The module widget for the module that should be edited. Used for
displaying and parsing information correctly.
Returns
-------
QAction
A Qt action for opening a dialog for editing the state of a module.
Intended to be added as an action to the given module widget.
"""
action = QAction("Edit state", module_widget)
def action_triggered():
dialog = ModuleStateDialog(module, module_widget)
dialog.state_edited.connect(module_widget.state_changed)
action.triggered.connect(action_triggered)
return action
from typing import Any from typing import Any
from qtpy.QtCore import Property, QPointF, QRectF, Qt, Slot
from qtpy.QtGui import QBrush, QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen from qtpy.QtCore import Property, QRectF, Qt
from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot from qtpy.QtCore import Slot
from qtpy.QtGui import QBrush, QColor, QFont, QFontMetrics, QPainter, QPainterPath, QPen
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QErrorMessage,
QGraphicsItem, QGraphicsItem,
QGraphicsSceneContextMenuEvent,
QGraphicsWidget, QGraphicsWidget,
QMenu,
QStyleOptionGraphicsItem, QStyleOptionGraphicsItem,
QWidget, QWidget,
QAction,
) )
from simudator.core.module import Module from simudator.core.module import Module
from simudator.gui.formatting import Format, FormatInfo, format_to_str
class ModuleWidget(QGraphicsWidget):
DEFAULT_WIDTH = 50*3
DEFAULT_HEIGHT = 50*3
update = pyqtSignal()
def __init__(self, module: Module, state_vars: list[str], actions: list[QAction], formatters: dict[str, Any], parent: QGraphicsItem | None = None, flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags()) -> None: class ModuleWidget(QGraphicsWidget):
"""
A general class for displaying an arbitrary processor module. The
appearance of a module can be customized by changing its properties and
by choosing which state variables to display and in what formats.
Parameters
----------
module : Module
The processor module to display.
state_vars : dict[str, FormatInfo]
The state variables of the module to display and in what formats to
display them.
parent : QGraphicsItem | None
Optional parent item of this module widget.
flags : Qt.WindowFlags | Qt.WindowType
Optional window flags for the window of this widget.
"""
DEFAULT_WIDTH = 50 * 3
DEFAULT_HEIGHT = 50 * 3
state_changed = pyqtSignal()
def __init__(
self,
module: Module,
state_vars: dict[str, FormatInfo],
parent: QGraphicsItem | None = None,
flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
) -> None:
super().__init__(parent, flags) super().__init__(parent, flags)
super().__init__(parent, flags) super().__init__(parent, flags)
self.setFlag(QGraphicsItem.ItemIsMovable) self.setFlag(QGraphicsItem.ItemIsMovable)
self._module = module self._module = module
self._state_vars = state_vars self._state_vars = state_vars
self._actions = actions
self._formatters = formatters
# Default values for properties for appearance # Default values for properties for appearance
# TODO: Put these values in constants? # TODO: Put these values in constants?
...@@ -41,9 +68,11 @@ class ModuleWidget(QGraphicsWidget): ...@@ -41,9 +68,11 @@ class ModuleWidget(QGraphicsWidget):
width = self.DEFAULT_WIDTH width = self.DEFAULT_WIDTH
height = self.DEFAULT_HEIGHT height = self.DEFAULT_HEIGHT
if len(self._state_vars) <= 1: if len(self._state_vars) <= 1:
height = QFontMetrics(self._text_font).height() + 2*self._padding height = QFontMetrics(self._text_font).height() + 2 * self._padding
else: else:
height = (self._padding + QFontMetrics(self._text_font).height()) * (len(state_vars) + 1) + self._padding height = (self._padding + QFontMetrics(self._text_font).height()) * (
len(state_vars) + 1
) + self._padding
self.resize(width, height) self.resize(width, height)
def paint( def paint(
...@@ -72,24 +101,30 @@ class ModuleWidget(QGraphicsWidget): ...@@ -72,24 +101,30 @@ class ModuleWidget(QGraphicsWidget):
text_rect = self.shape().boundingRect() text_rect = self.shape().boundingRect()
# Draw only name of the module if no state variables are to be shown # Draw only name of the module if no state variables are to be shown
if len(self._state_vars) == 0: if len(self._state_vars.keys()) == 0:
painter.drawText(text_rect, text_flags, self._module.get_state()["name"]) painter.drawText(text_rect, text_flags, self._module.get_state()["name"])
# Special case: only 1 state variable to show # Special case: only 1 state variable to show
elif len(self._state_vars) == 1: elif len(self._state_vars.keys()) == 1:
value = self._module.get_state()[self._state_vars[0]] state_var = list(self._state_vars.keys())[0]
painter.drawText(text_rect, text_flags, f"{self._module.get_state()["name"]}: {value}") format_info = self._state_vars[state_var]
value = format_to_str(format_info, self._module.get_state()[state_var])
painter.drawText(
text_rect, text_flags, f"{self._module.get_state()["name"]}: {value}"
)
# Draw the name of the module and each state variable with its value on one row each # Draw the name of the module and each state variable with its value on one row each
else: else:
text_rect = QRectF(0, 0, width, height/(len(self._state_vars)+1)) text_rect = QRectF(0, 0, width, height / (len(self._state_vars) + 1))
painter.drawText(text_rect, text_flags, self._module.get_state()["name"]) painter.drawText(text_rect, text_flags, self._module.get_state()["name"])
for i, state_var in enumerate(self._state_vars): for i, state_var in enumerate(self._state_vars):
y_start = height / (len(self._state_vars)+1) * (i+1) y_start = height / (len(self._state_vars) + 1) * (i + 1)
y_end = height / (len(self._state_vars) + 1 ) y_end = height / (len(self._state_vars) + 1)
text_rect = QRectF(0, y_start, width, y_end) text_rect = QRectF(0, y_start, width, y_end)
value = self._module.get_state()[state_var]
format_info = self._state_vars[state_var]
value = format_to_str(format_info, self._module.get_state()[state_var])
painter.drawText(text_rect, text_flags, f"{state_var}: {value}") painter.drawText(text_rect, text_flags, f"{state_var}: {value}")
painter.restore() painter.restore()
...@@ -98,7 +133,9 @@ class ModuleWidget(QGraphicsWidget): ...@@ -98,7 +133,9 @@ class ModuleWidget(QGraphicsWidget):
def update(self, rect: QRectF | None = None): def update(self, rect: QRectF | None = None):
# This "override" is needed in order to decorate update() as a # This "override" is needed in order to decorate update() as a
# pyqt slot # pyqt slot
super().update() if rect is None:
rect = self.boundingRect()
super().update(rect)
def shape(self) -> QPainterPath: def shape(self) -> QPainterPath:
path = QPainterPath() path = QPainterPath()
...@@ -111,6 +148,60 @@ class ModuleWidget(QGraphicsWidget): ...@@ -111,6 +148,60 @@ class ModuleWidget(QGraphicsWidget):
margin = self.outline_width / 2 margin = self.outline_width / 2
return QRectF(0 - margin, 0 - margin, width + margin, height + margin) return QRectF(0 - margin, 0 - margin, width + margin, height + margin)
@Slot(bool)
def show_ports(self, value: bool) -> None:
"""
Toggle the visibility of the signal ports of the displayed module.
Parameters
----------
value : bool
`True` to show all ports, `False` to hide them.
"""
pass
def contextMenuEvent(self, event: 'QGraphicsSceneContextMenuEvent') -> None:
"""
Show a context menu for this module, allowing the user to perform any
of the actions available to this module.
"""
menu = QMenu()
menu.addActions(self.actions())
menu.exec_(event.screenPos())
def get_state_vars(self) -> dict[str, FormatInfo]:
"""
Return a mapping from displayed state variables of the displayed
module to their respective format information.
Useful for viewing the supported formats and the selected format for a
state variable.
Returns
-------
dict[str, FormatInfo]
Mapping from state variable to FormatInfo instance.
"""
return self._state_vars
@Slot()
def set_format(self, state_var: str, format: Format) -> None:
"""
Set the format to be used when displaying and parsing a given state
variable of the displayed module.
Parameters
----------
state_va : str
The state variable to set the formats of.
format : Format
The format to use for the value of the state variable. Should be
one of the supported formats in the FormatInfo instance for the
state variable.
"""
self._state_vars[state_var].selected_format = format
self.update()
def outline_width(self) -> float: def outline_width(self) -> float:
"""Return the outline pen width. """Return the outline pen width.
...@@ -216,4 +307,5 @@ class ModuleWidget(QGraphicsWidget): ...@@ -216,4 +307,5 @@ class ModuleWidget(QGraphicsWidget):
outline = Property(QBrush, outline, set_outline) outline = Property(QBrush, outline, set_outline)
background = Property(QBrush, background, set_background) background = Property(QBrush, background, set_background)
text_color = Property(QColor, text_color, set_text_color) text_color = Property(QColor, text_color, set_text_color)
text_font = Property(QFont, text_font, set_text_font) text_font = Property(QFont, text_font, set_text_font)
\ No newline at end of file
from qtpy import QtCore
from qtpy.QtCore import QObject, QThreadPool from qtpy.QtCore import QObject, QThreadPool
from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot from qtpy.QtCore import Slot
...@@ -21,12 +20,12 @@ class ProcessorHandler(QObject): ...@@ -21,12 +20,12 @@ class ProcessorHandler(QObject):
running = pyqtSignal(bool) running = pyqtSignal(bool)
""" """
PyQT signal emitted when the processor has started or finished running. PyQT signal emitted when the processor has started or finished running.
Emits ``True`` when it has started and ``False`` when it has finished. Emits ``True`` when it has started and ``False`` when it has finished.
""" """
cycle_changed = pyqtSignal(int) cycle_changed = pyqtSignal(int)
""" """
PyQT signal emitted when the current clock cycle count of the processor PyQT signal emitted when the current clock cycle count of the processor
has changed. Emits the current clock cycle count as an ``int``. has changed. Emits the current clock cycle count as an ``int``.
""" """
changed = pyqtSignal() changed = pyqtSignal()
...@@ -105,7 +104,7 @@ class ProcessorHandler(QObject): ...@@ -105,7 +104,7 @@ class ProcessorHandler(QObject):
@Slot(int) @Slot(int)
def step_asm_instructions(self, instructions: int): def step_asm_instructions(self, instructions: int):
""" """
Run some numer of asm instructions. Run some number of asm instructions.
Parameters Parameters
---------- ----------
...@@ -333,3 +332,12 @@ class ProcessorHandler(QObject): ...@@ -333,3 +332,12 @@ class ProcessorHandler(QObject):
) )
if ok: if ok:
self._update_delay = delay self._update_delay = delay
@Slot()
def handle_module_change(self):
"""
Handle propagating changes when the state of a module has been edited
by the user.
"""
pass
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment