"""Grouped Rubar parameter editor, backed by the existing parameter list.""" from functools import partial from PyQt5.QtCore import QCoreApplication, QEvent, Qt from PyQt5.QtGui import QKeySequence from PyQt5.QtWidgets import ( QComboBox, QFormLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy, QTabWidget, QVBoxLayout, QWidget, ) from View.SolverParameters.RubarFields import ( FIELDS, GROUPS, OPTION_CHOICES, UNSUPPORTED_CHOICES, Field, active, canonical, validate, ) from View.SolverParameters.UndoCommand import SetCommand def tr(text): return QCoreApplication.translate('RubarParameters', text) class RubarForm(QWidget): def __init__(self, parameters, undo, editable=True, parent=None): super().__init__(parent) self.parameters = parameters self.stack = undo self.editable = editable self.editors = {} self.labels = {} self.errors = {} self.descriptions = dict(FIELDS) self.indices = {p.name: i for i, p in enumerate(parameters.parameters)} self._refreshing = False self._options_pending = False layout = QVBoxLayout(self) intro = QLabel(tr('Les valeurs valides sont appliquées à l’étude. ' 'Pensez à enregistrer l’étude pour les conserver.')) intro.setWordWrap(True) layout.addWidget(intro) self.notice = QLabel() self.notice.setWordWrap(True) self.notice.setTextFormat(Qt.PlainText) layout.addWidget(self.notice) self.tabs = QTabWidget() layout.addWidget(self.tabs) forms = [] for group in GROUPS: page = QWidget() form = QFormLayout(page) form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) form.setRowWrapPolicy(QFormLayout.WrapLongRows) form.setVerticalSpacing(12) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setWidget(page) self.tabs.addTab(scroll, tr(group)) forms.append(form) # Metadata order is stable even when stored parameter order changes. keys = [key for key in FIELDS if key in self.indices] keys += [key for key in self.indices if key not in FIELDS] for key in keys: field = self.descriptions.setdefault(key, Field( key, 0, key, 'Paramètre supplémentaire conservé tel quel.', kind='text')) self._add_field(field, forms[field.group]) self.help = QLabel(tr( 'Sélectionnez un réglage pour afficher son aide.')) self.help.setWordWrap(True) self.help.setTextFormat(Qt.PlainText) self.help.setMinimumHeight(60) layout.addWidget(self.help) self.stack.indexChanged.connect(self.refresh) self.refresh() def _add_field(self, field, form): key = field.key container = QWidget() box = QVBoxLayout(container) box.setContentsMargins(0, 0, 0, 0) if field.kind == 'options': editor = QWidget() subform = QFormLayout(editor) subform.setContentsMargins(0, 0, 0, 0) self.option_editors = [] for title, choices, tooltip in OPTION_CHOICES: combo = QComboBox() combo.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) combo.addItem(tr('À renseigner'), None) for value, label in choices.items(): combo.addItem(tr(label), value) combo.currentIndexChanged.connect(self._commit_options) combo.setProperty('parameter', key) combo.installEventFilter(self) combo.setToolTip(tr(tooltip) + '\n' + key) subform.addRow(tr(title), combo) self.option_editors.append(combo) elif field.kind == 'choice': editor = QComboBox() editor.setSizeAdjustPolicy( QComboBox.AdjustToMinimumContentsLengthWithIcon) for value, label in field.choices.items(): editor.addItem(tr(label), value) editor.currentIndexChanged.connect( partial(self._choice_changed, key)) else: editor = QLineEdit() editor.setPlaceholderText(tr('Valeur du solveur') if field.optional else tr('Saisir une valeur')) editor.textEdited.connect(partial(self._check_draft, key)) editor.editingFinished.connect(partial(self._finish_text, key)) editor.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) editor.setObjectName(key) editor.setProperty('parameter', key) editor.installEventFilter(self) editor.setToolTip( tr(field.help) + '\n' + key + '\nNotice §3.1 / §3.10') box.addWidget(editor) error = QLabel() error.setWordWrap(True) error.setTextFormat(Qt.PlainText) error.setStyleSheet('color: #b42318;') box.addWidget(error) label = QLabel(tr(field.label)) label.setWordWrap(True) label.setFixedWidth(310) label.setMinimumHeight(label.heightForWidth(310)) label.setBuddy(editor) label.setToolTip(editor.toolTip()) form.addRow(label, container) self.editors[key] = editor self.labels[key] = label self.errors[key] = error def eventFilter(self, watched, event): if event.type() in (QEvent.FocusIn, QEvent.Enter): key = watched.property('parameter') if key: field = self.descriptions[key] self.help.setText(tr(field.help) + '\n' + key) # Use the model undo stack even inside a line editor. if event.type() in (QEvent.ShortcutOverride, QEvent.KeyPress): if (event.matches(QKeySequence.Undo) or event.matches(QKeySequence.Redo)): if event.type() == QEvent.KeyPress: if event.matches(QKeySequence.Undo): self.undo() else: self.redo() event.accept() return True return super().eventFilter(watched, event) def values(self): return {p.name: str(p.value) for p in self.parameters.parameters} def _error(self, key, message): self.errors[key].setText(message) self.errors[key].setVisible(bool(message)) self.editors[key].setStyleSheet( 'QLineEdit { border: 1px solid #b42318; }' if message else '') def _validated(self, key, value): try: value = validate(self.descriptions[key], value) except ValueError as error: self._error(key, tr(str(error)) + tr(' Valeur non enregistrée.')) return None self._error(key, '') return value def _check_draft(self, key, value): self._validated(key, value) def _finish_text(self, key): editor = self.editors[key] if editor.isModified(): if self.commit(key, editor.text()): editor.setModified(False) def _choice_changed(self, key, index): if not self._refreshing: value = self.editors[key].itemData(index) if value is not None: self.commit(key, value) def _commit_options(self): if self._refreshing: return values = [combo.currentData() for combo in self.option_editors] key = 'rubarbe_sediment_option' if any(value is None for value in values): self._options_pending = True self._error(key, tr( 'Renseignez les trois choix. Valeur non enregistrée.')) else: self._options_pending = False self.commit(key, ''.join(values)) def apply_pending(self): """Commit focused text before Ctrl+S; leave invalid drafts visible.""" values = self.values() if (values.get('rubarbe_condam') == '0' and values.get('rubarbe_regime') == '1'): self.tabs.setCurrentIndex(1) self.editors['rubarbe_condam'].setFocus() return False for key, editor in self.editors.items(): if (isinstance(editor, QLineEdit) and editor.isModified() and active(self.descriptions[key], self.values())): if not self.commit(key, editor.text()): self.tabs.setCurrentIndex(self.descriptions[key].group) editor.setFocus() return False if (self._options_pending and active(FIELDS['rubarbe_sediment_option'], self.values())): self.tabs.setCurrentIndex(2) self.option_editors[0].setFocus() return False return True def commit(self, key, value): field = self.descriptions[key] if not self.editable or not active(field, self.values()): return False value = self._validated(key, value) if value is None: return False values = self.values() values[key] = value if (key in ('rubarbe_condam', 'rubarbe_regime') and values.get('rubarbe_condam') == '0' and values.get('rubarbe_regime') == '1'): self.refresh() return False if isinstance(self.editors[key], QLineEdit): self.editors[key].setModified(False) if self.values()[key] != value: self.stack.push(SetCommand(self.parameters, self.indices[key], 'value', value)) elif isinstance(self.editors[key], QLineEdit): self.editors[key].setText(value) return True def refresh(self, *args): self._refreshing = True values = self.values() for key, editor in self.editors.items(): field = self.descriptions[key] value = canonical(field, values[key]) enabled = active(field, values) editor.setEnabled(self.editable and enabled) self.labels[key].setEnabled(enabled) if isinstance(editor, QLineEdit) and editor.isModified(): self._validated(key, editor.text()) continue if field.kind == 'options' and self._options_pending: self.errors[key].setVisible(enabled) continue if field.kind == 'options': valid = len(value) == 3 and all(c in '12' for c in value) for i, combo in enumerate(self.option_editors): index = combo.findData(value[i]) if valid else 0 combo.setCurrentIndex(index) elif isinstance(editor, QComboBox): # Display unknown historical values without replacing them. for i in reversed(range(editor.count())): if editor.itemData(i) is None: editor.removeItem(i) index = editor.findData(value) if index < 0: editor.addItem(tr('Valeur non reconnue : ') + value, None) index = editor.count() - 1 editor.setCurrentIndex(index) else: editor.setText(values[key]) try: validate(field, values[key]) self._error(key, '') except ValueError as error: self._error(key, tr('Valeur enregistrée à vérifier : ') + values[key] + ' — ' + tr(str(error))) self.errors[key].setVisible( enabled and bool(self.errors[key].text())) self._update_context(values) for key, forbidden, blocked in ( ('rubarbe_regime', '1', values.get('rubarbe_condam') == '0'), ('rubarbe_condam', '0', values.get('rubarbe_regime') == '1'), ): editor = self.editors.get(key) if editor is None: continue index = editor.findData(forbidden) if index >= 0: editor.model().item(index).setEnabled(not blocked) # Unsupported choices stay visible, so a stored value is still # displayed, but cannot be selected anymore for (key, value), reason in UNSUPPORTED_CHOICES.items(): editor = self.editors.get(key) index = editor.findData(value) if editor is not None else -1 if index >= 0: item = editor.model().item(index) item.setEnabled(False) item.setToolTip(tr(reason)) self._refreshing = False def _update_context(self, values): warnings = [] if not self.editable: warnings.append(tr('Étude en lecture seule.')) transport = values.get('rubarbe_trased', '').lower() in ('y', 'o') if not transport: warnings.append(tr('Transport solide désactivé : les réglages ' 'sédimentaires sont conservés mais inactifs.')) elif (values.get('rubarbe_sediment_capsol') in ('2', '3') and values.get('rubarbe_sediment_choixc') == '2'): warnings.append(tr( 'La capacité solide choisie nécessite une méthode MPC ; ' 'la répartition uniforme est incompatible.')) if (values.get('rubarbe_condam') == '0' and values.get('rubarbe_regime') == '1'): warnings.append(tr('Une limite amont réfléchissante ne permet pas ' 'd’imposer aussi le tirant d’eau.')) for (key, value), reason in UNSUPPORTED_CHOICES.items(): if values.get(key) == value: warnings.append(tr(reason) + tr( ' Le calcul sera refusé : choisissez un autre réglage ' 'de la limite amont.')) try: from tools import old_pamhyr_date_to_timestamp def seconds(value): return (old_pamhyr_date_to_timestamp(value) if ':' in value else float(value.replace(',', '.'))) start = seconds(values['rubarbe_tinit']) if seconds(values['rubarbe_tmax']) <= start: warnings.append(tr('La fin du calcul doit suivre son début.')) except (KeyError, ValueError): pass if transport: try: if float(values.get('rubarbe_sediment_visc', '0')) > 0.001: warnings.append(tr( 'Viscosité élevée pour de l’eau : vérifiez la valeur. ' 'Le seuil critique adimensionnel se saisit dans son ' 'propre champ.')) except ValueError: pass if (FIELDS['rubarbe_sediment_tcadim'].key in self.editors and active(FIELDS['rubarbe_sediment_tcadim'], values) and not values.get('rubarbe_sediment_tcadim', '').strip()): warnings.append(tr( 'La loi et les options choisies utilisent un seuil ' 'critique adimensionnel : renseignez-le.')) self.notice.setText('\n'.join(warnings)) self.notice.setVisible(bool(warnings)) def undo(self): if self.editable: self._options_pending = False for editor in self.editors.values(): if isinstance(editor, QLineEdit): editor.setModified(False) self.stack.undo() self.refresh() def redo(self): if self.editable: self._options_pending = False for editor in self.editors.values(): if isinstance(editor, QLineEdit): editor.setModified(False) self.stack.redo() self.refresh()