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 Domains tab to project widget #57

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,21 @@ mark-parentheses = false
# if overriding a PyQt method, please add it here!
# names should be in alphabetical order for readability
extend-ignore-names = ['allKeys',
'addItem',
'addItems',
'columnCount',
'createEditor',
'eventFilter',
'headerData',
'mergeWith',
'resizeEvent',
'rowCount',
'setData',
'setEditorData',
'setModelData',
'setValue',
'showEvent',
'sizeHint',
'stepBy',
'textFromValue',
'valueFromText',]
'valueFromText',]
11 changes: 9 additions & 2 deletions rascal2/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from rascal2.widgets.controls import ControlsWidget
from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, get_validated_input
from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, MultiSelectComboBox, get_validated_input
from rascal2.widgets.plot import PlotWidget
from rascal2.widgets.terminal import TerminalWidget

__all__ = ["ControlsWidget", "AdaptiveDoubleSpinBox", "get_validated_input", "PlotWidget", "TerminalWidget"]
__all__ = [
"ControlsWidget",
"AdaptiveDoubleSpinBox",
"get_validated_input",
"MultiSelectComboBox",
"PlotWidget",
"TerminalWidget",
]
30 changes: 29 additions & 1 deletion rascal2/widgets/delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from PyQt6 import QtCore, QtGui, QtWidgets

from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, get_validated_input
from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, MultiSelectComboBox, get_validated_input


class ValidatedInputDelegate(QtWidgets.QStyledItemDelegate):
Expand Down Expand Up @@ -101,3 +101,31 @@ def setEditorData(self, editor: QtWidgets.QWidget, index):
def setModelData(self, editor, model, index):
data = editor.currentText()
model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole)


class MultiSelectLayerDelegate(QtWidgets.QStyledItemDelegate):
"""Item delegate for multiselecting layers."""

def __init__(self, project_widget, parent):
super().__init__(parent)
self.project_widget = project_widget

def createEditor(self, parent, option, index):
widget = MultiSelectComboBox(parent)

layers = self.project_widget.draft_project["layers"]
widget.addItems([layer.name for layer in layers])

return widget

def setEditorData(self, editor: MultiSelectComboBox, index):
# index.data produces the display string rather than the underlying data,
# so we split it back into a list here
data = index.data(QtCore.Qt.ItemDataRole.DisplayRole).split(", ")
layers = self.project_widget.draft_project["layers"]

editor.select_indices([i for i, layer in enumerate(layers) if layer.name in data])

def setModelData(self, editor, model, index):
data = editor.selected_items()
model.setData(index, data, QtCore.Qt.ItemDataRole.EditRole)
158 changes: 158 additions & 0 deletions rascal2/widgets/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,161 @@ def validate(self, input_text, pos) -> tuple[QtGui.QValidator.State, str, int]:
self.setDecimals(len(input_text.split(".")[-1]))
return (QtGui.QValidator.State.Acceptable, input_text, pos)
return super().validate(input_text, pos)


class MultiSelectComboBox(QtWidgets.QComboBox):
"""
A custom combo box widget that allows for multi-select functionality.

This widget provides the ability to select multiple items from a
dropdown list and display them in a comma-separated format in the
combo box's line edit area.

This is a simplified version of the combobox in
https://github.com/user0706/pyqt6-multiselect-combobox (MIT License)

"""

class Delegate(QtWidgets.QStyledItemDelegate):
def sizeHint(self, option, index):
size = super().sizeHint(option, index)
size.setHeight(20)
return size

def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

self.setEditable(True)
self.lineEdit().setReadOnly(True)

self.setItemDelegate(MultiSelectComboBox.Delegate())

self.model().dataChanged.connect(self.update_text)
self.lineEdit().installEventFilter(self)
self.view().viewport().installEventFilter(self)

def resizeEvent(self, event) -> None:
"""Resize event handler.

Parameters
----------
event
The resize event.

"""
self.update_text()
super().resizeEvent(event)

def eventFilter(self, obj, event) -> bool:
"""Event filter to handle mouse button release events.

Parameters
----------
obj
The object emitting the event.
event
The event being emitted.

Returns
-------
bool
True if the event was handled, False otherwise.

"""
if obj == self.view().viewport() and event.type() == QtCore.QEvent.Type.MouseButtonRelease:
index = self.view().indexAt(event.position().toPoint())
item = self.model().itemFromIndex(index)
if item.checkState() == QtCore.Qt.CheckState.Checked:
item.setCheckState(QtCore.Qt.CheckState.Unchecked)
else:
item.setCheckState(QtCore.Qt.CheckState.Checked)
return True
return False

def update_text(self) -> None:
"""Update the displayed text based on selected items."""
items = self.selected_items()

if items:
text = ", ".join([str(i) for i in items])
else:
text = ""

metrics = QtGui.QFontMetrics(self.lineEdit().font())
elided_text = metrics.elidedText(text, QtCore.Qt.TextElideMode.ElideRight, self.lineEdit().width())
self.lineEdit().setText(elided_text)

def addItem(self, text: str, data: str = None) -> None:
"""Add an item to the combo box.

Parameters
----------
text : str
The text to display.
data : str
The associated data. Default is None.

"""
item = QtGui.QStandardItem()
item.setText(text)
item.setData(data or text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable)
item.setData(QtCore.Qt.CheckState.Unchecked, QtCore.Qt.ItemDataRole.CheckStateRole)
self.model().appendRow(item)

def addItems(self, texts: list, data_list: list = None) -> None:
"""Add multiple items to the combo box.

Parameters
----------
texts : list
A list of items to add.
data_list : list
A list of associated data. Default is None.

"""
data_list = data_list or [None] * len(texts)
for text, data in zip(texts, data_list):
self.addItem(text, data)

def selected_items(self) -> list:
"""Get the currently selected data.

Returns
-------
list
A list of currently selected data.

"""
return [
self.model().item(i).data()
for i in range(self.model().rowCount())
if self.model().item(i).checkState() == QtCore.Qt.CheckState.Checked
]

def select_indices(self, indices: list) -> None:
"""Set the selected items based on the provided indices.

Parameters
----------
indexes : list
A list of indexes to select.

"""
for i in range(self.model().rowCount()):
self.model().item(i).setCheckState(
QtCore.Qt.CheckState.Checked if i in indices else QtCore.Qt.CheckState.Unchecked
)
self.update_text()

def showEvent(self, event) -> None:
"""Show event handler.

Parameters
----------
event
The show event.

"""
super().showEvent(event)
self.update_text()
52 changes: 51 additions & 1 deletion rascal2/widgets/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
from RATapi.utils.enums import Procedures

from rascal2.config import path_for
from rascal2.widgets.delegates import ParametersDelegate, ValidatedInputDelegate, ValueSpinBoxDelegate
from rascal2.widgets.delegates import (
MultiSelectLayerDelegate,
ParametersDelegate,
ValidatedInputDelegate,
ValueSpinBoxDelegate,
)


class ClassListModel(QtCore.QAbstractTableModel):
Expand Down Expand Up @@ -63,6 +68,8 @@ def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
# pyqt can't automatically coerce enums to strings...
if isinstance(data, Enum):
return str(data)
if isinstance(data, list):
return ", ".join(data)
return data
elif role == QtCore.Qt.ItemDataRole.CheckStateRole and self.index_header(index) == "fit":
return QtCore.Qt.CheckState.Checked if data else QtCore.Qt.CheckState.Unchecked
Expand Down Expand Up @@ -380,6 +387,16 @@ def set_absorption(self, absorption: bool):
self.endResetModel()


class ContrastsModel(ClassListModel):
"""Classlist model for Contrasts."""

def flags(self, index):
flags = super().flags(index)
if self.edit_mode:
flags |= QtCore.Qt.ItemFlag.ItemIsEditable
return flags


class LayerFieldWidget(ProjectFieldWidget):
"""Project field widget for Layer objects."""

Expand Down Expand Up @@ -411,3 +428,36 @@ def set_absorption(self, absorption: bool):
self.model.set_absorption(absorption)
if self.model.edit_mode:
self.edit()


class DomainsModel(ClassListModel):
"""Classlist model for domain contrasts."""

def flags(self, index):
flags = super().flags(index)
if self.edit_mode:
flags |= QtCore.Qt.ItemFlag.ItemIsEditable
return flags


class DomainContrastWidget(ProjectFieldWidget):
"""Subclass of field widgets for domain contrasts."""

classlist_model = DomainsModel

def __init__(self, field, parent):
super().__init__(field, parent)
self.project_widget = parent.parent

def update_model(self, classlist):
super().update_model(classlist)

header = self.table.horizontalHeader()
header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch)

def set_item_delegates(self):
self.table.setItemDelegateForColumn(
1, ValidatedInputDelegate(self.model.item_type.model_fields["name"], self.table)
)
self.table.setItemDelegateForColumn(2, MultiSelectLayerDelegate(self.project_widget, self.table))
15 changes: 12 additions & 3 deletions rascal2/widgets/project/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from RATapi.utils.enums import Calculations, Geometries, LayerModels

from rascal2.config import path_for
from rascal2.widgets.project.models import LayerFieldWidget, ParameterFieldWidget, ProjectFieldWidget
from rascal2.widgets.project.models import (
DomainContrastWidget,
LayerFieldWidget,
ParameterFieldWidget,
ProjectFieldWidget,
)


class ProjectWidget(QtWidgets.QWidget):
Expand Down Expand Up @@ -37,8 +42,8 @@ def __init__(self, parent):
"Layers": ["layers"],
"Data": [],
"Backgrounds": [],
"Domains": ["domain_ratios", "domain_contrasts"],
"Contrasts": [],
"Domains": [],
}

self.view_tabs = {}
Expand Down Expand Up @@ -257,11 +262,13 @@ def handle_tabs(self) -> None:
self.project_tab.setTabVisible(domain_tab_index, is_domains)
self.edit_project_tab.setTabVisible(domain_tab_index, is_domains)

# the layers tab should only be visible in standard layers
# the layers tab and domain contrasts table should only be visible in standard layers
layers_tab_index = list(self.view_tabs).index("Layers")
is_layers = self.model_combobox.currentText() == LayerModels.StandardLayers
self.project_tab.setTabVisible(layers_tab_index, is_layers)
self.edit_project_tab.setTabVisible(layers_tab_index, is_layers)
self.view_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers)
self.edit_tabs["Domains"].tables["domain_contrasts"].setVisible(is_layers)

def handle_controls_update(self):
"""Handle updates to Controls that need to be reflected in the project."""
Expand Down Expand Up @@ -362,6 +369,8 @@ def __init__(self, fields: list[str], parent, edit_mode: bool = False):
self.tables[field] = ParameterFieldWidget(field, self)
elif field == "layers":
self.tables[field] = LayerFieldWidget(field, self)
elif field == "domain_contrasts":
self.tables[field] = DomainContrastWidget(field, self)
else:
self.tables[field] = ProjectFieldWidget(field, self)
layout.addWidget(self.tables[field])
Expand Down
Loading
Loading