diff --git a/src/ert/gui/ertwidgets/__init__.py b/src/ert/gui/ertwidgets/__init__.py index e9d3cda9542..3d0c18867b5 100644 --- a/src/ert/gui/ertwidgets/__init__.py +++ b/src/ert/gui/ertwidgets/__init__.py @@ -25,6 +25,7 @@ def wrapper(*arg: Any) -> Any: from .ensembleselector import EnsembleSelector from .checklist import CheckList from .stringbox import StringBox +from .multilinestringbox import MultiLineStringBox from .listeditbox import ListEditBox from .customdialog import CustomDialog from .pathchooser import PathChooser @@ -51,6 +52,7 @@ def wrapper(*arg: Any) -> Any: "EnsembleSelector", "ErtMessageBox", "ListEditBox", + "MultiLineStringBox", "PathChooser", "PathModel", "SearchBox", diff --git a/src/ert/gui/ertwidgets/multilinestringbox.py b/src/ert/gui/ertwidgets/multilinestringbox.py new file mode 100644 index 00000000000..306b945638f --- /dev/null +++ b/src/ert/gui/ertwidgets/multilinestringbox.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional + +from qtpy.QtGui import QPalette +from qtpy.QtWidgets import QTextEdit + +from .validationsupport import ValidationSupport + +if TYPE_CHECKING: + from ert.validation import ArgumentDefinition + + from .models import TextModel + + +class MultiLineStringBox(QTextEdit): + """MultiLineStringBox shows a multiline string. The data structure expected and sent to the + getter and setter is a multiline string.""" + + def __init__( + self, + model: TextModel, + default_string: str = "", + placeholder_text: str = "", + minimum_width: int = 250, + readonly: bool = False, + ): + QTextEdit.__init__(self) + self.setMinimumWidth(minimum_width) + self._validation = ValidationSupport(self) + self._validator: Optional[ArgumentDefinition] = None + self._model = model + self._enable_validation = True + + if placeholder_text: + self.setPlaceholderText(placeholder_text) + self.textChanged.connect(self.stringBoxChanged) + + self.textChanged.connect(self.validateString) + + self._valid_color = self.palette().color(self.backgroundRole()) + self.setText(default_string) + + self._model.valueChanged.connect(self.modelChanged) + self.modelChanged() + self.setReadOnly(readonly) + + def validateString(self) -> None: + if not self._enable_validation or self._validator is None: + return + + string_to_validate = self.toPlainText() + if not string_to_validate and self.placeholderText(): + string_to_validate = self.placeholderText() + + validation_success = self._validator.validate(string_to_validate) + + palette = self.palette() + if not validation_success: + palette.setColor(QPalette.ColorRole.Base, ValidationSupport.ERROR_COLOR) + self.setPalette(palette) + self._validation.setValidationMessage( + str(validation_success), ValidationSupport.EXCLAMATION + ) + else: + palette.setColor(QPalette.ColorRole.Base, self._valid_color) + self.setPalette(palette) + self._validation.setValidationMessage("") + + def emitChange(self, q_string: Any) -> None: + self.textChanged.emit(str(q_string)) + + def stringBoxChanged(self) -> None: + """Called whenever the contents of the textedit changes.""" + text: Optional[str] = self.get_text + if not text: + text = None + + self._model.setValue(text) + + def modelChanged(self) -> None: + """Retrieves data from the model and inserts it into the textedit""" + text = self._model.getValue() + if text is None: + text = "" + # If model and view has same text, return + if text == self.toPlainText(): + return + self.setText(str(text)) + + @property + def model(self) -> TextModel: + return self._model + + def setValidator(self, validator: ArgumentDefinition) -> None: + self._validator = validator + + def getValidationSupport(self) -> ValidationSupport: + return self._validation + + def isValid(self) -> bool: + return self._validation.isValid() + + @property + def get_text(self) -> str: + return self.toPlainText() if self.toPlainText() else self.placeholderText() + + def enable_validation(self, enabled: bool) -> None: + self._enable_validation = enabled + + def refresh(self) -> None: + self.validateString() diff --git a/src/ert/gui/tools/load_results/load_results_panel.py b/src/ert/gui/tools/load_results/load_results_panel.py index 04e6313acd2..5612362edff 100644 --- a/src/ert/gui/tools/load_results/load_results_panel.py +++ b/src/ert/gui/tools/load_results/load_results_panel.py @@ -1,20 +1,21 @@ from __future__ import annotations from qtpy.QtCore import Qt, Signal -from qtpy.QtWidgets import QFormLayout, QMessageBox, QTextEdit, QWidget +from qtpy.QtWidgets import QFormLayout, QMessageBox, QWidget from ert.gui.ertnotifier import ErtNotifier from ert.gui.ertwidgets import ( ActiveRealizationsModel, EnsembleSelector, ErtMessageBox, + MultiLineStringBox, QApplication, StringBox, ValueModel, ) from ert.libres_facade import LibresFacade from ert.run_models.base_run_model import captured_logs -from ert.validation import IntegerArgument, RangeStringArgument +from ert.validation import IntegerArgument, RangeStringArgument, RunPathArgument class LoadResultsPanel(QWidget): @@ -34,13 +35,18 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier): layout = QFormLayout() - run_path_text = QTextEdit() - run_path_text.setText(self.readCurrentRunPath()) - run_path_text.setDisabled(True) - run_path_text.setFixedHeight(80) - - layout.addRow("Load data from current run path: ", run_path_text) + self.run_path_text_model = ValueModel() + self._run_path_field = MultiLineStringBox( + self.run_path_text_model, # type: ignore + default_string="", + readonly=True, + ) + self._run_path_field.setValidator(RunPathArgument()) + self._run_path_field.setObjectName("run_path_field_lrm") + self._run_path_field.setFixedHeight(80) + self._run_path_field.setText(self.readCurrentRunPath()) + layout.addRow("Load data from current run path: ", self._run_path_field) ensemble_selector = EnsembleSelector(self._notifier) layout.addRow("Load into ensemble:", ensemble_selector) self._ensemble_selector = ensemble_selector @@ -66,7 +72,9 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier): self._iterations_field.setValidator(IntegerArgument(from_value=0)) self._iterations_field.setObjectName("iterations_field_lrm") layout.addRow("Iteration to load:", self._iterations_field) - + self._run_path_field.getValidationSupport().validationChanged.connect( + self.panelConfigurationChanged + ) self._active_realizations_field.getValidationSupport().validationChanged.connect( self.panelConfigurationChanged ) @@ -76,6 +84,9 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier): self.setLayout(layout) + def refresh(self) -> None: + self._run_path_field.refresh() + def readCurrentRunPath(self) -> str: current_ensemble = self._notifier.current_ensemble_name run_path = self._facade.run_path @@ -85,7 +96,8 @@ def readCurrentRunPath(self) -> str: def isConfigurationValid(self) -> bool: return ( - self._active_realizations_field.isValid() + self._run_path_field.isValid() + and self._active_realizations_field.isValid() and self._iterations_field.isValid() ) diff --git a/src/ert/gui/tools/load_results/load_results_tool.py b/src/ert/gui/tools/load_results/load_results_tool.py index c2e8d21b5c1..da514a0163c 100644 --- a/src/ert/gui/tools/load_results/load_results_tool.py +++ b/src/ert/gui/tools/load_results/load_results_tool.py @@ -23,19 +23,24 @@ def __init__(self, facade: LibresFacade, notifier: ErtNotifier) -> None: def trigger(self) -> None: if self._import_widget is None: self._import_widget = LoadResultsPanel(self.facade, self._notifier) - self._dialog = ClosableDialog( - "Load results manually", - self._import_widget, - self.parent(), # type: ignore - ) - self._dialog.setObjectName("load_results_manually_tool") - loadButton = self._dialog.addButton("Load", self.load) + self._import_widget.panelConfigurationChanged.connect( + self.validationStatusChanged + ) + self._dialog = ClosableDialog( + "Load results manually", + self._import_widget, + self.parent(), # type: ignore + ) + self._loadButton = self._dialog.addButton("Load", self.load) + self._dialog.setObjectName("load_results_manually_tool") + + else: + self._import_widget.refresh() + if not self._import_widget._ensemble_selector.isEnabled(): - loadButton.setEnabled(False) - loadButton.setToolTip("Must load into a ensemble") - self._import_widget.panelConfigurationChanged.connect( - self.validationStatusChanged - ) + self._loadButton.setEnabled(False) + self._loadButton.setToolTip("Must load into a ensemble") + assert self._dialog is not None self._dialog.exec_() def load(self, _: Any) -> None: diff --git a/src/ert/validation/__init__.py b/src/ert/validation/__init__.py index c6d3d779cf5..b503bea56cc 100644 --- a/src/ert/validation/__init__.py +++ b/src/ert/validation/__init__.py @@ -6,6 +6,7 @@ from .proper_name_format_argument import ProperNameFormatArgument from .range_string_argument import RangeStringArgument from .rangestring import mask_to_rangestring, rangestring_to_list, rangestring_to_mask +from .runpath_argument import RunPathArgument from .validation_status import ValidationStatus __all__ = [ @@ -16,6 +17,7 @@ "ProperNameArgument", "ProperNameFormatArgument", "RangeStringArgument", + "RunPathArgument", "ValidationStatus", "mask_to_rangestring", "rangestring_to_list", diff --git a/src/ert/validation/runpath_argument.py b/src/ert/validation/runpath_argument.py new file mode 100644 index 00000000000..366dca9a37b --- /dev/null +++ b/src/ert/validation/runpath_argument.py @@ -0,0 +1,27 @@ +import os + +from .argument_definition import ArgumentDefinition +from .validation_status import ValidationStatus + + +class RunPathArgument(ArgumentDefinition): + INVALID_PATH = "The specified runpath does not exist." + MISSING_PERMISSION = "You are missing permissions for the specified runpath." + + def __init__(self, **kwargs: bool) -> None: + super().__init__(**kwargs) + + def validate(self, token: str) -> ValidationStatus: + parsed_runpath_without_suffix = "/".join(token.split("/")[:-2]) + validation_status = super().validate(token) + + if not os.path.isdir(parsed_runpath_without_suffix): + validation_status.setFailed() + validation_status.addToMessage(RunPathArgument.INVALID_PATH) + elif not os.access(parsed_runpath_without_suffix, os.R_OK | os.X_OK): + validation_status.setFailed() + validation_status.addToMessage(RunPathArgument.MISSING_PERMISSION) + else: + validation_status.setValue(token) + + return validation_status