Files
Pamhyr2/src/View/SolverParameters/RubarForm.py
T

377 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()