Skip to content

Commit b1e369b

Browse files
committed
added layers widget
fixed absorption saving fixed tests
1 parent 80c671d commit b1e369b

File tree

4 files changed

+302
-27
lines changed

4 files changed

+302
-27
lines changed

rascal2/widgets/delegates.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,28 @@ def setEditorData(self, editor: AdaptiveDoubleSpinBox, index):
7676
def setModelData(self, editor, model, index):
7777
data = editor.value()
7878
model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole)
79+
80+
81+
class ParametersDelegate(QtWidgets.QStyledItemDelegate):
82+
"""Item delegate to choose from existing draft project parameters."""
83+
84+
def __init__(self, project_widget, parent):
85+
super().__init__(parent)
86+
self.project_widget = project_widget
87+
88+
def createEditor(self, parent, option, index):
89+
widget = QtWidgets.QComboBox(parent)
90+
parameters = self.project_widget.draft_project["parameters"]
91+
names = [p.name for p in parameters]
92+
widget.addItems(names)
93+
widget.setCurrentText(index.data(QtCore.Qt.ItemDataRole.DisplayRole))
94+
95+
return widget
96+
97+
def setEditorData(self, editor: QtWidgets.QWidget, index):
98+
data = index.data(QtCore.Qt.ItemDataRole.DisplayRole)
99+
editor.setCurrentText(data)
100+
101+
def setModelData(self, editor, model, index):
102+
data = editor.currentText()
103+
model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole)

rascal2/widgets/project/models.py

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from RATapi.utils.enums import Procedures
99

1010
from rascal2.config import path_for
11-
from rascal2.widgets.delegates import ValidatedInputDelegate, ValueSpinBoxDelegate
11+
from rascal2.widgets.delegates import ParametersDelegate, ValidatedInputDelegate, ValueSpinBoxDelegate
1212

1313

1414
class ClassListModel(QtCore.QAbstractTableModel):
@@ -28,12 +28,21 @@ class ClassListModel(QtCore.QAbstractTableModel):
2828
def __init__(self, classlist: RATapi.ClassList, parent: QtWidgets.QWidget):
2929
super().__init__(parent)
3030
self.parent = parent
31+
32+
self.classlist: RATapi.ClassList
33+
self.item_type: type
34+
self.headers: list[str]
35+
36+
self.setup_classlist(classlist)
37+
self.edit_mode = False
38+
39+
def setup_classlist(self, classlist: RATapi.ClassList):
40+
"""Setup the ClassList, type and headers for the model."""
3141
self.classlist = classlist
3242
self.item_type = classlist._class_handle
3343
if not issubclass(self.item_type, pydantic.BaseModel):
3444
raise NotImplementedError("ClassListModel only works for classlists of Pydantic models!")
3545
self.headers = list(self.item_type.model_fields)
36-
self.edit_mode = False
3746

3847
def rowCount(self, parent=None) -> int:
3948
return len(self.classlist)
@@ -296,3 +305,104 @@ def edit(self):
296305
for i in range(0, self.model.rowCount()):
297306
if i in self.model.protected_indices:
298307
self.table.setIndexWidget(self.model.index(i, 0), None)
308+
309+
310+
class LayersModel(ClassListModel):
311+
"""Classlist model for Layers."""
312+
313+
def __init__(self, classlist: RATapi.ClassList, parent: QtWidgets.QWidget):
314+
super().__init__(classlist, parent)
315+
self.absorption = classlist._class_handle == RATapi.models.AbsorptionLayer
316+
self.SLD_imags = {}
317+
318+
def flags(self, index):
319+
flags = super().flags(index)
320+
if self.edit_mode:
321+
flags |= QtCore.Qt.ItemFlag.ItemIsEditable
322+
return flags
323+
324+
def append_item(self):
325+
kwargs = {"thickness": "", "SLD": "", "roughness": ""}
326+
if self.absorption:
327+
kwargs["SLD_imaginary"] = ""
328+
self.classlist.append(self.item_type(**kwargs))
329+
self.endResetModel()
330+
331+
def set_absorption(self, absorption: bool):
332+
"""Set whether the project is using absorption or not.
333+
334+
Parameters
335+
----------
336+
absorption : bool
337+
Whether the project is using absorption.
338+
339+
"""
340+
if self.absorption != absorption:
341+
self.beginResetModel()
342+
self.absorption = absorption
343+
if absorption:
344+
classlist = RATapi.ClassList(
345+
[
346+
RATapi.models.AbsorptionLayer(
347+
**dict(layer),
348+
SLD_imaginary=self.SLD_imags.get(layer.name, ""),
349+
)
350+
for layer in self.classlist
351+
]
352+
)
353+
# set handle manually for if classlist is empty
354+
classlist._class_handle = RATapi.models.AbsorptionLayer
355+
else:
356+
# we save the SLD_imaginary values so that they aren't lost if the
357+
# user accidentally toggles absorption off and on!
358+
self.SLD_imags = {layer.name: layer.SLD_imaginary for layer in self.classlist}
359+
classlist = RATapi.ClassList(
360+
[
361+
RATapi.models.Layer(
362+
name=layer.name,
363+
thickness=layer.thickness,
364+
SLD=layer.SLD_real,
365+
roughness=layer.roughness,
366+
hydration=layer.hydration,
367+
hydrate_with=layer.hydrate_with,
368+
)
369+
for layer in self.classlist
370+
]
371+
)
372+
classlist._class_handle = RATapi.models.Layer
373+
self.setup_classlist(classlist)
374+
self.parent.parent.parent.update_draft_project({"layers": classlist})
375+
self.endResetModel()
376+
377+
378+
class LayerFieldWidget(ProjectFieldWidget):
379+
"""Project field widget for Layer objects."""
380+
381+
classlist_model = LayersModel
382+
383+
def __init__(self, field, parent):
384+
super().__init__(field, parent)
385+
self.project_widget = parent.parent
386+
387+
def set_item_delegates(self):
388+
for i in range(1, self.model.columnCount()):
389+
if i in [1, self.model.columnCount() - 1]:
390+
header = self.model.headers[i - 1]
391+
self.table.setItemDelegateForColumn(
392+
i, ValidatedInputDelegate(self.model.item_type.model_fields[header], self.table)
393+
)
394+
else:
395+
self.table.setItemDelegateForColumn(i, ParametersDelegate(self.project_widget, self.table))
396+
397+
def set_absorption(self, absorption: bool):
398+
"""Set whether the classlist uses AbsorptionLayers.
399+
400+
Parameters
401+
----------
402+
absorption : bool
403+
Whether the classlist should use AbsorptionLayers.
404+
405+
"""
406+
self.model.set_absorption(absorption)
407+
if self.model.edit_mode:
408+
self.edit()

rascal2/widgets/project/project.py

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from RATapi.utils.enums import Calculations, Geometries, LayerModels
88

99
from rascal2.config import path_for
10-
from rascal2.widgets.project.models import ParameterFieldWidget, ProjectFieldWidget
10+
from rascal2.widgets.project.models import LayerFieldWidget, ParameterFieldWidget, ProjectFieldWidget
1111

1212

1313
class ProjectWidget(QtWidgets.QWidget):
@@ -34,7 +34,7 @@ def __init__(self, parent):
3434
self.tabs = {
3535
"Parameters": ["parameters"],
3636
"Experimental Parameters": ["scalefactors", "bulk_in", "bulk_out"],
37-
"Layers": [],
37+
"Layers": ["layers"],
3838
"Data": [],
3939
"Backgrounds": [],
4040
"Contrasts": [],
@@ -62,49 +62,66 @@ def __init__(self, parent):
6262
def create_project_view(self) -> None:
6363
"""Creates the project (non-edit) view"""
6464
project_widget = QtWidgets.QWidget()
65-
main_layout = QtWidgets.QGridLayout()
66-
main_layout.setVerticalSpacing(20)
65+
main_layout = QtWidgets.QVBoxLayout()
66+
main_layout.setSpacing(20)
6767

6868
self.edit_project_button = QtWidgets.QPushButton(
6969
"Edit Project", self, objectName="bluebutton", icon=QtGui.QIcon(path_for("edit.png"))
7070
)
7171
self.edit_project_button.clicked.connect(self.show_edit_view)
72-
main_layout.addWidget(self.edit_project_button, 0, 5)
72+
button_layout = QtWidgets.QHBoxLayout()
73+
button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
74+
button_layout.addWidget(self.edit_project_button)
75+
76+
main_layout.addLayout(button_layout)
77+
78+
settings_layout = QtWidgets.QHBoxLayout()
79+
settings_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
80+
81+
absorption_label = QtWidgets.QLabel("Absorption:", self, objectName="boldlabel")
82+
self.absorption_checkbox = QtWidgets.QCheckBox()
83+
# this is how you make a checkbox read-only but still checkable from inside code...
84+
self.absorption_checkbox.setAttribute(QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents)
85+
86+
settings_layout.addWidget(absorption_label)
87+
settings_layout.addWidget(self.absorption_checkbox)
7388

7489
self.calculation_label = QtWidgets.QLabel("Calculation:", self, objectName="boldlabel")
7590

7691
self.calculation_type = QtWidgets.QLineEdit(self)
7792
self.calculation_type.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
7893
self.calculation_type.setReadOnly(True)
7994

80-
main_layout.addWidget(self.calculation_label, 1, 0, 1, 1)
81-
main_layout.addWidget(self.calculation_type, 1, 1, 1, 1)
95+
settings_layout.addWidget(self.calculation_label)
96+
settings_layout.addWidget(self.calculation_type)
8297

8398
self.model_type_label = QtWidgets.QLabel("Model Type:", self, objectName="boldlabel")
8499

85100
self.model_type = QtWidgets.QLineEdit(self)
86101
self.model_type.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
87102
self.model_type.setReadOnly(True)
88103

89-
main_layout.addWidget(self.model_type_label, 1, 2, 1, 1)
90-
main_layout.addWidget(self.model_type, 1, 3, 1, 1)
104+
settings_layout.addWidget(self.model_type_label)
105+
settings_layout.addWidget(self.model_type)
91106

92107
self.geometry_label = QtWidgets.QLabel("Geometry:", self, objectName="boldlabel")
93108

94109
self.geometry_type = QtWidgets.QLineEdit(self)
95110
self.geometry_type.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
96111
self.geometry_type.setReadOnly(True)
97112

98-
main_layout.addWidget(self.geometry_label, 1, 4, 1, 1)
99-
main_layout.addWidget(self.geometry_type, 1, 5, 1, 1)
113+
settings_layout.addWidget(self.geometry_label)
114+
settings_layout.addWidget(self.geometry_type)
115+
116+
main_layout.addLayout(settings_layout)
100117

101118
self.project_tab = QtWidgets.QTabWidget()
102119

103120
for tab, fields in self.tabs.items():
104121
widget = self.view_tabs[tab] = ProjectTabWidget(fields, self)
105122
self.project_tab.addTab(widget, tab)
106123

107-
main_layout.addWidget(self.project_tab, 2, 0, 1, 6)
124+
main_layout.addWidget(self.project_tab)
108125
project_widget.setLayout(main_layout)
109126

110127
return project_widget
@@ -124,38 +141,58 @@ def create_edit_view(self) -> None:
124141
self.cancel_button.setIcon(QtGui.QIcon(path_for("cancel-dark.png")))
125142
self.cancel_button.clicked.connect(self.cancel_changes)
126143

127-
layout = QtWidgets.QHBoxLayout()
128-
layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
129-
layout.addWidget(self.save_project_button)
130-
layout.addWidget(self.cancel_button)
131-
main_layout.addLayout(layout)
144+
buttons_layout = QtWidgets.QHBoxLayout()
145+
buttons_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
146+
buttons_layout.addWidget(self.save_project_button)
147+
buttons_layout.addWidget(self.cancel_button)
148+
main_layout.addLayout(buttons_layout)
149+
150+
settings_layout = QtWidgets.QHBoxLayout()
151+
settings_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
152+
153+
absorption_label = QtWidgets.QLabel("Absorption:", self, objectName="boldlabel")
154+
self.edit_absorption_checkbox = QtWidgets.QCheckBox()
155+
156+
settings_layout.addWidget(absorption_label)
157+
settings_layout.addWidget(self.edit_absorption_checkbox)
132158

133159
self.edit_calculation_label = QtWidgets.QLabel("Calculation:", self, objectName="boldlabel")
134160

135161
self.calculation_combobox = QtWidgets.QComboBox(self)
162+
self.calculation_combobox.setSizePolicy(
163+
QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed
164+
)
136165
self.calculation_combobox.addItems([calc for calc in Calculations])
137166

138-
layout = QtWidgets.QHBoxLayout()
139-
layout.addWidget(self.edit_calculation_label)
140-
layout.addWidget(self.calculation_combobox)
167+
settings_layout.addWidget(self.edit_calculation_label)
168+
settings_layout.addWidget(self.calculation_combobox)
141169

142170
self.edit_model_type_label = QtWidgets.QLabel("Model Type:", self, objectName="boldlabel")
143171

144172
self.model_combobox = QtWidgets.QComboBox(self)
173+
self.model_combobox.setSizePolicy(
174+
QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed
175+
)
145176
self.model_combobox.addItems([model for model in LayerModels])
146177

147-
layout.addWidget(self.edit_model_type_label)
148-
layout.addWidget(self.model_combobox)
178+
settings_layout.addWidget(self.edit_model_type_label)
179+
settings_layout.addWidget(self.model_combobox)
149180

150181
self.edit_geometry_label = QtWidgets.QLabel("Geometry:", self, objectName="boldlabel")
151182

152183
self.geometry_combobox = QtWidgets.QComboBox(self)
184+
self.geometry_combobox.setSizePolicy(
185+
QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed
186+
)
153187
self.geometry_combobox.addItems([geo for geo in Geometries])
154188

155-
layout.addWidget(self.edit_geometry_label)
156-
layout.addWidget(self.geometry_combobox)
157-
main_layout.addLayout(layout)
189+
settings_layout.addWidget(self.edit_geometry_label)
190+
settings_layout.addWidget(self.geometry_combobox)
191+
main_layout.addLayout(settings_layout)
158192

193+
self.edit_absorption_checkbox.checkStateChanged.connect(
194+
lambda s: self.update_draft_project({"absorption": s == QtCore.Qt.CheckState.Checked})
195+
)
159196
self.calculation_combobox.currentTextChanged.connect(lambda s: self.update_draft_project({"calculation": s}))
160197
self.calculation_combobox.currentTextChanged.connect(lambda: self.handle_domains_tab())
161198
self.model_combobox.currentTextChanged.connect(lambda s: self.update_draft_project({"model": s}))
@@ -166,6 +203,10 @@ def create_edit_view(self) -> None:
166203
widget = self.edit_tabs[tab] = ProjectTabWidget(fields, self, edit_mode=True)
167204
self.edit_project_tab.addTab(widget, tab)
168205

206+
self.edit_absorption_checkbox.checkStateChanged.connect(
207+
lambda s: self.edit_tabs["Layers"].tables["layers"].set_absorption(s == QtCore.Qt.CheckState.Checked)
208+
)
209+
169210
main_layout.addWidget(self.edit_project_tab)
170211

171212
edit_project_widget.setLayout(main_layout)
@@ -178,10 +219,12 @@ def update_project_view(self) -> None:
178219
# because we don't want validation errors going off while editing the model is in-progress
179220
self.draft_project: dict = create_draft_project(self.parent_model.project)
180221

222+
self.absorption_checkbox.setChecked(self.parent_model.project.absorption)
181223
self.calculation_type.setText(self.parent_model.project.calculation)
182224
self.model_type.setText(self.parent_model.project.model)
183225
self.geometry_type.setText(self.parent_model.project.geometry)
184226

227+
self.edit_absorption_checkbox.setChecked(self.parent_model.project.absorption)
185228
self.calculation_combobox.setCurrentText(self.parent_model.project.calculation)
186229
self.model_combobox.setCurrentText(self.parent_model.project.model)
187230
self.geometry_combobox.setCurrentText(self.parent_model.project.geometry)
@@ -271,6 +314,8 @@ def __init__(self, fields: list[str], parent, edit_mode: bool = False):
271314
for field in self.fields:
272315
if field in RATapi.project.parameter_class_lists:
273316
self.tables[field] = ParameterFieldWidget(field, self)
317+
elif field == "layers":
318+
self.tables[field] = LayerFieldWidget(field, self)
274319
else:
275320
self.tables[field] = ProjectFieldWidget(field, self)
276321
layout.addWidget(self.tables[field])
@@ -302,6 +347,8 @@ def update_model(self, new_model):
302347
table.update_model(classlist)
303348
if self.edit_mode:
304349
table.edit()
350+
if "layers" in self.tables:
351+
self.tables["layers"].set_absorption(new_model["absorption"])
305352

306353
def handle_controls_update(self, controls):
307354
"""Reflect changes to the Controls object."""

0 commit comments

Comments
 (0)