Merge pull request #1313 from ispyisail/feature/live-tools
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m50s

Add printing, save/close and a layout check to live mode
This commit is contained in:
ispyisail
2026-10-05 21:33:18 +13:00
committed by GitHub
8 changed files with 950 additions and 120 deletions
+132 -108
View File
@@ -7185,12 +7185,12 @@ Please use the advanced editor for this.</translation>
<context>
<name>LiveServer</name>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="108"/>
<location filename="../sources/scripting/liveserver.cpp" line="116"/>
<source>Mode direct</source>
<translation>Live mode</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="109"/>
<location filename="../sources/scripting/liveserver.cpp" line="117"/>
<source>Le mode direct est activé : un assistant IA connecté pourra exécuter des scripts sur le projet ouvert.
Chaque action s&apos;annule d&apos;un Ctrl+Z, et le bouton « Arrêter » de la barre d&apos;état coupe la connexion.
@@ -7203,53 +7203,77 @@ Each action is undone with one Ctrl+Z, and the “Stop” button in the status b
This setting is in Configure QElectroTech &gt; General &gt; Projects.</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="116"/>
<location filename="../sources/scripting/liveserver.cpp" line="124"/>
<source>&amp;Continuer</source>
<translation>&amp;Continue</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="117"/>
<location filename="../sources/scripting/liveserver.cpp" line="125"/>
<source>&amp;Pas pour cette session</source>
<translation>&amp;Not this session</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="118"/>
<location filename="../sources/scripting/liveserver.cpp" line="126"/>
<source>&amp;Désactiver</source>
<translation>&amp;Turn off</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="354"/>
<location filename="../sources/scripting/liveserver.cpp" line="508"/>
<location filename="../sources/scripting/liveserver.cpp" line="379"/>
<location filename="../sources/scripting/liveserver.cpp" line="903"/>
<source>script</source>
<translation>script</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="456"/>
<location filename="../sources/scripting/liveserver.cpp" line="726"/>
<source>Impression</source>
<translation>Print</translation>
</message>
<message numerus="yes">
<location filename="../sources/scripting/liveserver.cpp" line="727"/>
<source>L&apos;assistant veut imprimer %n folio(s) de « %1 » sur « %2 ».</source>
<translation>
<numerusform>The assistant wants to print %n folio of “%1” on “%2”.</numerusform>
<numerusform>The assistant wants to print %n folios of “%1” on “%2”.</numerusform>
</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="730"/>
<source>&amp;Imprimer</source>
<translation>&amp;Print</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="731"/>
<source>&amp;Annuler</source>
<translation>&amp;Cancel</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="818"/>
<location filename="../sources/scripting/liveserver.cpp" line="851"/>
<source>Assistant : %1</source>
<translation>Assistant: %1</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="504"/>
<location filename="../sources/scripting/liveserver.cpp" line="899"/>
<source>L&apos;assistant veut exécuter un script</source>
<translation>The assistant wants to run a script</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="507"/>
<location filename="../sources/scripting/liveserver.cpp" line="902"/>
<source>« %1 » sur le projet ouvert. Une fois exécuté, Ctrl+Z l&apos;annule.</source>
<translation>“%1” on the open project. Once it has run, Ctrl+Z undoes it.</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="515"/>
<location filename="../sources/scripting/liveserver.cpp" line="910"/>
<source>&amp;Exécuter</source>
<translation>&amp;Run</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="516"/>
<location filename="../sources/scripting/liveserver.cpp" line="911"/>
<source>&amp;Refuser</source>
<translation>&amp;Decline</translation>
</message>
<message>
<location filename="../sources/scripting/liveserver.cpp" line="517"/>
<location filename="../sources/scripting/liveserver.cpp" line="912"/>
<source>&amp;Toujours pour cette session</source>
<translation>&amp;Always this session</translation>
</message>
@@ -7765,90 +7789,90 @@ Do you still want to link this slave contact?</translation>
<context>
<name>NewElementWizard</name>
<message>
<location filename="../sources/newelementwizard.cpp" line="119"/>
<location filename="../sources/newelementwizard.cpp" line="120"/>
<source>Vous n&apos;êtes pas obligé de préciser l&apos;extension *.elmt. Elle sera ajoutée automatiquement.</source>
<translation>You don&apos;t have to specify the *.elmt extension. It will be added automatically.</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="117"/>
<location filename="../sources/newelementwizard.cpp" line="118"/>
<source>nouvel_element</source>
<translation>new_element</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="50"/>
<location filename="../sources/newelementwizard.cpp" line="51"/>
<source>&amp;Suivant &gt;</source>
<translation>&amp;Next &gt;</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="49"/>
<location filename="../sources/newelementwizard.cpp" line="50"/>
<source>Créer un nouvel élément : Assistant</source>
<comment>window title</comment>
<translation>Create a new element: wizard</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="85"/>
<location filename="../sources/newelementwizard.cpp" line="86"/>
<source>Étape 1/3 : Catégorie parente</source>
<comment>wizard page title</comment>
<translation>Step 1 of 3: Parent category</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="86"/>
<location filename="../sources/newelementwizard.cpp" line="87"/>
<source>Sélectionnez une catégorie dans laquelle enregistrer le nouvel élément.</source>
<comment>wizard page subtitle</comment>
<translation>Select a category which to save the new element in.</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="113"/>
<location filename="../sources/newelementwizard.cpp" line="114"/>
<source>Étape 2/3 : Nom du fichier</source>
<comment>wizard page title</comment>
<translation>Step 2 of 3: Filename</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="114"/>
<location filename="../sources/newelementwizard.cpp" line="115"/>
<source>Indiquez le nom du fichier dans lequel enregistrer le nouvel élément.</source>
<comment>wizard page subtitle</comment>
<translation>Enter the name of the file for the new element.</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="138"/>
<location filename="../sources/newelementwizard.cpp" line="139"/>
<source>Étape 3/3 : Noms de l&apos;élément</source>
<comment>wizard page title</comment>
<translation>Step 3 of 3: Element names</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="139"/>
<location filename="../sources/newelementwizard.cpp" line="140"/>
<source>Indiquez le ou les noms de l&apos;élément.</source>
<comment>wizard page subtitle</comment>
<translation>Enter one or more names for the element.</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="144"/>
<location filename="../sources/newelementwizard.cpp" line="145"/>
<source>Nom du nouvel élément</source>
<comment>default name when creating a new element</comment>
<translation>New element name</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="202"/>
<location filename="../sources/newelementwizard.cpp" line="220"/>
<location filename="../sources/newelementwizard.cpp" line="233"/>
<location filename="../sources/newelementwizard.cpp" line="203"/>
<location filename="../sources/newelementwizard.cpp" line="221"/>
<location filename="../sources/newelementwizard.cpp" line="234"/>
<source>Erreur</source>
<comment>message box title</comment>
<translation>Error</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="203"/>
<location filename="../sources/newelementwizard.cpp" line="204"/>
<source>Vous devez sélectionner une catégorie.</source>
<comment>message box content</comment>
<translation>You must select a category.</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="221"/>
<location filename="../sources/newelementwizard.cpp" line="222"/>
<source>Vous devez entrer un nom de fichier</source>
<comment>message box content</comment>
<translation>You must enter a filename</translation>
</message>
<message>
<location filename="../sources/newelementwizard.cpp" line="234"/>
<location filename="../sources/newelementwizard.cpp" line="235"/>
<source>Un élément portant le même nom existe déjà</source>
<translation>An element with the same name already exists</translation>
</message>
@@ -9812,7 +9836,7 @@ Available options:
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="899"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4041"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4043"/>
<source>&amp;Fermer</source>
<translation>&amp;Close</translation>
</message>
@@ -9874,8 +9898,8 @@ Available options:
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="669"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3154"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3177"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3156"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3179"/>
<source>Supprimer le folio</source>
<translation>Delete this sheet</translation>
</message>
@@ -10152,7 +10176,7 @@ Available options:
<translation>Depth</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2335"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2337"/>
<source>Groupe</source>
<translation>Group</translation>
</message>
@@ -10364,7 +10388,7 @@ Available options:
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="1776"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2882"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2884"/>
<source>Échec de l&apos;ouverture du projet</source>
<comment>message box title</comment>
<translation>Unable to open project</translation>
@@ -10377,8 +10401,8 @@ Available options:
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="163"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3459"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3462"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3461"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3464"/>
<source>QElectroTech</source>
<comment>window title</comment>
<translation>QElectroTech</translation>
@@ -10648,8 +10672,8 @@ Hold Ctrl while moving to place freely.</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="755"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3827"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3843"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3829"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3845"/>
<source>Recharger les dessins des éléments</source>
<translation>Reload element drawings</translation>
</message>
@@ -11027,7 +11051,7 @@ Hold Ctrl while moving to place freely.</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="1459"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3956"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3958"/>
<source>Scripts</source>
<translation>Scripts</translation>
</message>
@@ -11105,19 +11129,19 @@ If you save the project, these conductors will disappear from the file. Close it
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2270"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2272"/>
<source>Suppression de borne impossible</source>
<translation>Unable to delete the terminal</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2271"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2273"/>
<source>La suppression ne peut être effectué car la selection possède une ou plusieurs bornes ponté et/ou appartenant à une borne à niveau multiple.
Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les supprimer</source>
<translation>The deletion cannot be performed because the selection contains one or more terminals that are bridged and/or belong to a multi-level terminal.
Unbridge and/or remove the levels from the affected terminals so that they can be deleted</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="2374"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2376"/>
<source>%n objet(s) remis sur la grille</source>
<translation>
<numerusform>%n item put back on the grid</numerusform>
@@ -11125,7 +11149,7 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="2375"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2377"/>
<source>%n objet(s) aligné(s)</source>
<translation>
<numerusform>%n item aligned</numerusform>
@@ -11133,22 +11157,22 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2381"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2383"/>
<source>La sélection est déjà sur la grille</source>
<translation>The selection is already on the grid</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2383"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2385"/>
<source>Sélectionnez au moins deux éléments, images, textes, formes ou groupes non verrouillés</source>
<translation>Select at least two unlocked elements, images, texts, shapes or groups</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2385"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2387"/>
<source>La sélection est déjà alignée, à la grille près</source>
<translation>The selection is already aligned, to the nearest grid step</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="2389"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2391"/>
<source>(%n objet(s) verrouillé(s) laissé(s) en place)</source>
<translation>
<numerusform>(%n locked object left in place)</numerusform>
@@ -11156,100 +11180,100 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2623"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2625"/>
<source>Éditer l&apos;élement</source>
<comment>edit element</comment>
<translation>Edit the element</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2630"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2632"/>
<source>Éditer le champ de texte</source>
<comment>edit text field</comment>
<translation>Edit the text field</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2637"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2639"/>
<source>Éditer l&apos;image</source>
<comment>edit image</comment>
<translation>Edit the image</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2644"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2646"/>
<source>Éditer le conducteur</source>
<comment>edit conductor</comment>
<translation>Edit conductor</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2652"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2654"/>
<source>Éditer l&apos;objet sélectionné</source>
<comment>edit selected item</comment>
<translation>Edit the selected object</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2883"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2885"/>
<source>Une erreur est survenue lors de l&apos;ouverture du fichier %1.</source>
<comment>message box content</comment>
<translation>An error occurred while opening file %1.</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="2931"/>
<location filename="../sources/qetdiagrameditor.cpp" line="2933"/>
<source>Active le projet « %1 »</source>
<translation>Activates the project « %1 »</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3155"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3157"/>
<source>Êtes-vous sûr de vouloir supprimer ce folio ?</source>
<translation>Are you sure you want to delete this sheet?</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3160"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3162"/>
<source>Supprimer les folios</source>
<translation>Delete the sheets</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3161"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3163"/>
<source>Êtes-vous sûr de vouloir supprimer les %1 folios sélectionnés ?</source>
<translation>Are you sure you want to delete the selected %1 sheets ?</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3178"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3180"/>
<source>Supprimer %1 folios</source>
<translation>Delete %1 sheets</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3216"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3234"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3252"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3270"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3288"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3306"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3324"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3218"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3236"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3254"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3272"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3290"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3308"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3326"/>
<source>Déplacer les folios</source>
<translation>Move the sheets</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3399"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3401"/>
<source>Erreur</source>
<comment>message box title</comment>
<translation>Error</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3525"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3527"/>
<source>Répéter : insérer « %1 »</source>
<translation>Repeat: insert &quot;%1&quot;</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3526"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3528"/>
<source>Répéter : %1</source>
<translation>Repeat: %1</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3808"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3810"/>
<source>folio %1</source>
<translation>sheet %1</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="3816"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3818"/>
<source>%n élément(s) redessiné(s).</source>
<translation>
<numerusform>%n element redrawn.</numerusform>
@@ -11257,7 +11281,7 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="3820"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3822"/>
<source>%n élément(s) dont la définition est introuvable ou illisible : leur dessin actuel a été conservé.</source>
<translation>
<numerusform>%n element for which no definition could be found or which were illegible: their current design has been retained.</numerusform>
@@ -11265,7 +11289,7 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="3832"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3834"/>
<source>%n élément(s) non redessiné(s) : leur taille, leur point de saisie ou leurs bornes ont changé (borne ajoutée, supprimée ou déplacée).</source>
<translation>
<numerusform>%n element not redrawn: its size, grip point or terminals have changed (a terminal was added, removed or moved).</numerusform>
@@ -11273,18 +11297,18 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3836"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3838"/>
<source>Pour les mettre à jour, il faut les supprimer puis les réinsérer. Attention : cette opération supprime les conducteurs déjà reliés à ces éléments, qu&apos;il faudra retracer.</source>
<translation>To update them, they must be deleted then reinserted. Warning: this operation removes the conductors already connected to these elements, which will need to be redrawn.</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3865"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3869"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3867"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3871"/>
<source>Exécuter un script</source>
<translation>Run a script</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3894"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3896"/>
<source>Les scripts sont désactivés.
Un script s&apos;exécute avec vos droits : il peut lire et modifier le projet ouvert et écrire des fichiers. N&apos;exécutez que des scripts dont vous connaissez l&apos;origine.
@@ -11297,28 +11321,28 @@ A script runs with your permissions: it can read and modify the open project and
Enable scripts? This setting can be changed in Configure QElectroTech &gt; General &gt; Projects.</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3871"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3873"/>
<source>Scripts JavaScript (*.js);;Tous les fichiers (*)</source>
<translation>JavaScript scripts (*.js);;All files (*)</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3970"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3972"/>
<source>Ignoré : %1</source>
<translation>Ignored: %1</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4004"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4064"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4006"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4066"/>
<source>Arrêter</source>
<translation>Stop</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4005"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4007"/>
<source>Arrêter l&apos;enregistrement de la macro</source>
<translation>Stop recording the macro</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="4015"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4017"/>
<source>● Enregistrement : %n étape(s)</source>
<translation>
<numerusform>● Recording: %n step</numerusform>
@@ -11326,12 +11350,12 @@ Enable scripts? This setting can be changed in Configure QElectroTech &gt; Gener
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4030"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4032"/>
<source>Macro enregistrée</source>
<translation>Macro recorded</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="4031"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4033"/>
<source>« %1 » : %n étape(s).
Pour en faire un script, demandez-le à votre assistant IA : le bouton ci-dessous copie la demande, il suffit de la coller dans sa fenêtre.</source>
@@ -11345,58 +11369,58 @@ To make a script of it, ask your AI assistant: the button below copies the reque
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4039"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4041"/>
<source>&amp;Copier la demande pour l&apos;assistant</source>
<translation>&amp;Copy the request for the assistant</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4040"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4042"/>
<source>&amp;Ouvrir le dossier</source>
<translation>&amp;Open the folder</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4046"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4048"/>
<source>Demande copiée : collez-la dans la fenêtre de l&apos;assistant</source>
<translation>Request copied: paste it into the assistant&apos;s window</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4065"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4067"/>
<source>Couper la connexion de l&apos;assistant pour le reste de la session</source>
<translation>Close the assistant&apos;s connection for the rest of the session</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4074"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4076"/>
<source>Mode direct : assistant connecté</source>
<translation>Live mode: assistant connected</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4075"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4077"/>
<source>Mode direct : en attente d&apos;un assistant</source>
<translation>Live mode: waiting for an assistant</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4083"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4127"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4085"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4129"/>
<source>Assistant</source>
<translation>Assistant</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4087"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4089"/>
<source>Demander avant d&apos;exécuter un script écrit par l&apos;assistant</source>
<translation>Ask before running a script the assistant wrote</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4089"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4091"/>
<source>Pour cette session seulement : chaque démarrage redemande</source>
<translation>For this session only: every start asks again</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4144"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4146"/>
<source>Mode direct : %1 %2 à %3</source>
<translation>Live mode: %1 %2 at %3</translation>
</message>
<message numerus="yes">
<location filename="../sources/qetdiagrameditor.cpp" line="4186"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4188"/>
<source>%n type(s) d&apos;objets masqué(s)</source>
<translation>
<numerusform>%n kind of item hidden</numerusform>
@@ -11404,8 +11428,8 @@ To make a script of it, ask your AI assistant: the button below copies the reque
</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="4202"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4207"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4204"/>
<location filename="../sources/qetdiagrameditor.cpp" line="4209"/>
<source>Textes %1</source>
<translation>Texts %1</translation>
</message>
@@ -14342,22 +14366,22 @@ Do you want to replace it ?</translation>
<translation>Creation of conductors</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3673"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3675"/>
<source>To install the plugin qet_tb_generator&lt;br&gt;Visit :&lt;br&gt;&lt;a href=&apos;https://pypi.python.org/pypi/qet-tb-generator&apos;&gt;qet-tb-generator&lt;/a&gt;&lt;br&gt;Requires python 3.5 or above.&lt;br&gt;&lt;B&gt;&lt;U&gt; First install on Windows&lt;/B&gt;&lt;/U&gt;&lt;br&gt;1. Install, if required, python 3.5 or above&lt;br&gt; Visit :&lt;br&gt;&lt;a href=&apos;https://www.python.org/downloads/&apos;&gt;python.org&lt;/a&gt;&lt;br&gt;2. pip install qet_tb_generator&lt;br&gt;&lt;B&gt;&lt;U&gt; Update on Windows&lt;/B&gt;&lt;/U&gt;&lt;br&gt;python -m pip install --upgrade qet_tb_generator&lt;br&gt;&gt;&gt;user could launch in a terminal this script in this directory&lt;br&gt; C:\users\XXXX\AppData\Local\Programs\Python\Python36-32\Scripts &lt;br&gt;</source>
<translation>To install the plugin qet_tb_generator&lt;br&gt;Visit :&lt;br&gt;&lt;a href=&apos;https://pypi.python.org/pypi/qet-tb-generator&apos;&gt;qet-tb-generator&lt;/a&gt;&lt;br&gt;Requires python 3.5 or above.&lt;br&gt;&lt;B&gt;&lt;U&gt; First install on Windows&lt;/B&gt;&lt;/U&gt;&lt;br&gt;1. Install, if required, python 3.5 or above&lt;br&gt; Visit :&lt;br&gt;&lt;a href=&apos;https://www.python.org/downloads/&apos;&gt;python.org&lt;/a&gt;&lt;br&gt;2. pip install qet_tb_generator&lt;br&gt;&lt;B&gt;&lt;U&gt; Update on Windows&lt;/B&gt;&lt;/U&gt;&lt;br&gt;python -m pip install --upgrade qet_tb_generator&lt;br&gt;&gt;&gt;user could launch in a terminal this script in this directory&lt;br&gt; C:\users\XXXX\AppData\Local\Programs\Python\Python36-32\Scripts &lt;br&gt;</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3693"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3695"/>
<source>To install the plugin qet_tb_generator&lt;br&gt;Visit :&lt;br&gt;&lt;a href=&apos;https://pypi.python.org/pypi/qet-tb-generator&apos;&gt;qet-tb-generator&lt;/a&gt;&lt;br&gt;&lt;B&gt;&lt;U&gt; First install on macOSX&lt;/B&gt;&lt;/U&gt;&lt;br&gt;1. Install, if required, python 3.11 bundle only, &lt;a href=&apos;https://www.python.org/ftp/python/3.11.2/python-3.11.2-macos11.pkg&apos;&gt;python-3.11.2-macos11.pkg&lt;/a&gt;&lt;br&gt;2 Run Profile.command script&lt;br&gt;because program use hardcoded PATH for localise qet-tb-generator plugin &lt;br&gt; Visit :&lt;br&gt;&lt;a href=&apos;https://qelectrotech.org/forum/viewtopic.php?pid=5674#p5674&apos;&gt;howto&lt;/a&gt;&lt;br&gt;2. pip3 install qet_tb_generator&lt;br&gt;&lt;B&gt;&lt;U&gt; Update on macOSX&lt;/B&gt;&lt;/U&gt;&lt;br&gt; pip3 install --upgrade qet_tb_generator&lt;br&gt;</source>
<translation>To install the plugin qet_tb_generator&lt;br&gt;Visit :&lt;br&gt;&lt;a href=&apos;https://pypi.python.org/pypi/qet-tb-generator&apos;&gt;qet-tb-generator&lt;/a&gt;&lt;br&gt;&lt;B&gt;&lt;U&gt; First install on macOSX&lt;/B&gt;&lt;/U&gt;&lt;br&gt;1. Install, if required, python 3.11 bundle only, &lt;a href=&apos;https://www.python.org/ftp/python/3.11.2/python-3.11.2-macos11.pkg&apos;&gt;python-3.11.2-macos11.pkg&lt;/a&gt;&lt;br&gt;2 Run Profile.command script&lt;br&gt;because program use hardcoded PATH for localise qet-tb-generator plugin &lt;br&gt; Visit :&lt;br&gt;&lt;a href=&apos;https://qelectrotech.org/forum/viewtopic.php?pid=5674#p5674&apos;&gt;howto&lt;/a&gt;&lt;br&gt;2. pip3 install qet_tb_generator&lt;br&gt;&lt;B&gt;&lt;U&gt; Update on macOSX&lt;/B&gt;&lt;/U&gt;&lt;br&gt; pip3 install --upgrade qet_tb_generator&lt;br&gt;</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3712"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3714"/>
<source>To install the plugin qet_tb_generator&lt;br&gt;Visit :&lt;br&gt;&lt;a href=&apos;https://pypi.python.org/pypi/qet-tb-generator&apos;&gt;qet-tb-generator&lt;/a&gt;&lt;br&gt;&lt;br&gt;Requires python 3.5 or above.&lt;br&gt;&lt;br&gt;&lt;B&gt;&lt;U&gt; First install on Linux&lt;/B&gt;&lt;/U&gt;&lt;br&gt;1. check you have pip3 installed: pip3 --version&lt;br&gt;If not install with: sudo apt-get install python3-pip&lt;br&gt;2. Install the program: sudo pip3 install qet_tb_generator&lt;br&gt;3. Run the program: qet_tb_generator&lt;br&gt;&lt;br&gt;&lt;B&gt;&lt;U&gt; Update on Linux&lt;/B&gt;&lt;/U&gt;&lt;br&gt;sudo pip3 install --upgrade qet_tb_generator&lt;br&gt;</source>
<translation>To install the plugin qet_tb_generator&lt;br&gt;Visit :&lt;br&gt;&lt;a href=&apos;https://pypi.python.org/pypi/qet-tb-generator&apos;&gt;qet-tb-generator&lt;/a&gt;&lt;br&gt;&lt;br&gt;Requires python 3.5 or above.&lt;br&gt;&lt;br&gt;&lt;B&gt;&lt;U&gt; First install on Linux&lt;/B&gt;&lt;/U&gt;&lt;br&gt;1. check you have pip3 installed: pip3 --version&lt;br&gt;If not install with: sudo apt-get install python3-pip&lt;br&gt;2. Install the program: sudo pip3 install qet_tb_generator&lt;br&gt;3. Run the program: qet_tb_generator&lt;br&gt;&lt;br&gt;&lt;B&gt;&lt;U&gt; Update on Linux&lt;/B&gt;&lt;/U&gt;&lt;br&gt;sudo pip3 install --upgrade qet_tb_generator&lt;br&gt;</translation>
</message>
<message>
<location filename="../sources/qetdiagrameditor.cpp" line="3732"/>
<location filename="../sources/qetdiagrameditor.cpp" line="3734"/>
<source>Error launching qet_tb_generator plugin</source>
<translation>Error launching qet_tb_generator plugin</translation>
</message>
@@ -14980,27 +15004,27 @@ Please download it by following the link and unzip it in the installation folder
<translation>Rotate an image</translation>
</message>
<message>
<location filename="../sources/scripting/qetscriptapi.cpp" line="3780"/>
<location filename="../sources/scripting/qetscriptapi.cpp" line="3799"/>
<source>Modifier un texte d&apos;élément</source>
<translation>Edit element text</translation>
</message>
<message>
<location filename="../sources/scripting/qetscriptapi.cpp" line="3862"/>
<location filename="../sources/scripting/qetscriptapi.cpp" line="3881"/>
<source>Numéroter automatiquement un élément</source>
<translation>Automatically number an element</translation>
</message>
<message>
<location filename="../sources/scripting/qetscriptapi.cpp" line="3904"/>
<location filename="../sources/scripting/qetscriptapi.cpp" line="3923"/>
<source>Renuméroter les éléments (%1)</source>
<translation>Renumber elements (%1)</translation>
</message>
<message>
<location filename="../sources/scripting/qetscriptapi.cpp" line="4816"/>
<location filename="../sources/scripting/qetscriptapi.cpp" line="4835"/>
<source>Rechercher et remplacer</source>
<translation>Search and replace</translation>
</message>
<message>
<location filename="../sources/scripting/qetscriptapi.cpp" line="5090"/>
<location filename="../sources/scripting/qetscriptapi.cpp" line="5109"/>
<location filename="../sources/scripting/qetscripting.cpp" line="145"/>
<location filename="../sources/scripting/qetscripting.cpp" line="230"/>
<source>Script</source>
+9 -2
View File
@@ -40,9 +40,9 @@ here read the model.
| `qet_project_new` | **start from nothing** — an empty project with a title and folios |
| `qet_element_search` | **find a symbol** in a collection by name (any language), type or terminal count |
| `qet_check` | **design-rule checks** — duplicate labels, unlabelled masters, unnumbered conductors, empty folios, terminals with more than four wires, folio reports with several wires |
| `qet_layout_check` | **does the drawing read well?** — a 0–100 score; wires that jog because two symbols are a few pixels out of line, symbols off the grid, wires through symbols, overlaps, crossings; and the moves that fix them, ready for `qet_edit` |
| `qet_layout_check` | **does the drawing read well?** — a 0–100 score; wires that jog because two symbols are a few pixels out of line, symbols off the grid, wires through symbols, overlaps, crossings, labels over wires, 4-way junction dots, side branches out of line; and the moves that fix them, ready for `qet_edit` |
| `qet_query` | **ask the project database** — read-only SQL over the views and tables |
| `qet_about` | **start here** — where QElectroTech keeps things, what is switched on, the stored scripts, the calls a script can make (from `qet-assistant.json`) |
| `qet_about` | **start here** — where QElectroTech keeps things, what is switched on, the stored scripts, the calls a script can make (from `qet-assistant.json`), and the installation's standard symbols if it has any |
| `qet_script_api` | **what a script can call** — every `qet.*` call of this build, and the header that makes a script a button |
| `qet_script_test` | **try a script** on a copy of a project: what it would change, what it logged, its errors |
| `qet_script_install` | **make a button** — store a script (and an SVG icon) where QElectroTech shows it in Project > Scripts and the Scripts toolbar |
@@ -233,6 +233,8 @@ file or one listed by whoever configured the server:
| `QET_MCP_BINARIES` | other executables a call may name, separated like `QET_MCP_WORKSPACE` (for comparing two builds) |
| `QET_MCP_ALLOW_ANY_BINARY=1` | turns the check off: a call can then run any program |
| `QET_MCP_ELEMENTS` | element collections a call may name as `elements_dir` besides the workspace and the installed one |
| `QET_MCP_CACHE_DIR` | where the parsed element index is kept between runs (default `~/.cache/qet-mcp`); a new server then loads a collection in well under a second instead of parsing every symbol |
| `QET_MCP_STANDARD_SYMBOLS` | the installation's standard symbols, one per device role (default `standard-symbols.json` in QElectroTech's data folder); `qet_about` lists them |
Anything else is refused, even a file inside the workspace: being there
makes it readable, not runnable. Before this rule any executable a call
@@ -339,6 +341,11 @@ QElectroTech, in front of you, so you can watch, stop or undo:
| `qet_live_new_project` | a new project, as File > New makes it, made current; optional title, number of folios and a file to save it to (never over an existing one) |
| `qet_live_open_project` | open a saved project and make it current, with no dialog; one already open is only made current |
| `qet_live_switch_project` | make another open project current, by its index in `qet_live_status`'s `projects` or its file |
| `qet_live_save_project` | save the current project, or save it as a new file (never over an existing one) |
| `qet_live_close_project` | close a project, only when it has no unsaved changes |
| `qet_live_print` | print folios to the default or a named printer with no print dialog -- QElectroTech always asks you first -- or to a new PDF file |
| `qet_live_changes` | the undo history, each step marked assistant or you; `since` gives what changed after a point |
| `qet_live_layout_check` | `qet_layout_check` on the drawing as it is on screen, unsaved changes included, without saving it |
Every `qet_live_*` call works on the current project. Pages and links
between pages need no tool of their own: in `qet_live_run_script`,
+398 -9
View File
@@ -2402,17 +2402,71 @@ _SECONDARY_FOLDER = re.compile(r"(?:^|/)\d+_(?:graphics|manufacturers_articles|m
def _collection_signature(root: Path):
"""Cheap change detector: file count and newest mtime, no parsing."""
"""Cheap change detector: file count and newest mtime, no parsing.
os.scandir() rather than Path.rglob(): the same answer in about a third
of the time, which every search pays."""
count, newest = 0, 0.0
for f in root.rglob("*.elmt"):
count += 1
stack = [str(root)]
while stack:
try:
newest = max(newest, f.stat().st_mtime)
it = os.scandir(stack.pop())
except OSError:
pass
continue
with it:
for entry in it:
try:
if entry.is_dir(follow_symlinks=False):
stack.append(entry.path)
elif entry.name.endswith(".elmt"):
count += 1
newest = max(newest, entry.stat().st_mtime)
except OSError:
pass
return count, newest
def _index_cache_file(root: Path) -> Path:
"""Where the parsed index of one collection is kept between runs."""
base = os.environ.get("QET_MCP_CACHE_DIR") or os.path.join(
os.environ.get("XDG_CACHE_HOME") or os.path.join(str(Path.home()), ".cache"), "qet-mcp")
import hashlib
digest = hashlib.sha1(str(root).encode("utf-8")).hexdigest()[:16]
return Path(base) / f"element-index-{digest}.json"
_INDEX_CACHE_FORMAT = 1
def _load_index_cache(root: Path, sig) -> list | None:
try:
data = json.loads(_index_cache_file(root).read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if (data.get("format") != _INDEX_CACHE_FORMAT or data.get("root") != str(root)
or tuple(data.get("sig") or ()) != tuple(sig)):
return None
items = data.get("items") or []
for it in items:
it["haystack"] = frozenset(it.get("haystack") or ())
return items
def _save_index_cache(root: Path, sig, items: list) -> None:
"""Best effort: a cache that cannot be written only costs the next
start its 6 s; it never fails a search."""
path = _index_cache_file(root)
try:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps({
"format": _INDEX_CACHE_FORMAT, "root": str(root), "sig": list(sig),
"items": [dict(it, haystack=sorted(it["haystack"])) for it in items]}),
encoding="utf-8")
os.replace(tmp, path)
except OSError:
pass
def _index_collection(root: Path) -> list:
"""Parse every .elmt under root once and keep what a search needs.
@@ -2426,6 +2480,10 @@ def _index_collection(root: Path) -> list:
cached = _ELEMENT_INDEX.get(key)
if cached and cached["sig"] == sig:
return cached["items"]
items = _load_index_cache(root.resolve(), sig)
if items is not None:
_ELEMENT_INDEX[key] = {"sig": sig, "items": items}
return items
items = []
for f in sorted(root.rglob("*.elmt")):
@@ -2457,6 +2515,7 @@ def _index_collection(root: Path) -> list:
"haystack": frozenset(_search_words(" ".join([*names.values(), rel, kind]))),
})
_ELEMENT_INDEX[key] = {"sig": sig, "items": items}
_save_index_cache(root.resolve(), sig, items)
return items
@@ -2768,6 +2827,27 @@ LAYOUT_RULES = {
"are on. An axis a straight wire lines it up on is left alone. "
"\"fix\" moves it onto the grid.",
},
"label_on_wire": {
"severity": "warning",
"note": "A symbol's label is drawn over a wire. House style: a label sits at "
"least 20 px past the outermost connected terminal, on a side no "
"wire arrives from. Move the label (setElementTextProperty x/y), not "
"the symbol. Needs a QElectroTech with elementTextGeometry(); older "
"builds skip this rule.",
},
"four_way_junction": {
"severity": "warning",
"note": "Wires leave one point in all four directions: a 4-way dot, which "
"reads as a crossing when printed. Stagger it into two 3-way "
"T-junctions a grid step or more apart.",
},
"misaligned_branch": {
"severity": "info",
"note": "Two symbols stacked in the same side branch (each fed by a wire with "
"a horizontal run) are a little out of line, so the branch is not one "
"straight column. House style: parallel branches share one vertical "
"axis. \"dx\" is how far apart their wire docks are.",
},
"crossing": {
"severity": "info",
"note": "Two wires cross. Counted so two drafts can be compared; some "
@@ -2784,13 +2864,25 @@ _LAYOUT_SEG = re.compile(r"^\s*\d+:\s*\(([^,]+),([^)]+)\)-\(([^,]+),([^)]+)\)")
_LAYOUT_JS = r"""
var only = @FOLIO@;
var byUuid = typeof qet.conductorPath === 'function';
function labelBox(f, u) {
if (typeof qet.elementTextGeometry !== 'function') return null;
var t = qet.elementTexts(f, u);
for (var i = 0; i < t.length; i++)
if (qet.elementTextProperty(f, u, i, 'source') === 'info'
&& qet.elementTextProperty(f, u, i, 'info') === 'label') {
var g = qet.elementTextGeometry(f, u, i);
return (g && g.right > g.left && qet.elementTextProperty(f, u, i, 'shows')) ? g : null;
}
return null;
}
for (var f = 0; f < qet.folioCount(); f++) {
if (only >= 0 && f !== only) continue;
var els = qet.elementUuids(f), E = [];
for (var i = 0; i < els.length; i++) {
E.push({uuid: els[i], name: qet.elementName(f, els[i]),
label: qet.elementLabel(f, els[i]), g: qet.elementGeometry(f, els[i]),
terminals: qet.elementTerminals(f, els[i]).length});
terminals: qet.elementTerminals(f, els[i]).length,
labelbox: labelBox(f, els[i])});
}
var cu = qet.conductorUuids(f), lines = qet.conductors(f), C = [];
for (var j = 0; j < cu.length; j++) {
@@ -2951,7 +3043,9 @@ def _layout_folio(data: dict, max_shift: float) -> dict:
"annotation": int(el.get("terminals", 1) or 0) == 0,
"x": float(g.get("x", 0)), "y": float(g.get("y", 0)),
"xy": (float(g.get("x", 0)), float(g.get("y", 0))),
"box": box, "docks": []}
"box": box, "docks": [],
"labelbox": _box(el.get("labelbox") or {})
if el.get("labelbox") else None}
wire_count = {}
wires, unread = [], []
for w in data.get("conductors") or []:
@@ -3214,6 +3308,8 @@ def _layout_folio(data: dict, max_shift: float) -> dict:
add("crossing", conductors=[w["uuid"], v["uuid"]], count=n,
note=LAYOUT_RULES["crossing"]["note"])
_house_style_rules(symbols, wires, add, dirty_symbols)
return {"folio": folio, "symbols": len(symbols), "wires": len(wires),
"unread": unread, "findings": findings, "dirty_wires": dirty_wires,
"dirty_symbols": dirty_symbols, "length": length, "crossings": crossings,
@@ -3221,6 +3317,86 @@ def _layout_folio(data: dict, max_shift: float) -> dict:
"fixes": [move_op(u) for u in move]}
def _house_style_rules(symbols: dict, wires: list, add, dirty_symbols: set) -> None:
"""The house-style rules: labels over wires, 4-way junctions, side
branches out of line. Pure geometry, like the rest of the scorer."""
def seg_hits_box(a, b, box, inset=1.0):
l, t, r, btm = box[0] + inset, box[1] + inset, box[2] - inset, box[3] - inset
if l >= r or t >= btm:
return False
(x1, y1), (x2, y2) = a, b
if abs(y1 - y2) < 1e-6: # horizontal
return t <= y1 <= btm and min(x1, x2) <= r and max(x1, x2) >= l
if abs(x1 - x2) < 1e-6: # vertical
return l <= x1 <= r and min(y1, y2) <= btm and max(y1, y2) >= t
return False
for s in symbols.values():
lb = s.get("labelbox")
if not lb:
continue
hit = [w["uuid"] for w in wires
if any(seg_hits_box(a, b, lb) for a, b in zip(w["pts"], w["pts"][1:]))]
if hit:
add("label_on_wire", element=s["uuid"], label=s["label"], name=s["name"],
conductors=hit, note=LAYOUT_RULES["label_on_wire"]["note"])
dirty_symbols.add(s["uuid"])
def key(p):
return (round(p[0]), round(p[1]))
def direction(a, b):
dx, dy = b[0] - a[0], b[1] - a[1]
if abs(dx) < 1e-6 and abs(dy) < 1e-6:
return None
if abs(dx) >= abs(dy):
return "e" if dx > 0 else "w"
return "s" if dy > 0 else "n"
ways, owners = {}, {}
for w in wires:
pts = w["pts"]
for i, p in enumerate(pts):
for q in ((pts[i - 1],) if i else ()) + ((pts[i + 1],) if i + 1 < len(pts) else ()):
d = direction(p, q)
if d:
ways.setdefault(key(p), set()).add(d)
owners.setdefault(key(p), set()).add(w["uuid"])
for pt, d in sorted(ways.items()):
if len(d) == 4 and len(owners[pt]) >= 2:
add("four_way_junction", at=list(pt), conductors=sorted(owners[pt]),
note=LAYOUT_RULES["four_way_junction"]["note"])
# A side branch symbol is fed only by wires that jog sideways; one
# joined to a neighbour by a straight vertical wire is in a column.
side, column = set(), set()
for w in wires:
xs = {round(p[0], 3) for p in w["pts"]}
ends = [e for e in w["ends"] if e]
if len(xs) == 1:
column.update(ends)
elif any(abs(a[1] - b[1]) < 1e-6 and abs(a[0] - b[0]) > 1e-6
for a, b in zip(w["pts"], w["pts"][1:])):
side.update(ends)
side -= column
cand = [s for s in symbols.values() if s["uuid"] in side and s["docks"]
and not s["annotation"]]
# Two symbols joined by a wire of their own are avoidable_bend's case.
joined = {frozenset(w["ends"]) for w in wires if all(w["ends"])}
for i, s in enumerate(cand):
for t in cand[i + 1:]:
sb, tb = s["box"], t["box"]
if sb[1] < tb[3] and tb[1] < sb[3]:
continue # side by side, not stacked
if frozenset((s["uuid"], t["uuid"])) in joined:
continue
dx = min(abs(p[0] - q[0]) for p in s["docks"] for q in t["docks"])
if 0.5 < dx <= 80:
add("misaligned_branch", elements=[s["uuid"], t["uuid"]],
labels=[s["label"], t["label"]], names=[s["name"], t["name"]],
dx=round(dx, 1), note=LAYOUT_RULES["misaligned_branch"]["note"])
def _layout_answer(folios: list, style: str, limit: int) -> dict:
"""Combine per-folio results into the tool's answer."""
symbols = sum(f["symbols"] for f in folios)
@@ -3253,6 +3429,9 @@ def _layout_answer(folios: list, style: str, limit: int) -> dict:
"overlaps": count["overlapping_symbols"],
"off_grid": count["off_grid"],
"crossings": sum(f["crossings"] for f in folios),
"labels_on_wires": count["label_on_wire"],
"four_way_junctions": count["four_way_junction"],
"misaligned_branches": count["misaligned_branch"],
"flow": {"vertical": round(vertical / total, 3) if total else 0.0,
"horizontal": round(horizontal / total, 3) if total else 0.0},
},
@@ -3669,6 +3848,36 @@ SERVER_INSTRUCTIONS = (
"a wire is straight only when its two terminals are exactly in line.")
def standard_symbols_file(info: dict | None = None) -> Path:
"""The installation's standard symbols: one symbol per device role,
chosen by the user. QET_MCP_STANDARD_SYMBOLS if set, else
standard-symbols.json in QElectroTech's data folder."""
explicit = os.environ.get("QET_MCP_STANDARD_SYMBOLS", "").strip()
if explicit:
return Path(explicit).expanduser()
data = ((info or {}).get("folders") or {}).get("data")
return (Path(data) if data else assistant_info_file().parent) / "standard-symbols.json"
def standard_symbols(info: dict | None = None):
"""The roles from standard-symbols.json, or None when there is none.
A file that cannot be read is reported, not hidden."""
path = standard_symbols_file(info)
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return None
except (OSError, ValueError) as exc:
return {"error": f"{path} could not be read: {exc}"}
roles = data.get("roles") if isinstance(data, dict) else None
if not isinstance(roles, list):
return {"error": f"{path} has no \"roles\" list"}
return {"file": str(path), "updated": data.get("updated"),
"roles": [{k: r.get(k) for k in ("id", "label", "letter", "path",
"terminals", "note") if r.get(k) is not None}
for r in roles if isinstance(r, dict) and r.get("path")]}
def tool_about() -> dict:
"""What this server and the QElectroTech it works with look like now."""
info = assistant_info()
@@ -3701,6 +3910,9 @@ def tool_about() -> dict:
"script_api": info.get("script_api"),
# Never the token: it is for the live tools, not the conversation.
"live": {"open": bool(live), "pid": (live or {}).get("pid")},
# One symbol per device role, the user's own choice: use these
# paths with add_element / qet.addElement before searching.
"standard_symbols": standard_symbols(info),
})
return out
@@ -4124,6 +4336,100 @@ def tool_live_switch_project(index: int | None = None, path: str = "") -> dict:
return _live_call({"cmd": "switch_project", "path": str(p)})
def _abs_path(value, arg: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"'{arg}' must be a file path")
p = Path(value).expanduser()
if not p.is_absolute():
raise ValueError(f"'{arg}' must be absolute, e.g. /home/me/projects/pump.qet")
return str(p)
def _index_arg(index):
if index is not None and (not isinstance(index, int) or isinstance(index, bool) or index < 0):
raise ValueError("'index' must be a whole number counted from 0")
return index
def tool_live_save_project(path: str = "") -> dict:
_require_script_consent()
request = {"cmd": "save_project"}
if path:
request["path"] = _abs_path(path, "path")
return _live_call(request)
def tool_live_close_project(index: int | None = None) -> dict:
_require_script_consent()
request = {"cmd": "close_project"}
if _index_arg(index) is not None:
request["index"] = index
return _live_call(request)
def tool_live_print(folios="all", printer: str = "", output_file: str = "") -> dict:
_require_script_consent()
if isinstance(folios, list):
if not folios or not all(isinstance(i, int) and not isinstance(i, bool) and i >= 0
for i in folios):
raise ValueError("'folios' as a list holds folio indexes counted from 0")
elif folios not in ("all", "current"):
raise ValueError("'folios' must be \"all\", \"current\" or a list of indexes")
if printer and output_file:
raise ValueError("give 'printer' or 'output_file', not both")
request = {"cmd": "print", "folios": folios}
if printer:
request["printer"] = printer
if output_file:
request["output_file"] = _abs_path(output_file, "output_file")
# The user answers QElectroTech's question before anything prints.
return _live_call(request, timeout=300)
def tool_live_changes(since: int | None = None) -> dict:
request = {"cmd": "changes"}
if since is not None:
if not isinstance(since, int) or isinstance(since, bool) or since < -1:
raise ValueError("'since' must be a step index from an earlier answer (or -1)")
request["since"] = since
return _live_call(request)
def tool_live_layout_check(folio: str | int = "current", style: str = "auto",
max_shift: float = 40, limit: int = 50,
elements_dir: str | None = None, timeout: int = 180,
binary: str | None = None) -> dict:
"""qet_layout_check on what the user sees: QElectroTech writes a copy of
the open project (unsaved changes included) to a private folder, the
check reads the copy, the copy is deleted. The user's project and file
are not touched, and no "run this script?" window is shown."""
binary = binary or resolve_binary()
if not binary:
raise ValueError("no QElectroTech binary to run the check with: set QET_BINARY")
work = Path(tempfile.mkdtemp(prefix="qet-live-check-"))
try:
snap = _live_call({"cmd": "snapshot", "path": str(work / "live.qet")})
if not snap.get("ok"):
return snap
if folio == "current":
folio_no = snap.get("folio", -1) + 1 or None
elif folio == "all":
folio_no = None
elif isinstance(folio, int) and not isinstance(folio, bool) and folio >= 1:
folio_no = folio
else:
raise ValueError("'folio' is \"current\", \"all\" or a folio number counted from 1")
answer = tool_layout_check(str(binary), snap["path"], folio=folio_no, style=style,
max_shift=max_shift, limit=limit,
elements_dir=elements_dir, timeout=timeout)
answer["checked"] = "the open project, as on screen (unsaved changes included)"
# The moves in "fixes" are for qet_edit on a file; live, apply them
# with qet_live_run_script (qet.moveElement), one undo step.
return answer
finally:
shutil.rmtree(work, ignore_errors=True)
def tool_live_undo_last() -> dict:
_require_script_consent()
return _live_call({"cmd": "undo_last"})
@@ -5257,6 +5563,84 @@ TOOLS = [
},
"handler": lambda a: tool_live_switch_project(a.get("index"), a.get("path", "")),
},
{
"name": "qet_live_save_project",
"description": "LIVE MODE. Save the current project to its own file, or with "
"'path' save it as a new file (absolute; never over an existing "
"file), which then becomes its file.",
"inputSchema": {"type": "object",
"properties": {"path": {"type": "string"}}},
"handler": lambda a: tool_live_save_project(a.get("path", "")),
},
{
"name": "qet_live_close_project",
"description": "LIVE MODE. Close an open project (the current one, or 'index' "
"from qet_live_status's \"projects\") -- only when it has no "
"unsaved changes: closing never discards work, so save it first.",
"inputSchema": {"type": "object",
"properties": {"index": {"type": "integer", "minimum": 0}}},
"handler": lambda a: tool_live_close_project(a.get("index")),
},
{
"name": "qet_live_print",
"description": "LIVE MODE. Print folios of the current project without the "
"print dialog: 'folios' is \"all\" (default), \"current\" or a "
"list of indexes from 0; 'printer' names a printer, else the "
"computer's default printer. QElectroTech always asks the user "
"before printing (paper cannot be taken back); \"refused by the "
"user\" means they said no. 'output_file' (absolute .pdf, must "
"not exist) prints to a PDF file instead, with no question.",
"inputSchema": {
"type": "object",
"properties": {
"folios": {"oneOf": [{"type": "string", "enum": ["all", "current"]},
{"type": "array", "items": {"type": "integer", "minimum": 0}}]},
"printer": {"type": "string"},
"output_file": {"type": "string"},
},
},
"handler": lambda a: tool_live_print(a.get("folios", "all"), a.get("printer", ""),
a.get("output_file", "")),
},
{
"name": "qet_live_changes",
"description": "LIVE MODE. The current project's undo history: each step's name, "
"whether the assistant or the user made it, and whether it is "
"undone; 'since' (a step index from an earlier answer, e.g. its "
"\"now\") keeps only the later steps -- what changed since then. "
"Changes nothing.",
"inputSchema": {"type": "object",
"properties": {"since": {"type": "integer", "minimum": -1}}},
"handler": lambda a: tool_live_changes(a.get("since")),
},
{
"name": "qet_live_layout_check",
"description": "LIVE MODE. qet_layout_check on the drawing the user has open, "
"as on screen (unsaved changes included), without saving it: "
"straight wires, symbols in line and on the grid, overlaps, "
"and the house-style rules -- labels over wires, 4-way junction "
"dots, side branches out of line. Run it before saying a "
"drawing is finished. 'folio': \"current\" (default), \"all\" or "
"a number from 1. Apply \"fixes\" with qet_live_run_script "
"(qet.moveElement), not qet_edit. Changes nothing.",
"inputSchema": {
"type": "object",
"properties": {
"folio": {"oneOf": [{"type": "string", "enum": ["current", "all"]},
{"type": "integer", "minimum": 1}]},
"style": {"type": "string", "enum": list(LAYOUT_STYLES)},
"max_shift": {"type": "number", "default": 40},
"limit": {"type": "integer", "default": 50},
"elements_dir": {"type": "string"},
"binary": {"type": "string", "description": "the qelectrotech executable "
"that runs the check; leave it out to use this server's"},
},
},
"handler": lambda a: tool_live_layout_check(a.get("folio", "current"),
a.get("style", "auto"),
a.get("max_shift", 40), a.get("limit", 50),
a.get("elements_dir"), binary=a.get("binary")),
},
{
"name": "qet_live_undo_last",
"description": "LIVE MODE. Undo the newest step in the open project, only if "
@@ -5396,6 +5780,11 @@ _DATA_PATHS = {
"qet_live_new_project": {"write": ("path",)},
"qet_live_open_project": {"read": ("path",)},
"qet_live_switch_project": {"read": ("path",)},
"qet_live_save_project": {"write": ("path",)},
"qet_live_print": {"write": ("output_file",)},
# Runs QElectroTech on a copy QElectroTech writes to a private folder:
# no client path, but the binary and elements_dir rules apply.
"qet_live_layout_check": {},
"qet_element_build": {"write": ("output",)},
# The scripts folder is chosen by scripts_dir(), never by the client,
# so only the project a script is tried on is a data path here.
@@ -5408,7 +5797,7 @@ _DATA_PATHS = {
# Tools that launch QElectroTech, and so take "binary" and "elements_dir".
_LAUNCHES_QET = {"qet_export", "qet_edit", "qet_query", "qet_continuity",
"qet_check", "qet_layout_check", "qet_project_new", "qet_script_api",
"qet_script_test", "qet_recording_check"}
"qet_script_test", "qet_recording_check", "qet_live_layout_check"}
# Tools that launch QElectroTech only when given this argument.
_LAUNCHES_QET_WITH = {"qet_script_install": "test_project"}
@@ -5418,7 +5807,7 @@ _LAUNCHES_QET_WITH = {"qet_script_install": "test_project"}
_OVERWRITE_OWN_FILE = {"qet_script_install"}
# Tools that create a file and never replace one, whatever the client asks:
# no "overwrite" in their schema, and the flag is ignored if sent anyway.
_NEVER_OVERWRITE = {"qet_live_new_project"}
_NEVER_OVERWRITE = {"qet_live_new_project", "qet_live_save_project", "qet_live_print"}
# qet_edit operations that name a file of their own.
_DATA_PATH_OPS = {"add_image": "file", "add_pdf_page": "file"}
+145 -1
View File
@@ -185,7 +185,9 @@ class ToolRegistry(unittest.TestCase):
"qet_live_undo_last", "qet_live_screenshot", "qet_about",
"qet_recording_list", "qet_recording_read", "qet_recording_check",
"qet_recording_remove", "qet_layout_check", "qet_live_new_project",
"qet_live_open_project", "qet_live_switch_project"})
"qet_live_open_project", "qet_live_switch_project", "qet_live_save_project",
"qet_live_close_project", "qet_live_print", "qet_live_changes",
"qet_live_layout_check"})
class EditValidation(unittest.TestCase):
@@ -3582,6 +3584,48 @@ class LiveClient(unittest.TestCase):
m.tool_live_open_project(bad)
self.assertEqual(len(self.seen), 3)
def test_save_close_print_changes_send_only_what_was_given(self):
self.session()
m.tool_live_save_project()
m.tool_live_save_project("/tmp/x/as.qet")
m.tool_live_close_project()
m.tool_live_close_project(2)
m.tool_live_print()
m.tool_live_print([0, 2], printer="HP")
m.tool_live_print("current", output_file="/tmp/x/out.pdf")
m.tool_live_changes()
m.tool_live_changes(4)
self.assertEqual([{k: v for k, v in r.items() if k not in ("token", "id")}
for r in self.seen], [
{"cmd": "save_project"}, {"cmd": "save_project", "path": "/tmp/x/as.qet"},
{"cmd": "close_project"}, {"cmd": "close_project", "index": 2},
{"cmd": "print", "folios": "all"},
{"cmd": "print", "folios": [0, 2], "printer": "HP"},
{"cmd": "print", "folios": "current", "output_file": "/tmp/x/out.pdf"},
{"cmd": "changes"}, {"cmd": "changes", "since": 4}])
bad = [lambda: m.tool_live_save_project("rel.qet"),
lambda: m.tool_live_close_project(-1),
lambda: m.tool_live_close_project(True),
lambda: m.tool_live_print("some"),
lambda: m.tool_live_print([]),
lambda: m.tool_live_print([0, -1]),
lambda: m.tool_live_print(printer="HP", output_file="/tmp/a.pdf"),
lambda: m.tool_live_print(output_file="out.pdf"),
lambda: m.tool_live_changes(-2)]
for call in bad:
with self.assertRaises(ValueError):
call()
self.assertEqual(len(self.seen), 9)
def test_writing_live_tools_need_script_consent(self):
self.session()
with mock.patch.dict(os.environ, {"QET_ENABLE_SCRIPTING": ""}):
for call in (m.tool_live_save_project, m.tool_live_close_project,
m.tool_live_print):
with self.assertRaises(ValueError):
call()
self.assertEqual(self.seen, [])
def test_new_project_needs_script_consent(self):
self.session()
with mock.patch.dict(os.environ, {"QET_ENABLE_SCRIPTING": ""}):
@@ -3593,6 +3637,106 @@ class LiveClient(unittest.TestCase):
self.info({"socket": self.sock_path + "-gone", "token": "T0K"})
with self.assertRaisesRegex(ValueError, "could not reach"):
m.tool_live_status()
class HouseStyleLayoutRules(unittest.TestCase):
"""The house-style rules of the layout check, on made-up geometry."""
def el(self, uuid, x, y, w=20, h=40, label="", labelbox=None):
return {"uuid": uuid, "name": uuid, "label": label, "terminals": 2,
"g": {"x": x, "y": y, "left": x - w / 2, "top": y - h / 2,
"right": x + w / 2, "bottom": y + h / 2}, "labelbox": labelbox}
def wire(self, uuid, a, b, path):
return {"uuid": uuid, "ends": [a + " terminal 0", b + " terminal 1"],
"path": [{"x": x, "y": y} for x, y in path]}
def rules(self, data):
return sorted(f["rule"] for f in m._layout_folio(dict(folio=0, **data), 40)["findings"]
if f["rule"] in ("label_on_wire", "four_way_junction", "misaligned_branch"))
def test_label_over_a_wire_and_clear_of_it(self):
els = [self.el("a", 100, 100, label="-K1",
labelbox={"left": 95, "top": 150, "right": 120, "bottom": 160}),
self.el("b", 100, 300)]
over = [self.wire("w", "a", "b", [(100, 120), (100, 280)])]
self.assertEqual(self.rules({"elements": els, "conductors": over}), ["label_on_wire"])
els[0]["labelbox"] = {"left": 120, "top": 150, "right": 145, "bottom": 160}
self.assertEqual(self.rules({"elements": els, "conductors": over}), [])
def test_four_way_dot_but_not_a_t(self):
els = [self.el(k, x, y) for k, x, y in
(("n", 200, 100), ("s", 200, 300), ("e", 300, 200), ("w", 100, 200))]
four = [self.wire("1", "n", "s", [(200, 120), (200, 200), (200, 280)]),
self.wire("2", "w", "e", [(110, 200), (200, 200), (290, 200)])]
# Two straight wires through one point have no vertex there...
self.assertEqual(self.rules({"elements": els, "conductors": four}), [])
star = [self.wire("1", "n", "s", [(200, 120), (200, 200)]),
self.wire("2", "s", "n", [(200, 280), (200, 200)]),
self.wire("3", "w", "e", [(110, 200), (200, 200)]),
self.wire("4", "e", "w", [(290, 200), (200, 200)])]
self.assertEqual(self.rules({"elements": els, "conductors": star}), ["four_way_junction"])
self.assertEqual(self.rules({"elements": els, "conductors": star[:3]}), [])
def test_side_branch_out_of_line_like_the_motor_starter(self):
# main column at x 500, branch symbols at 580 (hold-in) and 660 (lamp)
els = [self.el("s2", 500, 470), self.el("hold", 580, 470), self.el("coil", 500, 580),
self.el("lamp", 660, 580)]
wires = [self.wire("1", "s2", "hold", [(500, 450), (580, 450)]),
self.wire("2", "coil", "lamp", [(500, 560), (660, 560)]),
self.wire("3", "s2", "coil", [(500, 490), (500, 560)])]
self.assertEqual(self.rules({"elements": els, "conductors": wires}), ["misaligned_branch"])
els[3] = self.el("lamp", 580, 580)
wires[1] = self.wire("2", "coil", "lamp", [(500, 560), (580, 560)])
self.assertEqual(len(wires), 3)
self.assertEqual(self.rules({"elements": els, "conductors": wires}), [])
class StandardSymbols(unittest.TestCase):
def test_absent_listed_and_broken(self):
with tempfile.TemporaryDirectory() as d:
f = Path(d) / "standard-symbols.json"
with mock.patch.dict(os.environ, {"QET_MCP_STANDARD_SYMBOLS": str(f)}):
self.assertIsNone(m.standard_symbols())
f.write_text(json.dumps({"updated": "2026-10-05", "roles": [
{"id": "coil", "label": "Coil", "letter": "K", "terminals": ["A1", "A2"],
"path": "common://10_electric/x/bobine3.elmt", "score": 99},
{"id": "none_fit", "label": "Nothing", "path": None}]}))
got = m.standard_symbols()
self.assertEqual(got["roles"], [{"id": "coil", "label": "Coil", "letter": "K",
"path": "common://10_electric/x/bobine3.elmt",
"terminals": ["A1", "A2"]}])
f.write_text("{not json")
self.assertIn("could not be read", m.standard_symbols()["error"])
def test_default_location_is_qet_data_folder(self):
with mock.patch.dict(os.environ, {"QET_MCP_STANDARD_SYMBOLS": ""}):
self.assertEqual(m.standard_symbols_file({"folders": {"data": "/x/data"}}),
Path("/x/data/standard-symbols.json"))
class ElementIndexCache(unittest.TestCase):
def test_second_process_reads_the_cache_and_a_change_rebuilds(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d) / "elements"
root.mkdir()
elmt = ('<definition type="element" link_type="simple" width="20" height="20">'
'<names><name lang="en">{}</name></names><description>'
'<terminal x="0" y="-10" orientation="n" name="1"/></description></definition>')
(root / "a.elmt").write_text(elmt.format("Alpha coil"))
with mock.patch.dict(os.environ, {"QET_MCP_CACHE_DIR": str(Path(d) / "cache")}):
m._ELEMENT_INDEX.clear()
first = m._index_collection(root)
self.assertTrue(m._index_cache_file(root.resolve()).is_file())
m._ELEMENT_INDEX.clear() # a new server process
with mock.patch.object(m.ET, "parse", side_effect=AssertionError("parsed")):
again = m._index_collection(root)
self.assertEqual([i["path"] for i in again], [i["path"] for i in first])
self.assertEqual(again[0]["haystack"], first[0]["haystack"])
(root / "b.elmt").write_text(elmt.format("Beta lamp"))
m._ELEMENT_INDEX.clear()
self.assertEqual(len(m._index_collection(root)), 2)
m._ELEMENT_INDEX.clear()
class AssistantInfoFile(unittest.TestCase):
"""qet-assistant.json: QElectroTech says where things are; the server
believes it over its own per-platform guess."""
+240
View File
@@ -23,6 +23,7 @@
#include "scriptlibrary.h"
#include "../diagram.h"
#include "../diagramview.h"
#include "../qet.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetproject.h"
@@ -51,6 +52,10 @@
#include <QDir>
#include <QFileInfo>
#include <QMdiSubWindow>
#include <QPageLayout>
#include <QPainter>
#include <QPrinterInfo>
#include <QPrinter>
#include <QDialogButtonBox>
#include <QLabel>
#include "../projectview.h"
@@ -260,6 +265,16 @@ void LiveServer::handle(const QJsonObject &request)
answer = openProject(request.value(QStringLiteral("path")).toString());
} else if (cmd == QLatin1String("switch_project")) {
answer = switchProject(request);
} else if (cmd == QLatin1String("save_project")) {
answer = saveProject(request);
} else if (cmd == QLatin1String("close_project")) {
answer = closeProject(request);
} else if (cmd == QLatin1String("print")) {
answer = print(request);
} else if (cmd == QLatin1String("snapshot")) {
answer = snapshot(request.value(QStringLiteral("path")).toString());
} else if (cmd == QLatin1String("changes")) {
answer = changes(request);
} else if (cmd == QLatin1String("show_folio")) {
answer = showFolio(request.value(QStringLiteral("folio")).toInt(-1));
} else if (cmd == QLatin1String("undo_last")) {
@@ -596,6 +611,231 @@ QJsonObject LiveServer::switchProject(const QJsonObject &request)
return status();
}
/**
@brief LiveServer::saveProject
Save the current project to its own file, or, with "path", to a new
file that becomes its file (Save As) -- never over an existing one.
*/
QJsonObject LiveServer::saveProject(const QJsonObject &request)
{
QETDiagramEditor *e = editor();
QETProject *project = e ? e->currentProject() : nullptr;
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
if (project->isReadOnly()) return failure(QStringLiteral("the project is read-only"));
const QString path = request.value(QStringLiteral("path")).toString().trimmed();
if (path.isEmpty()) {
if (project->filePath().isEmpty())
return failure(QStringLiteral("the project has no file yet: give \"path\" to save it as one"));
} else {
const QFileInfo info(path);
if (info.isRelative()) return failure(QStringLiteral("path must be absolute: %1").arg(path));
if (info.exists())
return failure(QStringLiteral("%1 already exists; save as never replaces a file").arg(path));
if (!info.dir().exists())
return failure(QStringLiteral("the folder %1 does not exist").arg(info.absolutePath()));
project->setFilePath(info.absoluteFilePath());
}
const QETResult result = project->write();
if (!result.isOk()) return failure(QStringLiteral("not saved: %1").arg(result.errorMessage()));
QJsonObject answer = status();
answer.insert(QStringLiteral("saved"), project->filePath());
return answer;
}
/**
@brief LiveServer::closeProject
Close an open project (the current one, or "index"), only when it has
no unsaved changes: closing never discards the user's work, and never
asks them a question on the assistant's behalf.
*/
QJsonObject LiveServer::closeProject(const QJsonObject &request)
{
QETDiagramEditor *e = editor();
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
const QList<ProjectView *> views = e->openedProjects();
ProjectView *pv = nullptr;
if (request.contains(QStringLiteral("index"))) {
const int index = request.value(QStringLiteral("index")).toInt(-1);
if (index < 0 || index >= views.count())
return failure(QStringLiteral("no open project %1: %2 are open, counted from 0")
.arg(index).arg(views.count()));
pv = views.at(index);
} else {
pv = e->currentProjectView();
if (!pv) return failure(QStringLiteral("no project is open in QElectroTech"));
}
QETProject *project = pv->project();
if (project->projectWasModified())
return failure(QStringLiteral("\"%1\" has unsaved changes; save it first "
"(save_project) -- closing never discards them")
.arg(project->title()));
const QString title = project->title();
if (!e->closeProject(pv)) return failure(QStringLiteral("QElectroTech did not close \"%1\"").arg(title));
QJsonObject answer = status();
answer.insert(QStringLiteral("closed"), title);
return answer;
}
/**
@brief LiveServer::print
Print folios of the current project with no print dialog: "folios" is
"all" (default), "current" or a list of indexes; "printer" names one,
else the system's default printer. Paper cannot be taken back, so the
user is always asked first, whatever "always" says for scripts.
"output_file" prints to a new PDF file instead (no paper, so no
question; never over an existing file).
*/
QJsonObject LiveServer::print(const QJsonObject &request)
{
QETDiagramEditor *e = editor();
ProjectView *pv = e ? e->currentProjectView() : nullptr;
if (!pv) return failure(QStringLiteral("no project is open in QElectroTech"));
QETProject *project = pv->project();
const QList<Diagram *> all = project->diagrams();
QList<Diagram *> folios;
const QJsonValue which = request.value(QStringLiteral("folios"));
if (which.isUndefined() || which.toString() == QLatin1String("all")) {
folios = all;
} else if (which.toString() == QLatin1String("current")) {
if (DiagramView *dv = e->currentDiagramView()) folios << dv->diagram();
} else if (which.isArray()) {
for (const QJsonValue &v : which.toArray()) {
const int i = v.toInt(-1);
if (i < 0 || i >= all.count())
return failure(QStringLiteral("no folio %1: the project has %2, counted from 0")
.arg(v.toVariant().toString()).arg(all.count()));
folios << all.at(i);
}
} else {
return failure(QStringLiteral("folios must be \"all\", \"current\" or a list of indexes"));
}
if (folios.isEmpty()) return failure(QStringLiteral("no folio to print"));
const QString file = request.value(QStringLiteral("output_file")).toString().trimmed();
QPrinterInfo info;
if (file.isEmpty()) {
const QString name = request.value(QStringLiteral("printer")).toString().trimmed();
info = name.isEmpty() ? QPrinterInfo::defaultPrinter() : QPrinterInfo::printerInfo(name);
if (info.isNull())
return failure(name.isEmpty()
? QStringLiteral("this computer has no default printer")
: QStringLiteral("no printer called \"%1\" (printers: %2)")
.arg(name, QPrinterInfo::availablePrinterNames().join(QStringLiteral(", "))));
QMessageBox box(QMessageBox::Question, tr("Impression"),
tr("L'assistant veut imprimer %n folio(s) de « %1 » sur « %2 ».", "", int(folios.count()))
.arg(project->title(), info.printerName()),
QMessageBox::NoButton, e);
QPushButton *go = box.addButton(tr("&Imprimer"), QMessageBox::AcceptRole);
box.addButton(tr("&Annuler"), QMessageBox::RejectRole);
box.setDefaultButton(go);
box.exec();
if (box.clickedButton() != go) return failure(QStringLiteral("refused by the user"));
} else {
const QFileInfo fi(file);
if (fi.isRelative()) return failure(QStringLiteral("output_file must be absolute: %1").arg(file));
if (fi.exists()) return failure(QStringLiteral("%1 already exists; printing never replaces a file").arg(file));
}
QPrinter printer = file.isEmpty() ? QPrinter(info) : QPrinter();
//96 dpi, as the PDF export draws: symbols are replayed at the
//device's resolution, so at a printer's 600-1200 dpi they came out
//many times too big next to the wires (seen on a test print)
printer.setResolution(96);
if (!file.isEmpty()) {
printer.setOutputFormat(QPrinter::PdfFormat);
printer.setOutputFileName(file);
}
printer.setDocName(project->title().isEmpty() ? QStringLiteral("QElectroTech") : project->title());
const auto folioRect = [](Diagram *d) {
QRectF r = d->border_and_titleblock.borderAndTitleBlockRect();
r.adjust(0, 0, 1, 1);
return r.toAlignedRect();
};
//One orientation for the job: some printer drivers ignore a change
//between pages. The first folio decides.
const QRect first = folioRect(folios.first());
printer.setPageOrientation(first.width() > first.height() ? QPageLayout::Landscape
: QPageLayout::Portrait);
QPainter painter;
if (!painter.begin(&printer)) return failure(QStringLiteral("the printer could not be opened"));
for (int i = 0; i < folios.count(); ++i) {
Diagram *d = folios.at(i);
if (i) printer.newPage();
//As the PDF export draws a folio: no grid, guides or terminals
const bool grid = d->displayGrid(), guides = d->displayGuides();
const bool terms = d->drawTerminals(), names = d->drawTerminalNames();
d->setDisplayGrid(false); d->setDisplayGuides(false);
d->setDrawTerminals(false); d->setDrawTerminalNames(false);
d->render(&painter, printer.pageLayout().paintRectPixels(printer.resolution()),
folioRect(d), Qt::KeepAspectRatio);
d->setDisplayGrid(grid); d->setDisplayGuides(guides);
d->setDrawTerminals(terms); d->setDrawTerminalNames(names);
}
painter.end();
return {{QStringLiteral("ok"), true}, {QStringLiteral("printed"), int(folios.count())},
{QStringLiteral("to"), file.isEmpty() ? info.printerName() : file}};
}
/**
@brief LiveServer::snapshot
Write a copy of the current project, as it is on screen (unsaved
changes included), to a new file -- for a check to read -- without
saving the project or changing its file. Never over an existing file.
*/
QJsonObject LiveServer::snapshot(const QString &raw_path)
{
QETDiagramEditor *e = editor();
QETProject *project = e ? e->currentProject() : nullptr;
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
const QFileInfo info(raw_path.trimmed());
if (raw_path.trimmed().isEmpty() || info.isRelative())
return failure(QStringLiteral("path must be absolute"));
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
QDomDocument xml(project->toXml());
QString error;
if (!QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
return failure(QStringLiteral("snapshot not written: %1").arg(error));
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
{QStringLiteral("folio"), e->currentDiagramView()
? int(project->diagrams().indexOf(e->currentDiagramView()->diagram())) : -1}};
}
/**
@brief LiveServer::changes
The current project's undo history: each step's name, whether the
assistant made it, and whether it is undone. "since" (an index from an
earlier answer) keeps only the steps after it, so an assistant can say
what it changed since a point. Changes nothing.
*/
QJsonObject LiveServer::changes(const QJsonObject &request)
{
QETDiagramEditor *e = editor();
QETProject *project = e ? e->currentProject() : nullptr;
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
QUndoStack *stack = project->undoStack();
const QString prefix = tr("Assistant : %1").arg(QString());
const int since = request.value(QStringLiteral("since")).toInt(-1);
QJsonArray steps;
int assistant = 0, user = 0;
for (int i = qMax(0, since + 1); i < stack->count(); ++i) {
const QString text = stack->text(i);
const bool mine = text.startsWith(prefix);
const bool undone = i >= stack->index();
if (!undone) (mine ? assistant : user)++;
steps.append(QJsonObject{{QStringLiteral("index"), i}, {QStringLiteral("step"), text},
{QStringLiteral("by"), mine ? QStringLiteral("assistant")
: QStringLiteral("user")},
{QStringLiteral("undone"), undone}});
}
return {{QStringLiteral("ok"), true}, {QStringLiteral("project"), project->title()},
{QStringLiteral("steps"), steps},
{QStringLiteral("now"), stack->index() - 1},
{QStringLiteral("saved_at"), stack->cleanIndex() - 1},
{QStringLiteral("done_by_assistant"), assistant}, {QStringLiteral("done_by_user"), user}};
}
/**
@brief LiveServer::undoLast
Undo the newest step, only if the assistant made it: what the user did
+5
View File
@@ -89,6 +89,11 @@ class LiveServer : public QObject
QJsonObject openProject(const QString &path);
QJsonObject switchProject(const QJsonObject &request);
QJsonArray openProjects() const;
QJsonObject saveProject(const QJsonObject &request);
QJsonObject closeProject(const QJsonObject &request);
QJsonObject print(const QJsonObject &request);
QJsonObject changes(const QJsonObject &request);
QJsonObject snapshot(const QString &path);
QJsonObject undoLast();
QJsonObject screenshot();
bool confirm(const QString &name, const QString &source);
+19
View File
@@ -3489,6 +3489,25 @@ bool QetScriptApi::deleteImage(int folioIndex, int imageIndex)
return true;
}
/**
@brief QetScriptApi::elementTextGeometry
Where one of a symbol's text fields is drawn on the folio, in folio
coordinates: left, top, right, bottom of the text as shown. A label
that sits on a wire is visible here, not from x/y alone (x/y are
relative to the symbol and say nothing about the text's size).
@return an empty map when the text does not exist
*/
QVariantMap QetScriptApi::elementTextGeometry(int folioIndex, const QString &elementUuid,
int textIndex) const
{
DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex,
QStringLiteral("elementTextGeometry"));
if (!t) return {};
const QRectF r = t->sceneBoundingRect();
return {{QStringLiteral("left"), r.left()}, {QStringLiteral("top"), r.top()},
{QStringLiteral("right"), r.right()}, {QStringLiteral("bottom"), r.bottom()}};
}
/**
@brief QetScriptApi::addPdfPage
Render one page of a PDF file to an image and place it, as the "add
+2
View File
@@ -556,6 +556,8 @@ class QetScriptApi : public QObject
Q_INVOKABLE QString elementTextProperty(int folioIndex, const QString &elementUuid,
int textIndex, const QString &property) const;
Q_INVOKABLE bool deleteElementText(int folioIndex, const QString &elementUuid, int textIndex);
Q_INVOKABLE QVariantMap elementTextGeometry(int folioIndex, const QString &elementUuid,
int textIndex) const;
// -- copy elements (with the conductors between them) to a position --
Q_INVOKABLE QStringList duplicateElements(int fromFolioIndex, const QStringList &elementUuids,