Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds parameters and experimental parameters to the project widget #49

Merged
merged 20 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,16 @@ mark-parentheses = false
[tool.ruff.lint.pep8-naming]
# if overriding a PyQt method, please add it here!
# names should be in alphabetical order for readability
extend-ignore-names = ["allKeys", "createEditor", "mergeWith", "setEditorData", "textFromValue", ]
extend-ignore-names = ['allKeys',
'columnCount',
'createEditor',
'headerData',
'mergeWith',
'rowCount',
'setData',
'setEditorData',
'setModelData',
'setValue',
'stepBy',
'textFromValue',
'valueFromText',]
74 changes: 61 additions & 13 deletions rascal2/core/commands.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,98 @@
"""File for Qt commands."""

from enum import IntEnum, unique
from typing import Callable

from PyQt6 import QtGui
from RATapi import ClassList


@unique
class CommandID(IntEnum):
"""Unique ID for undoable commands"""

EditControls = 1000
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
EditProject = 2000


class EditControls(QtGui.QUndoCommand):
"""Command for editing the Controls object."""
class AbstractModelEdit(QtGui.QUndoCommand):
"""Command for editing an attribute of the model."""

def __init__(self, attr, value, presenter):
attribute = None

def __init__(self, new_values: dict, presenter):
super().__init__()
self.presenter = presenter
self.attr = attr
self.value = value
self.old_value = getattr(self.presenter.model.controls, self.attr)
self.setText(f"Set control {self.attr} to {self.value}")
self.new_values = new_values
if self.attribute is None:
raise NotImplementedError("AbstractEditModel should not be instantiated directly.")
else:
self.model_class = getattr(self.presenter.model, self.attribute)
self.old_values = {attr: getattr(self.model_class, attr) for attr in self.new_values}
self.update_text()

def update_text(self):
"""Update the undo command text."""
if len(self.new_values) == 1:
attr, value = list(self.new_values.items())[0]
if isinstance(list(self.new_values.values())[0], ClassList):
text = f"Changed values in {attr}"
else:
text = f"Set {self.attribute} {attr} to {value}"
else:
text = f"Save update to {self.attribute}"

self.setText(text)

@property
def update_attribute(self) -> Callable:
"""Return the method used to update the attribute."""
raise NotImplementedError

def undo(self):
self.presenter.model.update_controls({self.attr: self.old_value})
self.update_attribute(self.old_values)

def redo(self):
self.presenter.model.update_controls({self.attr: self.value})
self.update_attribute(self.new_values)

def mergeWith(self, command):
"""Merges consecutive Edit controls commands if the attributes are the
same."""

# We should think about if merging all Edit controls irrespective of
# attribute is the way to go for UX
if self.attr != command.attr:
if list(self.new_values.keys()) != list(command.new_values.keys()):
return False

if self.old_value == command.value:
if list(self.old_values.values()) == list(command.new_values.values()):
self.setObsolete(True)

self.value = command.value
self.setText(f"Set control {self.attr} to {self.value}")
self.new_values = command.new_values
self.update_text()
return True

def id(self):
"""Returns ID used for merging commands"""
raise NotImplementedError


class EditControls(AbstractModelEdit):
attribute = "controls"

@property
def update_attribute(self):
return self.presenter.model.update_controls

def id(self):
return CommandID.EditControls


class EditProject(AbstractModelEdit):
attribute = "project"

@property
def update_attribute(self):
return self.presenter.model.update_project

def id(self):
return CommandID.EditProject
Binary file added rascal2/static/images/delete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 14 additions & 13 deletions rascal2/ui/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create_project(self, name: str, save_path: str):
self.controls = RAT.Controls()
self.save_path = save_path

def update_project(self, problem_definition: RAT.rat_core.ProblemDefinition):
def handle_results(self, problem_definition: RAT.rat_core.ProblemDefinition):
"""Update the project given a set of results."""
parameter_field = {
"parameters": "params",
Expand All @@ -49,16 +49,17 @@ def update_project(self, problem_definition: RAT.rat_core.ProblemDefinition):
for index, value in enumerate(getattr(problem_definition, parameter_field[class_list])):
getattr(self.project, class_list)[index].value = value

def update_controls(self, new_values):
"""
def update_project(self, new_values: dict) -> None:
"""Replaces the project with a new project.

Parameters
----------
new_values: Dict
The attribute name-value pair to updated on the controls.
new_values : dict
New values to set in the project.

"""
vars(self.controls).update(new_values)
self.controls_updated.emit()
vars(self.project).update(new_values)
self.project_updated.emit()

def save_project(self):
"""Save the project to the save path."""
Expand Down Expand Up @@ -110,13 +111,13 @@ def load_r1_project(self, load_path: str):
self.controls = RAT.Controls()
self.save_path = str(Path(load_path).parent)

def edit_project(self, updated_project) -> None:
"""Updates the project.
def update_controls(self, new_values):
"""

Parameters
----------
updated_project : RAT.Project
The updated project.
new_values: Dict
The attribute name-value pair to updated on the controls.
"""
self.project = updated_project
self.project_updated.emit()
vars(self.controls).update(new_values)
self.controls_updated.emit()
23 changes: 16 additions & 7 deletions rascal2/ui/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def edit_controls(self, setting: str, value: Any):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self.model.controls.model_validate({setting: value})
self.view.undo_stack.push(commands.EditControls(setting, value, self))
self.view.undo_stack.push(commands.EditControls({setting: value}, self))

def save_project(self, save_as: bool = False):
"""Save the model.
Expand Down Expand Up @@ -150,7 +150,7 @@ def run(self):

def handle_results(self):
"""Handle a RAT run being finished."""
self.model.update_project(self.runner.updated_problem)
self.model.handle_results(self.runner.updated_problem)
self.view.handle_results(self.runner.results)

def handle_interrupt(self):
Expand All @@ -174,15 +174,24 @@ def handle_event(self):
elif isinstance(event, LogData):
self.view.logging.log(event.level, event.msg)

def edit_project(self, updated_project) -> None:
"""Updates the project.
def edit_project(self, updated_project: dict) -> None:
"""Edit the Project with a dictionary of attributes.

Parameters
----------
updated_project : RAT.Project
The updated project.
updated_project : dict
The updated project attributes.

Raises
------
ValidationError
If the updated project attributes are not valid.

"""
self.model.edit_project(updated_project)
project_dict = self.model.project.model_dump()
project_dict.update(updated_project)
self.model.project.model_validate(project_dict)
self.view.undo_stack.push(commands.EditProject(updated_project, self))


# '\d+\.\d+' is the regex for
Expand Down
11 changes: 7 additions & 4 deletions rascal2/ui/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def __init__(self):
# TODO replace the widgets below
# plotting: NO ISSUE YET
# https://github.com/RascalSoftware/RasCAL-2/issues/5
# project: NO ISSUE YET
self.plotting_widget = QtWidgets.QWidget()
self.terminal_widget = TerminalWidget(self)
self.controls_widget = ControlsWidget(self)
Expand Down Expand Up @@ -246,7 +245,7 @@ def setup_mdi(self):
# if windows are already created, don't set them up again,
# just refresh the widget data
if len(self.mdi.subWindowList()) == 4:
self.controls_widget.setup_controls()
self.setup_mdi_widgets()
return

widgets = {
Expand All @@ -255,8 +254,7 @@ def setup_mdi(self):
"Terminal": self.terminal_widget,
"Fitting Controls": self.controls_widget,
}
self.controls_widget.setup_controls()
self.project_widget.update_project_view()
self.setup_mdi_widgets()
self.terminal_widget.text_area.setVisible(True)

for title, widget in reversed(widgets.items()):
Expand All @@ -269,6 +267,11 @@ def setup_mdi(self):
self.startup_dlg = self.takeCentralWidget()
self.setCentralWidget(self.mdi)

def setup_mdi_widgets(self):
"""Performs setup of MDI widgets that relies on the Project existing."""
self.controls_widget.setup_controls()
self.project_widget.update_project_view()

def reset_mdi_layout(self):
"""Reset MDI layout to the default."""
if self.settings.mdi_defaults is None:
Expand Down
1 change: 1 addition & 0 deletions rascal2/widgets/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def __init__(self, parent, settings, presenter):
for i, setting in enumerate(settings):
field_info = controls_fields[setting]
self.rows[setting] = get_validated_input(field_info)
self.rows[setting].layout().setContentsMargins(5, 0, 0, 0)
self.datasetter[setting] = self.create_model_data_setter(setting)
self.rows[setting].edited_signal.connect(self.datasetter[setting])
label = QtWidgets.QLabel(setting)
Expand Down
78 changes: 58 additions & 20 deletions rascal2/widgets/delegates.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,78 @@
"""Delegates for items in Qt tables."""

from typing import Literal

from PyQt6 import QtCore, QtGui, QtWidgets

from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, get_validated_input


class EnumDelegate(QtWidgets.QStyledItemDelegate):
"""Item delegate for Enums."""
class ValidatedInputDelegate(QtWidgets.QStyledItemDelegate):
"""Item delegate for validated inputs."""

def __init__(self, parent, enum):
def __init__(self, field_info, parent):
super().__init__(parent)
self.enum = enum
self.table = parent
self.field_info = field_info

def createEditor(self, parent, option, index):
combobox = QtWidgets.QComboBox(parent)
combobox.addItems(str(e.value) for e in self.enum)
return combobox
widget = get_validated_input(self.field_info, parent)
widget.set_data(index.data(QtCore.Qt.ItemDataRole.DisplayRole))

# fill in background as otherwise you can see the original View text underneath
widget.setAutoFillBackground(True)
widget.setBackgroundRole(QtGui.QPalette.ColorRole.Base)

def setEditorData(self, editor: QtWidgets.QCheckBox, index):
return widget

def setEditorData(self, editor: QtWidgets.QWidget, index):
data = index.data(QtCore.Qt.ItemDataRole.DisplayRole)
editor.setCurrentText(data)
editor.set_data(data)

def setModelData(self, editor, model, index):
data = editor.get_data()
model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole)


class ValueSpinBoxDelegate(QtWidgets.QStyledItemDelegate):
"""Item delegate for parameter values between a dynamic min and max.

class BoolDelegate(QtWidgets.QStyledItemDelegate):
"""Item delegate for bools."""
Parameters
----------
field : Literal["min", "value", "max"]
The field of the parameter

def __init__(self, parent):
"""

def __init__(self, field: Literal["min", "value", "max"], parent):
super().__init__(parent)
self.parent = parent
self.table = parent
self.field = field

def createEditor(self, parent, option, index):
checkbox = QtWidgets.QCheckBox(parent)
widget = AdaptiveDoubleSpinBox(parent)

max_val = float("inf")
min_val = -float("inf")

if self.field in ["min", "value"]:
max_val = index.siblingAtColumn(index.column() + 1).data(QtCore.Qt.ItemDataRole.DisplayRole)
if self.field in ["value", "max"]:
min_val = index.siblingAtColumn(index.column() - 1).data(QtCore.Qt.ItemDataRole.DisplayRole)

widget.setMinimum(min_val)
widget.setMaximum(max_val)

# fill in background as otherwise you can see the original View text underneath
checkbox.setAutoFillBackground(True)
checkbox.setBackgroundRole(QtGui.QPalette.ColorRole.Base)
return checkbox
widget.setAutoFillBackground(True)
widget.setBackgroundRole(QtGui.QPalette.ColorRole.Base)

return widget

def setEditorData(self, editor: QtWidgets.QCheckBox, index):
def setEditorData(self, editor: AdaptiveDoubleSpinBox, index):
data = index.data(QtCore.Qt.ItemDataRole.DisplayRole)
data = data == "True" # data from model is given as a string
editor.setChecked(data)
editor.setValue(data)

def setModelData(self, editor, model, index):
data = editor.value()
model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole)
Loading
Loading