Merge pull request #7 from kumitterer/number-display-and-duration

Event duration changes. I will trust  you.
This commit is contained in:
Julia P 2024-04-03 20:03:37 +02:00 committed by GitHub
commit 627750da34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 275 additions and 124 deletions

View file

@ -1,4 +1,5 @@
import logging
from decimal import Decimal
from collections import OrderedDict
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from nqrduck.module.module_model import ModuleModel
@ -6,6 +7,7 @@ from nqrduck_spectrometer.pulsesequence import PulseSequence
logger = logging.getLogger(__name__)
class PulseProgrammerModel(ModuleModel):
pulse_parameter_options_changed = pyqtSignal()
events_changed = pyqtSignal()
@ -16,15 +18,33 @@ class PulseProgrammerModel(ModuleModel):
self.pulse_parameter_options = OrderedDict()
self.pulse_sequence = PulseSequence("Untitled pulse sequence")
def add_event(self, event_name):
self.pulse_sequence.events.append(PulseSequence.Event(event_name, "20u"))
logger.debug("Creating event %s with object id %s", event_name, id(self.pulse_sequence.events[-1]))
def add_event(self, event_name: str, duration: Decimal = 20):
"""Add a new event to the current pulse sequence.
Args:
event_name (str): A human-readable name for the event
duration (Decimal): The duration of the event in µs. Defaults to 20.
"""
self.pulse_sequence.events.append(
PulseSequence.Event(event_name, "%.16gu" % duration)
)
logger.debug(
"Creating event %s with object id %s",
event_name,
id(self.pulse_sequence.events[-1]),
)
# Create a default instance of the pulse parameter options and add it to the event
for name, pulse_parameter_class in self.pulse_parameter_options.items():
logger.debug("Adding pulse parameter %s to event %s", name, event_name)
self.pulse_sequence.events[-1].parameters[name] = pulse_parameter_class(name)
logger.debug("Created pulse parameter %s with object id %s", name, id(self.pulse_sequence.events[-1].parameters[name]))
self.pulse_sequence.events[-1].parameters[name] = pulse_parameter_class(
name
)
logger.debug(
"Created pulse parameter %s with object id %s",
name,
id(self.pulse_sequence.events[-1].parameters[name]),
)
logger.debug(self.pulse_sequence.to_json())
self.events_changed.emit()

View file

@ -3,12 +3,34 @@ import functools
from collections import OrderedDict
from pathlib import Path
from decimal import Decimal
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox, QGroupBox, QFormLayout, QTableWidget, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QDialog, QLineEdit, QDialogButtonBox, QWidget, QCheckBox, QToolButton, QFileDialog, QSizePolicy
from PyQt6.QtGui import QIcon, QValidator
from PyQt6.QtWidgets import (
QMessageBox,
QGroupBox,
QFormLayout,
QTableWidget,
QVBoxLayout,
QPushButton,
QHBoxLayout,
QLabel,
QDialog,
QLineEdit,
QDialogButtonBox,
QWidget,
QCheckBox,
QToolButton,
QFileDialog,
QSizePolicy,
)
from PyQt6.QtCore import pyqtSlot, pyqtSignal
from nqrduck.module.module_view import ModuleView
from nqrduck.assets.icons import Logos
from nqrduck_spectrometer.pulseparameters import BooleanOption, NumericOption, FunctionOption
from nqrduck.helpers.duckwidgets import DuckFloatEdit, DuckEdit
from nqrduck_spectrometer.pulseparameters import (
BooleanOption,
NumericOption,
FunctionOption,
)
logger = logging.getLogger(__name__)
@ -22,21 +44,23 @@ class PulseProgrammerView(ModuleView):
self.setup_variabletables()
logger.debug("Connecting pulse parameter options changed signal to on_pulse_parameter_options_changed")
self.module.model.pulse_parameter_options_changed.connect(self.on_pulse_parameter_options_changed)
logger.debug(
"Connecting pulse parameter options changed signal to on_pulse_parameter_options_changed"
)
self.module.model.pulse_parameter_options_changed.connect(
self.on_pulse_parameter_options_changed
)
def setup_variabletables(self) -> None:
"""Setup the table for the variables.
"""
"""Setup the table for the variables."""
pass
def setup_pulsetable(self) -> None:
"""Setup the table for the pulse sequence. Also add buttons for saving and loading pulse sequences and editing and creation of events
"""
"""Setup the table for the pulse sequence. Also add buttons for saving and loading pulse sequences and editing and creation of events"""
# Create pulse table
self.title = QLabel("Pulse Sequence: %s" % self.module.model.pulse_sequence.name)
self.title = QLabel(
"Pulse Sequence: %s" % self.module.model.pulse_sequence.name
)
# Make title bold
font = self.title.font()
font.setBold(True)
@ -44,7 +68,9 @@ class PulseProgrammerView(ModuleView):
# Table setup
self.pulse_table = QTableWidget(self)
self.pulse_table.setSizeAdjustPolicy(QTableWidget.SizeAdjustPolicy.AdjustToContents)
self.pulse_table.setSizeAdjustPolicy(
QTableWidget.SizeAdjustPolicy.AdjustToContents
)
self.pulse_table.setAlternatingRowColors(True)
layout = QVBoxLayout()
button_layout = QHBoxLayout()
@ -62,7 +88,9 @@ class PulseProgrammerView(ModuleView):
# Add button for save pulse sequence
self.save_pulse_sequence_button = QPushButton("Save pulse sequence")
self.save_pulse_sequence_button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.save_pulse_sequence_button.setSizePolicy(
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed
)
# Add the Save Icon to the button
icon = Logos.Save16x16()
self.save_pulse_sequence_button.setIconSize(icon.availableSizes()[0])
@ -83,7 +111,6 @@ class PulseProgrammerView(ModuleView):
self.module.model.events_changed.connect(self.on_events_changed)
self.module.model.pulse_sequence_changed.connect(self.on_pulse_sequence_changed)
button_layout.addStretch(1)
layout.addWidget(self.title)
layout.addLayout(button_layout)
@ -98,19 +125,21 @@ class PulseProgrammerView(ModuleView):
self.on_events_changed()
@pyqtSlot()
def on_pulse_sequence_changed(self) -> None:
"""This method is called whenever the pulse sequence changes. It updates the view to reflect the changes.
"""
logger.debug("Updating pulse sequence to %s", self.module.model.pulse_sequence.name)
"""This method is called whenever the pulse sequence changes. It updates the view to reflect the changes."""
logger.debug(
"Updating pulse sequence to %s", self.module.model.pulse_sequence.name
)
self.title.setText("Pulse Sequence: %s" % self.module.model.pulse_sequence.name)
@pyqtSlot()
def on_pulse_parameter_options_changed(self) -> None:
"""This method is called whenever the pulse parameter options change. It updates the view to reflect the changes.
"""
logger.debug("Updating pulse parameter options to %s", self.module.model.pulse_parameter_options.keys())
"""This method is called whenever the pulse parameter options change. It updates the view to reflect the changes."""
logger.debug(
"Updating pulse parameter options to %s",
self.module.model.pulse_parameter_options.keys(),
)
# We set it to the length of the pulse parameter options + 1 because we want to add a row for the parameter option buttons
self.pulse_table.setRowCount(len(self.module.model.pulse_parameter_options) + 1)
# Move the vertical header labels on row down
@ -120,21 +149,22 @@ class PulseProgrammerView(ModuleView):
@pyqtSlot()
def on_new_event_button_clicked(self) -> None:
"""This method is called whenever the new event button is clicked. It creates a new event and adds it to the pulse sequence.
"""
"""This method is called whenever the new event button is clicked. It creates a new event and adds it to the pulse sequence."""
# Create a QDialog for the new event
logger.debug("New event button clicked")
dialog = AddEventDialog(self)
result = dialog.exec()
if result:
event_name = dialog.get_name()
logger.debug("Adding new event with name %s", event_name)
self.module.model.add_event(event_name)
duration = dialog.get_duration()
logger.debug(
"Adding new event with name %s, duration %g", event_name, duration
)
self.module.model.add_event(event_name, duration)
@pyqtSlot()
def on_events_changed(self) -> None:
"""This method is called whenever the events in the pulse sequence change. It updates the view to reflect the changes.
"""
"""This method is called whenever the events in the pulse sequence change. It updates the view to reflect the changes."""
logger.debug("Updating events to %s", self.module.model.pulse_sequence.events)
# Add label for the event lengths
@ -145,7 +175,9 @@ class PulseProgrammerView(ModuleView):
for event in self.module.model.pulse_sequence.events:
logger.debug("Adding event to pulseprogrammer view: %s", event.name)
# Create a label for the event
event_label = QLabel("%s : %s µs" % (event.name, str(event.duration * Decimal(1e6))))
event_label = QLabel(
"%s : %.16g µs" % (event.name, (event.duration * Decimal(1e6)))
)
event_layout.addWidget(event_label)
# Delete the old widget and create a new one
@ -155,33 +187,52 @@ class PulseProgrammerView(ModuleView):
self.layout().addWidget(self.event_widget)
self.pulse_table.setColumnCount(len(self.module.model.pulse_sequence.events))
self.pulse_table.setHorizontalHeaderLabels([event.name for event in self.module.model.pulse_sequence.events])
self.pulse_table.setHorizontalHeaderLabels(
[event.name for event in self.module.model.pulse_sequence.events]
)
self.set_parameter_icons()
def set_parameter_icons(self) -> None:
"""This method sets the icons for the pulse parameter options.
"""
"""This method sets the icons for the pulse parameter options."""
for column_idx, event in enumerate(self.module.model.pulse_sequence.events):
for row_idx, parameter in enumerate(self.module.model.pulse_parameter_options.keys()):
for row_idx, parameter in enumerate(
self.module.model.pulse_parameter_options.keys()
):
if row_idx == 0:
event_options_widget = EventOptionsWidget(event)
# Connect the delete_event signal to the on_delete_event slot
func = functools.partial(self.module.controller.delete_event, event_name=event.name)
func = functools.partial(
self.module.controller.delete_event, event_name=event.name
)
event_options_widget.delete_event.connect(func)
# Connect the change_event_duration signal to the on_change_event_duration slot
event_options_widget.change_event_duration.connect(self.module.controller.change_event_duration)
event_options_widget.change_event_duration.connect(
self.module.controller.change_event_duration
)
# Connect the change_event_name signal to the on_change_event_name slot
event_options_widget.change_event_name.connect(self.module.controller.change_event_name)
event_options_widget.change_event_name.connect(
self.module.controller.change_event_name
)
# Connect the move_event_left signal to the on_move_event_left slot
event_options_widget.move_event_left.connect(self.module.controller.on_move_event_left)
event_options_widget.move_event_left.connect(
self.module.controller.on_move_event_left
)
# Connect the move_event_right signal to the on_move_event_right slot
event_options_widget.move_event_right.connect(self.module.controller.on_move_event_right)
event_options_widget.move_event_right.connect(
self.module.controller.on_move_event_right
)
self.pulse_table.setCellWidget(row_idx, column_idx, event_options_widget)
self.pulse_table.setRowHeight(row_idx, event_options_widget.layout().sizeHint().height())
self.pulse_table.setCellWidget(
row_idx, column_idx, event_options_widget
)
self.pulse_table.setRowHeight(
row_idx, event_options_widget.layout().sizeHint().height()
)
logger.debug("Adding button for event %s and parameter %s", event, parameter)
logger.debug(
"Adding button for event %s and parameter %s", event, parameter
)
logger.debug("Parameter object id: %s", id(event.parameters[parameter]))
button = QPushButton()
icon = event.parameters[parameter].get_pixmap()
@ -192,15 +243,21 @@ class PulseProgrammerView(ModuleView):
# We add 1 to the row index because the first row is used for the event options
self.pulse_table.setCellWidget(row_idx + 1, column_idx, button)
self.pulse_table.setRowHeight(row_idx + 1, icon.availableSizes()[0].height())
self.pulse_table.setColumnWidth(column_idx, icon.availableSizes()[0].width())
self.pulse_table.setRowHeight(
row_idx + 1, icon.availableSizes()[0].height()
)
self.pulse_table.setColumnWidth(
column_idx, icon.availableSizes()[0].width()
)
# Connect the button to the on_button_clicked slot
func = functools.partial(self.on_table_button_clicked, event=event, parameter=parameter)
func = functools.partial(
self.on_table_button_clicked, event=event, parameter=parameter
)
button.clicked.connect(func)
@pyqtSlot()
def on_table_button_clicked(self, event , parameter) -> None:
def on_table_button_clicked(self, event, parameter) -> None:
"""This method is called whenever a button in the pulse table is clicked. It opens a dialog to set the options for the parameter."""
logger.debug("Button for event %s and parameter %s clicked", event, parameter)
# Create a QDialog to set the options for the parameter.
@ -209,7 +266,13 @@ class PulseProgrammerView(ModuleView):
if result:
for option, function in dialog.return_functions.items():
logger.debug("Setting option %s of parameter %s in event %s to %s", option, parameter, event, function())
logger.debug(
"Setting option %s of parameter %s in event %s to %s",
option,
parameter,
event,
function(),
)
option.set_value(function())
self.set_parameter_icons()
@ -232,8 +295,9 @@ class PulseProgrammerView(ModuleView):
if file_name:
self.module.controller.load_pulse_sequence(file_name)
class EventOptionsWidget(QWidget):
""" This class is a widget that can be used to set the options for a pulse parameter.
"""This class is a widget that can be used to set the options for a pulse parameter.
This widget is then added to the the first row of the according event column in the pulse table.
It has a edit button that opens a dialog that allows the user to change the options for the event (name and duration).
Furthermore it has a delete button that deletes the event from the pulse sequence.
@ -299,7 +363,8 @@ class EventOptionsWidget(QWidget):
@pyqtSlot()
def edit_event(self) -> None:
"""This method is called when the edit button is clicked. It opens a dialog that allows the user to change the event name and duration.
If the user clicks ok, the change_event_name and change_event_duration signals are emitted."""
If the user clicks ok, the change_event_name and change_event_duration signals are emitted.
"""
logger.debug("Edit button clicked for event %s", self.event.name)
# Create a QDialog to edit the event
@ -325,7 +390,9 @@ class EventOptionsWidget(QWidget):
event_form_layout.addRow(duration_layout)
layout.addLayout(event_form_layout)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
@ -336,8 +403,9 @@ class EventOptionsWidget(QWidget):
if name_lineedit.text() != self.event.name:
self.change_event_name.emit(self.event.name, name_lineedit.text())
if duration_lineedit.text() != str(self.event.duration):
self.change_event_duration.emit(self.event.name, duration_lineedit.text())
self.change_event_duration.emit(
self.event.name, duration_lineedit.text()
)
@pyqtSlot()
def create_delete_event_dialog(self) -> None:
@ -351,7 +419,9 @@ class EventOptionsWidget(QWidget):
layout = QVBoxLayout()
label = QLabel("Are you sure you want to delete event %s?" % self.event.name)
layout.addWidget(label)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
@ -373,8 +443,9 @@ class EventOptionsWidget(QWidget):
class OptionsDialog(QDialog):
""" This dialog is created whenever the edit button for a pulse option is clicked.
It allows the user to change the options for the pulse parameter and creates the dialog in accordance to what can be set."""
"""This dialog is created whenever the edit button for a pulse option is clicked.
It allows the user to change the options for the pulse parameter and creates the dialog in accordance to what can be set.
"""
def __init__(self, event, parameter, parent=None):
super().__init__(parent)
@ -444,6 +515,7 @@ class OptionsDialog(QDialog):
self.layout.addWidget(self.buttons)
class FunctionOptionWidget(QWidget):
"""This class is a widget that can be used to set the options for a pulse parameter.
It plots the given function in time and frequency domain.
@ -459,7 +531,9 @@ class FunctionOptionWidget(QWidget):
inner_layout = QHBoxLayout()
for function in function_option.functions:
button = QPushButton(function.name)
button.clicked.connect(functools.partial(self.on_functionbutton_clicked, function=function))
button.clicked.connect(
functools.partial(self.on_functionbutton_clicked, function=function)
)
inner_layout.addWidget(button)
layout.addLayout(inner_layout)
@ -467,11 +541,13 @@ class FunctionOptionWidget(QWidget):
# Add Advanced settings button
self.advanced_settings_button = QPushButton("Show Advanced settings")
self.advanced_settings_button.clicked.connect(self.on_advanced_settings_button_clicked)
self.advanced_settings_button.clicked.connect(
self.on_advanced_settings_button_clicked
)
layout.addWidget(self.advanced_settings_button)
# Add advanced settings widget
self.advanced_settings = QGroupBox('Advanced Settings')
self.advanced_settings = QGroupBox("Advanced Settings")
self.advanced_settings.setHidden(True)
self.advanced_settings_layout = QFormLayout()
self.advanced_settings.setLayout(self.advanced_settings_layout)
@ -535,13 +611,14 @@ class FunctionOptionWidget(QWidget):
logger.debug("Invalid expression: %s", self.expr_lineedit.text())
self.expr_lineedit.setText(str(self.function_option.value.expr))
# Create message box that tells the user that the expression is invalid
self.create_message_box("Invalid expression", "The expression you entered is invalid. Please enter a valid expression.")
self.create_message_box(
"Invalid expression",
"The expression you entered is invalid. Please enter a valid expression.",
)
self.delete_active_function()
self.load_active_function()
@pyqtSlot()
def on_advanced_settings_button_clicked(self) -> None:
"""This function is called when the advanced settings button is clicked.
@ -549,11 +626,10 @@ class FunctionOptionWidget(QWidget):
"""
if self.advanced_settings.isHidden():
self.advanced_settings.setHidden(False)
self.advanced_settings_button.setText('Hide Advanced Settings')
self.advanced_settings_button.setText("Hide Advanced Settings")
else:
self.advanced_settings.setHidden(True)
self.advanced_settings_button.setText('Show Advanced Settings')
self.advanced_settings_button.setText("Show Advanced Settings")
@pyqtSlot()
def on_functionbutton_clicked(self, function) -> None:
@ -613,7 +689,9 @@ class FunctionOptionWidget(QWidget):
parameter_label = QLabel(parameter.name)
parameter_lineedit = QLineEdit(str(parameter.value))
# Add the parameter_lineedit editingFinished signal to the paramter.set_value slot
parameter_lineedit.editingFinished.connect(lambda: parameter.set_value(parameter_lineedit.text()))
parameter_lineedit.editingFinished.connect(
lambda: parameter.set_value(parameter_lineedit.text())
)
# Create a QHBoxLayout
hbox = QHBoxLayout()
@ -636,7 +714,7 @@ class FunctionOptionWidget(QWidget):
self.end_x_lineedit.setText(str(self.function_option.value.end_x))
self.expr_lineedit.setText(str(self.function_option.value.expr))
def create_message_box(self, message : str, information : str) -> None:
def create_message_box(self, message: str, information: str) -> None:
"""Creates a message box with the given message and information and shows it.
Args:
@ -652,25 +730,47 @@ class FunctionOptionWidget(QWidget):
class AddEventDialog(QDialog):
"""This dialog is created whenever a new event is added to the pulse sequence. It allows the user to enter a name for the event."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Add Event")
self.layout = QVBoxLayout(self)
self.layout = QFormLayout(self)
self.name_layout = QHBoxLayout()
self.label = QLabel("Enter event name:")
self.name_input = QLineEdit()
self.name_input = DuckEdit()
self.name_input.validator = self.NameInputValidator(self)
self.name_layout.addWidget(self.label)
self.name_layout.addWidget(self.name_input)
self.layout.addRow(self.name_layout)
self.duration_layout = QHBoxLayout()
self.duration_label = QLabel("Duration:")
self.duration_lineedit = DuckFloatEdit(min_value=0)
self.duration_lineedit.setText("20")
self.unit_label = QLabel("µs")
self.duration_layout.addWidget(self.duration_label)
self.duration_layout.addWidget(self.duration_lineedit)
self.duration_layout.addWidget(self.unit_label)
self.layout.addRow(self.duration_layout)
self.buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
self,
)
self.buttons.accepted.connect(self.check_input)
self.buttons.rejected.connect(self.reject)
self.layout.addWidget(self.label)
self.layout.addWidget(self.name_input)
self.layout.addWidget(self.buttons)
def get_name(self) -> str:
@ -680,20 +780,48 @@ class AddEventDialog(QDialog):
str: The name entered by the user"""
return self.name_input.text()
def check_input(self) -> None:
"""Checks if the name entered by the user is valid. If it is, the dialog is accepted. If not, the user is informed of the error.
def get_duration(self) -> Decimal:
"""Returns the duration entered by the user, or a fallback value."
Returns:
Decimal: The duration value provided by the user, or 20"""
return Decimal(self.duration_lineedit.text() or 20)
class NameInputValidator(QValidator):
"""A validator for the name input field.
This is used to validate the input of the QLineEdit widget.
"""
# Make sure that name is not empty and that event name doesn't already exist.
if self.name_input.text() == "":
self.label.setText("Please enter a name for the event.")
elif self.name_input.text() in self.parent().module.model.pulse_sequence.events:
self.label.setText("Event name already exists. Please enter a different name.")
else:
self.accept()
def validate(self, value, position):
"""Validates the input value.
Args:
value (str): The input value
position (int): The position of the cursor
Returns:
Tuple[QValidator.State, str, int]: The validation state, the fixed value, and the position
"""
if not value:
return (QValidator.State.Intermediate, value, position)
if any(
[
event.name == value
for event in self.parent()
.parent()
.module.model.pulse_sequence.events
]
):
return (QValidator.State.Invalid, value, position)
return (QValidator.State.Acceptable, value, position)
# This class should be refactored in the module view so it can be used by all modules
class QFileManager:
"""This class provides methods for opening and saving files."""
def __init__(self, parent=None):
self.parent = parent
@ -703,11 +831,13 @@ class QFileManager:
Returns:
str: The path of the file selected by the user.
"""
fileName, _ = QFileDialog.getOpenFileName(self.parent,
fileName, _ = QFileDialog.getOpenFileName(
self.parent,
"QFileManager - Open File",
"",
"Quack Files (*.quack);;All Files (*)",
options = QFileDialog.Option.ReadOnly)
options=QFileDialog.Option.ReadOnly,
)
if fileName:
return fileName
else:
@ -719,16 +849,17 @@ class QFileManager:
Returns:
str: The path of the file selected by the user.
"""
fileName, _ = QFileDialog.getSaveFileName(self.parent,
fileName, _ = QFileDialog.getSaveFileName(
self.parent,
"QFileManager - Save File",
"",
"Quack Files (*.quack);;All Files (*)",
options=QFileDialog.Option.DontUseNativeDialog)
options=QFileDialog.Option.DontUseNativeDialog,
)
if fileName:
# Append the .quack extension if not present
if not fileName.endswith('.quack'):
fileName += '.quack'
if not fileName.endswith(".quack"):
fileName += ".quack"
return fileName
else:
return None