Boundary Conditions: interface for temperature's boundary conditions

temperature
Dylan Jeannin 2026-06-26 19:02:49 +02:00
parent 95dd648b90
commit 2cbfaec0c2
16 changed files with 2095 additions and 1 deletions

View File

@ -0,0 +1,478 @@
# BoundaryConditionsTemperature.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import (
trace, timer,
old_pamhyr_date_to_timestamp,
date_iso_to_timestamp,
date_dmy_to_timestamp,
)
from Model.Tools.PamhyrDB import SQLSubModel
from Model.Except import NotImplementedMethodeError
from Model.Scenario import Scenario
logger = logging.getLogger()
class Data(SQLSubModel):
_sub_classes = []
def __init__(self,
data0, data1,
id: int = -1,
types=[float, float],
status=None,
owner_scenario=-1):
super(Data, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._types = types
self._data = [data0, data1]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_data_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
data0 TEXT NOT NULL,
data1 TEXT NOT NULL,
bca INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(bca)
REFERENCES boundary_condition_temperature(pamhyr_id)
)
""")
return True
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and minor == "0":
if int(release) < 11:
cls._db_create(execute)
created = True
if major == "0" and (int(minor) < 2 or
(int(minor) == 2 and int(release) < 7)):
if not cls.is_table_exists(execute,
"boundary_condition_data_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
bca = data["bca"]
table = execute(
"SELECT pamhyr_id, deleted, data0, data1, scenario FROM " +
"boundary_condition_data_temperature " +
f"WHERE scenario = {scenario.id} " +
# f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))}) " +
f"AND bca = {bca.id}"
)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
data0 = next(it)
data1 = next(it)
owner_scenario = next(it)
bc = cls(
data0, data1,
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
bca = data["bca"]
data0 = self._db_format(self._data[0])
data1 = self._db_format(self._data[1])
execute(
"INSERT INTO " +
"boundary_condition_data_temperature(" +
"pamhyr_id, deleted, data0, data1, bca, scenario) " +
f"VALUES ({self.id}, {self._db_format(self.is_deleted())}, " +
f"'{data0}', {data1}, {bca.id}, " +
f"{self._status.scenario_id})"
)
return True
def __getitem__(self, key):
return self._types[key](self._data[key])
def __setitem__(self, key, value):
self._data[key] = self._types[key](value)
class BoundaryConditionTemperature(SQLSubModel):
_sub_classes = [Data]
def __init__(self, id: int = -1, status=None, owner_scenario=-1):
super(BoundaryConditionTemperature, self).__init__(
id=id, status=status,
owner_scenario=owner_scenario
)
self._status = status
self._name = ""
self._node = None
self._data = []
self._header = []
self._types = [self.time_convert, float]
@classmethod
def _db_create(cls, execute, ext=""):
execute(f"""
CREATE TABLE boundary_condition_temperature{ext}(
{cls.create_db_add_pamhyr_id()},
deleted BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
node INTEGER,
{Scenario.create_db_add_scenario()},
{Scenario.create_db_add_scenario_fk()},
FOREIGN KEY(node) REFERENCES river_node(pamhyr_id)
)
""")
if ext != "":
return True
return cls._create_submodel(execute)
@classmethod
def _db_update(cls, execute, version, data=None):
major, minor, release = version.strip().split(".")
created = False
if major == "0" and (int(minor) < 2 or (
int(minor) == 2 and int(release) < 7)):
if not cls.is_table_exists(execute,
"boundary_condition_temperature"):
cls._db_create(execute)
created = True
if not created:
return cls._update_submodel(execute, version, data)
return True
@classmethod
def _db_load(cls, execute, data=None):
new = []
status = data['status']
nodes = data['nodes']
scenario = data["scenario"]
loaded = data['loaded_pid']
if scenario is None:
return new
sql = (
"SELECT pamhyr_id, deleted, name, node, scenario "
"FROM boundary_condition_temperature " +
f"WHERE scenario = {scenario.id} "
)
table = execute(sql)
if table is not None:
for row in table:
it = iter(row)
pid = next(it)
deleted = (next(it) == 1)
bc_name = next(it)
node = next(it)
owner_scenario = next(it)
bc = cls(
id=pid,
status=status,
owner_scenario=owner_scenario
)
if deleted:
bc.set_as_deleted()
bc.name = bc_name
bc.node = None
if node != -1:
tmp = next(
filter(
lambda n: n.id == node, nodes
),
None
)
if tmp is not None:
bc.node = tmp.id
else:
bc.node = -1
data["bca"] = bc
bc._data = Data._db_load(execute, data=data)
loaded.add(pid)
new.append(bc)
data["scenario"] = scenario.parent
new += cls._db_load(execute, data)
data["scenario"] = scenario
return new
def _db_save(self, execute, data=None):
if not self.must_be_saved():
return True
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE pamhyr_id = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE bca = {self.id} " +
f"AND scenario = {self._status.scenario_id} "
)
node = -1
if self._node is not None:
node = self._node
execute(
"INSERT INTO " +
"boundary_condition_temperature(" +
"pamhyr_id, deleted, name, " +
"node, scenario) " +
"VALUES (" +
f"{self.id}, {self._db_format(self.is_deleted())}, " +
f"'{self._db_format(self._name)}', {node}, " +
f"{self._status.scenario_id}" +
")"
)
data["bca"] = self
ind = 0
for d in self._data:
data["ind"] = ind
d._db_save(execute, data)
ind += 1
return True
def _data_traversal(self,
predicate=lambda obj, data: True,
modifier=lambda obj, data: None,
data={}):
if predicate(self, data):
modifier(self, data)
for d in self._data:
d._data_traversal(predicate, modifier, data)
def __len__(self):
return len(
list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
)
@classmethod
def time_convert(cls, data):
if type(data) is str:
if data.count("-") == 2:
return date_iso_to_timestamp(data)
if data.count("/") == 2:
return date_dmy_to_timestamp(data)
if data.count(":") == 3:
return old_pamhyr_date_to_timestamp(data)
if data.count(":") == 2:
return old_pamhyr_date_to_timestamp("00:" + data)
if data.count(".") == 1:
return round(float(data))
return int(data)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self.modified()
@property
def node(self):
return self._node
@node.setter
def node(self, node):
self._node = node
self.modified()
@property
def header(self):
return self._header.copy()
@header.setter
def header(self, header):
self._header = header
self.modified()
@property
def data(self):
return list(
filter(
lambda el: el is not None and not el.is_deleted(),
self._data
)
)
@property
def _default_0(self):
return self._types[0](0)
@property
def _default_1(self):
return self._types[1](0.0)
def new_from_data(self, header, data):
new_0 = self._default_0
new_1 = self._default_1
if len(header) != 0:
for i in [0, 1]:
for j in range(len(header)):
if self._header[i] == header[j]:
if i == 0:
new_0 = self._types[i](data[j].replace(",", "."))
else:
new_1 = self._types[i](data[j].replace(",", "."))
else:
new_0 = self._types[0](data[0].replace(",", "."))
new_1 = self._types[1](data[1].replace(",", "."))
return (new_0, new_1)
def add(self, index: int):
value = Data(
self._default_0, self._default_1,
status=self._status
)
self._data.insert(index, value)
self.modified()
return value
def insert(self, index: int, value):
self._data.insert(index, value)
self.modified()
def delete_i(self, indexes):
list(
map(
lambda e: e[1].set_as_deleted(),
filter(
lambda e: e[0] in indexes,
enumerate(self.data)
)
)
)
self.modified()
def sort(self, _reverse=False, key=None):
if key is None:
self._data.sort(reverse=_reverse)
else:
self._data.sort(reverse=_reverse, key=key)
self.modified()
def index(self, bc):
self._data.index(bc)
def get_i(self, index):
return self.data[index]
def get_range(self, _range):
lst = []
for r in _range:
lst.append(r)
return lst
def _set_i_c_v(self, index, column, value):
v = self._data[index]
v[column] = self._types[column](value)
self._data[index] = v
self.modified()
def set_i_0(self, index: int, value):
self._set_i_c_v(index, 0, value)
def set_i_1(self, index: int, value):
self._set_i_c_v(index, 1, value)

View File

@ -0,0 +1,71 @@
# BoundaryConditionsTemperatureList.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import copy
from tools import trace, timer
from Model.Tools.PamhyrListExt import PamhyrModelList
from Model.Except import NotImplementedMethodeError
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
class BoundaryConditionsTemperatureList(PamhyrModelList):
_sub_classes = [
BoundaryConditionTemperature,
]
@classmethod
def _db_load(cls, execute, data=None):
new = cls(status=data['status'])
if data is None:
data = {}
new._lst = BoundaryConditionTemperature._db_load(execute, data)
return new
def _db_save(self, execute, data=None):
execute(
"DELETE FROM boundary_condition_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
execute(
"DELETE FROM boundary_condition_data_temperature " +
f"WHERE scenario = {self._status.scenario_id}"
)
if data is None:
data = {}
for bca in self._lst:
bca._db_save(execute, data=data)
return True
def new(self, index):
n = BoundaryConditionTemperature(status=self._status)
self._lst.insert(index, n)
self._status.modified()
return n
@property
def BCs_Temperature_List(self):
return self.lst

View File

@ -63,6 +63,9 @@ from Model.DIFAdisTS.DIFAdisTSList import DIFAdisTSList
from Model.InitialConditionsTemperature.InitialConditionsTemperatureList \
import InitialConditionsTemperatureList
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
from Model.GeoTIFF.GeoTIFFList import GeoTIFFList
from Model.Results.Results import Results
@ -480,6 +483,7 @@ class River(Graph):
D90AdisTSList,
DIFAdisTSList,
InitialConditionsTemperatureList,
BoundaryConditionsTemperatureList,
GeoTIFFList,
Results
]
@ -519,6 +523,9 @@ class River(Graph):
self._DIFAdisTS = DIFAdisTSList(status=self._status)
self._InitialConditionsTemperature = InitialConditionsTemperatureList(
status=self._status)
self._BoundaryConditionsTemperature = (
BoundaryConditionsTemperatureList(
status=self._status))
self._geotiff = GeoTIFFList(status=self._status)
@ -637,6 +644,9 @@ class River(Graph):
new._InitialConditionsTemperature = \
InitialConditionsTemperatureList._db_load(execute, data)
new._BoundaryConditionsTemperature = \
BoundaryConditionsTemperatureList._db_load(execute, data)
new._geotiff = GeoTIFFList._db_load(execute, data)
return new
@ -671,7 +681,9 @@ class River(Graph):
objs.append(self._LateralContributionsAdisTS)
objs.append(self._D90AdisTS)
objs.append(self._DIFAdisTS)
objs.append(self._InitialConditionsTemperature)
objs.append(self._BoundaryConditionsTemperature)
objs.append(self._geotiff)
@ -752,6 +764,7 @@ class River(Graph):
self._LateralContributionsAdisTS,
self._D90AdisTS, self._DIFAdisTS,
self._InitialConditionsTemperature,
self._BoundaryConditionsTemperature,
self._geotiff,
]
@ -889,6 +902,10 @@ Last export at: @date."""
def ic_temperature(self):
return self._InitialConditionsTemperature
@property
def boundary_conditions_temperature(self):
return self._BoundaryConditionsTemperature
def get_params(self, solver):
if solver in self._parameters:
return self._parameters[solver]

View File

@ -0,0 +1,105 @@
# Plot.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from datetime import datetime
from tools import timer, trace
from View.Tools.PamhyrPlot import PamhyrPlot
from PyQt5.QtCore import (
QCoreApplication
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class Plot(PamhyrPlot):
def __init__(self, mode="time", data=None,
trad=None, canvas=None, toolbar=None,
parent=None):
super(Plot, self).__init__(
canvas=canvas,
trad=trad,
data=data,
toolbar=toolbar,
parent=parent
)
self._table_headers = self._trad.get_dict("table_headers")
header = self.data.header
self.label_x = self._table_headers[header[0]]
self.label_y = self._table_headers[header[1]]
self._mode = mode
self._isometric_axis = False
self._auto_relim_update = True
self._autoscale_update = False
def custom_ticks(self):
if self.data.header[0] != "time":
return
self.set_ticks_time_formater()
@timer
def draw(self):
self.init_axes()
if len(self.data) == 0:
self._init = False
return
self.draw_data()
self.custom_ticks()
self.idle()
self._init = True
def draw_data(self):
# Plot data
x = list(map(lambda v: v[0], self.data.data))
y = list(map(lambda v: v[1], self.data.data))
self._line, = self.canvas.axes.plot(
x, y,
color=self.color_plot,
**self.plot_default_kargs
)
@timer
def update(self, ind=None):
if not self._init:
self.draw()
return
self.update_data()
self.update_idle()
def update_data(self):
x = list(map(lambda v: v[0], self.data.data))
y = list(map(lambda v: v[1], self.data.data))
self._line.set_data(x, y)

View File

@ -0,0 +1,185 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from datetime import date, time, datetime, timedelta
from tools import (
trace, timer,
timestamp_to_old_pamhyr_date,
old_pamhyr_date_to_timestamp
)
from View.Tools.PamhyrTable import PamhyrTableModel
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect, QTime, QDateTime,
)
from PyQt5.QtWidgets import (
QTableView, QAbstractItemView, QSpinBox,
QTimeEdit, QDateTimeEdit, QItemDelegate,
)
from View.BoundaryConditionsTemperature.Edit.UndoCommand import (
AddCommand, DelCommand, SetDataCommand, PasteCommand, SortCommand,
)
_translate = QCoreApplication.translate
logger = logging.getLogger()
class TableModel(PamhyrTableModel):
def get_true_data_row(self, row):
if len(self._data.data) > 0:
bc = self._data.data[row]
else:
return 0
return next(
map(
lambda e: e[0],
filter(
lambda e: e[1] == bc,
enumerate(self._data._data)
)
), 0
)
def data(self, index, role):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter | Qt.AlignVCenter
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
value = QVariant()
if 0 <= column < 2:
row = self.get_true_data_row(row)
v = self._data._data[row][column]
if self._data._types[column] == float:
value = f"{v:.2f}"
elif self._data.header[column] == "time":
if self._opt_data == "time":
value = timestamp_to_old_pamhyr_date(int(v))
else:
value = str(datetime.fromtimestamp(v))
else:
value = f"{v}"
return value
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
row = self.get_true_data_row(row)
self._undo.push(
SetDataCommand(
self._data, row, column, value
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
self.beginInsertRows(parent, row, row - 1)
self._undo.push(
AddCommand(
self._data, row
)
)
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
_rows = list(
map(
lambda r: self.get_true_data_row(r),
rows
)
)
self._undo.push(
DelCommand(
self._data, _rows
)
)
self.endRemoveRows()
self.layoutChanged.emit()
def sort(self, _reverse, parent=QModelIndex()):
self.layoutAboutToBeChanged.emit()
self._undo.push(
SortCommand(
self._data, _reverse
)
)
self.layoutAboutToBeChanged.emit()
self.update()
def paste(self, row, header, data):
if len(data) == 0:
return
self.layoutAboutToBeChanged.emit()
self._undo.push(
PasteCommand(
self._data, row,
list(
map(
lambda d: self._data.new_from_data(header, d),
data
)
)
)
)
self.layoutAboutToBeChanged.emit()
self.update()
def update(self):
# self.auto_sort()
self.layoutChanged.emit()

View File

@ -0,0 +1,137 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
logger = logging.getLogger()
class SetDataCommand(QUndoCommand):
def __init__(self, data, index, column, new_value):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._column = column
self._old = self._data.get_i(self._index)[self._column]
self._new = new_value
def undo(self):
self._data._set_i_c_v(self._index, self._column, self._old)
def redo(self):
self._data._set_i_c_v(self._index, self._column, self._new)
class AddCommand(QUndoCommand):
def __init__(self, data, index):
QUndoCommand.__init__(self)
self._data = data
self._index = index
self._new = None
def undo(self):
del self._data._data[self._index]
def redo(self):
if self._new is None:
self._new = self._data.add(self._index)
else:
self._data.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, data, rows):
QUndoCommand.__init__(self)
self._data = data
self._rows = rows
self._bc = []
for row in rows:
self._bc.append((row, self._data._data[row]))
self._bc.sort()
def undo(self):
for row, el in self._bc:
el.set_as_not_deleted()
def redo(self):
for row, el in self._bc:
el.set_as_deleted()
class PasteCommand(QUndoCommand):
def __init__(self, data, row, bcs):
QUndoCommand.__init__(self)
self._data = data
self._row = row
self._bcs = bcs
self._bcs.reverse()
def undo(self):
self._data.delete_i(
range(self._row, self._row + len(self._bcs))
)
def redo(self):
for bc in self._bcs:
self._data.insert(self._row, bc)
class SortCommand(QUndoCommand):
def __init__(self, data, _reverse):
QUndoCommand.__init__(self)
self._data = data
self._reverse = _reverse
self._old = self._data.data
self._indexes = None
def undo(self):
ll = self._data.data
self._data.sort(
key=lambda x: self._indexes[ll.index(x)]
)
def redo(self):
self._data.sort(
_reverse=self._reverse,
key=lambda x: x[0]
)
if self._indexes is None:
self._indexes = list(
map(
lambda p: self._old.index(p),
self._data.data
)
)
self._old = None

View File

@ -0,0 +1,229 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import timer, trace
from View.Tools.PamhyrWindow import PamhyrWindow
from View.Tools.PamhyrWidget import PamhyrWidget
from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5 import QtCore
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel, QCoreApplication,
pyqtSlot, pyqtSignal,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QHeaderView, QDoubleSpinBox, QVBoxLayout,
)
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.BoundaryConditionsTemperature.Edit.translate import BCETranslate
from View.BoundaryConditionsTemperature.Edit.Table import TableModel
from View.BoundaryConditionsTemperature.Edit.Plot import Plot
_translate = QCoreApplication.translate
logger = logging.getLogger()
class EditBoundaryConditionWindow(PamhyrWindow):
_pamhyr_ui = "EditBoundaryConditionsTemperature"
_pamhyr_name = "Edit Boundary Conditions Temperature"
def __init__(self, data=None, study=None, config=None, parent=None):
self._data = data
trad = BCETranslate()
name = trad[self._pamhyr_name]
if self._data is not None:
n = self._data.node
node_name = next(filter(
lambda x: x.id == n, study.river._nodes
)).name
name += (
f" - {study.name} " +
f"({node_name})"
)
super(EditBoundaryConditionWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._hash_data.append(data)
self.setup_table()
self.setup_plot()
self.setup_connections()
def setup_table(self):
table_headers = self._trad.get_dict("table_headers")
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
self._data.header = ["time", "temperature"]
headers = {}
for h in self._data.header:
headers[h] = table_headers[h]
self._delegate_time = PamhyrExTimeDelegate(
data=self._data,
mode=self._study.time_system,
parent=self
)
table = self.find(QTableView, "tableView")
self._table = TableModel(
table_view=table,
table_headers=headers,
editable_headers=editable_headers,
delegates={
# "time": self._delegate_time,
},
data=self._data,
undo=self._undo_stack,
opt_data=self._study.time_system
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_plot(self):
self.canvas = MplCanvas(width=5, height=4, dpi=100)
self.canvas.setObjectName("canvas")
self.toolbar = PamhyrPlotToolbar(
self.canvas, self
)
self.verticalLayout.addWidget(self.toolbar)
self.verticalLayout.addWidget(self.canvas)
self.plot = Plot(
canvas=self.canvas,
data=self._data,
mode=self._study.time_system,
trad=self._trad,
toolbar=self.toolbar,
)
self.plot.draw()
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_sort").triggered.connect(self.sort)
self._table.dataChanged.connect(self.update)
def update(self):
self.plot.update()
def index_selected_row(self):
table = self.find(QTableView, "tableView")
return table.selectionModel()\
.selectedRows()[0]\
.row()
def index_selected_rows(self):
table = self.find(QTableView, "tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
rows = self.index_selected_rows()
if len(self._data) == 0 or len(rows) == 0:
self._table.add(0)
else:
self._table.add(rows[0])
self.plot.update()
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
self.plot.update()
def sort(self):
self._table.sort(False)
self.plot.update()
def _copy(self):
rows = self.index_selected_rows()
table = []
table.append(self._data.header)
data = self._data.data
for row in rows:
table.append(list(data[row]))
self.copyTableIntoClipboard(table)
def _paste(self):
header, data = self.parseClipboardTable()
logger.debug(f"paste: h:{header}, d:{data}")
if len(data) == 0:
return
row = 0
rows = self.index_selected_rows()
if len(rows) != 0:
row = rows[0]
self._table.paste(row, header, data)
self.plot.update()
def _undo(self):
self._table.undo()
self.plot.update()
def _redo(self):
self._table.redo()
self.plot.update()

View File

@ -0,0 +1,40 @@
# translate.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
from View.BoundaryCondition.translate import BCTranslate
_translate = QCoreApplication.translate
class BCETranslate(BCTranslate):
def __init__(self):
super(BCETranslate, self).__init__()
self._dict["Edit Boundary Conditions Temperature"] = _translate(
"BoundaryConditionsTemperature",
"Edit boundary conditions Temperature"
)
self._sub_dict["table_headers"] = {
"time": self._dict["time"],
"temperature": self._dict["unit_temperature"],
}

View File

@ -0,0 +1,219 @@
# Table.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
import traceback
from tools import trace, timer
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox,
)
from View.Tools.PamhyrTable import PamhyrTableModel
from View.BoundaryConditionsTemperature.UndoCommand import (
SetNodeCommand, SetNameCommand,
AddCommand, DelCommand
)
logger = logging.getLogger()
_translate = QCoreApplication.translate
class ComboBoxDelegate(QItemDelegate):
def __init__(self, data=None, mode="node", tab="",
trad=None, parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self._data = data
self._mode = mode
self._tab = tab
self._trad = trad
def createEditor(self, parent, option, index):
self.editor = QComboBox(parent)
if self._mode == "node":
self.editor.addItems(
[self._trad["not_associated"]] +
self._data.nodes_names()
)
self.editor.setCurrentText(index.data(Qt.DisplayRole))
return self.editor
def setEditorData(self, editor, index):
value = index.data(Qt.DisplayRole)
self.editor.currentTextChanged.connect(self.currentItemChanged)
def setModelData(self, editor, model, index):
text = str(editor.currentText())
model.setData(index, text)
editor.close()
editor.deleteLater()
def updateEditorGeometry(self, editor, option, index):
r = QRect(option.rect)
if self.editor.windowFlags() & Qt.Popup:
if editor.parent() is not None:
r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft()))
editor.setGeometry(r)
@pyqtSlot()
def currentItemChanged(self):
self.commitData.emit(self.sender())
class TableModel(PamhyrTableModel):
def __init__(self, bc_list=None, trad=None, **kwargs):
self._trad = trad
self._bc_list = bc_list
super(TableModel, self).__init__(trad=trad, **kwargs)
def _setup_lst(self):
self._lst = self._bc_list.lst
def get(self, row):
if row < 0 or row >= len(self._lst):
return None
return self._lst[row]
def _global_row(self, row):
bc = self.get(row)
if bc is None:
return None
return self._bc_list.index(bc)
def rowCount(self, parent=QModelIndex()):
return len(self._lst)
def data(self, index, role):
if role != Qt.ItemDataRole.DisplayRole:
return QVariant()
row = index.row()
column = index.column()
if self._headers[column] == "name":
n = self._lst[row].name
if n is None or n == "":
return self._trad["not_associated"]
return n
elif self._headers[column] == "node":
n = self._lst[row].node
if n is None:
return self._trad["not_associated"]
tmp = next(filter(lambda x: x.id == n, self._data._nodes), None)
if tmp is not None:
return tmp.name
else:
return self._trad["not_associated"]
return QVariant()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
if self.is_same_data(index, value):
return False
row = index.row()
column = index.column()
try:
if self._headers[column] == "name":
global_row = self._global_row(row)
self._undo.push(
SetNameCommand(
self._bc_list, global_row, value
)
)
elif self._headers[column] == "node":
global_row = self._global_row(row)
node = next(
filter(lambda n: n.name == value, self._data.nodes()),
None
)
self._undo.push(
SetNodeCommand(
self._bc_list, global_row, node
)
)
except Exception as e:
logger.info(e)
logger.debug(traceback.format_exc())
self.dataChanged.emit(index, index)
return True
def add(self, row, parent=QModelIndex()):
row = len(self._lst)
self.beginInsertRows(parent, row, row)
self._undo.push(
AddCommand(
self._bc_list, len(self._bc_list)
)
)
self._setup_lst()
self.endInsertRows()
self.layoutChanged.emit()
def delete(self, rows, parent=QModelIndex()):
self.beginRemoveRows(parent, rows[0], rows[-1])
row_by_bc = {
id(bc): row for row, bc in enumerate(self._bc_list._lst)
}
global_rows = [
row_by_bc[id(self._lst[row])]
for row in rows
if 0 <= row < len(self._lst)
]
self._undo.push(
DelCommand(
self._bc_list, global_rows
)
)
self._setup_lst()
self.endRemoveRows()
self.layoutChanged.emit()
def undo(self):
self._undo.undo()
self._setup_lst()
self.layoutChanged.emit()
def redo(self):
self._undo.redo()
self._setup_lst()
self.layoutChanged.emit()

View File

@ -0,0 +1,100 @@
# UndoCommand.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from copy import deepcopy
from tools import trace, timer
from PyQt5.QtWidgets import (
QMessageBox, QUndoCommand, QUndoStack,
)
from Model.BoundaryConditionsTemperature.BoundaryConditionTemperature \
import BoundaryConditionTemperature
from Model.BoundaryConditionsTemperature.BoundaryConditionsTemperatureList \
import BoundaryConditionsTemperatureList
class SetNodeCommand(QUndoCommand):
def __init__(self, bcs, index, node):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._old = self._bcs.get(self._index).node
self._new = node.id if node is not None else None
def undo(self):
self._bcs.get(self._index).node = self._old
def redo(self):
self._bcs.get(self._index).node = self._new
class SetNameCommand(QUndoCommand):
def __init__(self, bcs, index, name):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._name = name
self._old = self._bcs.get(self._index).name
self._new = self._name
def undo(self):
self._bcs.get(self._index).name = self._old
def redo(self):
self._bcs.get(self._index).name = self._new
class AddCommand(QUndoCommand):
def __init__(self, bcs, index):
QUndoCommand.__init__(self)
self._bcs = bcs
self._index = index
self._new = None
def undo(self):
self._bcs.delete_i([self._index])
def redo(self):
if self._new is None:
self._new = self._bcs.new(self._index)
else:
self._bcs.insert(self._index, self._new)
class DelCommand(QUndoCommand):
def __init__(self, bcs, rows):
QUndoCommand.__init__(self)
self._bcs = bcs
self._rows = rows
self._bc = []
for row in rows:
self._bc.append((row, self._bcs._lst[row]))
self._bc.sort()
def undo(self):
for row, el in self._bc:
self._bcs.insert(row, el)
def redo(self):
self._bcs.delete_i(self._rows)

View File

@ -0,0 +1,192 @@
# Window.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import logging
from tools import trace, timer, logger_exception
from View.Tools.PamhyrWindow import PamhyrWindow
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5.QtCore import (
Qt, QVariant, QAbstractTableModel,
QCoreApplication, QModelIndex, pyqtSlot,
QRect,
)
from PyQt5.QtWidgets import (
QDialogButtonBox, QPushButton, QLineEdit,
QFileDialog, QTableView, QAbstractItemView,
QUndoStack, QShortcut, QAction, QItemDelegate,
QComboBox, QVBoxLayout, QHeaderView, QTabWidget,
QWidget,
)
from View.BoundaryConditionsTemperature.Table import (
TableModel, ComboBoxDelegate
)
from View.Network.GraphWidget import GraphWidget
from View.BoundaryConditionsTemperature.translate import BCTemperatureTranslate
from View.BoundaryConditionsTemperature.Edit.Window \
import EditBoundaryConditionWindow
_translate = QCoreApplication.translate
logger = logging.getLogger()
class BoundaryConditionsTemperatureWindow(PamhyrWindow):
_pamhyr_ui = "BoundaryConditionsTemperature"
_pamhyr_name = "Boundary conditions temperature"
def __init__(self, data=None, study=None,
config=None, parent=None):
self._data = data
trad = BCTemperatureTranslate()
name = (
trad[self._pamhyr_name] +
" - " + study.name
)
super(BoundaryConditionsTemperatureWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
self._bcs = self._study.river.boundary_conditions_temperature
self.setup_graph()
self.setup_table()
self.setup_connections()
def setup_table(self):
self._delegate_node = ComboBoxDelegate(
trad=self._trad,
data=self._study.river,
mode="node",
parent=self
)
if self._study.is_editable():
editable_headers = self._trad.get_dict("table_headers")
else:
editable_headers = []
table = self.find(QTableView, f"tableView")
self._table = TableModel(
table_view=table,
table_headers=self._trad.get_dict("table_headers"),
editable_headers=editable_headers,
delegates={
"node": self._delegate_node,
},
trad=self._trad,
bc_list=self._study.river.boundary_conditions_temperature,
undo=self._undo_stack,
data=self._study.river
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_graph(self):
self.graph_widget = GraphWidget(
self._study.river,
min_size=None, size=(200, 200),
only_display=True,
parent=self
)
self.graph_layout = self.find(QVBoxLayout, "verticalLayout")
self.graph_layout.addWidget(self.graph_widget)
def setup_connections(self):
if self._study.is_editable():
self.find(QAction, "action_add").triggered.connect(self.add)
self.find(QAction, "action_del").triggered.connect(self.delete)
self.find(QAction, "action_edit").triggered.connect(self.edit)
def index_selected_row(self):
table = self.find(QTableView, f"tableView")
return table.selectionModel()\
.selectedRows()[0]\
.row()
def index_selected_rows(self):
table = self.find(QTableView, f"tableView")
return list(
# Delete duplicate
set(
map(
lambda i: i.row(),
table.selectedIndexes()
)
)
)
def add(self):
self._table.add(self._table.rowCount())
def delete(self):
rows = self.index_selected_rows()
if len(rows) == 0:
return
self._table.delete(rows)
def _copy(self):
logger.info("TODO: copy")
def _paste(self):
logger.info("TODO: paste")
def _undo(self):
self._table.undo()
def _redo(self):
self._table.redo()
def edit(self):
rows = self.index_selected_rows()
for row in rows:
data = self._table.get(row)
if data.node is None:
continue
if self.sub_window_exists(
EditBoundaryConditionWindow,
data=[self._study, None, data]
):
continue
win = EditBoundaryConditionWindow(
data=data,
study=self._study,
parent=self
)
win.show()

View File

@ -0,0 +1,37 @@
# translate.py -- Pamhyr
# Copyright (C) 2023-2025 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
from PyQt5.QtCore import QCoreApplication
from View.Translate import MainTranslate
_translate = QCoreApplication.translate
class BCTemperatureTranslate(MainTranslate):
def __init__(self):
super(BCTemperatureTranslate, self).__init__()
self._dict["Boundary conditions temperature"] = _translate(
"BoundaryConditionsTemperature", "Boundary conditions temperature"
)
self._sub_dict["table_headers"] = {
"name": self._dict["name"],
"node": _translate("BoundaryCondition", "Node"),
}

View File

@ -110,6 +110,10 @@ from View.InitialConditionsTemperature.Window import (
InitialConditionsTemperatureWindow
)
from View.BoundaryConditionsTemperature.Window import (
BoundaryConditionsTemperatureWindow
)
from Solver.Solvers import GenericSolver
from Model.Results.Results import Results
@ -276,6 +280,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# Menu action
"action_menu_initial_conditions_temperature":
self.open_initial_conditions_temperature,
"action_menu_boundary_conditions_temperature":
self.open_boundary_conditions_temperature,
"action_menu_dif": self.open_dif,
"action_menu_d90": self.open_d90,
"action_menu_pollutants": self.open_pollutants,
@ -1031,8 +1037,24 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
# SUB WINDOWS #
###############
def open_initial_conditions_temperature(self):
def open_boundary_conditions_temperature(self):
river = self._study.river
bclist = river.boundary_conditions_temperature.lst
if self.sub_window_exists(
BoundaryConditionsTemperatureWindow,
data=[self._study, None, bclist]
):
return
bound = BoundaryConditionsTemperatureWindow(
study=self._study,
parent=self,
data=bclist,
)
bound.show()
def open_initial_conditions_temperature(self):
iclist = self._study.river.ic_temperature.lst
if len(iclist) != 0:
ic_default = iclist[0]
@ -2178,6 +2200,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit):
logger.debug("No study open for sql debuging...")
return
# todo : gérer le cas où le dossier a un espace dans le nom
# (ne veut pas ouvrir sqlitebrowser)
file = self._study.filename
_ = subprocess.Popen(
f"sqlitebrowser {file}",

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>861</width>
<height>498</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTableView" name="tableView">
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>861</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_del"/>
<addaction name="action_edit"/>
</widget>
<action name="action_add">
<property name="checkable">
<bool>false</bool>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add a new boundary condition or punctual contribution</string>
</property>
<property name="shortcut">
<string>Ctrl+N</string>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete current selected rows</string>
</property>
<property name="shortcut">
<string>Ctrl+D</string>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset>
<normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset>
</property>
<property name="text">
<string>Edit</string>
</property>
<property name="toolTip">
<string>Edit boundary condition or punctual contribution</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>450</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="locale">
<locale language="English" country="Europe"/>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_table">
<item>
<widget class="QTableView" name="tableView">
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_del"/>
<addaction name="action_sort"/>
</widget>
<action name="action_add">
<property name="checkable">
<bool>false</bool>
</property>
<property name="icon">
<iconset>
<normaloff>ressources/add.png</normaloff>ressources/add.png</iconset>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="toolTip">
<string>Add a new point in boundary condition or punctual contribution</string>
</property>
<property name="shortcut">
<string>Ctrl+N</string>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset>
<normaloff>ressources/del.png</normaloff>ressources/del.png</iconset>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="toolTip">
<string>Delete current selected rows</string>
</property>
<property name="shortcut">
<string>Ctrl+D</string>
</property>
</action>
<action name="action_sort">
<property name="icon">
<iconset>
<normaloff>ressources/sort_1-9.png</normaloff>ressources/sort_1-9.png</iconset>
</property>
<property name="text">
<string>Sort</string>
</property>
<property name="toolTip">
<string>Sort points</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -838,11 +838,17 @@
</property>
</action>
<action name="action_menu_dif_temperature">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>DIF ?</string>
</property>
</action>
<action name="action_menu_lateral_contribution_temperature">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Lateral contributions ?</string>
</property>