diff --git a/CHANGELOG.md b/CHANGELOG.md index c5348c8..da755ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +### Version 0.0.3 (26-04-2024) +- Switched to new formbuilder provided by the nqrduck core ### Version 0.0.2 (18-04-2024) - Automatic deployment to PyPI diff --git a/pyproject.toml b/pyproject.toml index 340e5f4..5b7fa97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "nqrduck-pulseprogrammer" -version = "0.0.2" +version = "0.0.3" authors = [ { name="jupfi", email="support@nqrduck.cool" }, ] diff --git a/src/nqrduck_pulseprogrammer/model.py b/src/nqrduck_pulseprogrammer/model.py index 8fe445e..7b2197e 100644 --- a/src/nqrduck_pulseprogrammer/model.py +++ b/src/nqrduck_pulseprogrammer/model.py @@ -1,5 +1,4 @@ import logging -from decimal import Decimal from collections import OrderedDict from PyQt6.QtCore import pyqtSignal from nqrduck.module.module_model import ModuleModel @@ -18,15 +17,15 @@ class PulseProgrammerModel(ModuleModel): self.pulse_parameter_options = OrderedDict() self.pulse_sequence = PulseSequence("Untitled pulse sequence") - def add_event(self, event_name: str, duration: Decimal = 20): + def add_event(self, event_name: str, duration: float = 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. + duration (float): The duration of the event in µs. Defaults to 20. """ self.pulse_sequence.events.append( - PulseSequence.Event(event_name, "%.16gu" % duration) + PulseSequence.Event(event_name, "%.16gu" % float(duration)) ) logger.debug( "Creating event %s with object id %s", diff --git a/src/nqrduck_pulseprogrammer/view.py b/src/nqrduck_pulseprogrammer/view.py index 61978aa..8ba069a 100644 --- a/src/nqrduck_pulseprogrammer/view.py +++ b/src/nqrduck_pulseprogrammer/view.py @@ -1,11 +1,7 @@ import logging import functools -from collections import OrderedDict -from decimal import Decimal from PyQt6.QtGui import QValidator from PyQt6.QtWidgets import ( - QMessageBox, - QGroupBox, QFormLayout, QTableWidget, QVBoxLayout, @@ -16,7 +12,6 @@ from PyQt6.QtWidgets import ( QLineEdit, QDialogButtonBox, QWidget, - QCheckBox, QToolButton, QFileDialog, QSizePolicy, @@ -30,6 +25,14 @@ from nqrduck_spectrometer.pulseparameters import ( NumericOption, FunctionOption, ) +from nqrduck.helpers.formbuilder import ( + DuckFormBuilder, + DuckFormFunctionSelectionField, + DuckFormCheckboxField, + DuckFormDropdownField, + DuckFormFloatField, + DuckFormIntField, +) logger = logging.getLogger(__name__) @@ -175,7 +178,7 @@ class PulseProgrammerView(ModuleView): logger.debug("Adding event to pulseprogrammer view: %s", event.name) # Create a label for the event event_label = QLabel( - "%s : %.16g µs" % (event.name, (event.duration * Decimal(1e6))) + "%s : %.16g µs" % (event.name, (event.duration * 1e6)) ) event_layout.addWidget(event_label) @@ -260,19 +263,65 @@ class PulseProgrammerView(ModuleView): """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. - dialog = OptionsDialog(event, parameter, self) + description = f"Set options for {parameter}" + dialog = DuckFormBuilder(parameter, description=description, parent=self) + + # Adding fields for the options + form_options = [] + for option in event.parameters[parameter].options: + if isinstance(option, BooleanOption): + boolean_form = DuckFormCheckboxField( + option.name, tooltip=None, default=option.value + ) + dialog.add_field(boolean_form) + form_options.append(option) + elif isinstance(option, NumericOption): + # We only show the slider if both min and max values are set + if option.min_value is not None and option.max_value is not None: + slider = True + else: + slider = False + + numeric_field = DuckFormFloatField( + option.name, + tooltip=None, + default=option.value, + min_value=option.min_value, + max_value=option.max_value, + slider=slider, + ) + + dialog.add_field(numeric_field) + form_options.append(option) + elif isinstance(option, FunctionOption): + logger.debug(f"Functions: {option.functions}") + + # When loading a pulse sequence, the instance of the objects will be different + # Therefore we need to operate on the classes + for function in option.functions: + if function.__class__.__name__ == option.value.__class__.__name__: + default_function = function + + index = option.functions.index(default_function) + + function_field = DuckFormFunctionSelectionField( + option.name, + tooltip=None, + functions=option.functions, + duration=event.duration, + default_function=index, + ) + dialog.add_field(function_field) + form_options.append(option) + result = dialog.exec() + options = event.parameters[parameter].options + 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(), - ) - option.set_value(function()) + values = dialog.get_values() + for i, value in enumerate(values): + options[i].value = value self.set_parameter_icons() @@ -382,7 +431,7 @@ class EventOptionsWidget(QWidget): duration_label = QLabel("Duration:") duration_lineedit = QLineEdit() unit_label = QLabel("µs") - duration_lineedit.setText("%.16g" % (self.event.duration * Decimal(1e6))) + duration_lineedit.setText("%.16g" % (self.event.duration * 1e6)) duration_layout.addWidget(duration_label) duration_layout.addWidget(duration_lineedit) duration_layout.addWidget(unit_label) @@ -441,294 +490,6 @@ class EventOptionsWidget(QWidget): self.move_event_right.emit(self.event.name) -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. - """ - - def __init__(self, event, parameter, parent=None): - super().__init__(parent) - self.parent = parent - - self.setWindowTitle("Options") - - self.layout = QVBoxLayout(self) - - numeric_layout = QFormLayout() - numeric_layout.setHorizontalSpacing(30) - - self.label = QLabel("Change options for the pulse parameter: %s" % parameter) - self.layout.addWidget(self.label) - - self.layout.addLayout(numeric_layout) - - # If the parameter is a string, we first need to get the parameter object from the according event - if isinstance(parameter, str): - parameter = event.parameters[parameter] - - options = parameter.get_options() - - # Based on these options we will now create our selection widget - self.return_functions = OrderedDict() - - # If the options are a list , we will create a QComboBox - for option in options: - if option == list: - pass - # If the options are boolean, we will create a QCheckBox - elif isinstance(option, BooleanOption): - check_box = QCheckBox() - - def checkbox_result(): - return check_box.isChecked() - - check_box.setChecked(option.value) - self.layout.addWidget(check_box) - self.return_functions[option] = checkbox_result - - # If the options are a float/int we will create a QSpinBox - elif isinstance(option, NumericOption): - numeric_label = QLabel(option.name) - numeric_lineedit = QLineEdit(str(option.value)) - numeric_lineedit.setMaximumWidth(300) - numeric_layout.addRow(numeric_label, numeric_lineedit) - - self.return_functions[option] = numeric_lineedit.text - - # If the options are a string we will create a QLineEdit - elif option == str: - pass - - elif isinstance(option, FunctionOption): - function_option = FunctionOptionWidget(option, event, parent) - self.layout.addWidget(function_option) - - logger.debug("Return functions are: %s" % self.return_functions.items()) - - self.buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, - self, - ) - self.buttons.accepted.connect(self.accept) - self.buttons.rejected.connect(self.reject) - - 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. - One can also select the function from a list of functions represented as buttons. - """ - - def __init__(self, function_option, event, parent=None): - super().__init__(parent) - self.parent = parent - - self.function_option = function_option - self.event = event - layout = QVBoxLayout() - inner_layout = QHBoxLayout() - for function in function_option.functions: - button = QPushButton(function.name) - button.clicked.connect( - functools.partial(self.on_functionbutton_clicked, function=function) - ) - inner_layout.addWidget(button) - - layout.addLayout(inner_layout) - self.setLayout(layout) - - # Add Advanced settings button - self.advanced_settings_button = QPushButton("Show Advanced settings") - 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.setHidden(True) - self.advanced_settings_layout = QFormLayout() - self.advanced_settings.setLayout(self.advanced_settings_layout) - layout.addWidget(self.advanced_settings) - - # Add the advanced settings - # Advanced settings are resolution, start_x = -1, end_x and the expr of the function_option.value - resolution_layout = QHBoxLayout() - resolution_label = QLabel("Resolution:") - self.resolution_lineedit = QLineEdit(str(function_option.value.resolution)) - resolution_layout.addWidget(resolution_label) - resolution_layout.addWidget(self.resolution_lineedit) - resolution_layout.addStretch(1) - self.advanced_settings_layout.addRow(resolution_label, resolution_layout) - - start_x_layout = QHBoxLayout() - start_x_label = QLabel("Start x:") - self.start_x_lineedit = QLineEdit(str(function_option.value.start_x)) - start_x_layout.addWidget(start_x_label) - start_x_layout.addWidget(self.start_x_lineedit) - start_x_layout.addStretch(1) - self.advanced_settings_layout.addRow(start_x_label, start_x_layout) - - end_x_layout = QHBoxLayout() - end_x_label = QLabel("End x:") - self.end_x_lineedit = QLineEdit(str(function_option.value.end_x)) - end_x_layout.addWidget(end_x_label) - end_x_layout.addWidget(self.end_x_lineedit) - end_x_layout.addStretch(1) - self.advanced_settings_layout.addRow(end_x_label, end_x_layout) - - expr_layout = QHBoxLayout() - expr_label = QLabel("Expression:") - self.expr_lineedit = QLineEdit(str(function_option.value.expr)) - expr_layout.addWidget(expr_label) - expr_layout.addWidget(self.expr_lineedit) - expr_layout.addStretch(1) - self.advanced_settings_layout.addRow(expr_label, expr_layout) - - # Add buttton for replotting of the active function with the new parameters - self.replot_button = QPushButton("Replot") - self.replot_button.clicked.connect(self.on_replot_button_clicked) - layout.addWidget(self.replot_button) - - # Display the active function - self.load_active_function() - - @pyqtSlot() - def on_replot_button_clicked(self) -> None: - """This function is called when the replot button is clicked. - It will update the parameters of the function and replots the function. - """ - logger.debug("Replot button clicked") - # Update the resolution, start_x, end_x and expr lineedits - self.function_option.value.resolution = self.resolution_lineedit.text() - self.function_option.value.start_x = self.start_x_lineedit.text() - self.function_option.value.end_x = self.end_x_lineedit.text() - try: - self.function_option.value.expr = self.expr_lineedit.text() - except SyntaxError: - 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.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. - It will show or hide the advanced settings. - """ - if self.advanced_settings.isHidden(): - self.advanced_settings.setHidden(False) - self.advanced_settings_button.setText("Hide Advanced Settings") - else: - self.advanced_settings.setHidden(True) - self.advanced_settings_button.setText("Show Advanced Settings") - - @pyqtSlot() - def on_functionbutton_clicked(self, function) -> None: - """This function is called when a function button is clicked. - It will update the function_option.value to the function that was clicked. - """ - logger.debug("Button for function %s clicked", function.name) - self.function_option.set_value(function) - self.delete_active_function() - self.load_active_function() - - def delete_active_function(self) -> None: - """This function is called when the active function is deleted. - It will remove the active function from the layout. - """ - # Remove the plotter with object name "plotter" from the layout - for i in reversed(range(self.layout().count())): - item = self.layout().itemAt(i) - if item.widget() and item.widget().objectName() == "active_function": - item.widget().deleteLater() - break - - def load_active_function(self) -> None: - """This function is called when the active function is loaded. - It will add the active function to the layout. - """ - # New QWidget for the active function - active_function_Widget = QWidget() - active_function_Widget.setObjectName("active_function") - - function_layout = QVBoxLayout() - - plot_layout = QHBoxLayout() - - # Add plot for time domain - time_domain_layout = QVBoxLayout() - time_domain_label = QLabel("Time domain:") - time_domain_layout.addWidget(time_domain_label) - plot = self.function_option.value.time_domain_plot(self.event.duration) - time_domain_layout.addWidget(plot) - plot_layout.addLayout(time_domain_layout) - - # Add plot for frequency domain - frequency_domain_layout = QVBoxLayout() - frequency_domain_label = QLabel("Frequency domain:") - frequency_domain_layout.addWidget(frequency_domain_label) - plot = self.function_option.value.frequency_domain_plot(self.event.duration) - frequency_domain_layout.addWidget(plot) - plot_layout.addLayout(frequency_domain_layout) - - function_layout.addLayout(plot_layout) - - parameter_layout = QFormLayout() - parameter_label = QLabel("Parameters:") - parameter_layout.addRow(parameter_label) - for parameter in self.function_option.value.parameters: - 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()) - ) - - # Create a QHBoxLayout - hbox = QHBoxLayout() - - # Add your QLineEdit and a stretch to the QHBoxLayout - hbox.addWidget(parameter_lineedit) - hbox.addStretch(1) - - # Use addRow() method to add label and the QHBoxLayout next to each other - parameter_layout.addRow(parameter_label, hbox) - - function_layout.addLayout(parameter_layout) - function_layout.addStretch(1) - active_function_Widget.setLayout(function_layout) - self.layout().addWidget(active_function_Widget) - - # Update the resolution, start_x, end_x and expr lineedits - self.resolution_lineedit.setText(str(self.function_option.value.resolution)) - self.start_x_lineedit.setText(str(self.function_option.value.start_x)) - 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: - """Creates a message box with the given message and information and shows it. - - Args: - message (str): The message to be shown in the message box - information (str): The information to be shown in the message box - """ - msg = QMessageBox(parent=self.parent) - msg.setIcon(QMessageBox.Icon.Warning) - msg.setText(message) - msg.setInformativeText(information) - msg.setWindowTitle("Warning") - msg.exec() - - 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.""" @@ -782,13 +543,13 @@ class AddEventDialog(QDialog): """ return self.name_input.text() - def get_duration(self) -> Decimal: + def get_duration(self) -> float: """Returns the duration entered by the user, or a fallback value." Returns: - Decimal: The duration value provided by the user, or 20 + float: The duration value provided by the user, or 20 """ - return Decimal(self.duration_lineedit.text() or 20) + return self.duration_lineedit.text() or 20 def check_input(self) -> None: """Checks if the name and duration entered by the user is valid. If it is, the dialog is accepted. If not, the user is informed of the error."""