import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import qt.koriolan.shipheart 1.0 // ============================================================================ // home.qml (client shipheart_wasm) - 4 domaines (circuit/batterie/capteur/ // reservoir), via l'API réelle (DataProvider). Écriture (consigne) sur // circuit uniquement pour l'instant. // // 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: ["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] readonly property int espace_onglets: 10 readonly property int rayon_onglets: 20 // 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] 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 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 "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 {} //-------------------------------------------------------------------------------------------------- // 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; modele_onglets_actuel = modele_onglets_pour(domaine_actif); modele_elements_actuel = modele_elements_pour(domaine_actif); // 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 //-------------------------------------------------------------------------------------------------- // 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]) }); } } //-------------------------------------------------------------------------------------------------- // 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]); } //-------------------------------------------------------------------------------------------------- // 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 === "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) // 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: { 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 { Layout.fillWidth: true height: shipheart_hauteur_bandeau_haut spacing: espace_onglets Repeater { model: domaines_libelles delegate: Rectangle { width: (page_accueil.width - 3 * espace_onglets) / 4 height: shipheart_hauteur_bandeau_haut radius: rayon_onglets color: (index === domaine_actif_index) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton Text { anchors.centerIn: parent text: modelData color: couleur_blanc_casse font.pixelSize: 13 font.bold: (index === domaine_actif_index) } MouseArea { anchors.fill: parent onClicked: { charger_domaine(index) } } } } } //----------------------------------------------------------- // Onglets (familles du domaine actif) : Rectangle { Layout.fillWidth: true height: shipheart_hauteur_bandeau_haut color: "transparent" visible: modele_onglets_actuel.count > 1 GridView { id: grille_onglets anchors.fill: parent cellWidth: width / Math.max(1, Math.min(modele_onglets_actuel.count, 5)) cellHeight: shipheart_hauteur_bandeau_haut model: modele_onglets_actuel clip: true delegate: Rectangle { width: grille_onglets.cellWidth - 2 height: grille_onglets.cellHeight radius: rayon_onglets color: model.actif ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton border.color: (model.etat === 2) ? "red" : (model.etat === 1) ? "orange" : "transparent" border.width: 2 Text { text: model.libelle anchors.centerIn: parent color: couleur_blanc_casse font.pixelSize: 14 font.bold: model.actif } MouseArea { anchors.fill: parent onClicked: { 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, model.id); } } } } } //----------------------------------------------------------- // É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 Layout.fillHeight: true cellWidth: width / 2 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 color: shipheart_couleur_fond_bouton border.color: (model.etat === 2) ? "red" : (model.etat === 1) ? "orange" : "transparent" border.width: 2 Column { anchors.centerIn: parent Rectangle { width: bouton_element.width height: bouton_element.height / 3 radius: 5 color: "transparent" Text { text: model.nom color: couleur_blanc_casse font.pixelSize: 16 font.bold: true anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter } } 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 onClicked: { // 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); } } } } Label { anchors.centerIn: parent visible: elements_grid.count === 0 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 } } } }