4 pages de détail avec courbe (Canvas), navigation depuis home.qml, sélecteurs de variable pour circuits/batteries

This commit is contained in:
2026-08-26 17:26:52 +02:00
parent d9593518d0
commit 85ba4be46b
144 changed files with 2344 additions and 115 deletions
+239 -88
View File
@@ -9,19 +9,21 @@ import qt.koriolan.shipheart 1.0
// reservoir), via l'API réelle (DataProvider). Écriture (consigne) sur
// circuit uniquement pour l'instant.
//
// Architecture : les onglets (familles) partagent UN SEUL ListModel (forme
// identique dans les 4 domaines : id/libelle/etat/actif). Les ÉLÉMENTS, en
// revanche, utilisent UN MODÈLE SÉPARÉ PAR DOMAINE - piège QML rencontré en
// pratique : un ListModel fige les champs reconnus ("rôles") dès sa première
// utilisation, même après clear() ; réutiliser un même modèle pour des formes
// JSON différentes (tension/intensite pour circuit, valeur/unite pour
// capteur...) laisse les champs propres à chaque domaine à 'undefined'.
// Architecture : les ONGLETS et les ÉLÉMENTS ont chacun UN MODÈLE SÉPARÉ PAR
// DOMAINE (8 ListModel au total). Deux raisons distinctes :
// - Éléments : évite le piège des "rôles" QML figés (tension/intensite pour
// circuit, valeur/unite pour capteur... - cf notes historiques).
// - Onglets ET éléments : préserve les dernières données connues d'un
// domaine quand on le quitte puis qu'on y revient, y compris hors ligne -
// on ne vide JAMAIS un modèle avant d'avoir une nouvelle donnée réelle à
// afficher à la place (sinon : données qui "disparaissent" en changeant
// d'onglet, cf retours d'expérience).
// ============================================================================
Shipheart_page {
id: page_accueil
readonly property var domaines: ["circuit", "batterie", "capteur", "reservoir"]
readonly property var domaines: ["circuits", "batteries", "capteurs", "reservoirs"]
readonly property var domaines_libelles: [qsTr("Circuits"), qsTr("Batteries"), qsTr("Capteurs"), qsTr("Réservoirs")]
property int domaine_actif_index: 0
readonly property string domaine_actif: domaines[domaine_actif_index]
@@ -29,95 +31,194 @@ Shipheart_page {
readonly property int espace_onglets: 10
readonly property int rayon_onglets: 20
property int id_groupe: 0
// Famille sélectionnée, conservée INDÉPENDAMMENT par domaine - permet de
// revenir sur un domaine déjà visité sans perdre sa sélection, et permet
// au minuteur de savoir, pour CHAQUE domaine, s'il a déjà une famille
// active ou doit encore en obtenir une.
property var id_groupe_par_domaine: ({"circuits": 0, "batteries": 0, "capteurs": 0, "reservoirs": 0})
readonly property int id_groupe: id_groupe_par_domaine[domaine_actif]
ListModel { id: modele_onglets }
function definir_id_groupe(type, valeur) {
var copie = id_groupe_par_domaine;
copie[type] = valeur;
id_groupe_par_domaine = copie; // réassignation complète : nécessaire pour déclencher la notification QML sur un objet JS
}
// Un ListModel par domaine pour les onglets ET pour les éléments :
ListModel { id: modele_onglets_circuit }
ListModel { id: modele_onglets_batterie }
ListModel { id: modele_onglets_capteur }
ListModel { id: modele_onglets_reservoir }
property var modele_onglets_actuel: modele_onglets_circuit
// Un ListModel par domaine, pour éviter le piège des "rôles" figés de QML :
ListModel { id: modele_elements_circuit }
ListModel { id: modele_elements_batterie }
ListModel { id: modele_elements_capteur }
ListModel { id: modele_elements_reservoir }
property var modele_elements_actuel: modele_elements_circuit
//--------------------------------------------------------------------------------------------------
// Fonction de recherche du modèle d'onglet pour un domaine donné :
// Reçoit : le nom du domaine ("circuits", "batteries", "capteurs" ou "reservoirs")
// Retour : le modèle d'onglets correspondant au domaine
//--------------------------------------------------------------------------------------------------
function modele_onglets_pour(type) {
switch (type) {
case "circuits": return modele_onglets_circuit;
case "batteries": return modele_onglets_batterie;
case "capteurs": return modele_onglets_capteur;
case "reservoirs": return modele_onglets_reservoir;
}
return null;
}
//--------------------------------------------------------------------------------------------------
// Fonction de recherche du modèle d'éléments pour un domaine donné :
// Reçoit : le nom du domaine ("circuits", "batteries", "capteurs" ou "reservoirs")
// Retour : le modèle d'onglets correspondant au domaine
//--------------------------------------------------------------------------------------------------
function modele_elements_pour(type) {
switch (type) {
case "circuit": return modele_elements_circuit;
case "batterie": return modele_elements_batterie;
case "capteur": return modele_elements_capteur;
case "reservoir": return modele_elements_reservoir;
case "circuits": return modele_elements_circuit;
case "batteries": return modele_elements_batterie;
case "capteurs": return modele_elements_capteur;
case "reservoirs": return modele_elements_reservoir;
}
return null;
}
property DataProvider dataProvider: DataProvider {}
function charger_domaine(index) {
//--------------------------------------------------------------------------------------------------
// Fonction de chargement des données liées à un domaine donné :
// Reçoit : l'index du domaine
// Retour : rien
//--------------------------------------------------------------------------------------------------
function charger_domaine(index)
{
domaine_actif_index = index;
id_groupe = 0;
modele_onglets.clear();
modele_onglets_actuel = modele_onglets_pour(domaine_actif);
modele_elements_actuel = modele_elements_pour(domaine_actif);
modele_elements_actuel.clear();
// Pas de clear() ici volontairement : si ce domaine a déjà été chargé
// avec succès plus tôt (avant une éventuelle coupure), ses dernières
// données restent affichées jusqu'à ce qu'une nouvelle récupération
// réussisse - jamais vidées juste parce qu'on regarde ailleurs puis
// qu'on revient.
dataProvider.get_familles(domaine_actif);
}
// Connexions (déclenchées sur apparition d'un signal de DataProvider) :
Connections {
target: dataProvider
function onFamillesRecues(type, familles) {
if (type !== domaine_actif) return; // Réponse d'un domaine quitté entretemps - ignorée
modele_onglets.clear();
for (var i = 0; i < familles.length; i++) {
var f = familles[i];
modele_onglets.append({
"id": f.id,
"libelle": f.nom,
"etat": f.etat,
"actif": (f.id === id_groupe)
});
if (id_groupe === 0 && i === 0) {
id_groupe = f.id;
dataProvider.get_elements(domaine_actif, id_groupe);
//--------------------------------------------------------------------------------------------------
// Réception du la iste des familles :
// La demande a été faite préalablement par un 'dataProvider.get_familles(domaine_actif)'
//--------------------------------------------------------------------------------------------------
function onFamillesRecues(type, familles)
{
var m = modele_onglets_pour(type);
m.clear();
for (var i = 0; i < familles.length; i++)
{
// Traitement de la première famille à afficher :
if (id_groupe_par_domaine[type] === 0)
{
definir_id_groupe(type, familles[i].id); // On sélectionne la première récupérée
dataProvider.get_elements(type, familles[i].id); // Au passage, on demande la récupération des éléments de cette famille
}
// Ajout de la famille au modèle d'onglets :
m.append({
"id": familles[i].id,
"libelle": familles[i].nom,
"etat": familles[i].etat,
"actif": (familles[i].id === id_groupe_par_domaine[type])
});
}
}
function onElementsRecus(type, famille, elements) {
if (type !== domaine_actif || famille !== id_groupe) return;
//--------------------------------------------------------------------------------------------------
// Réception de la iste des éléments d'une famille :
// La demande a été faite préalablement par un 'dataProvider.get_elements(domaine_actif, famille_active)'
//--------------------------------------------------------------------------------------------------
function onElementsRecus(type, famille, elements)
{
if (famille !== id_groupe_par_domaine[type]) return; // Réponse obsolète (famille changée entretemps pour ce domaine)
var m = modele_elements_pour(type);
m.clear();
for (var j = 0; j < elements.length; j++) m.append(elements[j]);
}
function onCircuitModifie(id, succes) {
//--------------------------------------------------------------------------------------------------
// Réception de la réponse à une commande de circuit :
// La demande a été faite préalablement par un 'dataProvider.set_circuit(id_circuit, nouvelle_consigne)'
//--------------------------------------------------------------------------------------------------
function onCircuitModifie(id, succes)
{
if (!succes) console.log("Échec de la commande sur le circuit " + id);
if (domaine_actif === "circuit" && id_groupe > 0) dataProvider.get_elements("circuit", id_groupe);
if ((domaine_actif === "circuits") && (id_groupe > 0)) dataProvider.get_elements("circuits", id_groupe); // Rafraichissement de l'affichage
}
//--------------------------------------------------------------------------------------------------
// Réception d'une erreur du DataProvider :
//--------------------------------------------------------------------------------------------------
function onErreur(message) {
console.log("DataProvider erreur : " + message);
}
}
Component.onCompleted: charger_domaine(0)
Component.onCompleted: charger_domaine(0) // A l'ouverture, chargement du premier domaine dans la liste (par défaut)
// Rafraîchissement périodique du domaine actuellement affiché.
// get_familles() est TOUJOURS retentée, même sans famille encore
// sélectionnée pour ce domaine (id_groupe===0) - c'est ce qui permet de
// se rétablir tout seul après une reconnexion, sans action de
// l'utilisateur, même pour un domaine qui n'avait encore jamais réussi
// à charger quoi que ce soit.
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
if (id_groupe > 0) {
dataProvider.get_elements(domaine_actif, id_groupe);
dataProvider.get_familles(domaine_actif);
}
dataProvider.get_familles(domaine_actif);
if (id_groupe > 0) dataProvider.get_elements(domaine_actif, id_groupe);
}
}
//====================================================================================
// Affichage :
//====================================================================================
ColumnLayout {
anchors.fill: parent
spacing: 10
//-----------------------------------------------------------
// Bandeau hors ligne - permanent, ne disparaît que sur reconnexion
// réelle (dataProvider.connecte redevient true après un succès) :
Rectangle {
Layout.fillWidth: true
height: visible ? 40 : 0
visible: !dataProvider.connecte
color: couleur_rouge_transparent
border.color: couleur_orange
border.width: 1
Text {
anchors.centerIn: parent
color: couleur_orange
font.pixelSize: 13
font.bold: true
text: dataProvider.aDejaEteConnecte
? qsTr("⚠ Bateau non connecté depuis le %1").arg(
Qt.formatDateTime(dataProvider.derniereConnexionReussie, "dd/MM/yyyy à HH:mm:ss"))
: qsTr("⚠ Bateau jamais connecté durant cette session")
}
}
//-----------------------------------------------------------
// Sélecteur de domaine :
Row {
@@ -143,7 +244,9 @@ Shipheart_page {
MouseArea {
anchors.fill: parent
onClicked: charger_domaine(index)
onClicked: {
charger_domaine(index)
}
}
}
}
@@ -155,14 +258,14 @@ Shipheart_page {
Layout.fillWidth: true
height: shipheart_hauteur_bandeau_haut
color: "transparent"
visible: modele_onglets.count > 1
visible: modele_onglets_actuel.count > 1
GridView {
id: grille_onglets
anchors.fill: parent
cellWidth: width / Math.max(1, Math.min(modele_onglets.count, 5))
cellWidth: width / Math.max(1, Math.min(modele_onglets_actuel.count, 5))
cellHeight: shipheart_hauteur_bandeau_haut
model: modele_onglets
model: modele_onglets_actuel
clip: true
delegate: Rectangle {
@@ -184,11 +287,11 @@ Shipheart_page {
MouseArea {
anchors.fill: parent
onClicked: {
id_groupe = model.id;
for (var k = 0; k < modele_onglets.count; k++) {
modele_onglets.set(k, {"actif": (modele_onglets.get(k).id === model.id)});
definir_id_groupe(domaine_actif, model.id);
for (var k = 0; k < modele_onglets_actuel.count; k++) {
modele_onglets_actuel.set(k, {"actif": (modele_onglets_actuel.get(k).id === model.id)});
}
dataProvider.get_elements(domaine_actif, id_groupe);
dataProvider.get_elements(domaine_actif, model.id);
}
}
}
@@ -197,6 +300,8 @@ Shipheart_page {
//-----------------------------------------------------------
// Éléments de la famille sélectionnée, dans le domaine actif :
// (opacité réduite quand hors ligne - les valeurs affichées sont
// les dernières connues, plus garanties à jour, cf bandeau ci-dessus)
GridView {
id: elements_grid
Layout.fillWidth: true
@@ -205,8 +310,10 @@ Shipheart_page {
cellHeight: height / 4
model: modele_elements_actuel
clip: true
opacity: dataProvider.connecte ? 1.0 : 0.45
delegate: Rectangle {
id: bouton_element
width: elements_grid.cellWidth - 4
height: elements_grid.cellHeight - 4
radius: 10
@@ -216,52 +323,94 @@ Shipheart_page {
Column {
anchors.centerIn: parent
spacing: 4
Text {
text: model.nom
color: couleur_blanc_casse
font.pixelSize: 14
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
width: bouton_element.width
height: bouton_element.height / 3
radius: 5
color: "transparent"
Text {
text: {
switch (domaine_actif) {
case "circuit":
return model.tension.toFixed(1) + " V " + model.intensite.toFixed(2) + " A";
case "batterie":
return model.niveau.toFixed(0) + " % " + model.tension.toFixed(2) + " V";
case "capteur":
return model.valeur + (model.unite ? (" " + model.unite) : "");
case "reservoir":
return model.niveau.toFixed(0) + " % (" + model.capacite.toFixed(0) + " L)";
default:
return "";
}
Text {
text: model.nom
color: couleur_blanc_casse
font.pixelSize: 16
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
color: couleur_blanc_casse
font.pixelSize: 12
anchors.horizontalCenter: parent.horizontalCenter
}
// Indication visuelle discrète que l'élément est cliquable (circuits) :
Text {
visible: (domaine_actif === "circuit")
text: (model.consigne > 0) ? qsTr("● Marche (toucher pour arrêter)") : qsTr("○ Arrêt (toucher pour démarrer)")
color: (model.consigne > 0) ? "lightgreen" : couleur_blanc_casse
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: bouton_element.width
height: bouton_element.height / 3
radius: 5
color: "transparent"
Text {
text: {
switch (domaine_actif) {
case "circuits":
return model.tension.toFixed(1) + " V | " + model.intensite.toFixed(2) + " A";
case "batteries":
return model.niveau.toFixed(0) + " % | " + model.tension.toFixed(2) + " V";
case "capteurs":
return model.valeur + (model.unite ? (" " + model.unite) : "");
case "reservoirs":
return model.niveau.toFixed(0) + " % (" + model.capacite.toFixed(0) + " L)";
default:
return "";
}
}
color: couleur_blanc_casse
font.pixelSize: 12
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
width: bouton_element.width
height: bouton_element.height / 3
radius: 5
color: (model.consigne > 0) ? couleur_green_yellow_transparent : "transparent"
visible: (domaine_actif === "circuits")
Text {
text: (model.consigne > 0) ? qsTr("On") : qsTr("Off")
color: (model.consigne > 0) ? couleur_green_yellow : couleur_blanc_casse
font.pixelSize: 18
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
}
MouseArea {
anchors.fill: parent
enabled: (domaine_actif === "circuit")
onClicked: {
var nouvelle_consigne = (model.consigne > 0) ? 0 : 100;
dataProvider.set_circuit(model.id, nouvelle_consigne);
// Clic court : navigation vers la page de détail,
// une par domaine (toutes construites maintenant).
switch (domaine_actif) {
case "capteurs":
stackView.push("detail_capteur.qml", {"idElement": model.id, "nomElement": model.nom});
break;
case "reservoirs":
stackView.push("detail_reservoir.qml", {"idElement": model.id, "nomElement": model.nom});
break;
case "circuits":
stackView.push("detail_circuit.qml", {"idElement": model.id, "nomElement": model.nom});
break;
case "batteries":
stackView.push("detail_batterie.qml", {"idElement": model.id, "nomElement": model.nom});
break;
}
}
onPressAndHold: {
// Appui long : bascule marche/arrêt (circuits uniquement).
if (domaine_actif === "circuits" && dataProvider.connecte) {
var nouvelle_consigne = (model.consigne > 0) ? 0 : 100;
dataProvider.set_circuit(model.id, nouvelle_consigne);
}
}
}
}
@@ -269,8 +418,10 @@ Shipheart_page {
Label {
anchors.centerIn: parent
visible: elements_grid.count === 0
text: qsTr("Aucun élément dans ce groupe.")
color: couleur_rouge_transparent
text: dataProvider.connecte
? qsTr("Aucun élément dans ce groupe.")
: qsTr("Hors ligne - dernières données indisponibles pour ce groupe.")
color: couleur_orange_transparent
}
}
}