Le poste de contrôle (PC) / Les onglets /
Entrepôt des nodes

Rangement des modèles de nodes dans des dossiers.


Avant-propos

Python : les tuples, les dictionnaires.

Avant de commencer, nous allons corriger un oubli : L'état de la toolbar (la barre d'outils) n'est pas mémorisé.
Ajouter cette ligne aux événements de UiMain :
        self.toolBar.installEventFilter(self)

Vérifier.


Description

Un modèle de node est entreposé dans un sous-dossier de /nodes.
Par conséquent, les nodes concernant les indicateurs seront dans le dossier Robot/nodes/indicateurs.


 

L'image montre :

  1. Onglets, disposés sur le côté gauche (friendly names).
  2. Modèles de nodes, de type 'Générateur'.

 

  • Les onglets sont dépourvus du bouton de fermeture.
  • Toutefois, ils sont déplaçables.
    • Leur position sera mémorisée.
  • Cette image nous indique que le dossier /nodes possède les sous-dossiers suivants (minuscules et sans accents) :
    • afficheurs
    • generateurs
    • indicateurs
    • operateurs
    • oscillateurs
  • Les sous-dossiers seront créés par vous-même, à la main.
  • Chaque modèle de node est composé de 2 fichiers : .py et .png
    • Par exemple, pour les historiques : histos.py et histos.png

Contrainte : Un dossier aura un name (nom) et un friendly name (littéralement 'nom amical') :


Préparation :
  1. Modifier fen_mere.ui avec Qt Designer - Suivre cette procédure :
    1. Poser un QtabWidget dans dockNodes. Renommez-le tabNodes
    2. Sélectionner dockNodes.
    3. Le mettre en page verticalement => tabNode occupe alors tout l'espace de dockNodes.
    4. Par défaut, les onglets sont en haut : Les placer sur le côté gauche (West), comme sur l'image ci-dessus.
    5. Supprimer les onglets Tab 1 et Tab 2 (Ils seront recréés par programmation).
    6. Enregistrer.
    7. Quitter (On oublie la phase de compilation qui est automatique).
  2. L'affichage des icones dans les onglets est confié à une nouvelle classe : GroupNodes.
    • Elle sera étudiée au prochain tuto : "Peuplement des nodes".
    • Pour l'instant, nous allons la créer très minimaliste.
  3. Créer la classe GroupNodes dans le fichier Robot/pc/dock_nodes.py :
    from PyQt5.QtWidgets import QListWidget
    
    
    class GroupNodes(QListWidget):
        pass
    
  4. Ajouter l'attribut self.do_groups_nodes à la classe UiMain, c'est un dictionnaire d'objets (d'où le do_) :
            self.do_groups_nodes = {  # Chaque onglet du dockable 'Noeuds' = Un groupes de modèles de nodes.
                'Afficheurs': GroupNodes(),
                'Générateurs': GroupNodes(),
                'Indicateurs': GroupNodes(),
                'Opérateurs': GroupNodes(),
                'Oscillateurs': GroupNodes(),
            }
    
    Ne pas oublier d'importer la classe GroupNodes.
    • Libre à vous de modifier ce dictionnaire pour l'adapter à vos besoins spécifiques.
  5. Affichage des onglets : créer la méthode show_dock_nodes() dans UiMain :
        def show_dock_nodes(self):
            """ - Les onglets affichés sont ceux fournis par le dictionnaire self.do_groups_nodes.
                    |_ Chaque entrée de self.do_groups_nodes -> clé: valeur
                    |_ la clé (friendly name) est celle affichée sur l'onglet.
                    |_ la valeur est un objet GroupNodes chargé d'afficher le contenu.
                - Tri des onglets et activation du dernier activé (selon l'état mémorisé).
            :return: NA
            """
            """ Lecture des valeurs mémorisées. """
            l_memo, i_memo = ut.restore_dock_nodes(self)
    
            """ Ajout des onglets. """
            # A coder ...
    
            """ Sélection de l'onglet mémorisé. """
            # A coder ...
    
  6. Appeler cette méthode dans UiMain.__init__() après la déclaration du dictionnaire :
            self.show_dock_nodes()
    
  7. Créer les méthodes save_dock_nodes() et restore_dock_nodes() dans la classe utils.py > Utils :
        def save_dock_nodes(self, win):
            """ Mémorisation de l'ordre des onglets. """
            print('Save dock nodes')        # Provisoire.
            # à coder ...
    
        def restore_dock_nodes(self, win):
            """
            :param win: objet = instance (unique) de la fenêtre Main.
            :return: tuple (liste mémorisée des onglets, index mémorisé de l'onglet actif)
            """
            print('Restore dock nodes')     # Provisoire.
            # à coder ...
            return [], 0    # Tuple provisoire.
    
    
  8. Un événement pour appeler automatiquement la méthode save_dock_nodes() :
            """ Événements. """
            self.tabNodes.currentChanged.connect(lambda: ut.save_dock_nodes(self))   # Déplacement d'onglet.
    
  9. Un flag pour déclarer ces onglets 'déplaçables' à la souris :
            """ Flags """
            self.tabNodes.setMovable(True)
    

    En principe, les événements et les flags sont déclarés dans __init__()

A faire : Coder aux 3 emplacements "# à coder ...".

Vérification

TDD → Test ajouté : test_dock_nodes_tabs(). Remplacez tout le code de test_main.py par celui-ci :

from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import QSettings, QRect, QTimer, Qt
from PyQt5.QtTest import QTest
from pc.main import UiMain, NewGraph
import pytest
import sys
import os
import random


class Main(UiMain):
    """ Classe dérivée de la fenêtre à tester. """

    def __init__(self, normal, dockable):
        super().__init__()
        self.premier_passage = normal is not None
        self.dockable = dockable
        self.sauvage = normal is False
        self.settings_ante = QSettings(f"{os.getcwd()}{os.sep}ante.conf", QSettings.IniFormat)

    def moveEvent(self, ev):
        if not self.dockable:
            super().moveEvent(ev)

    def resizeEvent(self, ev):
        if not self.dockable:
            super().resizeEvent(ev)

    def closeEvent(self, ev):
        if self.premier_passage:
            """ Premier passage : Mémorisation des états. """
            self.settings_ante.setValue('Geometrie', self.geometry())
            self.settings_ante.setValue('dockNodes visible', self.dockNodes.isVisible())
            self.settings_ante.setValue('dockNodes détaché', self.dockNodes.isFloating())
            self.settings_ante.setValue('dockNodes géométrie', self.dockNodes.geometry())
            self.settings_ante.setValue('dockParams visible', self.dockParams.isVisible())
            self.settings_ante.setValue('dockParams détaché', self.dockParams.isFloating())
            self.settings_ante.setValue('dockParams géométrie', self.dockParams.geometry())
        if not self.sauvage:
            """ Non exécuté en mode 'sauvage'. """
            super().closeEvent(ev)


# @pytest.mark.skip()  # Commenter pour tester
class TestMain:
    g_appli = QApplication(sys.argv)
    tempo = QTimer()
    tempo.setSingleShot(True)

    @staticmethod
    @pytest.fixture(scope="session", autouse=True)
    def cleanup(request):
        """ Appel final pour effacer les fichiers temporaires créés par les tests. """

        def remove_files():
            test_folder = os.getcwd() + os.sep
            backups_folder = test_folder.replace('tests', 'backups')
            files = [f"{test_folder}ante.conf", f"{test_folder}params.conf"]
            folders = ['Test creation de graphe', 'test_graph_1', 'test_graph_2', 'test_graph_3']
            for file in files:
                if os.path.isfile(file):
                    os.remove(file)
            for folder in folders:
                if os.path.isdir(backups_folder + folder):
                    os.rmdir(backups_folder + folder)

        request.addfinalizer(remove_files)

    @staticmethod
    def get_rect(t_x, t_y, t_w, t_h):
        """ Retourne un QRect aléatoire. """
        return QRect(random.randint(*t_x), random.randint(*t_y), random.randint(*t_w), random.randint(*t_h))

    @staticmethod
    def open_window(normal=None, dockable=False):
        win = Main(normal, dockable)
        if normal is not None:
            win.settings_ante.clear()
        win.show()
        return win

    def connect(self, function, delay):
        try:
            self.tempo.timeout.disconnect()
        except TypeError:
            pass
        self.tempo.timeout.connect(function)
        self.tempo.start(delay)

    def fenetre_modif(self, normal):
        win = self.open_window(normal)
        win.setGeometry(self.get_rect((50, 800), (50, 800), (300, 800), (300, 800)))
        self.connect(win.close, 300)  # {x}ms : On laisse le temps à la fenêtre main de mémoriser ses états.

        """ Boucle d'exécution. """
        self.g_appli.exec()

    def fenetre_assert(self):
        def geometry(qrect):
            return f"x = {qrect.x()}, y = {qrect.y()}, largeur = {qrect.width()}, hauteur = {qrect.height()}"

        win = self.open_window()
        self.connect(win.close, 50)

        geometry_ante = win.settings_ante.value('Geometrie', QRect(200, 200, 800, 600))
        geometry_now = win.geometry()
        print(
            "\n- La fenêtre doit apparaître avec la même géométrie (taille et position) "
            "que lors de sa dernière fermeture."
            f"\n- Géométrie à la dernière fermeture :   {geometry(geometry_ante)}."
            f"\n- Géométrie à l'ouverture actuelle :    {geometry(geometry_now)}."
        )
        assert geometry_now == geometry_ante

        """ Boucle d'exécution. """
        self.g_appli.exec()

    """ *************************************************************************************** """
    """                                        LES TESTS                                        """
    """ *************************************************************************************** """

    @pytest.mark.skip()  # Commenter pour tester
    def test_ferme_normal(self):
        """ Phase 1 : Affichage de la fenêtre, plusieurs déplacements et changements de taille, aléatoires.
        Fermeture de la fenêtre, mémorisation de sa géométrie. """
        self.fenetre_modif(normal=True)  # True = Fermeture normale.

        """ Phase 2: Réouverture de la fenêtre, lecture de sa géométrie, comparaison. """
        self.fenetre_assert()

    @pytest.mark.skip()  # Commenter pour tester
    def test_ferme_sauvage(self):
        self.fenetre_modif(normal=False)  # False = Fermeture sauvage.
        self.fenetre_assert()

    @pytest.mark.skip()  # Commenter pour tester
    def test_dock_state(self):
        def update_dock():
            self.connect(win.close, 500)  # {x} ms : On laisse le temps à la fenêtre main de mémoriser ses états.

            """ Modification des états des dockables, de façon aléatoire. """
            for o_dock in [win.dockNodes, win.dockParams]:
                x, y = win.x() + win.width() // 2, win.y() + win.height() // 2
                o_dock.setFloating(random.choice([False, True]))
                if o_dock.isFloating():
                    o_dock.setGeometry(self.get_rect((x - 50, x + 50), (y - 50, y + 50), (50, 400), (50, 400)))
                else:
                    o_dock.setFixedWidth(random.randint(82, 200))

        def state_dock():
            self.connect(win.close, 50)  # Obligatoirement avant les asserts.

            """ Vérification des résultats. """
            msg = 'DockNodes : Flottant (détaché).'
            assert win.dockNodes.isFloating() == win.settings_ante.value('dockNodes détaché'), msg
            res, qrect_now, qrect_ante = 0, win.dockNodes.geometry().getRect(), win.settings_ante.value(
                'dockNodes géométrie').getRect()
            if win.dockNodes.isFloating():
                msg = 'DockNodes : Géométrie (x, y, w, h).'
                for i in range(4):
                    """ Tolérence : on accepte 3 bonnes valeurs sur 4. """
                    res += qrect_now[i] == qrect_ante[i]
                assert res > 2, msg
            else:
                msg = 'DockNodes : Largeur.'  # 0=x, 1=y, 2=width, 3=height
                assert qrect_now[2] == qrect_ante[2], msg  # [2]=width

            msg = 'DockParams : Flottant (détaché).'
            assert win.dockParams.isFloating() == win.settings_ante.value('dockParams détaché'), msg
            res, qrect_now, qrect_ante = 0, win.dockParams.geometry().getRect(), win.settings_ante.value(
                'dockParams géométrie').getRect()
            if win.dockParams.isFloating():
                msg = 'DockParams : Géométrie (x, y, w, h).'
                for i in range(4):
                    res += qrect_now[i] == qrect_ante[i]
                assert res > 2, msg
            else:
                msg = 'DockParams : Largeur.'
                assert qrect_now[2] == qrect_ante[2], msg

        """ Phase 1 : Modification aléatoire des dockables (détaché ou attaché, position, taille, visibilité). """
        win = self.open_window(normal=True)
        update_dock()
        """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
        self.g_appli.exec()

        """ Phase 2 : Ouverture fenêtre, lecture de l'état des dockables, assertions et fermeture. """
        win = self.open_window()
        state_dock()
        """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
        self.g_appli.exec()

    @pytest.mark.skip()  # Commenter pour tester
    def test_dock_visible(self):
        def update_dock():
            self.connect(win.close, 300)  # {x} ms : On laisse le temps à la fenêtre main de mémoriser ses états.
            """ Modification des visibilités, de façon aléatoire. """
            for o_dock in [win.dockNodes, win.dockParams]:
                o_dock.setVisible(random.choice([False, True]))

        def state_dock():
            self.connect(win.close, 50)  # Obligatoirement avant les asserts.
            """ Vérification des résultats. """
            try:
                msg = 'DockNodes : Visibilité.'
                assert win.dockNodes.isVisible() == win.settings_ante.value('dockNodes visible'), msg
                msg = 'DockParams : Visibilité.'
                assert win.dockParams.isVisible() == win.settings_ante.value('dockParams visible'), msg
            except (Exception,):
                pass

        for i in range(4):
            """ Phase 1 : Modification aléatoire des dockables (détaché ou attaché, position, taille, visibilité). """
            win = self.open_window(normal=True, dockable=True)
            update_dock()
            """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
            self.g_appli.exec()

            """ Phase 2 : Ouverture fenêtre, lecture de l'état des dockables, assertions et fermeture. """
            win = self.open_window()
            state_dock()
            """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
            self.g_appli.exec()

    @pytest.mark.skip()
    def test_new_graph(self):
        def write():
            """ Écriture animée. """
            new_graph.graph_name.setText(graph_name[:indx[0]])
            indx[0] += 1
            if indx[0] <= len(graph_name):
                """ Écriture +1 caractère chaque xxx ms. """
                self.tempo.start(100)
            else:
                """ Simulation de clic sur le bouton 'Ok'. """
                self.connect(new_graph.accept, 1000)

        graph_name = 'Test creation de graphe'
        bk_folder = f"{os.getcwd()}{os.sep}..{os.sep}backups"
        created_folder = bk_folder + os.sep + graph_name
        win = self.open_window()
        new_graph = NewGraph(win)  # Type QDialog
        indx = [0]
        self.connect(write, 10)

        new_graph.exec()
        self.connect(win.close, 50)  # Obligatoirement avant les asserts.
        self.g_appli.exec()

        """ Le dossier 'D:/Robot/backups/Test creation de graphe' doit avoir été créé. Vérification : """
        assert os.path.isdir(created_folder), f"Le dossier '{created_folder}' n'a pas été créé."

    @pytest.mark.skip()
    def test_open_graph(self):
        """ - État initial : à la création, il n'y a aucun onglet. Plus tard, il pourrait y en avoir un ou plusieurs.
            - Création de 3 dossiers supplémentaires dans /backups : /test_graph_1, /test_graph_2 et /test_graph_3.
            - Pour chaque dossier, appel de la méthode open_graph() :
                - On vérifie que le dernier ajouté est sélectionné.
                - On vérifie que la fenêtre Main a 3 onglets de plus dans sa zône centrale.
                - On vérifie qu'ils sont déplaçables à la souris par drag & drop.
                - On vérifie également qu'ils possèdent la croix de fermeture.
                - Tentative d'ajout de /test_graph_2, qui est déjà présent :
                    - On vérifie le rejet de doublons.
                    - On vérifie néanmoins qu'il est sélectionné.
            - Fermeture des 3 onglets => Vérification.
            - Suppression des 3 dossiers de /backups.
        """
        """ Ouverture et géométrie de la fenêtre Main. """
        win = self.open_window()
        win.setGeometry(QRect(100, 100, 900, 400))
        self.connect(win.close, 800)  # Fermeture différée.

        """ Fermeture éventuelle des onglets. """
        for i in range(3):
            win.close_graph(0)

        """ Création et affichage des onglets de 3 graphes. """
        for i, dir_name in enumerate(['test_graph_1', 'test_graph_2', 'test_graph_3']):
            created_folder = os.path.abspath(f"{os.getcwd()}{os.sep}..{os.sep}backups{os.sep}{dir_name}")
            os.makedirs(created_folder, exist_ok=True)  # Création dossier. Si ce dossier existe, pas d'erreur renvoyée.
            win.open_graph(dir_name)
            assert win.tabGraphs.currentIndex() == i, "L'onglet sélectionné n'est pas le dernier ajouté."

        """ Asserts. """
        nb_now = win.tabGraphs.count()  # Le nombre d'onglets a augmenté de 3.
        vrai = True
        assert nb_now == 3, "L'ajout d'onglets a échoué."
        assert win.tabGraphs.isMovable() == vrai, "Les onglets ne sont pas déplaçables à la souris."
        assert win.tabGraphs.tabsClosable() == vrai, "Les onglets ne possèdent pas la croix de fermeture."

        """ Tentative d'ouverture d'un graphe déjà ouvert. """
        win.open_graph("test_graph_2")
        assert nb_now == win.tabGraphs.count(), "La gestion des doublons ne fonctionne pas."
        assert win.tabGraphs.currentIndex() == 1, "L'onglet sélectionné n'est pas celui du graphe choisi."

        """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
        self.g_appli.exec()

    @pytest.mark.skip()
    def test_close_graph(self):
        """ - Ouverture de la fenêtre Main.
            - Création de 3 dossiers dans /backups/ (qui contiendront les graphes).
            - Pour chaque dossier, création d'un onglet dans la zône principale.
            - Fermeture successive des 3 onglets.
            - A chaque fois, on compte le nombre d'onglets restant.
            - Les résultats à obtenir : 2, 1 puis 0.
        """
        """ Ouverture et géométrie de la fenêtre Main. """
        win = self.open_window()
        win.setGeometry(QRect(200, 200, 900, 400))
        self.connect(win.close, 800)  # Fermeture différée.

        """ Création et affichage des onglets de 3 graphes. """
        for i, dir_name in enumerate(['test_graph_1', 'test_graph_2', 'test_graph_3']):
            created_folder = os.path.abspath(f"{os.getcwd()}{os.sep}..{os.sep}backups{os.sep}{dir_name}")
            os.makedirs(created_folder, exist_ok=True)  # Création dossier. Si ce dossier existe, pas d'erreur renvoyée.
            win.open_graph(dir_name)
        nb_ante = win.tabGraphs.count()

        """ Fermeture successive des 3 onglets. """
        for i in range(1, 4):
            close_button = win.tabGraphs.tabBar().tabButton(0, QTabBar.RightSide)
            QTest.mouseClick(close_button, Qt.LeftButton, Qt.NoModifier)
            nb_now = win.tabGraphs.count()
            assert nb_now == nb_ante - i, "La fermeture des onglets a échoué."

        """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
        self.g_appli.exec()

    @pytest.mark.skip()
    def test_memo_tabs(self):
        """ - Phase 1 :
                - Ouverture de la fenêtre Main.
                - Fermeture éventuelle des onglets.
                - Mélange de la liste ['test_graph_1', 'test_graph_2', 'test_graph_3']
                - Création de 3 dossiers, selon la liste, dans /backups/ (qui contiendront les graphes).
                - Pour chaque dossier, création d'un onglet dans la zône principale.
                - Activation d'un aonglet au hasard.
                - Mémorisation de l'état des onglets.
                - Fermeture de la fenêtre Main.
            - Phase 2 :
                - Ouverture de la fenêtre Main.
                - Assert : Comparaison de l'état des onglets avec celui mémorisé.
                - Fermeture de la fenêtre Main, fin du test.
        """
        """ Ouverture et géométrie de la fenêtre Main. """
        win = self.open_window()
        win.setGeometry(QRect(200, 200, 900, 400))

        """ Fermeture éventuelle des onglets. """
        for i in range(3):
            win.close_graph(0)

        """ Phase 1 : Création et affichage des onglets de 3 graphes. """
        l_tabs_ante = ['test_graph_1', 'test_graph_2', 'test_graph_3']
        random.shuffle(l_tabs_ante)
        current_index_ante = random.randint(0, 2)
        for i, dir_name in enumerate(l_tabs_ante):
            created_folder = os.path.abspath(f"{os.getcwd()}{os.sep}..{os.sep}backups{os.sep}{dir_name}")
            os.makedirs(created_folder, exist_ok=True)  # Création dossier. Si ce dossier existe, pas d'erreur renvoyée.
            win.open_graph(dir_name)
        win.tabGraphs.setCurrentIndex(current_index_ante)

        """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
        self.connect(win.close, 800)  # Fermeture différée.
        self.g_appli.exec()

        """ Phase 2 : Ouverture de la fenêtre Main. """
        win = self.open_window()
        win.setGeometry(QRect(200, 200, 900, 400))

        l_tabs_now = []
        for i in range(win.tabGraphs.count()):
            l_tabs_now.append(win.tabGraphs.widget(i).graph_name)
        current_index_now = win.tabGraphs.currentIndex()

        """ Asserts. """
        fo = 'Fermeture, puis ouverture : '
        if current_index_now >= 0:
            assert current_index_now == current_index_ante, fo + "L'onglet sélectionné n'est pas le même."
        assert len(l_tabs_now) == len(l_tabs_ante), fo + "Le nombre d'onglets n'est pas le même."
        assert l_tabs_now == l_tabs_ante, fo + "L'ordre des onglets n'est pas le même."

        """ Boucle d'exécution. Sortie lorsque la fenêtre se ferme. """
        self.connect(win.close, 800)  # Fermeture différée.
        self.g_appli.exec()

    # @pytest.mark.skip()
    def test_dock_nodes_tabs(self):
        """ - Ouverture de la fenêtre.
            - Assert 1 : On devrait avoir autant d'onglets que de clés dans le dictionnaire.
            - Tri aléatoire les onglets -> Mémorisation.
            - Sélection d'un onglet au hasard -> Mémorisation.
            - Fermeture de la fenêtre.
            - Réouverture de la fenêtre.
            - Assert 2 : L'ordre des onglets doit être celui précédemment mémorisé.
            _ Assert 3 : L'onglet sélectionné doit être celui précédemment mémorisé.
        """
        """ Ouverture de la fenêtre. """
        win = self.open_window()

        """ Consigne : Onglets qui devraient être affichés = d_friendly. """
        l_nodes_tabs = []
        for friendly_name in win.do_groups_nodes.keys():
            l_nodes_tabs.append(friendly_name)

        """ Assert 1. """
        nb_tabs = win.tabNodes.count()
        msg = f"Le nombre d'onglets affichés ({nb_tabs}) est différent de celui calculé ({len(l_nodes_tabs)})."
        assert len(l_nodes_tabs) == nb_tabs, msg

        """ Mélange des onglets. """
        for i in range(nb_tabs):
            n_from = random.randint(0, nb_tabs-1)
            n_to = random.randint(0, nb_tabs-1)
            win.tabNodes.tabBar().moveTab(n_from, n_to)

        """ Sélection d'un onglet (Pas le 0 qui est la valeur par défaut). """
        n_selected_ante = random.randint(1, nb_tabs-1)
        win.tabNodes.setCurrentIndex(n_selected_ante)

        """ Mémorisation de l'ordre des onglets. """
        l_nodes_ante = []
        for i in range(win.tabNodes.count()):
            l_nodes_ante.append(win.tabNodes.tabText(i))

        # """ Fermeture asynchrone de la fenêtre. """
        self.connect(win.close, 1000)
        self.g_appli.exec()

        """ Réouverture de la fenêtre. """
        win = self.open_window()

        """ Assert 2. Réel : Onglets effectivement affichés. """
        l_nodes_now = []
        for i in range(win.tabNodes.count()):
            l_nodes_now.append(win.tabNodes.tabText(i))
        msg = "L'ordre des onglets du dock_nodes n'a pas été mémorisé."
        assert l_nodes_ante == l_nodes_now, msg

        """ Assert 3. Réel : Onglet sélectionné. """
        n_selected_now = win.tabNodes.currentIndex()
        msg = f"L'onglet actif ({n_selected_now}) du dock_nodes n'a pas été mémorisé ({n_selected_ante}).'"
        assert n_selected_now == n_selected_ante, msg
        self.connect(win.close, 1000)
        self.g_appli.exec()
Attention : Tous les tests sont ignorés (@pytest.mark.skip()), sauf le dernier.
Lorsque vous aurez réussi à coder pour faire passer ce test, réactivez-les tous pour un test global.
Refactoring - à faire après que les tests aient abouti avec succès :
  • On remarque que la méthode main.py > UiMain.__init_() est assez chargée. Je vous propose de la scinder en la restructurant comme ceci :
        def __init__(self):
            super().__init__()
            self.setupUi(self)
            self.do_groups_nodes = None
            self.setup()
            self.set_events()
            self.set_flags()
    
        def setup(self):
            self.do_groups_nodes = {     # Chaque onglet du dockable 'Noeuds' = Un groupes de modèles de nodes.
                'Afficheurs': GroupNodes(),
                'Générateurs': GroupNodes(),
                'Indicateurs': GroupNodes(),
                'Opérateurs': GroupNodes(),
                'Oscillateurs': GroupNodes(),
            }
            ut.restore_state(self)          # Restauration de l'état de la fenêtre.
            ut.restore_tabs(self)           # Restauration des onglets du panneau central.
            self.show_dock_nodes()
    
        def set_events(self):
            """ Événements. """
            self.dockNodes.installEventFilter(self)
            self.dockParams.installEventFilter(self)
            self.toolBar.installEventFilter(self)
            self.actionNodes.triggered.connect(self.visibility_nodes)
            self.actionParams.triggered.connect(self.visibility_params)
            self.actionNew.triggered.connect(self.new_graph)
            self.actionOpen.triggered.connect(self.open_graph)
            self.tabGraphs.tabCloseRequested.connect(self.close_graph)
            self.tabGraphs.currentChanged.connect(lambda: ut.save_tabs(self))   # Ouverture et déplacement d'onglet.
            self.tabNodes.currentChanged.connect(lambda: ut.save_dock_nodes(self))   # Déplacement d'onglet.
    
        def set_flags(self):
            """ Flags. """
            self.tabGraphs.setTabsClosable(True)
            self.tabGraphs.setMovable(True)
            self.tabNodes.setMovable(True)
    
Relancez tous les tests.

Bon courage et bon coding !


Snippets

Essayez de résoudre cette fonctionnalité par vous-même.
Consultez les réponses (snippets) seulement si vous n'avez pas trop de temps.

Bonjour les codeurs !