Le poste de contrôle (PC) / Les graphes /
Edges : 2 - Création par drag & drop, édition

Création simple et ludique, par drag & drop


Avant-propos

Optionnel : Amélioration de l'affichage d'un node.

Petite erreur d'arithmétique.


Description

Création d'un edge :

  1. Bouton gauche de la souris appuyé sur un socket de sortie.
  2. Drag : déplacer la souris en gardant son bouton appuyé.
  3. Drop : relâcher le bouton sur un socket d'entrée d'un autre node.

Dynamique de la construction d'un edge :


Plan du tuto :
  1. Refactoring.
  2. Suppression de nodes et d'edges.
  3. Survol d'un socket de sortie : changement de sa couleur et de sa taille, indiquant que l'on peut commencer le drag.
  4. Bouton gauche de la souris, appuyé sur un socket de sortie : Création d'un "mi-edge".
  5. Drop hors d'un socket d'entrée : reset.
  6. Drag : déplacement de la souris, bouton gauche appuyé.
  7. Survol pendant le drag d'un socket d'entrée valide : changement de sa couleur et de sa taille.
  8. Drop sur un socket d'entrée valide : validation, enregistrement, reset.
  9. Backup et contraintes technologiques.

1 - Refactoring :

Retour au plan


2 - Suppression de nodes et d'edges :

Retour au plan

Si l'on supprime un node, dans l'état actuel du code, on obtient ceci :


3 - Survol d'un socket de sortie :

Retour au plan

Socket de sortie agrandi. Couleur de fond : blanche. 


4 - Bouton gauche de la souris, appuyé sur un socket de sortie : Création d'un "mi-edge"

Retour au plan

Constatations évidentes et énoncé du problème :

Création du mi-edge


Refactoring

self.o_socket_in : Attention, malgré son nom qui prête à confusion, cet attribut, qui normalement est un o_socket, est un QPointF pendant la phase de construction du node !


5 - Drop hors d'un socket d'entrée : reset.

Retour au plan


6 - Drag  : déplacement de la souris, bouton gauche appuyé.

Retour au plan

 

 

Importer Qt, modifier les valeur à votre convenance.

Les valeurs choisies sont : blanc, pointillés, épaisseur 1.


7 - Survol pendant le drag d'un socket d'entrée valide : changement de sa couleur et de sa taille.

Retour au plan

Drag en cours : l'entrée1 accepte le drop.


8 - Drop sur un socket d'entrée valide : validation, enregistrement, reset.

Retour au plan

 


9 - Backup et contraintes technologiques :

Retour au plan


Vérification

TDD : 4 tests - Créer le fichier /tests/test_edges.py

Retirer le mode DEBUG.

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer
from pc.main import UiMain
import pytest
import os
import sys


class TestEdges:
    g_appli = QApplication(sys.argv)
    tempo = QTimer()
    tempo.setSingleShot(True)

    @pytest.fixture(autouse=True)  # scope=session (un seul appel), autouse=True (un appel par test)
    def setup(self):
        bk_test = os.path.abspath(f"{os.path.dirname(__file__)}/../backups/tests")
        os.makedirs(bk_test, exist_ok=True)  # Création dossier.
        o_main = self.open_window()
        o_main.resize(1200, 600)
        o_main.open_graph('tests')
        o_scene = None
        for i in range(o_main.tabGraphs.count()):
            if o_main.tabGraphs.widget(i).graph_name == 'tests':
                o_scene = o_main.tabGraphs.widget(i)
                break
        assert o_scene is not None
        assert o_scene.__class__.__name__ == 'CtrlScene'
        return {
            'main': o_main,
            'scene': o_scene
        }

    @staticmethod
    @pytest.fixture(scope="session", autouse=True)
    def cleanup(request):
        def remove_files():
            """ Appel final pour effacer les fichiers temporaires créés par les tests. """
            tests_folder = os.path.dirname(__file__)
            graph_folder = tests_folder.replace('tests', f'backups{os.sep}tests')
            files = [f'{tests_folder}{os.sep}params.conf']
            for file in files:
                if os.path.isfile(file):
                    os.remove(file)
            if os.path.isdir(graph_folder):
                os.rmdir(graph_folder)

        request.addfinalizer(remove_files)

    @staticmethod
    def open_window():
        win = UiMain()
        win.show()
        return win

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

    @staticmethod
    def build_graph(setup, l_nodes=None, s_edges=None):
        o_main = setup['main']
        o_scene = setup['scene']
        o_scene.o_pkl.od_pkl.clear()

        """ Valeurs par défaut. """
        if l_nodes is None:
            l_nodes = [
                (90, 'nodes.indicateurs.macd', (-256, -32)),
                (91, 'nodes.operateurs.union', (148, -96)),
            ]
        if s_edges is None:
            s_edges = {
                ((90, 0), (91, 4)),
                ((90, 2), (91, 2))
            }

        """ Dans od_pkl puis affichage. """
        for node in l_nodes:
            o_scene.o_pkl.od_pkl.write([f'Node{node[0]}', 'path'], node[1])
            o_scene.o_pkl.od_pkl.write([f'Node{node[0]}', 'position'], node[2])
        o_scene.o_pkl.od_pkl.write('edges', s_edges)
        o_scene.show_nodes()
        o_scene.show_edges()

        l_items = o_scene.o_grscene.items()
        l_grnodes, l_grsockets, l_gredges = list(), list(), list()
        for o_gritem in l_items:
            if o_gritem.__class__.__name__ == 'UiNode':
                l_grnodes.append(o_gritem)
            elif o_gritem.__class__.__name__ == 'UiSocket':
                l_grsockets.append(o_gritem)
            elif o_gritem.__class__.__name__ == 'UiEdge':
                l_gredges.append(o_gritem)

        return o_main, o_scene, l_grnodes, l_grsockets, l_gredges

    # ***************************************** TESTS *****************************************
    # @pytest.mark.skip
    def test_tid(self, setup):
        o_main, o_scene, lo_grnodes, lo_grsockets, lo_gredges = self.build_graph(setup)
        for o_gredge in lo_gredges:
            assert o_gredge.o_edge.get_tid() == (o_gredge.o_edge.o_socket_out.t_id, o_gredge.o_edge.o_socket_in.t_id)
        self.connect(o_main.close, 1000)
        self.g_appli.exec()

    # @pytest.mark.skip
    def test_save_edge(self, setup):
        o_main, o_scene, lo_grnodes, lo_grsockets, lo_gredges = self.build_graph(setup)
        o_scene.o_grscene.removeItem(lo_gredges[0])
        o_scene.save_edges()
        assert o_scene.o_pkl.od_pkl.read('edges') == set(o_gredge.o_edge.get_tid() for o_gredge in lo_gredges[1:])

        self.connect(o_main.close, 1000)
        self.g_appli.exec()

    # @pytest.mark.skip
    def test_get_gredges(self, setup):
        s_edges = {
            ((90, 0), (91, 4)),
            ((90, 2), (91, 0)),
            ((90, 2), (91, 1)),
            ((90, 2), (91, 2)),
        }
        o_main, o_scene, lo_grnodes, lo_grsockets, lo_gredges = self.build_graph(setup, s_edges=s_edges)
        for o_grsocket in lo_grsockets:
            s_gredges = set()
            for o_gredge in o_grsocket.o_socket.o_node.o_scene.o_grscene.items():
                if o_gredge.__class__.__name__ == 'UiEdge':
                    """ Choix du socket accroché à la bonne extrémité du edge. """
                    o_edge = o_gredge.o_edge
                    o_socket = o_edge.o_socket_in if o_grsocket.o_socket.b_input else o_edge.o_socket_out
                    if o_socket == o_grsocket.o_socket:
                        s_gredges.add(o_gredge)
            assert o_grsocket.o_socket.get_gredges() == s_gredges   # Méthode testée : CtrlSocket.get_gredges()

        self.connect(o_main.close, 1000)
        self.g_appli.exec()

    @pytest.mark.skip
    def test_get_grsockets(self, setup):
        o_main, o_scene, lo_grnodes, lo_grsockets, lo_gredges = self.build_graph(setup)
        l_grsockets_in, l_grsockets_out = list(), list()
        for o_grsocket in lo_grsockets:
            if o_grsocket.o_socket.b_input:
                l_grsockets_in.append(o_grsocket)
            else:
                l_grsockets_out.append(o_grsocket)

        t_grsockets = o_scene.get_grsockets()
        assert t_grsockets[0] == l_grsockets_in
        assert t_grsockets[1] == l_grsockets_out

        self.connect(o_main.close, 1000)
        self.g_appli.exec()

 


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 !