mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-05 18:54:14 +02:00
Merge pull request #1313 from ispyisail/feature/live-tools
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m50s
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:
+132
-108
@@ -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'annule d'un Ctrl+Z, et le bouton « Arrêter » de la barre d'é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 > General > Projects.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="116"/>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="124"/>
|
||||
<source>&Continuer</source>
|
||||
<translation>&Continue</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="117"/>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="125"/>
|
||||
<source>&Pas pour cette session</source>
|
||||
<translation>&Not this session</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="118"/>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="126"/>
|
||||
<source>&Désactiver</source>
|
||||
<translation>&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'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>&Imprimer</source>
|
||||
<translation>&Print</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="731"/>
|
||||
<source>&Annuler</source>
|
||||
<translation>&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'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'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>&Exécuter</source>
|
||||
<translation>&Run</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="516"/>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="911"/>
|
||||
<source>&Refuser</source>
|
||||
<translation>&Decline</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="517"/>
|
||||
<location filename="../sources/scripting/liveserver.cpp" line="912"/>
|
||||
<source>&Toujours pour cette session</source>
|
||||
<translation>&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'êtes pas obligé de préciser l'extension *.elmt. Elle sera ajoutée automatiquement.</source>
|
||||
<translation>You don'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>&Suivant ></source>
|
||||
<translation>&Next ></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'é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'é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>&Fermer</source>
|
||||
<translation>&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'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'é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'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'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'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 "%1"</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'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'exécute avec vos droits : il peut lire et modifier le projet ouvert et écrire des fichiers. N'exécutez que des scripts dont vous connaissez l'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 > General > 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'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 > 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>&Copier la demande pour l'assistant</source>
|
||||
<translation>&Copy the request for the assistant</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="4040"/>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="4042"/>
|
||||
<source>&Ouvrir le dossier</source>
|
||||
<translation>&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'assistant</source>
|
||||
<translation>Request copied: paste it into the assistant'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'assistant pour le reste de la session</source>
|
||||
<translation>Close the assistant'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'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'exécuter un script écrit par l'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'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<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br>Requires python 3.5 or above.<br><B><U> First install on Windows</B></U><br>1. Install, if required, python 3.5 or above<br> Visit :<br><a href='https://www.python.org/downloads/'>python.org</a><br>2. pip install qet_tb_generator<br><B><U> Update on Windows</B></U><br>python -m pip install --upgrade qet_tb_generator<br>>>user could launch in a terminal this script in this directory<br> C:\users\XXXX\AppData\Local\Programs\Python\Python36-32\Scripts <br></source>
|
||||
<translation>To install the plugin qet_tb_generator<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br>Requires python 3.5 or above.<br><B><U> First install on Windows</B></U><br>1. Install, if required, python 3.5 or above<br> Visit :<br><a href='https://www.python.org/downloads/'>python.org</a><br>2. pip install qet_tb_generator<br><B><U> Update on Windows</B></U><br>python -m pip install --upgrade qet_tb_generator<br>>>user could launch in a terminal this script in this directory<br> C:\users\XXXX\AppData\Local\Programs\Python\Python36-32\Scripts <br></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<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><B><U> First install on macOSX</B></U><br>1. Install, if required, python 3.11 bundle only, <a href='https://www.python.org/ftp/python/3.11.2/python-3.11.2-macos11.pkg'>python-3.11.2-macos11.pkg</a><br>2 Run Profile.command script<br>because program use hardcoded PATH for localise qet-tb-generator plugin <br> Visit :<br><a href='https://qelectrotech.org/forum/viewtopic.php?pid=5674#p5674'>howto</a><br>2. pip3 install qet_tb_generator<br><B><U> Update on macOSX</B></U><br> pip3 install --upgrade qet_tb_generator<br></source>
|
||||
<translation>To install the plugin qet_tb_generator<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><B><U> First install on macOSX</B></U><br>1. Install, if required, python 3.11 bundle only, <a href='https://www.python.org/ftp/python/3.11.2/python-3.11.2-macos11.pkg'>python-3.11.2-macos11.pkg</a><br>2 Run Profile.command script<br>because program use hardcoded PATH for localise qet-tb-generator plugin <br> Visit :<br><a href='https://qelectrotech.org/forum/viewtopic.php?pid=5674#p5674'>howto</a><br>2. pip3 install qet_tb_generator<br><B><U> Update on macOSX</B></U><br> pip3 install --upgrade qet_tb_generator<br></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<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><br>Requires python 3.5 or above.<br><br><B><U> First install on Linux</B></U><br>1. check you have pip3 installed: pip3 --version<br>If not install with: sudo apt-get install python3-pip<br>2. Install the program: sudo pip3 install qet_tb_generator<br>3. Run the program: qet_tb_generator<br><br><B><U> Update on Linux</B></U><br>sudo pip3 install --upgrade qet_tb_generator<br></source>
|
||||
<translation>To install the plugin qet_tb_generator<br>Visit :<br><a href='https://pypi.python.org/pypi/qet-tb-generator'>qet-tb-generator</a><br><br>Requires python 3.5 or above.<br><br><B><U> First install on Linux</B></U><br>1. check you have pip3 installed: pip3 --version<br>If not install with: sudo apt-get install python3-pip<br>2. Install the program: sudo pip3 install qet_tb_generator<br>3. Run the program: qet_tb_generator<br><br><B><U> Update on Linux</B></U><br>sudo pip3 install --upgrade qet_tb_generator<br></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'é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>
|
||||
|
||||
@@ -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
@@ -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"}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user