Merge remote-tracking branch 'upstream/master' into prefix-button
Conflicts in cmake/qet_compilation_vars.cmake only, where upstream added terminaluuids.cpp/.h next to the qetlabelsfile.cpp/.h this branch adds : both are kept, qetlabelsfile first to stay alphabetical.
@@ -227,6 +227,23 @@ jobs:
|
||||
cp -r "$GITHUB_WORKSPACE/titleblocks" "$FILES/titleblocks" || true
|
||||
cp -r "$GITHUB_WORKSPACE/examples" "$FILES/examples" || true
|
||||
cp -r "$GITHUB_WORKSPACE/fonts" "$FILES/fonts" || true
|
||||
# The MCP server finds bin/QElectroTech.exe and elements/ from mcp/.
|
||||
mkdir -p "$FILES/mcp"
|
||||
cp "$GITHUB_WORKSPACE/misc/qet-mcp/qet_mcp.py" "$GITHUB_WORKSPACE/misc/qet-mcp/README.md" "$FILES/mcp/"
|
||||
# Python for it, so Windows users need not install one: the official
|
||||
# embeddable package, pinned by version and checksum (python.org's
|
||||
# own sha256 for this file). The installer offers it unticked; the
|
||||
# portable folder and the MSI carry it.
|
||||
PY_EMBED_VERSION=3.14.7
|
||||
PY_EMBED_SHA256=d297e5ff019966817ad8502465176139f2d3d840fa4ed84b13bed399a6ab1f15
|
||||
PY_ZIP="$GITHUB_WORKSPACE/python-embed.zip"
|
||||
curl -fsSL -o "$PY_ZIP" \
|
||||
"https://www.python.org/ftp/python/$PY_EMBED_VERSION/python-$PY_EMBED_VERSION-embed-amd64.zip"
|
||||
echo "$PY_EMBED_SHA256 $PY_ZIP" | sha256sum -c - \
|
||||
|| { echo "ERROR: embeddable Python checksum mismatch"; exit 1; }
|
||||
python3 -m zipfile -e "$PY_ZIP" "$FILES/mcp/python"
|
||||
[ -f "$FILES/mcp/python/python.exe" ] \
|
||||
|| { echo "ERROR: python.exe missing from the embeddable package"; exit 1; }
|
||||
|
||||
# --- Translations ---
|
||||
# Since PR #751 the .qm files are no longer tracked in git: lrelease
|
||||
@@ -305,7 +322,7 @@ jobs:
|
||||
&& echo " OK : $f" \
|
||||
|| echo " MISSING: $f"
|
||||
done
|
||||
for d in ico elements lang titleblocks fonts examples bin; do
|
||||
for d in ico elements lang titleblocks fonts examples bin mcp; do
|
||||
[ -d "$FILES/$d" ] \
|
||||
&& echo " OK : $d/" \
|
||||
|| echo " MISSING: $d/"
|
||||
|
||||
@@ -80,7 +80,7 @@ set(QET_HAS_QTPDF FALSE)
|
||||
find_package(Qt6 QUIET COMPONENTS Pdf)
|
||||
if(TARGET Qt6::Pdf AND NOT Qt6_VERSION VERSION_LESS 6.4.0)
|
||||
set(QET_HAS_QTPDF TRUE)
|
||||
list(APPEND QET_PRIVATE_LIBRARIES Qt::Pdf)
|
||||
list(APPEND QET_PRIVATE_LIBRARIES Qt6::Pdf)
|
||||
add_compile_definitions(QET_HAS_QTPDF)
|
||||
else()
|
||||
message(STATUS "QtPdf module not available (or Qt < 6.4): PDF page import feature disabled")
|
||||
@@ -93,10 +93,10 @@ endif()
|
||||
# the application depends on. When it's missing the feature is silently
|
||||
# disabled - see the QET_HAS_SCRIPTING guard in qetscripting.*.
|
||||
set(QET_HAS_SCRIPTING FALSE)
|
||||
find_package(Qt${QT_VERSION_MAJOR} QUIET COMPONENTS Qml)
|
||||
if(TARGET Qt${QT_VERSION_MAJOR}::Qml)
|
||||
find_package(Qt6 QUIET COMPONENTS Qml)
|
||||
if(TARGET Qt6::Qml)
|
||||
set(QET_HAS_SCRIPTING TRUE)
|
||||
list(APPEND QET_PRIVATE_LIBRARIES Qt::Qml)
|
||||
list(APPEND QET_PRIVATE_LIBRARIES Qt6::Qml)
|
||||
add_compile_definitions(QET_HAS_SCRIPTING)
|
||||
else()
|
||||
message(STATUS "Qt Qml module not available: JavaScript scripting (--run) disabled")
|
||||
@@ -358,6 +358,10 @@ if (NOT MINGW)
|
||||
install(DIRECTORY elements DESTINATION share/qelectrotech)
|
||||
install(DIRECTORY examples DESTINATION share/qelectrotech)
|
||||
install(DIRECTORY titleblocks DESTINATION share/qelectrotech)
|
||||
# The MCP server (misc/qet-mcp) finds this QElectroTech and its elements
|
||||
# from here: <prefix>/share/qelectrotech/mcp -> <prefix>/bin.
|
||||
install(PROGRAMS misc/qet-mcp/qet_mcp.py DESTINATION share/qelectrotech/mcp)
|
||||
install(FILES misc/qet-mcp/README.md DESTINATION share/qelectrotech/mcp)
|
||||
install(FILES LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog DESTINATION share/doc/qelectrotech)
|
||||
if(UNIX AND NOT APPLE)
|
||||
install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/applications)
|
||||
|
||||
@@ -274,6 +274,21 @@ Section "$(Fonts)" SEC05
|
||||
File /nonfatal /r "./files/fonts"
|
||||
SectionEnd
|
||||
|
||||
; The MCP server lets an AI assistant open, check and edit drawings. It does
|
||||
; nothing until a user sets up an assistant to start it.
|
||||
Section "$(Mcp)" SEC06
|
||||
SetOutPath "$INSTDIR\mcp"
|
||||
File /nonfatal "./files/mcp/*"
|
||||
SectionEnd
|
||||
|
||||
; Python for the MCP server, from python.org (about 12 MB). Unticked by
|
||||
; default: only someone setting up an AI assistant needs it, and then only
|
||||
; without a Python of their own. Uninstall removes it with $INSTDIR.
|
||||
Section /o "$(McpPython)" SEC07
|
||||
SetOutPath "$INSTDIR\mcp\python"
|
||||
File /nonfatal /r "./files/mcp/python/*"
|
||||
SectionEnd
|
||||
|
||||
;--------------------------------
|
||||
; Component descriptions
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||
@@ -282,6 +297,8 @@ SectionEnd
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC03} $(var3)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC04} $(var4)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC05} $(var5)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC06} $(var6)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC07} $(var7)
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||
|
||||
;--------------------------------
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
LangString var3 ${LANG_ENGLISH} "Examples of cartridges"
|
||||
LangString var4 ${LANG_ENGLISH} "Examples of diagrams"
|
||||
LangString var5 ${LANG_ENGLISH} "Fonts"
|
||||
LangString Mcp ${LANG_ENGLISH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_ENGLISH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_ENGLISH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_ENGLISH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_ENGLISH} "Uninstallation of the previous version failed.$\nPlease uninstall ${SOFT_NAME} manually before continuing."
|
||||
|
||||
|
||||
@@ -43,6 +47,10 @@
|
||||
LangString var3 ${LANG_KOREAN} "표제란 예제"
|
||||
LangString var4 ${LANG_KOREAN} "도면 예제"
|
||||
LangString var5 ${LANG_KOREAN} "글꼴"
|
||||
LangString Mcp ${LANG_KOREAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_KOREAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_KOREAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_KOREAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_KOREAN} "이전 버전을 제거하지 못했습니다.$\n계속하기 전에 ${SOFT_NAME}을(를) 수동으로 제거해 주세요."
|
||||
|
||||
|
||||
@@ -67,6 +75,10 @@
|
||||
LangString var3 ${LANG_POLISH} "Przykłady tabliczek rysunkowych"
|
||||
LangString var4 ${LANG_POLISH} "Przykłady schematów"
|
||||
LangString var5 ${LANG_POLISH} "Czcionki"
|
||||
LangString Mcp ${LANG_POLISH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_POLISH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_POLISH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_POLISH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_POLISH} "Odinstalowanie poprzedniej wersji nie powiodło się.$\nPrzed kontynuowaniem odinstaluj ręcznie program ${SOFT_NAME}."
|
||||
|
||||
|
||||
@@ -91,6 +103,10 @@
|
||||
LangString var3 ${LANG_GREEK} "Παραδείγματα πινακίδων"
|
||||
LangString var4 ${LANG_GREEK} "Παραδείγματα διαγραμμάτων"
|
||||
LangString var5 ${LANG_GREEK} "Γραμματοσειρές"
|
||||
LangString Mcp ${LANG_GREEK} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_GREEK} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_GREEK} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_GREEK} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_GREEK} "Η απεγκατάσταση της προηγούμενης έκδοσης απέτυχε.$\nΠαρακαλώ απεγκαταστήστε χειροκίνητα το ${SOFT_NAME} πριν συνεχίσετε."
|
||||
|
||||
|
||||
@@ -115,6 +131,10 @@
|
||||
LangString var3 ${LANG_CZECH} "Příklady popisových polí"
|
||||
LangString var4 ${LANG_CZECH} "Příklady schémat"
|
||||
LangString var5 ${LANG_CZECH} "Písma"
|
||||
LangString Mcp ${LANG_CZECH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_CZECH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_CZECH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_CZECH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_CZECH} "Odinstalování předchozí verze se nezdařilo.$\nPřed pokračováním prosím odinstalujte ${SOFT_NAME} ručně."
|
||||
|
||||
|
||||
@@ -139,6 +159,10 @@
|
||||
LangString var3 ${LANG_SPANISH} "Ejemplos de cartelas"
|
||||
LangString var4 ${LANG_SPANISH} "Ejemplos de esquemas"
|
||||
LangString var5 ${LANG_SPANISH} "Fuentes"
|
||||
LangString Mcp ${LANG_SPANISH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_SPANISH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_SPANISH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_SPANISH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_SPANISH} "La desinstalación de la versión anterior ha fallado.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar."
|
||||
|
||||
|
||||
@@ -163,6 +187,10 @@
|
||||
LangString var3 ${LANG_GERMAN} "Schriftfeld-Beispiele"
|
||||
LangString var4 ${LANG_GERMAN} "Schaltplan-Beispiele"
|
||||
LangString var5 ${LANG_GERMAN} "Schriften"
|
||||
LangString Mcp ${LANG_GERMAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_GERMAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_GERMAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_GERMAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_GERMAN} "Die Deinstallation der vorherigen Version ist fehlgeschlagen.$\nBitte deinstallieren Sie ${SOFT_NAME} manuell, bevor Sie fortfahren."
|
||||
|
||||
|
||||
@@ -187,6 +215,10 @@
|
||||
LangString var3 ${LANG_RUSSIAN} "Примеры штампов"
|
||||
LangString var4 ${LANG_RUSSIAN} "Примеры схем"
|
||||
LangString var5 ${LANG_RUSSIAN} "Шрифты"
|
||||
LangString Mcp ${LANG_RUSSIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_RUSSIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_RUSSIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_RUSSIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_RUSSIAN} "Удаление предыдущей версии завершилось с ошибкой.$\nПожалуйста, удалите ${SOFT_NAME} вручную перед продолжением."
|
||||
|
||||
|
||||
@@ -211,6 +243,10 @@
|
||||
LangString var3 ${LANG_ARABIC} "أمثلة على كتل العنوان"
|
||||
LangString var4 ${LANG_ARABIC} "أمثلة على المخططات"
|
||||
LangString var5 ${LANG_ARABIC} "الخطوط"
|
||||
LangString Mcp ${LANG_ARABIC} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_ARABIC} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_ARABIC} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_ARABIC} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_ARABIC} "فشل إلغاء تثبيت الإصدار السابق. يرجى إلغاء تثبيت ${SOFT_NAME} يدويًا قبل المتابعة."
|
||||
|
||||
|
||||
@@ -235,6 +271,10 @@
|
||||
LangString var3 ${LANG_CATALAN} "Exemples de cartutxos"
|
||||
LangString var4 ${LANG_CATALAN} "Exemples d'esquemes"
|
||||
LangString var5 ${LANG_CATALAN} "Tipus de lletra"
|
||||
LangString Mcp ${LANG_CATALAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_CATALAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_CATALAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_CATALAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_CATALAN} "La desinstal·lació de la versió anterior ha fallat.$\nSi us plau, desinstal·leu ${SOFT_NAME} manualment abans de continuar."
|
||||
|
||||
|
||||
@@ -259,6 +299,10 @@
|
||||
LangString var3 ${LANG_ITALIAN} "Cartigli di esempio"
|
||||
LangString var4 ${LANG_ITALIAN} "Schemi di esempio"
|
||||
LangString var5 ${LANG_ITALIAN} "Caratteri"
|
||||
LangString Mcp ${LANG_ITALIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_ITALIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_ITALIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_ITALIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_ITALIAN} "La disinstallazione della versione precedente non è riuscita.$\nSi prega di disinstallare ${SOFT_NAME} manualmente prima di continuare."
|
||||
|
||||
|
||||
@@ -283,6 +327,10 @@
|
||||
LangString var3 ${LANG_PORTUGUESE} "Exemplos de legendas"
|
||||
LangString var4 ${LANG_PORTUGUESE} "Exemplos de esquemas"
|
||||
LangString var5 ${LANG_PORTUGUESE} "Fontes"
|
||||
LangString Mcp ${LANG_PORTUGUESE} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_PORTUGUESE} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_PORTUGUESE} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_PORTUGUESE} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_PORTUGUESE} "A desinstalação da versão anterior falhou.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar."
|
||||
|
||||
|
||||
@@ -307,6 +355,10 @@
|
||||
LangString var3 ${LANG_ROMANIAN} "Exemple de cartușe"
|
||||
LangString var4 ${LANG_ROMANIAN} "Exemple de scheme"
|
||||
LangString var5 ${LANG_ROMANIAN} "Fonturi"
|
||||
LangString Mcp ${LANG_ROMANIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_ROMANIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_ROMANIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_ROMANIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_ROMANIAN} "Dezinstalarea versiunii anterioare a eșuat.$\nVă rugăm să dezinstalați ${SOFT_NAME} manual înainte de a continua."
|
||||
|
||||
|
||||
@@ -331,6 +383,10 @@
|
||||
LangString var3 ${LANG_CROATIAN} "Primjeri zaglavlja"
|
||||
LangString var4 ${LANG_CROATIAN} "Primjeri shema"
|
||||
LangString var5 ${LANG_CROATIAN} "Fontovi"
|
||||
LangString Mcp ${LANG_CROATIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_CROATIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_CROATIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_CROATIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_CROATIAN} "Deinstalacija prethodne verzije nije uspjela.$\nMolimo deinstalirajte ${SOFT_NAME} ručno prije nastavka."
|
||||
|
||||
|
||||
@@ -355,6 +411,10 @@
|
||||
LangString var3 ${LANG_DUTCH} "Voorbeelden van titelblokken"
|
||||
LangString var4 ${LANG_DUTCH} "Voorbeelden van schema's"
|
||||
LangString var5 ${LANG_DUTCH} "Lettertypen"
|
||||
LangString Mcp ${LANG_DUTCH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_DUTCH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_DUTCH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_DUTCH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_DUTCH} "Het verwijderen van de vorige versie is mislukt.$\nVerwijder ${SOFT_NAME} handmatig voordat u verdergaat."
|
||||
|
||||
|
||||
@@ -379,6 +439,10 @@
|
||||
LangString var3 ${LANG_DANISH} "Titel blokke eksempler"
|
||||
LangString var4 ${LANG_DANISH} "Diagram eksempler"
|
||||
LangString var5 ${LANG_DANISH} "Skrifttyper"
|
||||
LangString Mcp ${LANG_DANISH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_DANISH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_DANISH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_DANISH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_DANISH} "Afinstallation af den tidligere version mislykkedes.$\nAfinstaller venligst ${SOFT_NAME} manuelt, inden du fortsætter."
|
||||
|
||||
|
||||
|
||||
@@ -19,4 +19,8 @@
|
||||
LangString var3 ${LANG_FRENCH} "Exemples de cartouches"
|
||||
LangString var4 ${LANG_FRENCH} "Exemples de schémas"
|
||||
LangString var5 ${LANG_FRENCH} "Polices"
|
||||
LangString Mcp ${LANG_FRENCH} "Assistant IA (MCP)"
|
||||
LangString var6 ${LANG_FRENCH} "Permet à un assistant IA d'ouvrir, vérifier et modifier vos schémas. Nécessite Python : le vôtre, ou celui proposé ci-dessous"
|
||||
LangString McpPython ${LANG_FRENCH} "Python pour l'assistant IA"
|
||||
LangString var7 ${LANG_FRENCH} "Python de python.org, utilisé uniquement par le composant Assistant IA. Inutile si Python est déjà installé (environ 12 Mo)"
|
||||
LangString uninstFailed ${LANG_FRENCH} "La désinstallation de la version précédente a échoué.$\nVeuillez désinstaller ${SOFT_NAME} manuellement avant de continuer."
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
LangString var3 ${LANG_HUNGARIAN} "Rajzfejléc-példák"
|
||||
LangString var4 ${LANG_HUNGARIAN} "Kapcsolási rajz példák"
|
||||
LangString var5 ${LANG_HUNGARIAN} "Betűtípusok"
|
||||
LangString Mcp ${LANG_HUNGARIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_HUNGARIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_HUNGARIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_HUNGARIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_HUNGARIAN} "Az előző verzió eltávolítása nem sikerült.$\nKérjük, távolítsa el manuálisan a ${SOFT_NAME} programot, mielőtt folytatná."
|
||||
|
||||
|
||||
@@ -52,6 +56,10 @@
|
||||
LangString var3 ${LANG_JAPANESE} "表題欄の例"
|
||||
LangString var4 ${LANG_JAPANESE} "回路図の例"
|
||||
LangString var5 ${LANG_JAPANESE} "フォント"
|
||||
LangString Mcp ${LANG_JAPANESE} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_JAPANESE} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_JAPANESE} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_JAPANESE} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_JAPANESE} "前のバージョンのアンインストールに失敗しました。$\n続行する前に、${SOFT_NAME} を手動でアンインストールしてください。"
|
||||
|
||||
|
||||
@@ -79,6 +87,10 @@
|
||||
LangString var3 ${LANG_MONGOLIAN} "Гарчгийн блокын жишээнүүд"
|
||||
LangString var4 ${LANG_MONGOLIAN} "Схемийн жишээнүүд"
|
||||
LangString var5 ${LANG_MONGOLIAN} "Фонт"
|
||||
LangString Mcp ${LANG_MONGOLIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_MONGOLIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_MONGOLIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_MONGOLIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_MONGOLIAN} "Өмнөх хувилбарыг устгаж чадсангүй.$\nҮргэлжлүүлэхийн өмнө ${SOFT_NAME}-г гараар устгана уу."
|
||||
|
||||
|
||||
@@ -106,6 +118,10 @@
|
||||
LangString var3 ${LANG_NORWEGIAN} "Eksempler på titelblokker"
|
||||
LangString var4 ${LANG_NORWEGIAN} "Eksempler på skjemaer"
|
||||
LangString var5 ${LANG_NORWEGIAN} "Skrifttyper"
|
||||
LangString Mcp ${LANG_NORWEGIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_NORWEGIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_NORWEGIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_NORWEGIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_NORWEGIAN} "Avinstallasjon av forrige versjon mislyktes.$\nVennligst avinstaller ${SOFT_NAME} manuelt før du fortsetter."
|
||||
|
||||
|
||||
@@ -143,6 +159,10 @@
|
||||
LangString var3 ${LANG_PORTUGUESEBR} "Exemplos de legendas"
|
||||
LangString var4 ${LANG_PORTUGUESEBR} "Exemplos de esquemas"
|
||||
LangString var5 ${LANG_PORTUGUESEBR} "Fontes"
|
||||
LangString Mcp ${LANG_PORTUGUESEBR} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_PORTUGUESEBR} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_PORTUGUESEBR} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_PORTUGUESEBR} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_PORTUGUESEBR} "A desinstalação da versão anterior falhou.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar."
|
||||
|
||||
|
||||
@@ -170,6 +190,10 @@
|
||||
LangString var3 ${LANG_SERBIAN} "Примери заглавља"
|
||||
LangString var4 ${LANG_SERBIAN} "Примери шема"
|
||||
LangString var5 ${LANG_SERBIAN} "Фонтови"
|
||||
LangString Mcp ${LANG_SERBIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_SERBIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_SERBIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_SERBIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_SERBIAN} "Деинсталација претходне верзије није успела.$\nМолимо деинсталирајте ${SOFT_NAME} ручно пре наставка."
|
||||
|
||||
|
||||
@@ -197,6 +221,10 @@
|
||||
LangString var3 ${LANG_SLOVAK} "Príklady popisových polí"
|
||||
LangString var4 ${LANG_SLOVAK} "Príklady schém"
|
||||
LangString var5 ${LANG_SLOVAK} "Písma"
|
||||
LangString Mcp ${LANG_SLOVAK} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_SLOVAK} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_SLOVAK} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_SLOVAK} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_SLOVAK} "Odinštalovanie predchádzajúcej verzie zlyhalo.$\nPred pokračovaním odinštalujte ${SOFT_NAME} ručne."
|
||||
|
||||
|
||||
@@ -224,6 +252,10 @@
|
||||
LangString var3 ${LANG_SLOVENIAN} "Primeri glav risb"
|
||||
LangString var4 ${LANG_SLOVENIAN} "Primeri shem"
|
||||
LangString var5 ${LANG_SLOVENIAN} "Pisave"
|
||||
LangString Mcp ${LANG_SLOVENIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_SLOVENIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_SLOVENIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_SLOVENIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_SLOVENIAN} "Odstranitev prejšnje različice ni uspela.$\nPred nadaljevanjem ročno odstranite ${SOFT_NAME}."
|
||||
|
||||
|
||||
@@ -251,6 +283,10 @@
|
||||
LangString var3 ${LANG_SWEDISH} "Exempel på ritningshuvuden"
|
||||
LangString var4 ${LANG_SWEDISH} "Exempel på scheman"
|
||||
LangString var5 ${LANG_SWEDISH} "Teckensnitt"
|
||||
LangString Mcp ${LANG_SWEDISH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_SWEDISH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_SWEDISH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_SWEDISH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_SWEDISH} "Avinstallationen av den föregående versionen misslyckades.$\nAvinstallera ${SOFT_NAME} manuellt innan du fortsätter."
|
||||
|
||||
|
||||
@@ -278,6 +314,10 @@
|
||||
LangString var3 ${LANG_TURKISH} "Başlık bloğu örnekleri"
|
||||
LangString var4 ${LANG_TURKISH} "Şema örnekleri"
|
||||
LangString var5 ${LANG_TURKISH} "Yazı Tipleri"
|
||||
LangString Mcp ${LANG_TURKISH} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_TURKISH} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_TURKISH} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_TURKISH} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_TURKISH} "Önceki sürümün kaldırılması başarısız oldu.$\nDevam etmeden önce lütfen ${SOFT_NAME}'i manuel olarak kaldırın."
|
||||
|
||||
|
||||
@@ -305,6 +345,10 @@
|
||||
LangString var3 ${LANG_UKRAINIAN} "Приклади штампів"
|
||||
LangString var4 ${LANG_UKRAINIAN} "Приклади схем"
|
||||
LangString var5 ${LANG_UKRAINIAN} "Шрифти"
|
||||
LangString Mcp ${LANG_UKRAINIAN} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_UKRAINIAN} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_UKRAINIAN} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_UKRAINIAN} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_UKRAINIAN} "Видалення попередньої версії завершилося помилкою.$\nБудь ласка, видаліть ${SOFT_NAME} вручну перед продовженням."
|
||||
|
||||
|
||||
@@ -332,4 +376,8 @@
|
||||
LangString var3 ${LANG_SIMPCHINESE} "标题栏示例"
|
||||
LangString var4 ${LANG_SIMPCHINESE} "图纸示例"
|
||||
LangString var5 ${LANG_SIMPCHINESE} "字体"
|
||||
LangString Mcp ${LANG_SIMPCHINESE} "AI assistant (MCP)"
|
||||
LangString var6 ${LANG_SIMPCHINESE} "Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
|
||||
LangString McpPython ${LANG_SIMPCHINESE} "Python for the AI assistant"
|
||||
LangString var7 ${LANG_SIMPCHINESE} "Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
|
||||
LangString uninstFailed ${LANG_SIMPCHINESE} "卸载旧版本失败。$\n请在继续之前手动卸载 ${SOFT_NAME}。"
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[-HKEY_CURRENT_USER\Software\Classes\Applications\qelectrotech.exe\shell\open\command]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\.qet]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\qet_diagram_file]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\.elmt]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\qet_element_file]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\.titleblock]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file]
|
||||
[-HKEY_CLASSES_ROOT\Applications\qelectrotech.exe\shell\open\command]
|
||||
[-HKEY_CLASSES_ROOT\.qet]
|
||||
[-HKEY_CLASSES_ROOT\qet_diagram_file]
|
||||
|
||||
@@ -5,7 +5,9 @@ rem detecte le dossier courant et suppose que celui-ci contient bin\qelectrotech
|
||||
set current_dir=%~dp0
|
||||
cd /d %current_dir%
|
||||
|
||||
set expected_qet_exe=%current_dir%Lancer QET.bat
|
||||
rem l'archive portable fournit "Lancer QET_qt6.bat", l'installeur "Lancer QET.bat"
|
||||
set expected_qet_exe=%current_dir%Lancer QET_qt6.bat
|
||||
if not exist "%expected_qet_exe%" set expected_qet_exe=%current_dir%Lancer QET.bat
|
||||
set expected_project_ico=%current_dir%ico\application-x-qet-project.ico
|
||||
set expected_element_ico=%current_dir%ico\application-x-qet-element.ico
|
||||
set expected_titleblock_ico=%current_dir%ico\application-x-qet-titleblock.ico
|
||||
@@ -45,49 +47,50 @@ set final_element_ico=%expected_element_ico:\=\\%
|
||||
set final_titleblock_ico=%expected_titleblock_ico:\=\\%
|
||||
|
||||
rem genere le fichier .reg pour enregistrer les associations de fichiers
|
||||
rem HKEY_CURRENT_USER : ne demande pas les droits administrateur
|
||||
set reg_file=qet_install_file_associations.reg
|
||||
(
|
||||
echo Windows Registry Editor Version 5.00
|
||||
echo.
|
||||
|
||||
rem Declaration de l'application
|
||||
echo [HKEY_CLASSES_ROOT\Applications\qelectrotech.exe\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\Applications\qelectrotech.exe\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem association de fichier *.qet
|
||||
echo [HKEY_CLASSES_ROOT\.qet]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.qet]
|
||||
echo @="qet_diagram_file"
|
||||
echo [HKEY_CLASSES_ROOT\qet_diagram_file]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file]
|
||||
echo @="QET diagram"
|
||||
echo "EditFlags"=dword:00000000
|
||||
echo "BrowserFlags"=dword:00000008
|
||||
echo [HKEY_CLASSES_ROOT\qet_diagram_file\DefaultIcon]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file\DefaultIcon]
|
||||
echo @="%final_project_ico%,0"
|
||||
echo [HKEY_CLASSES_ROOT\qet_diagram_file\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem association de fichier *.elmt
|
||||
echo [HKEY_CLASSES_ROOT\.elmt]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.elmt]
|
||||
echo @="qet_element_file"
|
||||
echo [HKEY_CLASSES_ROOT\qet_element_file]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_element_file]
|
||||
echo @="QET element"
|
||||
echo "EditFlags"=dword:00000000
|
||||
echo "BrowserFlags"=dword:00000008
|
||||
echo [HKEY_CLASSES_ROOT\qet_element_file\DefaultIcon]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_element_file\DefaultIcon]
|
||||
echo @="%final_element_ico%,0"
|
||||
echo [HKEY_CLASSES_ROOT\qet_element_file\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_element_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem association de fichier *.titleblock
|
||||
echo [HKEY_CLASSES_ROOT\.titleblock]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.titleblock]
|
||||
echo @="qet_titleblock_file"
|
||||
echo [HKEY_CLASSES_ROOT\qet_titleblock_file]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file]
|
||||
echo @="QET title block template"
|
||||
echo "EditFlags"=dword:00000000
|
||||
echo "BrowserFlags"=dword:00000008
|
||||
echo [HKEY_CLASSES_ROOT\qet_titleblock_file\DefaultIcon]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file\DefaultIcon]
|
||||
echo @="%final_titleblock_ico%,0"
|
||||
echo [HKEY_CLASSES_ROOT\qet_titleblock_file\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
) > %reg_file%
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ message("QET_DIR :" ${QET_DIR})
|
||||
message("GIT_COMMIT_SHA :" ${GIT_COMMIT_SHA})
|
||||
|
||||
if(BUILD_WITH_KF)
|
||||
message("KF_GIT_TAG :" ${KF_GIT_TAG})
|
||||
message("KF_GIT_TAG :" ${KF_GIT_TAG})
|
||||
else()
|
||||
add_definitions(-DBUILD_WITHOUT_KF)
|
||||
endif()
|
||||
|
||||
@@ -477,6 +477,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ElementsCollection/fileelementcollectionitem.h
|
||||
${QET_DIR}/sources/ElementsCollection/qetlabelsfile.cpp
|
||||
${QET_DIR}/sources/ElementsCollection/qetlabelsfile.h
|
||||
${QET_DIR}/sources/ElementsCollection/terminaluuids.cpp
|
||||
${QET_DIR}/sources/ElementsCollection/terminaluuids.h
|
||||
${QET_DIR}/sources/ElementsCollection/xmlelementcollection.cpp
|
||||
${QET_DIR}/sources/ElementsCollection/xmlelementcollection.h
|
||||
${QET_DIR}/sources/ElementsCollection/xmlprojectelementcollectionitem.cpp
|
||||
@@ -714,6 +716,10 @@ set(QET_SRC_FILES
|
||||
|
||||
${QET_DIR}/sources/ui/aboutqetdialog.cpp
|
||||
${QET_DIR}/sources/ui/aboutqetdialog.h
|
||||
${QET_DIR}/sources/ui/aiassistantdialog.cpp
|
||||
${QET_DIR}/sources/ui/aiassistantdialog.h
|
||||
${QET_DIR}/sources/ui/aiassistantsetup.cpp
|
||||
${QET_DIR}/sources/ui/aiassistantsetup.h
|
||||
${QET_DIR}/sources/ui/abstractelementpropertieseditorwidget.cpp
|
||||
${QET_DIR}/sources/ui/abstractelementpropertieseditorwidget.h
|
||||
${QET_DIR}/sources/ui/alignmenttextdialog.cpp
|
||||
|
||||
@@ -267,12 +267,19 @@
|
||||
<file alias="themes/qet/128x128/project.png">128x128/project.png</file>
|
||||
<file alias="themes/qet/128x128/settings.png">128x128/settings.png</file>
|
||||
<file alias="themes/qet/128x128/terminalstrip.png">128x128/terminalstrip.png</file>
|
||||
<file alias="themes/qet/scalable/align-horizontal-center.svg">scalable/align-horizontal-center.svg</file>
|
||||
<file alias="themes/qet/scalable/align-horizontal-left.svg">scalable/align-horizontal-left.svg</file>
|
||||
<file alias="themes/qet/scalable/align-horizontal-right.svg">scalable/align-horizontal-right.svg</file>
|
||||
<file alias="themes/qet/scalable/align-vertical-bottom.svg">scalable/align-vertical-bottom.svg</file>
|
||||
<file alias="themes/qet/scalable/align-vertical-center.svg">scalable/align-vertical-center.svg</file>
|
||||
<file alias="themes/qet/scalable/align-vertical-top.svg">scalable/align-vertical-top.svg</file>
|
||||
<file alias="themes/qet/scalable/diagram.svg">scalable/diagram.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-delete.svg">scalable/folio-delete.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-new.svg">scalable/folio-new.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-properties.svg">scalable/folio-properties.svg</file>
|
||||
<file alias="themes/qet/scalable/label.svg">scalable/label.svg</file>
|
||||
<file alias="themes/qet/scalable/pdf-import.svg">scalable/pdf-import.svg</file>
|
||||
<file alias="themes/qet/scalable/snap-to-grid.svg">scalable/snap-to-grid.svg</file>
|
||||
<file alias="themes/qet/scalable/edit-opacity.svg">breeze-icons/scalable/apps/hidef/edit-opacity.svg</file>
|
||||
<file alias="themes/qet/scalable/image-flip-horizontal.svg">breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file>
|
||||
<file alias="themes/qet/scalable/image-flip-vertical.svg">breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file>
|
||||
@@ -457,12 +464,19 @@
|
||||
<file>themes/qet-dark/48x48/view-pim-journal.png</file>
|
||||
<file>themes/qet-dark/128x128/plasmagik.png</file>
|
||||
<file>themes/qet-dark/128x128/settings.png</file>
|
||||
<file>themes/qet-dark/scalable/align-horizontal-center.svg</file>
|
||||
<file>themes/qet-dark/scalable/align-horizontal-left.svg</file>
|
||||
<file>themes/qet-dark/scalable/align-horizontal-right.svg</file>
|
||||
<file>themes/qet-dark/scalable/align-vertical-bottom.svg</file>
|
||||
<file>themes/qet-dark/scalable/align-vertical-center.svg</file>
|
||||
<file>themes/qet-dark/scalable/align-vertical-top.svg</file>
|
||||
<file>themes/qet-dark/scalable/diagram.svg</file>
|
||||
<file>themes/qet-dark/scalable/folio-delete.svg</file>
|
||||
<file>themes/qet-dark/scalable/folio-new.svg</file>
|
||||
<file>themes/qet-dark/scalable/folio-properties.svg</file>
|
||||
<file>themes/qet-dark/scalable/label.svg</file>
|
||||
<file>themes/qet-dark/scalable/pdf-import.svg</file>
|
||||
<file>themes/qet-dark/scalable/snap-to-grid.svg</file>
|
||||
<file>themes/qet-dark/scalable/edit-opacity.svg</file>
|
||||
<file>themes/qet-dark/scalable/image-flip-horizontal.svg</file>
|
||||
<file>themes/qet-dark/scalable/image-flip-vertical.svg</file>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 4 5 h 14 v 4 h -14 Z M 7 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 10 1 h 2 v 3 h -2 Z M 10 10 h 2 v 2 h -2 Z M 10 18 h 2 v 3 h -2 Z M 3 4 h 16 v 6 h -16 Z M 4 5 h 14 v 4 h -14 Z M 6 12 h 10 v 6 h -10 Z M 7 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 5 5 h 13 v 4 h -13 Z M 5 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 2 1 h 2 v 20 h -2 Z M 4 4 h 15 v 6 h -15 Z M 5 5 h 13 v 4 h -13 Z M 4 12 h 10 v 6 h -10 Z M 5 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 654 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 4 5 h 13 v 4 h -13 Z M 9 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 18 1 h 2 v 20 h -2 Z M 3 4 h 15 v 6 h -15 Z M 4 5 h 13 v 4 h -13 Z M 8 12 h 10 v 6 h -10 Z M 9 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 5 4 h 4 v 13 h -4 Z M 13 9 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 1 18 h 20 v 2 h -20 Z M 4 3 h 6 v 15 h -6 Z M 5 4 h 4 v 13 h -4 Z M 12 8 h 6 v 10 h -6 Z M 13 9 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 652 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 5 4 h 4 v 14 h -4 Z M 13 7 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 1 10 h 3 v 2 h -3 Z M 10 10 h 2 v 2 h -2 Z M 18 10 h 3 v 2 h -3 Z M 4 3 h 6 v 16 h -6 Z M 5 4 h 4 v 14 h -4 Z M 12 6 h 6 v 10 h -6 Z M 13 7 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 696 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 5 5 h 4 v 13 h -4 Z M 13 5 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 1 2 h 20 v 2 h -20 Z M 4 4 h 6 v 15 h -6 Z M 5 5 h 4 v 13 h -4 Z M 12 4 h 6 v 10 h -6 Z M 13 5 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 651 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:0.35;stroke:none" d="M 2 0 h 1 v 22 h -1 Z M 8 0 h 1 v 22 h -1 Z M 14 0 h 1 v 22 h -1 Z M 20 0 h 1 v 22 h -1 Z M 0 2 h 22 v 1 h -22 Z M 0 8 h 22 v 1 h -22 Z M 0 14 h 22 v 1 h -22 Z M 0 20 h 22 v 1 h -22 Z M 9 9 h 5 v 5 h -5 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 8 8 h 7 v 7 h -7 Z M 9 9 h 5 v 5 h -5 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 742 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 4 5 h 14 v 4 h -14 Z M 7 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 10 1 h 2 v 3 h -2 Z M 10 10 h 2 v 2 h -2 Z M 10 18 h 2 v 3 h -2 Z M 3 4 h 16 v 6 h -16 Z M 4 5 h 14 v 4 h -14 Z M 6 12 h 10 v 6 h -10 Z M 7 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 690 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 5 5 h 13 v 4 h -13 Z M 5 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 2 1 h 2 v 20 h -2 Z M 4 4 h 15 v 6 h -15 Z M 5 5 h 13 v 4 h -13 Z M 4 12 h 10 v 6 h -10 Z M 5 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 644 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 4 5 h 13 v 4 h -13 Z M 9 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 18 1 h 2 v 20 h -2 Z M 3 4 h 15 v 6 h -15 Z M 4 5 h 13 v 4 h -13 Z M 8 12 h 10 v 6 h -10 Z M 9 13 h 8 v 4 h -8 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 645 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 5 4 h 4 v 13 h -4 Z M 13 9 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 1 18 h 20 v 2 h -20 Z M 4 3 h 6 v 15 h -6 Z M 5 4 h 4 v 13 h -4 Z M 12 8 h 6 v 10 h -6 Z M 13 9 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 5 4 h 4 v 14 h -4 Z M 13 7 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 1 10 h 3 v 2 h -3 Z M 10 10 h 2 v 2 h -2 Z M 18 10 h 3 v 2 h -3 Z M 4 3 h 6 v 16 h -6 Z M 5 4 h 4 v 14 h -4 Z M 12 6 h 6 v 10 h -6 Z M 13 7 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 686 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 5 5 h 4 v 13 h -4 Z M 13 5 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 1 2 h 20 v 2 h -20 Z M 4 4 h 6 v 15 h -6 Z M 5 5 h 4 v 13 h -4 Z M 12 4 h 6 v 10 h -6 Z M 13 5 h 4 v 8 h -4 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 641 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:#dcdcdc;fill-opacity:0.35;stroke:none" d="M 2 0 h 1 v 22 h -1 Z M 8 0 h 1 v 22 h -1 Z M 14 0 h 1 v 22 h -1 Z M 20 0 h 1 v 22 h -1 Z M 0 2 h 22 v 1 h -22 Z M 0 8 h 22 v 1 h -22 Z M 0 14 h 22 v 1 h -22 Z M 0 20 h 22 v 1 h -22 Z M 9 9 h 5 v 5 h -5 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none" d="M 8 8 h 7 v 7 h -7 Z M 9 9 h 5 v 5 h -5 Z" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 732 B |
@@ -865,7 +865,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
|
||||
<message>
|
||||
<location filename="../sources/ui/borderpropertieswidget.ui" line="20"/>
|
||||
<source>Dimensions du folio</source>
|
||||
<translation>Dimensions of folio</translation>
|
||||
<translation>Folio size</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -996,8 +996,8 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
|
||||
<source>Modifier la couleur de %n conducteur(s)</source>
|
||||
<comment>undo caption</comment>
|
||||
<translation>
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
<numerusform>Change the colour of %n conductor</numerusform>
|
||||
<numerusform>Change the colour of %n conductors</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -3045,12 +3045,12 @@ The element's display name is edited separately in the element properties.<
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/elementpropertieseditorwidget.cpp" line="209"/>
|
||||
<source>Renvoi de folio suivant</source>
|
||||
<translation>Reference folio following</translation>
|
||||
<translation>Next folio reference</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/elementpropertieseditorwidget.cpp" line="210"/>
|
||||
<source>Renvoi de folio précédent</source>
|
||||
<translation>Previous reference folio</translation>
|
||||
<translation>Previous folio reference</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/elementpropertieseditorwidget.cpp" line="211"/>
|
||||
@@ -3281,7 +3281,7 @@ The element's display name is edited separately in the element properties.<
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/elementpropertieseditorwidget.cpp" line="888"/>
|
||||
<source>Réf.</source>
|
||||
<translation>Réf.</translation>
|
||||
<translation>Ref.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/elementpropertieseditorwidget.cpp" line="895"/>
|
||||
@@ -3629,7 +3629,7 @@ The element's display name is edited separately in the element properties.<
|
||||
<message>
|
||||
<location filename="../sources/dataBase/ui/elementquerywidget.cpp" line="45"/>
|
||||
<source>Titre du folio</source>
|
||||
<translation>Title of folio</translation>
|
||||
<translation>Folio title</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/dataBase/ui/elementquerywidget.cpp" line="46"/>
|
||||
@@ -4040,16 +4040,16 @@ By importing this file, you confirm that:
|
||||
<location filename="../sources/ElementsCollection/elementscollectionwidget.cpp" line="939"/>
|
||||
<source>%n élément(s), répartie(s)</source>
|
||||
<translation>
|
||||
<numerusform>%n element, part</numerusform>
|
||||
<numerusform>%n elements, parts</numerusform>
|
||||
<numerusform>%n element, spread</numerusform>
|
||||
<numerusform>%n elements, spread</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<location filename="../sources/ElementsCollection/elementscollectionwidget.cpp" line="941"/>
|
||||
<source>dans %n dossier(s).</source>
|
||||
<translation>
|
||||
<numerusform>in %n folder.</numerusform>
|
||||
<numerusform>in %n folders.</numerusform>
|
||||
<numerusform>across %n folder.</numerusform>
|
||||
<numerusform>across %n folders.</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -4215,7 +4215,7 @@ By importing this file, you confirm that:
|
||||
<message>
|
||||
<location filename="../sources/elementspanelwidget.cpp" line="74"/>
|
||||
<source>Remonter ce folio</source>
|
||||
<translation>Move up this folio</translation>
|
||||
<translation>Move this folio up</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/elementspanelwidget.cpp" line="75"/>
|
||||
@@ -4225,17 +4225,17 @@ By importing this file, you confirm that:
|
||||
<message>
|
||||
<location filename="../sources/elementspanelwidget.cpp" line="76"/>
|
||||
<source>Remonter ce folio x10</source>
|
||||
<translation>Move up this folio x10</translation>
|
||||
<translation>Move this folio up x10</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/elementspanelwidget.cpp" line="77"/>
|
||||
<source>Remonter ce folio x100</source>
|
||||
<translation>Move up this folio x100</translation>
|
||||
<translation>Move this folio up x100</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/elementspanelwidget.cpp" line="78"/>
|
||||
<source>Remonter ce folio au debut</source>
|
||||
<translation>Move up this folio to the beginning</translation>
|
||||
<translation>Move this folio to the beginning</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/elementspanelwidget.cpp" line="79"/>
|
||||
@@ -4381,7 +4381,7 @@ By importing this file, you confirm that:
|
||||
<message>
|
||||
<location filename="../sources/exportdialog.cpp" line="156"/>
|
||||
<source>Titre du folio</source>
|
||||
<translation>Title of folio</translation>
|
||||
<translation>Folio title</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/exportdialog.cpp" line="836"/>
|
||||
@@ -4477,7 +4477,7 @@ By importing this file, you confirm that:
|
||||
<message>
|
||||
<location filename="../sources/exportpropertieswidget.cpp" line="188"/>
|
||||
<source>Exporter entièrement le folio</source>
|
||||
<translation>Export fully folio</translation>
|
||||
<translation>Export the whole folio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/exportpropertieswidget.cpp" line="191"/>
|
||||
@@ -4770,7 +4770,7 @@ that you create. Text and number inputs are
|
||||
<message>
|
||||
<location filename="../sources/TerminalStrip/ui/freeterminalmodel.cpp" line="228"/>
|
||||
<source>Référence croisé</source>
|
||||
<translation>Cross reference</translation>
|
||||
<translation>Cross-reference</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/TerminalStrip/ui/freeterminalmodel.cpp" line="229"/>
|
||||
@@ -5609,8 +5609,8 @@ Any setting other than “No rounding” may cause rendering errors in the proje
|
||||
<location filename="../sources/qetgraphicsitem/ViewItem/ui/graphicstablepropertieseditor.cpp" line="424"/>
|
||||
<source><center>ATTENTION :</center>
|
||||
il manque %1 lignes afin d'afficher l'intégralité des informations</source>
|
||||
<translation><center>ATTENTION :</center>
|
||||
lines are missing %1 to display all the informations</translation>
|
||||
<translation><center>WARNING:</center>
|
||||
%1 more rows are needed to display all the information</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetgraphicsitem/ViewItem/ui/graphicstablepropertieseditor.cpp" line="536"/>
|
||||
@@ -6195,13 +6195,13 @@ Please use the advanced editor for this.</translation>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="526"/>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="552"/>
|
||||
<source>N° de folio</source>
|
||||
<translation>N° of folio</translation>
|
||||
<translation>Folio no.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="518"/>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="541"/>
|
||||
<source>Label de folio</source>
|
||||
<translation>Label folio</translation>
|
||||
<translation>Folio label</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="520"/>
|
||||
@@ -6209,7 +6209,7 @@ Please use the advanced editor for this.</translation>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="543"/>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="554"/>
|
||||
<source>Titre de folio</source>
|
||||
<translation>Title of folio</translation>
|
||||
<translation>Folio title</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="519"/>
|
||||
@@ -6297,12 +6297,12 @@ Please use the advanced editor for this.</translation>
|
||||
<message>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="236"/>
|
||||
<source>Report de folio</source>
|
||||
<translation>Folio referencing</translation>
|
||||
<translation>Folio reference</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/linksingleelementwidget.cpp" line="238"/>
|
||||
<source>Référence croisée (esclave)</source>
|
||||
<translation>Cross Reference (slave)</translation>
|
||||
<translation>Cross-reference (slave)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/linksingleelementwidget.ui" line="42"/>
|
||||
@@ -6364,18 +6364,18 @@ Please use the advanced editor for this.</translation>
|
||||
<message>
|
||||
<location filename="../sources/ui/masterpropertieswidget.cpp" line="70"/>
|
||||
<source>N° de folio</source>
|
||||
<translation>N° of folio</translation>
|
||||
<translation>Folio no.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/masterpropertieswidget.cpp" line="64"/>
|
||||
<source>Label de folio</source>
|
||||
<translation>Label folio</translation>
|
||||
<translation>Folio label</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/masterpropertieswidget.cpp" line="65"/>
|
||||
<location filename="../sources/ui/masterpropertieswidget.cpp" line="71"/>
|
||||
<source>Titre de folio</source>
|
||||
<translation>Title of folio</translation>
|
||||
<translation>Folio title</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/masterpropertieswidget.cpp" line="66"/>
|
||||
@@ -6583,12 +6583,12 @@ Do you still want to link this slave contact?</translation>
|
||||
<message>
|
||||
<location filename="../sources/ui/configpage/configpages.cpp" line="155"/>
|
||||
<source>Reports de folio</source>
|
||||
<translation>Folio referencings</translation>
|
||||
<translation>Folio references</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/configpage/configpages.cpp" line="156"/>
|
||||
<source>Références croisées</source>
|
||||
<translation>Cross References</translation>
|
||||
<translation>Cross-references</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/configpage/configpages.cpp" line="157"/>
|
||||
@@ -6816,7 +6816,7 @@ Do you still want to link this slave contact?</translation>
|
||||
<location filename="../sources/autoNum/ui/numparteditorw.cpp" line="291"/>
|
||||
<location filename="../sources/autoNum/ui/numparteditorw.cpp" line="481"/>
|
||||
<source>N° folio</source>
|
||||
<translation>N° scheme</translation>
|
||||
<translation>Folio no.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/autoNum/ui/numparteditorw.cpp" line="146"/>
|
||||
@@ -7216,7 +7216,7 @@ Voltage / Protocol : %1</translation>
|
||||
<source>
|
||||
Numéro : %1</source>
|
||||
<translation>
|
||||
Numéro : %1</translation>
|
||||
Number: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/potentialselectordialog.cpp" line="205"/>
|
||||
@@ -7265,7 +7265,7 @@ Conductor section : %1</translation>
|
||||
Veuillez saisir une formule compatible pour ce potentiel.
|
||||
Les variables suivantes sont incompatibles :
|
||||
%sequf_ %seqtf_ %seqhf_ %id %F %M %LM</source>
|
||||
<translation>The new potential formula contains variables incompatible with the folio reports.
|
||||
<translation>The new potential formula contains variables incompatible with folio references.
|
||||
Please enter a compatible formula for this potential.
|
||||
The following variables are incompatible:
|
||||
%sequf_ %seqtf_ %seqhf_ %id %F %M %LM</translation>
|
||||
@@ -8562,7 +8562,7 @@ Available options:
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="566"/>
|
||||
<source>Édite les propriétés du folio (dimensions, informations du cartouche, propriétés des conducteurs...)</source>
|
||||
<comment>status bar tip</comment>
|
||||
<translation>Edits the properties of the folio (size, title block informations, conductor properties...)</translation>
|
||||
<translation>Edits the properties of the folio (size, title block information, conductor properties...)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="737"/>
|
||||
@@ -9172,12 +9172,12 @@ Hold Ctrl while moving to place freely.</translation>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="1027"/>
|
||||
<source>Ajoute une courbe de Bézier sur le folio actuel</source>
|
||||
<translation>Adds a Bézier curve on the current sheet</translation>
|
||||
<translation>Adds a Bézier curve to the current folio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="1028"/>
|
||||
<source>Ajoute un plan de bornier sur le folio actuel</source>
|
||||
<translation>Add a terminal plan on the current sheet</translation>
|
||||
<translation>Adds a terminal plan to the current folio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="1078"/>
|
||||
@@ -9273,8 +9273,8 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="2101"/>
|
||||
<source>%n objet(s) remis sur la grille</source>
|
||||
<translation>
|
||||
<numerusform>%n item placed on the grid</numerusform>
|
||||
<numerusform>%n item placed on the grid</numerusform>
|
||||
<numerusform>%n item put back on the grid</numerusform>
|
||||
<numerusform>%n items put back on the grid</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -9387,8 +9387,8 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="3518"/>
|
||||
<source>%n élément(s) redessiné(s).</source>
|
||||
<translation>
|
||||
<numerusform>%n element redesigned.</numerusform>
|
||||
<numerusform>%n elements redesigned.</numerusform>
|
||||
<numerusform>%n element redrawn.</numerusform>
|
||||
<numerusform>%n elements redrawn.</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
@@ -9403,8 +9403,8 @@ Unbridge and/or remove the levels from the affected terminals so that they can b
|
||||
<location filename="../sources/qetdiagrameditor.cpp" line="3534"/>
|
||||
<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: their size, grip or boundaries have changed (a boundary has been added, removed or moved).</numerusform>
|
||||
<numerusform>%n elements not redrawn: their size, grip or boundaries have changed (a boundary has been added, removed or moved).</numerusform>
|
||||
<numerusform>%n element not redrawn: its size, grip point or terminals have changed (a terminal was added, removed or moved).</numerusform>
|
||||
<numerusform>%n elements not redrawn: their size, grip point or terminals have changed (a terminal was added, removed or moved).</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -9858,7 +9858,7 @@ Enable scripts? This setting can be changed in Configure QElectroTech > Gener
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/qetelementeditor.cpp" line="810"/>
|
||||
<source><br><b>Erreur</b> :<br>Les reports de folio doivent posséder une seul borne.<br><b>Solution</b> :<br>Verifier que l'élément ne possède qu'une seul borne</source>
|
||||
<translation><br> <b> Error </ b>: <br> folio referencings must have a single terminal <br> <b> Solution </ b> :<br> Check that the element has only one terminal</translation>
|
||||
<translation><br><b>Error</b>:<br>Folio references must have a single terminal.<br><b>Solution</b>:<br>Check that the element has only one terminal</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/editor/ui/qetelementeditor.cpp" line="843"/>
|
||||
@@ -9871,8 +9871,8 @@ Enable scripts? This setting can be changed in Configure QElectroTech > Gener
|
||||
<source> %n erreur(s)</source>
|
||||
<comment>errors</comment>
|
||||
<translation>
|
||||
<numerusform> %n erreur</numerusform>
|
||||
<numerusform> %n erreurs</numerusform>
|
||||
<numerusform> %n error</numerusform>
|
||||
<numerusform> %n errors</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -11138,8 +11138,8 @@ What do you wish to do ?</translation>
|
||||
<source>%n forme(s)</source>
|
||||
<comment>Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.</comment>
|
||||
<translation>
|
||||
<numerusform>%n forme</numerusform>
|
||||
<numerusform>%n formes</numerusform>
|
||||
<numerusform>%n shape</numerusform>
|
||||
<numerusform>%n shapes</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
@@ -11252,7 +11252,7 @@ What do you wish to do ?</translation>
|
||||
<message>
|
||||
<location filename="../sources/exportdialog.cpp" line="1154"/>
|
||||
<source>Folio sans titre</source>
|
||||
<translation>Folio Untitled</translation>
|
||||
<translation>Untitled folio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/exportdialog.cpp" line="1156"/>
|
||||
@@ -11645,8 +11645,8 @@ the translated name of this folder could not be read, so its folder name is disp
|
||||
<source>Ajouter %n conducteur(s)</source>
|
||||
<comment>add a numbers of conductor one or more</comment>
|
||||
<translation>
|
||||
<numerusform>add %n conductor</numerusform>
|
||||
<numerusform>add %n conductors</numerusform>
|
||||
<numerusform>Add %n conductor</numerusform>
|
||||
<numerusform>Add %n conductors</numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
@@ -13849,7 +13849,7 @@ E.g. associating the name "volta" with the value "1745" will
|
||||
<message>
|
||||
<location filename="../sources/ui/reportpropertiewidget.ui" line="20"/>
|
||||
<source>Label de report de folio</source>
|
||||
<translation>Label of folio referencing</translation>
|
||||
<translation>Folio reference label</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/reportpropertiewidget.ui" line="35"/>
|
||||
@@ -13861,14 +13861,14 @@ Créer votre propre texte en vous aidant des variables suivantes :
|
||||
%LM : la localisation
|
||||
%l : le numéro de ligne
|
||||
%c : le numéro de colonne</source>
|
||||
<translation>You can define a custom label for folio reports.
|
||||
Create your own text by using the following variables:
|
||||
% f: the folio position in the project
|
||||
% F: the folio number
|
||||
% M: the installation
|
||||
% LM: the location
|
||||
% l: the line number
|
||||
% c: the column number</translation>
|
||||
<translation>You can define a custom label for folio references.
|
||||
Create your own text using the following variables:
|
||||
%f: the folio position in the project
|
||||
%F: the folio number
|
||||
%M: the installation
|
||||
%LM: the location
|
||||
%l: the row number
|
||||
%c: the column number</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -14031,7 +14031,7 @@ Create your own text by using the following variables:
|
||||
<message>
|
||||
<location filename="../sources/SearchAndReplace/ui/searchandreplacewidget.cpp" line="200"/>
|
||||
<source>Eléments report de folio</source>
|
||||
<translation>Folio referencings elements</translation>
|
||||
<translation>Folio reference elements</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/SearchAndReplace/ui/searchandreplacewidget.cpp" line="206"/>
|
||||
@@ -17384,7 +17384,7 @@ The other fields are not used.</translation>
|
||||
<message>
|
||||
<location filename="../sources/TerminalStrip/ui/terminalstripmodel.cpp" line="291"/>
|
||||
<source>Référence croisé</source>
|
||||
<translation>Cross reference</translation>
|
||||
<translation>Cross-reference</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/TerminalStrip/ui/terminalstripmodel.cpp" line="292"/>
|
||||
@@ -17673,7 +17673,7 @@ The other fields are not used.</translation>
|
||||
<message>
|
||||
<location filename="../sources/ui/titleblockpropertieswidget.ui" line="32"/>
|
||||
<source>Informations des cartouches</source>
|
||||
<translation>Title block informations</translation>
|
||||
<translation>Title block information</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/titleblockpropertieswidget.ui" line="237"/>
|
||||
@@ -18547,13 +18547,13 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol
|
||||
%c : le numéro de colonne
|
||||
%M: Installation
|
||||
%LM: Localisation </source>
|
||||
<translation>Create your own text by helping you of the following variables :
|
||||
%f : the folio number
|
||||
% F: folio label
|
||||
% l : the line number
|
||||
% c : column number
|
||||
% M: Plant
|
||||
% LM: Location </translation>
|
||||
<translation>Create your own text using the following variables:
|
||||
%f: the folio number
|
||||
%F: the folio label
|
||||
%l: the row number
|
||||
%c: the column number
|
||||
%M: the installation
|
||||
%LM: the location</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../sources/ui/xrefpropertieswidget.ui" line="280"/>
|
||||
|
||||
@@ -90,12 +90,19 @@ ALIASES = {
|
||||
# SVG icons referenced from sources/qeticons.cpp. ico/scalable/ holds the
|
||||
# ones drawn for QET as vectors; one file serves every size.
|
||||
SVGS = [
|
||||
"scalable/align-horizontal-center.svg",
|
||||
"scalable/align-horizontal-left.svg",
|
||||
"scalable/align-horizontal-right.svg",
|
||||
"scalable/align-vertical-bottom.svg",
|
||||
"scalable/align-vertical-center.svg",
|
||||
"scalable/align-vertical-top.svg",
|
||||
"scalable/diagram.svg",
|
||||
"scalable/folio-delete.svg",
|
||||
"scalable/folio-new.svg",
|
||||
"scalable/folio-properties.svg",
|
||||
"scalable/label.svg",
|
||||
"scalable/pdf-import.svg",
|
||||
"scalable/snap-to-grid.svg",
|
||||
"breeze-icons/scalable/apps/hidef/edit-opacity.svg",
|
||||
"breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg",
|
||||
"breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg",
|
||||
|
||||
@@ -54,6 +54,9 @@ misc/qet-mcp/qet_mcp.py --list
|
||||
|
||||
# speak MCP on stdin/stdout
|
||||
misc/qet-mcp/qet_mcp.py
|
||||
|
||||
# run one tool and exit, no MCP client needed
|
||||
misc/qet-mcp/qet_mcp.py --call qet_project_info '{"path": "drawing.qet"}'
|
||||
```
|
||||
|
||||
Register it with an MCP client, for example:
|
||||
@@ -66,6 +69,7 @@ Register it with an MCP client, for example:
|
||||
"args": ["/path/to/qelectrotech/misc/qet-mcp/qet_mcp.py"],
|
||||
"env": {
|
||||
"QET_MCP_WORKSPACE": "/home/you/drawings",
|
||||
"QET_BINARY": "/usr/bin/qelectrotech",
|
||||
"QET_ENABLE_SCRIPTING": "1"
|
||||
}
|
||||
}
|
||||
@@ -73,6 +77,97 @@ Register it with an MCP client, for example:
|
||||
}
|
||||
```
|
||||
|
||||
`QET_BINARY` is the QElectroTech the tools launch. Leave it out when
|
||||
`qelectrotech` is on your `PATH`, or when the server is installed with
|
||||
QElectroTech (as `<prefix>/share/qelectrotech/mcp/qet_mcp.py`, which also
|
||||
finds the installed element collection).
|
||||
|
||||
## Installed with QElectroTech
|
||||
|
||||
QElectroTech's packages install the server next to the program, and from
|
||||
there it finds that QElectroTech and its element collection by itself:
|
||||
|
||||
| Package | Server | Finds |
|
||||
|---|---|---|
|
||||
| `make install`, Linux distributions | `<prefix>/share/qelectrotech/mcp/qet_mcp.py` | `<prefix>/bin/qelectrotech`, `<prefix>/share/qelectrotech/elements` |
|
||||
| Windows installer, MSI, portable folder | `<folder>\mcp\qet_mcp.py` (the "AI assistant (MCP)" component) | `<folder>\bin\QElectroTech.exe`, `<folder>\elements` |
|
||||
|
||||
So a client configuration needs only the path to the server and the
|
||||
workspace. On Windows, the installer also offers **Python for the AI
|
||||
assistant** (unticked by default; always in the portable folder and the
|
||||
MSI): Python from python.org in `<folder>\mcp\python`, for anyone without
|
||||
a Python of their own. Then:
|
||||
|
||||
```json
|
||||
"command": "C:\\Program Files\\QElectroTech\\mcp\\python\\python.exe",
|
||||
"args": ["C:\\Program Files\\QElectroTech\\mcp\\qet_mcp.py"]
|
||||
```
|
||||
|
||||
Snap and flatpak install it too. Their QElectroTech is built to run inside
|
||||
the package's sandbox, and starting it from the server has not been tested:
|
||||
the tools that read a file work, exports and edits may not. If they fail,
|
||||
point `QET_BINARY` at a QElectroTech installed another way.
|
||||
|
||||
## Using it from the Claude app
|
||||
|
||||
A web chat in a browser cannot start a program on your computer, so it
|
||||
cannot run this server. The Claude desktop app for Windows and macOS can,
|
||||
and it uses the same account as the website.
|
||||
|
||||
1. Install Python 3.9 or later. Nothing else is needed.
|
||||
2. In the desktop app, open **Settings → Developer → Edit Config**. This
|
||||
opens `claude_desktop_config.json`.
|
||||
3. Add the server, with your own paths:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"qet": {
|
||||
"command": "python",
|
||||
"args": ["C:\\path\\to\\qelectrotech\\misc\\qet-mcp\\qet_mcp.py"],
|
||||
"env": {
|
||||
"QET_MCP_WORKSPACE": "C:\\Users\\you\\Documents\\drawings",
|
||||
"QET_ENABLE_SCRIPTING": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On macOS use `python3` and ordinary `/` paths. In JSON every `\` in a
|
||||
Windows path is written `\\`.
|
||||
4. Quit the app completely and start it again. The tools appear under the
|
||||
chat box's tools menu.
|
||||
5. If `qelectrotech` is not on your `PATH`, add `"QET_BINARY"` to the
|
||||
`env` block with the full path to the executable
|
||||
(`C:\\Program Files\\...\\qelectrotech.exe`, written with `\\`).
|
||||
|
||||
Only files under `QET_MCP_WORKSPACE` can be read or written (see
|
||||
[What the server is allowed to touch](#what-the-server-is-allowed-to-touch)).
|
||||
Leave out `QET_ENABLE_SCRIPTING` if you do not want the assistant to edit
|
||||
projects; see the next section for what that switches off.
|
||||
|
||||
The test suite runs on Linux. The server uses nothing platform-specific,
|
||||
but it has not yet been tested on Windows or macOS.
|
||||
|
||||
### With only a browser
|
||||
|
||||
If your web chat can run Python (on claude.ai, code execution), upload
|
||||
`qet_mcp.py` together with your project and ask the assistant to use
|
||||
`--call`:
|
||||
|
||||
```bash
|
||||
python3 qet_mcp.py --call qet_elements '{"path": "drawing.qet"}'
|
||||
echo '{"path": "drawing.qet"}' | python3 qet_mcp.py --call qet_check -
|
||||
```
|
||||
|
||||
It prints the tool's JSON result and exits 0, or 1 if the tool reported an
|
||||
error, or 2 if the call itself was malformed. The workspace rule applies as
|
||||
it does in a server. The sandbox has no QElectroTech in it, so only the
|
||||
tools that read files work there: `qet_project_info`, `qet_elements`,
|
||||
`qet_conductors`, `qet_items`, `qet_diff`, `qet_scan`,
|
||||
`qet_element_info`, `qet_element_search` and `qet_element_build`.
|
||||
|
||||
## Five tools need scripting switched on
|
||||
|
||||
A QElectroTech with JavaScript scripting switched off refuses `--run`, and
|
||||
@@ -116,11 +211,22 @@ Set the workspace to the folder your drawings live in. A path outside it is
|
||||
refused with an error naming what was allowed; symlinks are resolved first,
|
||||
so a link planted inside the workspace is judged by where it points.
|
||||
|
||||
Two arguments are deliberately **not** confined: `binary` (the
|
||||
`qelectrotech` executable) and `elements_dir` (the element collection).
|
||||
Those are configuration, chosen once by whoever runs the server, and both
|
||||
normally live in `/usr` or a build tree — outside any sensible workspace.
|
||||
Confining them would reject the ordinary case while stopping nothing.
|
||||
**The client does not choose what program runs.** The tools that launch
|
||||
QElectroTech use the one the server found (`QET_BINARY`, the install it
|
||||
ships in, or `PATH`). A call may still name `binary`, but only as that same
|
||||
file or one listed by whoever configured the server:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `QET_BINARY` | the QElectroTech to launch |
|
||||
| `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 |
|
||||
|
||||
Anything else is refused, even a file inside the workspace: being there
|
||||
makes it readable, not runnable. Before this rule any executable a call
|
||||
named was run, with the call's own paths as arguments, so text inside a
|
||||
project could steer an assistant into starting another program.
|
||||
|
||||
`QET_MCP_ALLOW_ANY_PATH=1` is equivalent to granting the client local
|
||||
filesystem access with this process's privileges. It exists so that is a
|
||||
@@ -158,7 +264,6 @@ the answer a screenshot gave wrongly.
|
||||
|
||||
```json
|
||||
{"name": "qet_edit", "arguments": {
|
||||
"binary": "/path/to/qelectrotech",
|
||||
"project": "in.qet", "output": "out.qet",
|
||||
"elements_dir": "/path/to/qelectrotech/elements",
|
||||
"operations": [
|
||||
@@ -214,7 +319,7 @@ to emit.
|
||||
|
||||
```json
|
||||
{"name": "qet_query", "arguments": {
|
||||
"binary": "/path/to/qelectrotech", "project": "industrial.qet",
|
||||
"project": "industrial.qet",
|
||||
"sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view WHERE label <> '' GROUP BY label HAVING n > 1 ORDER BY n DESC"}}
|
||||
```
|
||||
|
||||
@@ -296,6 +401,10 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
an index would shift. A folio saved without a uuid shows it empty:
|
||||
QElectroTech gives it one on load and writes it on the next save, so it
|
||||
appears after a first `qet_edit`. Needs `qet.folioIndex()` in the build.
|
||||
The `"$id"` of an `add_folio` or `insert_folio` works the same way: it
|
||||
keeps naming that folio after a later `insert_folio` or `remove_folio` in
|
||||
the same run (on a build without `qet.folioUuid()`, it is the index the
|
||||
folio had when it was made, as before).
|
||||
- **A conductor can be named by its uuid** (`qet_conductors` reports it):
|
||||
`set_conductor`, `move_conductor_segment` and `delete_conductor` take
|
||||
`"conductor": "{uuid}"` in place of `element` + `terminal`, which works
|
||||
@@ -305,9 +414,19 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
`qet.conductorEnds()` in the build. A uuid names one wire, but
|
||||
`set_conductor` still changes the whole potential, the same as by
|
||||
terminal. A project saved before conductors carried a uuid has none in
|
||||
the file: QElectroTech makes a new one on every load and does not save
|
||||
it, so `qet_conductors` reports `uuid` as empty and those conductors are
|
||||
named by `element` + `terminal` until wire uuids last (#1103).
|
||||
the file until it is saved once: since #1107 QElectroTech works one out
|
||||
from the wire's two ends on load and writes it on the next save, so it
|
||||
appears after a first `qet_edit`.
|
||||
- **A terminal can be named by its uuid**: `terminal`, `from_terminal` and
|
||||
`to_terminal` take the terminal's uuid (as `qet_element_info` lists it)
|
||||
in place of its index, on the op's own element (for `add_conductor`, on
|
||||
that end's element). It is turned at run time into the index the call
|
||||
takes; if the element has no terminal with it the op fails and its
|
||||
`note` says so. Unlike the index, which is a sort by position, it is
|
||||
defined between two terminals at the same point. Needs
|
||||
`qet.terminalIndex()` in the build. A symbol file saved without terminal
|
||||
uuids lists them empty; QElectroTech gives the terminals of every
|
||||
project's copy of it a uuid on opening (#1118), written on the next save.
|
||||
- **`qet_export` isolates its launch.** SingleApplication keys its socket
|
||||
on `applicationFilePath()`, so a second launch of the same binary path
|
||||
forwards its request to an already-running instance and returns *that*
|
||||
@@ -347,7 +466,8 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
changes nothing. `addElement` and the move/delete verbs shipped with the
|
||||
scripting API; `addConductor`, `rotateElement`, `setElementLabel`,
|
||||
`setElementInfo` and `setFolioTitle` are newer.
|
||||
- **`elements_dir` is not optional for `common://` paths.** The sandboxed
|
||||
- **`elements_dir` is not optional for `common://` paths** unless the
|
||||
server is installed with QElectroTech, which fills it in. The sandboxed
|
||||
run has its own empty HOME, so QElectroTech falls back to the compiled-in
|
||||
collection path, which on a machine that never ran `make install` does not
|
||||
exist. The only symptom is `addElement` reporting that a file plainly
|
||||
|
||||
@@ -47,6 +47,11 @@ PROTOCOL
|
||||
Line-delimited JSON-RPC 2.0 on stdin/stdout, per MCP's stdio transport.
|
||||
Nothing but protocol goes to stdout; diagnostics go to stderr.
|
||||
No third-party dependencies — the MCP SDK is not assumed to be present.
|
||||
|
||||
`--call <tool> [arguments]` runs one tool without an MCP client, for an
|
||||
assistant that can execute Python but cannot launch a server (a web chat
|
||||
with code execution). It goes through the same dispatcher, so the
|
||||
workspace policy applies exactly as it does over stdio.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -126,12 +131,56 @@ def _wires(diagram: ET.Element):
|
||||
|
||||
|
||||
def _conductors(root: ET.Element):
|
||||
definitions = _definition_terminals(root)
|
||||
for i, d in _folios(root):
|
||||
index = _terminal_index(d)
|
||||
index = _terminal_index(d, definitions)
|
||||
for c in _wires(d):
|
||||
yield i, c, index
|
||||
|
||||
|
||||
def _uuid_key(value: str) -> str:
|
||||
return (value or "").strip().strip("{}").lower()
|
||||
|
||||
|
||||
# Orientation as a placed symbol's <terminal> record writes it (an int,
|
||||
# Qet::Orientation) or as a definition does (n/e/s/w).
|
||||
_ORIENTATIONS = {"n": 0, "e": 1, "s": 2, "w": 3, "0": 0, "1": 1, "2": 2, "3": 3}
|
||||
|
||||
# Where QElectroTech docks a wire, relative to the terminal's position in
|
||||
# its definition (Terminal's constructor, Terminal::terminalSize = 4). A
|
||||
# placed symbol's <terminal> record is written at that point.
|
||||
_DOCK_OFFSET = {0: (0.0, 4.0), 1: (-4.0, 0.0), 2: (0.0, -4.0), 3: (4.0, 0.0)}
|
||||
|
||||
|
||||
def _definition_terminals(root: ET.Element) -> dict:
|
||||
"""Map each symbol stored in the project ("embed://" + its path in the
|
||||
<collection>) to its terminals: {terminal uuid: (x, y, orientation)},
|
||||
the position being the one in the definition."""
|
||||
out = {}
|
||||
|
||||
def walk(node, path):
|
||||
for child in node:
|
||||
if child.tag == "category":
|
||||
walk(child, path + [child.get("name", "")])
|
||||
elif child.tag == "element":
|
||||
terminals = {}
|
||||
for t in child.findall("definition/description/terminal"):
|
||||
try:
|
||||
terminals[_uuid_key(t.get("uuid"))] = (
|
||||
float(t.get("x")), float(t.get("y")),
|
||||
_ORIENTATIONS.get((t.get("orientation") or "n")[:1], 0))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
terminals.pop("", None)
|
||||
if terminals:
|
||||
out["embed://" + "/".join(path + [child.get("name", "")])] = terminals
|
||||
|
||||
collection = root.find("collection")
|
||||
if collection is not None:
|
||||
walk(collection, [])
|
||||
return out
|
||||
|
||||
|
||||
def _element_row(folio: int, el: ET.Element) -> dict:
|
||||
info = _element_info(el)
|
||||
etype = el.get("type", "")
|
||||
@@ -147,7 +196,7 @@ def _element_row(folio: int, el: ET.Element) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _terminal_index(diagram: ET.Element) -> dict:
|
||||
def _terminal_index(diagram: ET.Element, definitions: dict | None = None) -> dict:
|
||||
"""Map a folio's terminal ids to an identity that survives a save.
|
||||
|
||||
A conductor names its ends with terminal1/terminal2, which are plain
|
||||
@@ -170,6 +219,17 @@ def _terminal_index(diagram: ET.Element) -> dict:
|
||||
Conductors in the corpus carry no element1/element2 attribute -- 0 of
|
||||
47 in ArduinoLCD.qet, 0 of 67 in 741.qet -- so this mapping has to be
|
||||
built from the elements rather than read off the conductor.
|
||||
|
||||
A conductor can also name its ends by terminal uuid (element1 +
|
||||
terminal1), and QElectroTech writes that form as soon as the terminal
|
||||
has a uuid -- which, since a project gives every terminal one on
|
||||
opening, is the first save of any older file. So the same untouched
|
||||
conductor is written in the numbered form before a save and the uuid
|
||||
form after it. With @p definitions (from _definition_terminals()), a
|
||||
uuid end is resolved too, keyed (element uuid, terminal uuid), to the
|
||||
very same identity as the numbered end: the terminal's definition
|
||||
position, moved to where the wire docks, is where the placed symbol's
|
||||
<terminal> record is.
|
||||
"""
|
||||
index = {}
|
||||
for el in diagram.iter("element"):
|
||||
@@ -184,12 +244,26 @@ def _terminal_index(diagram: ET.Element) -> dict:
|
||||
# marked with a "#" so the caller can see the diff is on the
|
||||
# unstable footing that file forces.
|
||||
continue
|
||||
records = []
|
||||
for t in el.iter("terminal"):
|
||||
tid = t.get("id")
|
||||
if tid is None:
|
||||
continue
|
||||
index[tid] = (f"{uuid}@{t.get('x','?')},{t.get('y','?')}"
|
||||
f",{t.get('orientation','?')}")
|
||||
key = (f"{uuid}@{t.get('x','?')},{t.get('y','?')}"
|
||||
f",{t.get('orientation','?')}")
|
||||
index[tid] = key
|
||||
records.append((t, key))
|
||||
for tuuid, (x, y, o) in (definitions or {}).get(el.get("type", ""), {}).items():
|
||||
dx, dy = _DOCK_OFFSET[o]
|
||||
for t, key in records:
|
||||
try:
|
||||
if (abs(float(t.get("x")) - (x + dx)) < 1e-6
|
||||
and abs(float(t.get("y")) - (y + dy)) < 1e-6
|
||||
and _ORIENTATIONS.get((t.get("orientation") or "")[:1]) == o):
|
||||
index[(_uuid_key(uuid), tuuid)] = key
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return index
|
||||
|
||||
|
||||
@@ -214,7 +288,8 @@ def _conductor_key(folio: int, c: ET.Element, index: dict) -> str:
|
||||
tid = c.get(term_attr, "?")
|
||||
owner = c.get(elem_attr)
|
||||
if owner:
|
||||
ends.append(f"{owner}/{tid or c.get(name_attr, '?')}")
|
||||
ends.append(index.get((_uuid_key(owner), _uuid_key(tid)))
|
||||
or f"{owner}/{tid or c.get(name_attr, '?')}")
|
||||
else:
|
||||
# An id with no element behind it stays visible as itself
|
||||
# rather than silently collapsing conductors onto one key.
|
||||
@@ -809,6 +884,44 @@ def tool_element_info(path: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _launch_executable(src: Path, sandbox: Path, windows: bool) -> Path:
|
||||
"""The executable _run_qet() starts: a private copy, except on Windows.
|
||||
|
||||
The copy gives each run its own SingleApplication key, which is derived
|
||||
from the executable's path. On Windows a program loads its DLLs from its
|
||||
own folder, so a copy on its own dies before main() (0xC0000135, DLL not
|
||||
found) and nothing could ever be exported or edited there. Run the
|
||||
original instead: every flag this server passes is a CLI export flag or
|
||||
--run, and QElectroTech handles both and returns before it constructs
|
||||
SingleApplication (main.cpp), so there is no instance to be handed to.
|
||||
The copy stays elsewhere for builds from before that early return.
|
||||
"""
|
||||
if windows:
|
||||
return src
|
||||
exe = sandbox / f"qet-mcp-{os.getpid()}"
|
||||
shutil.copy2(src, exe)
|
||||
return exe
|
||||
|
||||
|
||||
def _launch_env(env: dict, home: Path, windows: bool) -> dict:
|
||||
"""The environment _run_qet() starts QElectroTech in.
|
||||
|
||||
A private HOME and XDG directories, and, except on Windows, Qt's
|
||||
offscreen platform so no display is needed. The Windows packages ship
|
||||
only the qwindows platform plugin: asked for "offscreen", Qt finds no
|
||||
plugin and stops at a message box nobody can close, so every call hung
|
||||
until its timeout. The export flags and --run open no window there, so
|
||||
the default platform is what they need.
|
||||
"""
|
||||
env = dict(env,
|
||||
HOME=str(home),
|
||||
XDG_CONFIG_HOME=str(home / ".config"),
|
||||
XDG_DATA_HOME=str(home / ".local" / "share"))
|
||||
if not windows:
|
||||
env["QT_QPA_PLATFORM"] = "offscreen"
|
||||
return env
|
||||
|
||||
|
||||
def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
elements_dir: str | None = None,
|
||||
script: str | None = None, tail: int = 4000) -> dict:
|
||||
@@ -850,8 +963,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
raise ValueError(f"not an executable: {src}")
|
||||
with tempfile.TemporaryDirectory(prefix="qet-mcp-") as tmp:
|
||||
sandbox = Path(tmp)
|
||||
exe = sandbox / f"qet-mcp-{os.getpid()}"
|
||||
shutil.copy2(src, exe)
|
||||
exe = _launch_executable(src, sandbox, os.name == "nt")
|
||||
home = sandbox / "home"
|
||||
(home / ".config").mkdir(parents=True)
|
||||
(home / ".local" / "share").mkdir(parents=True)
|
||||
@@ -868,11 +980,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
script_path = sandbox / "qet-mcp-edit.js"
|
||||
script_path.write_text(script, encoding="utf-8")
|
||||
args = ["--run", str(script_path), *args]
|
||||
env = dict(os.environ,
|
||||
HOME=str(home),
|
||||
XDG_CONFIG_HOME=str(home / ".config"),
|
||||
XDG_DATA_HOME=str(home / ".local" / "share"),
|
||||
QT_QPA_PLATFORM="offscreen")
|
||||
env = _launch_env(dict(os.environ), home, os.name == "nt")
|
||||
try:
|
||||
p = subprocess.run([str(exe), *args], env=env, timeout=timeout,
|
||||
capture_output=True, text=True)
|
||||
@@ -1222,14 +1330,14 @@ OPS = {
|
||||
"set_info": ("setElementInfo", [("folio", "folio"), ("element", "elmt"),
|
||||
("key", "str"), ("value", "str")]),
|
||||
"add_conductor": ("addConductor", [("folio", "folio"),
|
||||
("from", "elmt"), ("from_terminal", "num"),
|
||||
("to", "elmt"), ("to_terminal", "num")]),
|
||||
("from", "elmt"), ("from_terminal", "term"),
|
||||
("to", "elmt"), ("to_terminal", "term")]),
|
||||
"delete_element": ("deleteElement", [("folio", "folio"), ("element", "elmt")]),
|
||||
"set_conductor": ("setConductorProperty", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "num"), ("property", "str"),
|
||||
("terminal", "term"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
"move_conductor_segment": ("moveConductorSegment", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "num"), ("segment", "num"),
|
||||
("terminal", "term"), ("segment", "num"),
|
||||
("dx", "num"), ("dy", "num")]),
|
||||
"link_elements": ("linkElements", [("folio", "folio"), ("element", "elmt"),
|
||||
("to_folio", "folio"), ("to", "elmt")]),
|
||||
@@ -1250,7 +1358,7 @@ OPS = {
|
||||
# uuid, which does not shift when another one is added. So
|
||||
# it can be named as "$id". Indexes shift when one is added or deleted.
|
||||
"delete_conductor": ("deleteConductor", [("folio", "folio"), ("element", "elmt"),
|
||||
("terminal", "num")]),
|
||||
("terminal", "term")]),
|
||||
"remove_folio": ("removeFolio", [("folio", "folio")]),
|
||||
"set_folio": ("setFolioProperty", [("folio", "folio"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
@@ -1355,6 +1463,10 @@ SEARCH_REPLACE_KINDS = ["element_info", "conductor", "text"]
|
||||
FOLIO_PROPERTIES = ["title", "author", "filename", "plant", "locmach",
|
||||
"indexrev", "folio", "template"]
|
||||
|
||||
# The ops that make a folio: their "$id" is an index the next insert_folio
|
||||
# or remove_folio can shift, so the script also keeps the folio's uuid.
|
||||
FOLIO_MAKING_OPS = ("add_folio", "insert_folio")
|
||||
|
||||
# The ops that address one conductor by element + terminal, and so also
|
||||
# take "conductor": "{uuid}" (qet_conductors reports each one's uuid).
|
||||
CONDUCTOR_UUID_OPS = ("set_conductor", "move_conductor_segment", "delete_conductor")
|
||||
@@ -1394,14 +1506,19 @@ def _build_script(operations: list, output: str) -> str:
|
||||
JavaScript exception.
|
||||
"""
|
||||
refs: set[str] = set()
|
||||
# "$name"s made by an op that creates a folio: held as the folio's uuid
|
||||
# too, since a later insert_folio or remove_folio shifts its index.
|
||||
folio_refs: set[str] = set()
|
||||
# Resolvers a uuid reference needs; required only when one is used, so
|
||||
# an index-only edit still runs on a build that predates them.
|
||||
uuid_methods: set[str] = set()
|
||||
folio_js = "0"
|
||||
element_js = None # the op's element, for lookups scoped to it
|
||||
owner_js = None # the element a terminal argument belongs to
|
||||
lines = [
|
||||
"// generated by qet-mcp; do not edit",
|
||||
"var R = {};", # $name -> value from an earlier op
|
||||
"var F = {};", # $name -> uuid of a folio an op made
|
||||
"var missing = [];",
|
||||
"var need = @NEED@;",
|
||||
"for (var i = 0; i < need.length; i++) {",
|
||||
@@ -1439,6 +1556,16 @@ def _build_script(operations: list, output: str) -> str:
|
||||
"{kind: 'op_note', index: index, note: why}));",
|
||||
" return null;",
|
||||
"}",
|
||||
# A terminal named by uuid is turned into the index the calls take,
|
||||
# on its own element; -1, with the reason logged, if that element
|
||||
# has no terminal with it.
|
||||
"function qetMcpTerminal(index, key, folio, element, uuid) {",
|
||||
" var t = qet.terminalIndex(folio, element, uuid);",
|
||||
" if (t < 0) qet.log(" + _js(_MARKER) + " + JSON.stringify({kind: 'op_note', "
|
||||
"index: index, note: key + ': no terminal ' + uuid + ' on element ' + element + "
|
||||
"' (or two of its terminals carry it)'}));",
|
||||
" return t;",
|
||||
"}",
|
||||
"if (missing.length === 0) {",
|
||||
]
|
||||
|
||||
@@ -1464,6 +1591,11 @@ def _build_script(operations: list, output: str) -> str:
|
||||
raise ValueError(
|
||||
f"operation {op_index} refers to {value!r}, which no earlier "
|
||||
f"operation defined (set \"id\": {name!r} on the op that creates it)")
|
||||
if kind == "folio" and name in folio_refs:
|
||||
# the folio's index now, not when it was made; the stored
|
||||
# index on a build that cannot report folio uuids
|
||||
ref = _js(name)
|
||||
return f"(F[{ref}] ? qet.folioIndex(F[{ref}]) : R[{ref}])"
|
||||
return f"R[{_js(name)}]"
|
||||
if kind == "num":
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
@@ -1491,6 +1623,20 @@ def _build_script(operations: list, output: str) -> str:
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a table index "
|
||||
f"or its uuid, got {value!r}")
|
||||
return _js(value)
|
||||
if kind == "term":
|
||||
# A terminal's uuid comes from its symbol's definition, so it
|
||||
# names a terminal only on its element: the op's element, or for
|
||||
# add_conductor the end's own. Turned into the index the call
|
||||
# takes at run time; unlike the index, it is defined between two
|
||||
# terminals at the same point.
|
||||
if isinstance(value, str) and _UUID_RE.fullmatch(value):
|
||||
uuid_methods.add("terminalIndex")
|
||||
return (f"qetMcpTerminal({op_index}, {_js(key)}, {folio_js}, {owner_js}, "
|
||||
f"{_js(value)})")
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a terminal index "
|
||||
f"or its uuid, got {value!r}")
|
||||
return _js(value)
|
||||
if kind == "element_text":
|
||||
# A field's uuid is unique only within its element (copies keep
|
||||
# them), so the lookup takes the op's element too.
|
||||
@@ -1630,6 +1776,7 @@ def _build_script(operations: list, output: str) -> str:
|
||||
raise ValueError(f"operation {i} ({name}) is missing {key!r}")
|
||||
folio_js = args[0] if args else "0"
|
||||
element_js = args[1] if len(args) > 1 else None
|
||||
owner_js = args[-1] if args else None # a terminal's element precedes it
|
||||
args.append(ref_or(op[key], kind, i, key))
|
||||
if conductor_js is not None:
|
||||
args[1], args[2] = f"e{i}.element", f"e{i}.terminal"
|
||||
@@ -1651,6 +1798,10 @@ def _build_script(operations: list, output: str) -> str:
|
||||
if ident is not None:
|
||||
lines.append(f" R[{_js(ident)}] = v{i};")
|
||||
refs.add(ident)
|
||||
if name in FOLIO_MAKING_OPS:
|
||||
lines.append(f" F[{_js(ident)}] = (typeof qet.folioUuid === 'function' "
|
||||
f"&& v{i} >= 0) ? qet.folioUuid(v{i}) : '';")
|
||||
folio_refs.add(ident)
|
||||
lines.append(
|
||||
f" qet.log({_js(_MARKER)} + JSON.stringify("
|
||||
f"{{kind: 'op', index: {i}, op: {_js(name)}, "
|
||||
@@ -1712,7 +1863,10 @@ def _parse_script_output(text: str) -> dict:
|
||||
(isinstance(r, int) and not isinstance(r, bool) and r == -1))
|
||||
ops.append(rec)
|
||||
elif rec.get("kind") == "op_note":
|
||||
notes[rec.get("index")] = rec.get("note")
|
||||
# An op can log more than one (add_conductor, one per end):
|
||||
# keep them all rather than only the last.
|
||||
idx = rec.get("index")
|
||||
notes[idx] = (notes[idx] + "; " if idx in notes else "") + str(rec.get("note"))
|
||||
elif rec.get("kind") == "save":
|
||||
saved = bool(rec.get("result"))
|
||||
stopped = bool(rec.get("stopped_early"))
|
||||
@@ -2518,7 +2672,7 @@ TOOLS = [
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||
"binary": {"type": "string", "description": "the qelectrotech executable; leave it out to use the one this server is configured with. Any other is refused unless its configuration allows it"},
|
||||
"project": {"type": "string"},
|
||||
"format": {"type": "string", "enum": sorted(EXPORT_FORMATS)},
|
||||
"output": {"type": "string"},
|
||||
@@ -2527,7 +2681,7 @@ TOOLS = [
|
||||
"without this an existing file is never clobbered"},
|
||||
"timeout": {"type": "integer", "default": 180},
|
||||
},
|
||||
"required": ["binary", "project", "format", "output"],
|
||||
"required": ["project", "format", "output"],
|
||||
},
|
||||
"handler": lambda a: tool_export(a["binary"], a["project"], a["format"],
|
||||
a["output"], a.get("timeout", 180)),
|
||||
@@ -2545,7 +2699,7 @@ TOOLS = [
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||
"binary": {"type": "string", "description": "the qelectrotech executable; leave it out to use the one this server is configured with. Any other is refused unless its configuration allows it"},
|
||||
"project": {"type": "string", "description": "the .qet to start from; not modified"},
|
||||
"output": {"type": "string", "description": "where to write the edited project"},
|
||||
"overwrite": {"type": "boolean", "default": False,
|
||||
@@ -2565,8 +2719,14 @@ TOOLS = [
|
||||
"qet_project_info call 1 is \"folio\": 0 here. A folio can "
|
||||
"be given as its uuid instead (qet_project_info lists them), "
|
||||
"which still names the same folio after an earlier op adds or "
|
||||
"removes one. Terminals are numbered by their "
|
||||
"index in the element definition; qet_element_info lists them. "
|
||||
"removes one; so does the \"$id\" of an add_folio or "
|
||||
"insert_folio. A terminal (\"terminal\", \"from_terminal\", "
|
||||
"\"to_terminal\") is given by its index -- qet_element_info "
|
||||
"lists terminals in index order, top to bottom then left to "
|
||||
"right -- or by its uuid, which qet_element_info also lists and "
|
||||
"which, unlike the index, tells apart two terminals at the same "
|
||||
"point; a terminal uuid names a terminal of the op's own "
|
||||
"element (for add_conductor, of that end's element). "
|
||||
"set_conductor addresses a conductor as the one on a given "
|
||||
"terminal and applies the change to its whole electrical "
|
||||
"potential, so name a terminal carrying exactly one conductor, "
|
||||
@@ -2758,7 +2918,7 @@ TOOLS = [
|
||||
},
|
||||
"timeout": {"type": "integer", "default": 180},
|
||||
},
|
||||
"required": ["binary", "project", "output", "operations"],
|
||||
"required": ["project", "output", "operations"],
|
||||
},
|
||||
"handler": lambda a: tool_edit(a["binary"], a["project"], a["operations"],
|
||||
a["output"], a.get("elements_dir"),
|
||||
@@ -2776,7 +2936,7 @@ TOOLS = [
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||
"binary": {"type": "string", "description": "the qelectrotech executable; leave it out to use the one this server is configured with. Any other is refused unless its configuration allows it"},
|
||||
"project": {"type": "string", "description": "the .qet to query; never modified"},
|
||||
"sql": {"type": "string",
|
||||
"description": "a single SELECT or WITH...SELECT. "
|
||||
@@ -2784,7 +2944,7 @@ TOOLS = [
|
||||
"elements_dir": {"type": "string"},
|
||||
"timeout": {"type": "integer", "default": 180},
|
||||
},
|
||||
"required": ["binary", "project"],
|
||||
"required": ["project"],
|
||||
},
|
||||
"handler": lambda a: tool_query(a["binary"], a["project"], a.get("sql", ""),
|
||||
a.get("elements_dir"), a.get("timeout", 180)),
|
||||
@@ -2811,7 +2971,7 @@ TOOLS = [
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||
"binary": {"type": "string", "description": "the qelectrotech executable; leave it out to use the one this server is configured with. Any other is refused unless its configuration allows it"},
|
||||
"project": {"type": "string", "description": "the .qet to check; never modified"},
|
||||
"folio": {"type": "integer", "description":
|
||||
"check one folio only; omit for the whole project. An index "
|
||||
@@ -2822,7 +2982,7 @@ TOOLS = [
|
||||
"elements_dir": {"type": "string"},
|
||||
"timeout": {"type": "integer", "default": 180},
|
||||
},
|
||||
"required": ["binary", "project"],
|
||||
"required": ["project"],
|
||||
},
|
||||
"handler": lambda a: tool_continuity(a["binary"], a["project"], a.get("folio"),
|
||||
a.get("elements_dir"), a.get("timeout", 180)),
|
||||
@@ -2838,7 +2998,7 @@ TOOLS = [
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||
"binary": {"type": "string", "description": "the qelectrotech executable; leave it out to use the one this server is configured with. Any other is refused unless its configuration allows it"},
|
||||
"output": {"type": "string", "description": "where to write the new .qet"},
|
||||
"title": {"type": "string", "description": "the project title"},
|
||||
"folios": {"description": "how many empty folios, or a list of folio titles",
|
||||
@@ -2850,7 +3010,7 @@ TOOLS = [
|
||||
"elements_dir": {"type": "string"},
|
||||
"timeout": {"type": "integer", "default": 180},
|
||||
},
|
||||
"required": ["binary", "output", "title"],
|
||||
"required": ["output", "title"],
|
||||
},
|
||||
"handler": lambda a: tool_project_new(
|
||||
a["binary"], a["output"], a["title"], a.get("folios", 1), a.get("author", ""),
|
||||
@@ -2899,7 +3059,7 @@ TOOLS = [
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
|
||||
"binary": {"type": "string", "description": "the qelectrotech executable; leave it out to use the one this server is configured with. Any other is refused unless its configuration allows it"},
|
||||
"project": {"type": "string"},
|
||||
"checks": {"type": "array", "items": {"type": "string", "enum": sorted(CHECKS)},
|
||||
"description": "which checks to run; omit for all"},
|
||||
@@ -2908,7 +3068,7 @@ TOOLS = [
|
||||
"elements_dir": {"type": "string"},
|
||||
"timeout": {"type": "integer", "default": 180},
|
||||
},
|
||||
"required": ["binary", "project"],
|
||||
"required": ["project"],
|
||||
},
|
||||
"handler": lambda a: tool_check(a["binary"], a["project"], a.get("checks"),
|
||||
a.get("sample", 10), a.get("elements_dir"),
|
||||
@@ -2988,17 +3148,21 @@ _BY_NAME = {t["name"]: t for t in TOOLS}
|
||||
# sandboxed HOME each QElectroTech launch gets isolates *settings*, not the
|
||||
# filesystem.
|
||||
#
|
||||
# So data paths are confined to a workspace. Two kinds of path are treated
|
||||
# differently, deliberately:
|
||||
# So data paths are confined to a workspace, and the program the server
|
||||
# launches is not the client's to choose:
|
||||
#
|
||||
# data chosen by the client per call -- the projects, directories,
|
||||
# images and outputs below. Confined.
|
||||
# configuration chosen once by whoever runs the server -- "binary" (the
|
||||
# qelectrotech executable) and "elements_dir" (the element
|
||||
# collection). Both normally live in /usr or a build tree,
|
||||
# i.e. outside any sane workspace, so confining them would
|
||||
# reject the ordinary case while stopping nothing: they are
|
||||
# not where a model gets to point the server at /etc.
|
||||
# images and outputs below. Confined to the workspace.
|
||||
# executable "binary". Resolved by the server itself (resolve_binary());
|
||||
# a client may name it only when it is that same file or one
|
||||
# whoever configured the server listed in QET_MCP_BINARIES.
|
||||
# It once counted as configuration and went unchecked, but it
|
||||
# is a per-call argument: a model steered by text in a
|
||||
# project could run any program on the machine with it.
|
||||
# collection "elements_dir". Normally outside the workspace (in /usr or
|
||||
# a build tree), so allowed there, in the collection of the
|
||||
# resolved install, or in a directory listed in
|
||||
# QET_MCP_ELEMENTS.
|
||||
#
|
||||
# Enforced here, at the dispatcher, because this is the trust boundary --
|
||||
# the point where model-supplied arguments enter. Calling the tool_* helpers
|
||||
@@ -3022,6 +3186,10 @@ _DATA_PATHS = {
|
||||
"qet_element_build": {"write": ("output",)},
|
||||
}
|
||||
|
||||
# Tools that launch QElectroTech, and so take "binary" and "elements_dir".
|
||||
_LAUNCHES_QET = {"qet_export", "qet_edit", "qet_query", "qet_continuity",
|
||||
"qet_check", "qet_project_new"}
|
||||
|
||||
# qet_edit operations that name a file of their own.
|
||||
_DATA_PATH_OPS = {"add_image": "file", "add_pdf_page": "file"}
|
||||
|
||||
@@ -3060,6 +3228,118 @@ def _within_workspace(path: Path, roots: list) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _env_paths(name: str) -> list:
|
||||
"""An os.pathsep-separated list of paths from the environment, resolved."""
|
||||
out = []
|
||||
for part in os.environ.get(name, "").split(os.pathsep):
|
||||
if part.strip():
|
||||
try:
|
||||
out.append(Path(part).expanduser().resolve())
|
||||
except OSError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _installation() -> tuple | None:
|
||||
"""(program directory, element collection) of the QElectroTech this
|
||||
script was installed with, or None when it runs from anywhere else.
|
||||
|
||||
Two layouts, both put there by QElectroTech's own packaging:
|
||||
<prefix>/share/qelectrotech/mcp/ -> <prefix>/bin, <prefix>/share/qelectrotech/elements
|
||||
(make install: Linux, snap, flatpak, macOS)
|
||||
<root>/mcp/ -> <root>/bin, <root>/elements
|
||||
(the Windows installers and portable folder)
|
||||
"""
|
||||
here = Path(__file__).resolve().parent
|
||||
if here.name != "mcp":
|
||||
return None
|
||||
if here.parent.name == "qelectrotech" and here.parent.parent.name == "share":
|
||||
prefix = here.parent.parent.parent
|
||||
return prefix / "bin", here.parent / "elements"
|
||||
if (here.parent / "bin").is_dir():
|
||||
return here.parent / "bin", here.parent / "elements"
|
||||
return None
|
||||
|
||||
|
||||
def resolve_binary() -> Path | None:
|
||||
"""The QElectroTech this server launches, found without asking the client.
|
||||
|
||||
QET_BINARY first, then the install this script ships in, then
|
||||
qelectrotech on PATH. None when there is none; the tools that launch
|
||||
QElectroTech then say how to set it.
|
||||
"""
|
||||
env = os.environ.get("QET_BINARY", "").strip()
|
||||
if env:
|
||||
return Path(env).expanduser().resolve()
|
||||
install = _installation()
|
||||
if install is not None:
|
||||
# The Windows build names it QElectroTech.exe; only a case-sensitive
|
||||
# file system tells the spellings apart.
|
||||
for name in ("qelectrotech", "qelectrotech.exe", "QElectroTech.exe"):
|
||||
cand = install[0] / name
|
||||
if cand.is_file():
|
||||
return cand.resolve()
|
||||
found = shutil.which("qelectrotech")
|
||||
return Path(found).resolve() if found else None
|
||||
|
||||
|
||||
def default_elements_dir() -> Path | None:
|
||||
"""The element collection of the install this script ships in, if any."""
|
||||
install = _installation()
|
||||
if install is not None and install[1].is_dir():
|
||||
return install[1].resolve()
|
||||
return None
|
||||
|
||||
|
||||
def _check_binary(arguments: dict) -> None:
|
||||
"""Fill in "binary", or refuse one that is not the server's own choice."""
|
||||
if os.environ.get("QET_MCP_ALLOW_ANY_BINARY") == "1" and arguments.get("binary"):
|
||||
return
|
||||
default = resolve_binary()
|
||||
raw = arguments.get("binary")
|
||||
if not raw:
|
||||
if default is None:
|
||||
raise ValueError(
|
||||
"no QElectroTech found: set QET_BINARY to the qelectrotech "
|
||||
"executable in the environment this server is started in")
|
||||
arguments["binary"] = str(default)
|
||||
return
|
||||
if not isinstance(raw, str):
|
||||
raise ValueError("'binary' must be a path")
|
||||
given = Path(raw).expanduser().resolve()
|
||||
allowed = ([default] if default else []) + _env_paths("QET_MCP_BINARIES")
|
||||
if given not in allowed:
|
||||
raise ValueError(
|
||||
f"'binary' is not an allowed QElectroTech: {given}. Leave it out "
|
||||
"to use " + (str(default) if default else "QET_BINARY")
|
||||
+ "; whoever configured this server can list others in "
|
||||
"QET_MCP_BINARIES, or set QET_MCP_ALLOW_ANY_BINARY=1 to "
|
||||
"disable this check (which lets the client run any program).")
|
||||
arguments["binary"] = str(given)
|
||||
|
||||
|
||||
def _check_elements_dir(arguments: dict, roots: list) -> None:
|
||||
"""Fill in "elements_dir" from the install, or confine a given one."""
|
||||
raw = arguments.get("elements_dir")
|
||||
if not raw:
|
||||
default = default_elements_dir()
|
||||
if default is not None:
|
||||
arguments["elements_dir"] = str(default)
|
||||
return
|
||||
if not isinstance(raw, str):
|
||||
raise ValueError("'elements_dir' must be a path")
|
||||
given = Path(raw).expanduser().resolve()
|
||||
extra = _env_paths("QET_MCP_ELEMENTS")
|
||||
default = default_elements_dir()
|
||||
if default is not None:
|
||||
extra.append(default)
|
||||
if roots and not _within_workspace(given, roots + extra):
|
||||
raise ValueError(
|
||||
f"'elements_dir' is outside the workspace: {given}. Leave it out "
|
||||
"to use the installed collection, or list the directory in "
|
||||
"QET_MCP_ELEMENTS.")
|
||||
|
||||
|
||||
def _check_path(raw, arg: str, mode: str, roots: list) -> Path:
|
||||
"""Resolve one path and refuse it if it leaves the workspace.
|
||||
|
||||
@@ -3083,12 +3363,20 @@ def _check_path(raw, arg: str, mode: str, roots: list) -> Path:
|
||||
|
||||
|
||||
def enforce_path_policy(tool_name: str, arguments: dict) -> None:
|
||||
"""Apply the workspace and overwrite policy to one tool call."""
|
||||
"""Apply the workspace, executable and overwrite policy to one tool call.
|
||||
|
||||
For a tool that launches QElectroTech this also fills in "binary" and,
|
||||
when the install has one, "elements_dir", so a client need not know them.
|
||||
"""
|
||||
spec = _DATA_PATHS.get(tool_name)
|
||||
if spec is None:
|
||||
return
|
||||
roots = workspace_roots()
|
||||
|
||||
if tool_name in _LAUNCHES_QET:
|
||||
_check_binary(arguments)
|
||||
_check_elements_dir(arguments, roots)
|
||||
|
||||
for arg in spec.get("read", ()):
|
||||
if arg in arguments:
|
||||
_check_path(arguments[arg], arg, "read", roots)
|
||||
@@ -3192,11 +3480,48 @@ def serve(stdin=sys.stdin, stdout=sys.stdout) -> None:
|
||||
print(json.dumps(reply, ensure_ascii=False), file=stdout, flush=True)
|
||||
|
||||
|
||||
def call_once(argv: list[str], stdin=sys.stdin, stdout=sys.stdout,
|
||||
stderr=sys.stderr) -> int:
|
||||
"""--call <tool> [arguments]: one tools/call, the result's text on stdout.
|
||||
|
||||
arguments is a JSON object, or "-" to read it from stdin (which spares
|
||||
the caller from quoting JSON for a shell). Exit status: 0 the tool
|
||||
succeeded, 1 the tool reported an error, 2 the call itself was malformed.
|
||||
"""
|
||||
if not argv or len(argv) > 2:
|
||||
print("usage: qet_mcp.py --call <tool> ['<json arguments>' | -]",
|
||||
file=stderr)
|
||||
return 2
|
||||
name, raw = argv[0], (argv[1] if len(argv) == 2 else "{}")
|
||||
if raw == "-":
|
||||
raw = stdin.read()
|
||||
try:
|
||||
arguments = json.loads(raw) if raw.strip() else {}
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"arguments are not valid JSON: {exc}", file=stderr)
|
||||
return 2
|
||||
if not isinstance(arguments, dict):
|
||||
print("arguments must be a JSON object", file=stderr)
|
||||
return 2
|
||||
|
||||
reply = handle({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||
"params": {"name": name, "arguments": arguments}})
|
||||
if "error" in reply:
|
||||
print(reply["error"]["message"], file=stderr)
|
||||
return 2
|
||||
result = reply["result"]
|
||||
for part in result["content"]:
|
||||
print(part["text"], file=stdout)
|
||||
return 1 if result.get("isError") else 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) > 1 and sys.argv[1] in ("--list", "-l"):
|
||||
for t in TOOLS:
|
||||
print(f"{t['name']}\n {t['description']}\n")
|
||||
return 0
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--call":
|
||||
return call_once(sys.argv[2:])
|
||||
serve()
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[-HKEY_CURRENT_USER\Software\Classes\Applications\qelectrotech.exe\shell\open\command]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\.qet]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\qet_diagram_file]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\.elmt]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\qet_element_file]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\.titleblock]
|
||||
[-HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file]
|
||||
[-HKEY_CLASSES_ROOT\Applications\qelectrotech.exe\shell\open\command]
|
||||
[-HKEY_CLASSES_ROOT\.qet]
|
||||
[-HKEY_CLASSES_ROOT\qet_diagram_file]
|
||||
|
||||
@@ -5,7 +5,9 @@ rem detecte le dossier courant et suppose que celui-ci contient bin\qelectrotech
|
||||
set current_dir=%~dp0
|
||||
cd /d %current_dir%
|
||||
|
||||
set expected_qet_exe=%current_dir%Lancer QET.bat
|
||||
rem l'archive portable fournit "Lancer QET_qt6.bat", l'installeur "Lancer QET.bat"
|
||||
set expected_qet_exe=%current_dir%Lancer QET_qt6.bat
|
||||
if not exist "%expected_qet_exe%" set expected_qet_exe=%current_dir%Lancer QET.bat
|
||||
set expected_project_ico=%current_dir%ico\application-x-qet-project.ico
|
||||
set expected_element_ico=%current_dir%ico\application-x-qet-element.ico
|
||||
set expected_titleblock_ico=%current_dir%ico\application-x-qet-titleblock.ico
|
||||
@@ -45,49 +47,50 @@ set final_element_ico=%expected_element_ico:\=\\%
|
||||
set final_titleblock_ico=%expected_titleblock_ico:\=\\%
|
||||
|
||||
rem genere le fichier .reg pour enregistrer les associations de fichiers
|
||||
rem HKEY_CURRENT_USER : ne demande pas les droits administrateur
|
||||
set reg_file=qet_install_file_associations.reg
|
||||
(
|
||||
echo Windows Registry Editor Version 5.00
|
||||
echo.
|
||||
|
||||
rem Declaration de l'application
|
||||
echo [HKEY_CLASSES_ROOT\Applications\qelectrotech.exe\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\Applications\qelectrotech.exe\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem association de fichier *.qet
|
||||
echo [HKEY_CLASSES_ROOT\.qet]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.qet]
|
||||
echo @="qet_diagram_file"
|
||||
echo [HKEY_CLASSES_ROOT\qet_diagram_file]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file]
|
||||
echo @="QET diagram"
|
||||
echo "EditFlags"=dword:00000000
|
||||
echo "BrowserFlags"=dword:00000008
|
||||
echo [HKEY_CLASSES_ROOT\qet_diagram_file\DefaultIcon]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file\DefaultIcon]
|
||||
echo @="%final_project_ico%,0"
|
||||
echo [HKEY_CLASSES_ROOT\qet_diagram_file\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_diagram_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem association de fichier *.elmt
|
||||
echo [HKEY_CLASSES_ROOT\.elmt]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.elmt]
|
||||
echo @="qet_element_file"
|
||||
echo [HKEY_CLASSES_ROOT\qet_element_file]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_element_file]
|
||||
echo @="QET element"
|
||||
echo "EditFlags"=dword:00000000
|
||||
echo "BrowserFlags"=dword:00000008
|
||||
echo [HKEY_CLASSES_ROOT\qet_element_file\DefaultIcon]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_element_file\DefaultIcon]
|
||||
echo @="%final_element_ico%,0"
|
||||
echo [HKEY_CLASSES_ROOT\qet_element_file\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_element_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
|
||||
rem association de fichier *.titleblock
|
||||
echo [HKEY_CLASSES_ROOT\.titleblock]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\.titleblock]
|
||||
echo @="qet_titleblock_file"
|
||||
echo [HKEY_CLASSES_ROOT\qet_titleblock_file]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file]
|
||||
echo @="QET title block template"
|
||||
echo "EditFlags"=dword:00000000
|
||||
echo "BrowserFlags"=dword:00000008
|
||||
echo [HKEY_CLASSES_ROOT\qet_titleblock_file\DefaultIcon]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file\DefaultIcon]
|
||||
echo @="%final_titleblock_ico%,0"
|
||||
echo [HKEY_CLASSES_ROOT\qet_titleblock_file\shell\open\command]
|
||||
echo [HKEY_CURRENT_USER\Software\Classes\qet_titleblock_file\shell\open\command]
|
||||
echo @="\"%final_qet_exe%\" \"%%1\""
|
||||
) > %reg_file%
|
||||
|
||||
|
||||
@@ -480,6 +480,22 @@ void FileElementCollectionItem::setUpIcon()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief FileElementCollectionItem::clearData
|
||||
Reset the data, and let setUpIcon() build the icon again.
|
||||
Without this the guard in setUpIcon() keeps the icon cleared here
|
||||
empty until the whole collection is reloaded, e.g. after an element
|
||||
is saved from the element editor (see locationWasSaved()).
|
||||
*/
|
||||
void FileElementCollectionItem::clearData()
|
||||
{
|
||||
// Reset the flag only after the base class has cleared the icon:
|
||||
// its setIcon() emits dataChanged(), which re-enters setUpIcon(),
|
||||
// and that call must still see the flag set and return early.
|
||||
ElementCollectionItem::clearData();
|
||||
m_icon_initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief FileElementCollectionItem::setPathName
|
||||
Set the name of this item in the file system path.
|
||||
|
||||
@@ -55,6 +55,7 @@ class FileElementCollectionItem : public ElementCollectionItem
|
||||
|
||||
void setUpData() override;
|
||||
void setUpIcon() override;
|
||||
void clearData() override;
|
||||
|
||||
private:
|
||||
void setPathName(const QString& path_name,
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "terminaluuids.h"
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QHash>
|
||||
#include <QUuid>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
@return the <terminal> of the definition held by the embedded
|
||||
collection element @p collection_element
|
||||
*/
|
||||
//The <terminal> of the <definition> @p definition
|
||||
QList<QDomElement> definitionTerminals(const QDomElement &definition)
|
||||
{
|
||||
QList<QDomElement> terminals;
|
||||
const QDomElement description =
|
||||
definition.firstChildElement(QStringLiteral("description"));
|
||||
for (QDomElement t = description.firstChildElement(QStringLiteral("terminal"));
|
||||
!t.isNull();
|
||||
t = t.nextSiblingElement(QStringLiteral("terminal"))) {
|
||||
terminals << t;
|
||||
}
|
||||
return terminals;
|
||||
}
|
||||
|
||||
QList<QDomElement> terminalsOf(const QDomElement &collection_element)
|
||||
{
|
||||
return definitionTerminals(
|
||||
collection_element.firstChildElement(QStringLiteral("definition")));
|
||||
}
|
||||
|
||||
//Place and orientation, "10" and "10.0" being the same place
|
||||
QString terminalPlace(const QDomElement &terminal)
|
||||
{
|
||||
return QStringLiteral("%1|%2|%3")
|
||||
.arg(QString::number(terminal.attribute(QStringLiteral("x")).toDouble()),
|
||||
QString::number(terminal.attribute(QStringLiteral("y")).toDouble()),
|
||||
terminal.attribute(QStringLiteral("orientation")));
|
||||
}
|
||||
|
||||
//Qet::orientationFromString(), without pulling in qet.cpp
|
||||
int orientationOf(const QDomElement &terminal)
|
||||
{
|
||||
const QString o = terminal.attribute(QStringLiteral("orientation"));
|
||||
if (o.startsWith(QLatin1Char('e'))) return 1;
|
||||
if (o.startsWith(QLatin1Char('s'))) return 2;
|
||||
if (o.startsWith(QLatin1Char('w'))) return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
//The <terminal> of one <definition> get a uuid where missing
|
||||
int fillDefinition(const QDomElement &definition)
|
||||
{
|
||||
const QList<QDomElement> terminals = definitionTerminals(definition);
|
||||
QSet<QUuid> taken;
|
||||
for (const QDomElement &t : terminals) {
|
||||
const QUuid uuid(t.attribute(QStringLiteral("uuid")));
|
||||
if (!uuid.isNull()) {
|
||||
taken << uuid;
|
||||
}
|
||||
}
|
||||
|
||||
int filled = 0;
|
||||
QHash<QString, int> seen_at;
|
||||
for (QDomElement t : terminals) {
|
||||
//Same count as Terminal::setPlaceRank(): every terminal before
|
||||
//this one at the same point, with or without a uuid
|
||||
const int rank = seen_at[terminalPlace(t)]++;
|
||||
if (!QUuid(t.attribute(QStringLiteral("uuid"))).isNull()) {
|
||||
continue;
|
||||
}
|
||||
const qreal x = t.attribute(QStringLiteral("x")).toDouble();
|
||||
const qreal y = t.attribute(QStringLiteral("y")).toDouble();
|
||||
const int orientation = orientationOf(t);
|
||||
QUuid uuid;
|
||||
for (int occurrence = rank ; uuid.isNull() || taken.contains(uuid) ; ++occurrence) {
|
||||
uuid = TerminalUuids::derived(x, y, orientation, occurrence);
|
||||
}
|
||||
taken << uuid;
|
||||
t.setAttribute(QStringLiteral("uuid"), uuid.toString());
|
||||
++filled;
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
int fillDirectory(const QDomElement &directory)
|
||||
{
|
||||
int filled = 0;
|
||||
for (QDomElement child = directory.firstChildElement();
|
||||
!child.isNull();
|
||||
child = child.nextSiblingElement()) {
|
||||
if (child.tagName() == QLatin1String("category")) {
|
||||
filled += fillDirectory(child);
|
||||
} else if (child.tagName() == QLatin1String("element")) {
|
||||
filled += fillDefinition(
|
||||
child.firstChildElement(QStringLiteral("definition")));
|
||||
}
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::keep
|
||||
Give each terminal of @p new_element the uuid of the terminal of
|
||||
@p old_element at the same place and orientation. Both are <element>
|
||||
of an embedded collection.
|
||||
|
||||
A wire is saved against the uuids of the terminals it joins, and on
|
||||
loading it is reattached to a terminal with that uuid or not at all.
|
||||
The symbols already on the folios keep the terminals of the definition
|
||||
they were built from, so when the project's definition is replaced by
|
||||
one whose terminal uuids differ -- another copy of the same symbol, or
|
||||
one saved by a different version of the collection -- every wire on
|
||||
those symbols was lost the next time the project was opened.
|
||||
|
||||
A terminal of the new definition that already carries one of the old
|
||||
uuids is that same terminal, perhaps moved: it keeps it, and that uuid
|
||||
is not given to anything else. Other terminals that moved, and new
|
||||
ones, keep their own. No uuid is ever given to two terminals. Where the
|
||||
old definition has two terminals at one place, they are matched in
|
||||
order.
|
||||
*/
|
||||
void TerminalUuids::keep(const QDomElement &old_element, QDomElement &new_element)
|
||||
{
|
||||
QHash<QString, QStringList> old_uuids;
|
||||
QSet<QString> all_old_uuids;
|
||||
for (const QDomElement &t : terminalsOf(old_element)) {
|
||||
const QString uuid = t.attribute(QStringLiteral("uuid"));
|
||||
if (!uuid.isEmpty()) {
|
||||
old_uuids[terminalPlace(t)] << uuid;
|
||||
all_old_uuids << uuid;
|
||||
}
|
||||
}
|
||||
if (old_uuids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<QDomElement> new_terminals = terminalsOf(new_element);
|
||||
QSet<QString> taken;
|
||||
for (const QDomElement &t : new_terminals) {
|
||||
taken << t.attribute(QStringLiteral("uuid"));
|
||||
}
|
||||
|
||||
for (QDomElement t : new_terminals) {
|
||||
const QString own = t.attribute(QStringLiteral("uuid"));
|
||||
if (all_old_uuids.contains(own)) {
|
||||
continue;
|
||||
}
|
||||
QStringList &uuids = old_uuids[terminalPlace(t)];
|
||||
while (!uuids.isEmpty() && taken.contains(uuids.first())) {
|
||||
uuids.removeFirst();
|
||||
}
|
||||
if (!uuids.isEmpty()) {
|
||||
const QString uuid = uuids.takeFirst();
|
||||
taken.remove(own);
|
||||
taken << uuid;
|
||||
t.setAttribute(QStringLiteral("uuid"), uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::keepInDirectory
|
||||
keep() for every symbol of @p new_directory that has a counterpart of
|
||||
the same name at the same place under @p old_directory. Both are
|
||||
<category> of an embedded collection; used when a whole category of
|
||||
the project is replaced.
|
||||
*/
|
||||
void TerminalUuids::keepInDirectory(const QDomElement &old_directory,
|
||||
QDomElement &new_directory)
|
||||
{
|
||||
for (QDomElement child = new_directory.firstChildElement();
|
||||
!child.isNull();
|
||||
child = child.nextSiblingElement()) {
|
||||
if (child.tagName() != QLatin1String("category")
|
||||
&& child.tagName() != QLatin1String("element")) {
|
||||
continue;
|
||||
}
|
||||
const QString name = child.attribute(QStringLiteral("name"));
|
||||
QDomElement old_child = old_directory.firstChildElement(child.tagName());
|
||||
while (!old_child.isNull()
|
||||
&& old_child.attribute(QStringLiteral("name")) != name) {
|
||||
old_child = old_child.nextSiblingElement(child.tagName());
|
||||
}
|
||||
if (old_child.isNull()) {
|
||||
continue;
|
||||
}
|
||||
if (child.tagName() == QLatin1String("category")) {
|
||||
keepInDirectory(old_child, child);
|
||||
} else {
|
||||
keep(old_child, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::derived
|
||||
The identity of a terminal that carries no uuid, worked out from where
|
||||
it is inside its symbol: its local position and orientation, which are
|
||||
what the project file itself uses to tell terminals apart. UUID v5 in a
|
||||
fixed namespace, so the same terminal gets the same value in any
|
||||
project, on any machine, and it cannot collide with the random (v4)
|
||||
uuids the element editor gives.
|
||||
|
||||
@param orientation Qet::Orientation, as an int
|
||||
@param occurrence 0 for the value Terminal::stableUuid() uses; 1, 2...
|
||||
for a second, third... terminal at the same point of the same symbol
|
||||
*/
|
||||
QUuid TerminalUuids::derived(qreal x, qreal y, int orientation, int occurrence)
|
||||
{
|
||||
//Fixed namespace for terminal identities derived from geometry.
|
||||
static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
|
||||
QString key = QStringLiteral("%1|%2|%3")
|
||||
.arg(x, 0, 'f', 4)
|
||||
.arg(y, 0, 'f', 4)
|
||||
.arg(orientation);
|
||||
if (occurrence > 0) {
|
||||
key += QStringLiteral("|%1").arg(occurrence);
|
||||
}
|
||||
return QUuid::createUuidV5(derived_ns, key);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::fillMissing
|
||||
Give every terminal without a uuid, in every symbol of the embedded
|
||||
collection @p collection_root, the value derived() works out for it --
|
||||
the one QElectroTech already used for it as Terminal::stableUuid(), so
|
||||
nothing keyed on terminals changes. Saved with the project, it becomes
|
||||
a lasting identity: moving the terminal in the symbol editor no longer
|
||||
changes it.
|
||||
|
||||
Where one symbol has two terminals at one point, the second gets the
|
||||
next occurrence; a value is never given twice within a symbol.
|
||||
@return the number of terminals given a uuid
|
||||
*/
|
||||
int TerminalUuids::fillMissing(const QDomElement &collection_root)
|
||||
{
|
||||
return fillDirectory(collection_root);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TerminalUuids::fillMissingInDefinition
|
||||
fillMissing() for a single element definition (@p definition is its
|
||||
<definition>), as the element editor reads it from a file: an old
|
||||
symbol opened and saved there gets the uuids a project gives the same
|
||||
terminals, not random ones that would make every copy of it differ.
|
||||
@return the number of terminals given a uuid
|
||||
*/
|
||||
int TerminalUuids::fillMissingInDefinition(const QDomElement &definition)
|
||||
{
|
||||
return fillDefinition(definition);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef TERMINALUUIDS_H
|
||||
#define TERMINALUUIDS_H
|
||||
|
||||
#include <QUuid>
|
||||
|
||||
class QDomElement;
|
||||
|
||||
namespace TerminalUuids
|
||||
{
|
||||
QUuid derived(qreal x, qreal y, int orientation, int occurrence = 0);
|
||||
int fillMissing(const QDomElement &collection_root);
|
||||
int fillMissingInDefinition(const QDomElement &definition);
|
||||
void keep(const QDomElement &old_element, QDomElement &new_element);
|
||||
void keepInDirectory(const QDomElement &old_directory,
|
||||
QDomElement &new_directory);
|
||||
}
|
||||
|
||||
#endif // TERMINALUUIDS_H
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../qetproject.h"
|
||||
#include "../qetxml.h"
|
||||
#include "elementslocation.h"
|
||||
#include "terminaluuids.h"
|
||||
|
||||
/**
|
||||
@brief XmlElementCollection::XmlElementCollection
|
||||
@@ -127,9 +128,13 @@ XmlElementCollection::XmlElementCollection(const QDomElement &dom_element,
|
||||
QObject(project),
|
||||
m_project(project)
|
||||
{
|
||||
if (dom_element.tagName() == "collection")
|
||||
if (dom_element.tagName() == "collection") {
|
||||
m_dom_document.appendChild(m_dom_document.importNode(
|
||||
dom_element, true));
|
||||
//Before any folio is built from these symbols, so that their
|
||||
//terminals carry the uuid the next save writes
|
||||
TerminalUuids::fillMissing(root());
|
||||
}
|
||||
else
|
||||
qDebug() << "XmlElementCollection : tagName of dom_element is not collection";
|
||||
}
|
||||
@@ -869,6 +874,15 @@ ElementsLocation XmlElementCollection::copyDirectory(
|
||||
created_location.setPath(destination.projectCollectionPath() % "/" % new_dir_name);
|
||||
}
|
||||
|
||||
//The symbols of the replaced directory keep their terminal uuids,
|
||||
//see TerminalUuids::keep()
|
||||
if (!element.isNull()) {
|
||||
QDomElement new_dir_dom = directory(created_location.collectionPath(false));
|
||||
if (!new_dir_dom.isNull()) {
|
||||
TerminalUuids::keepInDirectory(element, new_dir_dom);
|
||||
}
|
||||
}
|
||||
|
||||
emit directorieAdded(created_location.collectionPath(false));
|
||||
return created_location;
|
||||
}
|
||||
@@ -914,6 +928,7 @@ ElementsLocation XmlElementCollection::copyElement(
|
||||
% "/" % new_elmt_name);
|
||||
bool removed = false;
|
||||
if (!element.isNull()) {
|
||||
TerminalUuids::keep(element, elmt_dom);
|
||||
element.parentNode().removeChild(element);
|
||||
removed = true;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,20 @@ void SearchAndReplaceWorker::replaceDiagram(QList<Diagram *> diagram_list)
|
||||
}
|
||||
}
|
||||
|
||||
new_properties.context.add(m_titleblock_properties.context);
|
||||
// The replace dialog lists every custom variable of the folios
|
||||
// (#1113), so an empty value means "leave unchanged", exactly like
|
||||
// the fields of the main tab.
|
||||
const DiagramContext &change = m_titleblock_properties.context;
|
||||
for (const QString &key : change.keys()) {
|
||||
const QString value = change.value(key).toString();
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
new_properties.context.addValue(
|
||||
key,
|
||||
applyChange(new_properties.context.value(key).toString(),
|
||||
value));
|
||||
}
|
||||
|
||||
if (old_propertie != new_properties) {
|
||||
project->undoStack()->push(new ChangeTitleBlockCommand(d, old_propertie, new_properties));
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "../../qeticons.h"
|
||||
#include "../../qetinformation.h"
|
||||
#include "../../qetproject.h"
|
||||
#include "../../ui/titleblockpropertieswidget.h"
|
||||
#include "replaceadvanceddialog.h"
|
||||
#include "replaceconductordialog.h"
|
||||
#include "replaceelementdialog.h"
|
||||
@@ -1109,7 +1110,26 @@ void SearchAndReplaceWidget::on_m_previous_pb_clicked()
|
||||
void SearchAndReplaceWidget::on_m_folio_pb_clicked()
|
||||
{
|
||||
ReplaceFolioDialog *dialog = new ReplaceFolioDialog(this);
|
||||
dialog->setTitleBlockProperties(m_worker.m_titleblock_properties);
|
||||
|
||||
// List the custom variables the folios already use, so the user only
|
||||
// has to type a value instead of guessing each name (#1113).
|
||||
TitleBlockProperties properties = m_worker.m_titleblock_properties;
|
||||
for (const QPointer<Diagram> &diagram : std::as_const(m_diagram_hash))
|
||||
{
|
||||
if (!diagram) {
|
||||
continue;
|
||||
}
|
||||
for (const QString &key :
|
||||
diagram->border_and_titleblock.exportTitleBlock().context.keys()) {
|
||||
if (!properties.context.contains(key)) {
|
||||
properties.context.addValue(key, QString());
|
||||
}
|
||||
}
|
||||
TitleBlockPropertiesWidget::addTemplateVariables(
|
||||
properties.context,
|
||||
diagram->border_and_titleblock.titleBlockTemplate());
|
||||
}
|
||||
dialog->setTitleBlockProperties(properties);
|
||||
|
||||
int result = dialog->exec();
|
||||
if (result == QDialogButtonBox::AcceptRole)
|
||||
|
||||
@@ -52,7 +52,7 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) :
|
||||
connect(ui->m_table_view->selectionModel(), &QItemSelectionModel::selectionChanged,
|
||||
this, &FreeTerminalEditor::selectionChanged);
|
||||
|
||||
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [=](const QModelIndex &index)
|
||||
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &index)
|
||||
{
|
||||
if (m_model->columnTypeForIndex(index) == FreeTerminalModel::XRef)
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ TerminalStripEditor::TerminalStripEditor(QETProject *project, QWidget *parent) :
|
||||
selectionChanged();
|
||||
|
||||
//Go the diagram of double clicked terminal
|
||||
connect(ui->m_table_widget, &QAbstractItemView::doubleClicked, this, [=](const QModelIndex &index)
|
||||
connect(ui->m_table_widget, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &index)
|
||||
{
|
||||
if (m_model->columnTypeForIndex(index) == TerminalStripModel::XRef)
|
||||
{
|
||||
|
||||
@@ -110,7 +110,7 @@ TerminalStripModel::TerminalStripModel(TerminalStrip *terminal_strip, QObject *p
|
||||
{
|
||||
fillPhysicalTerminalData();
|
||||
|
||||
connect(terminal_strip, &TerminalStrip::bridgeColorChanged, this, [=] {
|
||||
connect(terminal_strip, &TerminalStrip::bridgeColorChanged, this, [this] {
|
||||
emit dataChanged(index(0, LEVEL_0_CELL),
|
||||
index(rowCount(), LEVEL_3_CELL));
|
||||
});
|
||||
|
||||
@@ -221,12 +221,56 @@ namespace autonum
|
||||
Diagram *diagram,
|
||||
const Element *elmt,
|
||||
const Conductor *cndr)
|
||||
{
|
||||
if (!diagram) {
|
||||
return formula;
|
||||
}
|
||||
|
||||
FormulaContext context;
|
||||
const BorderTitleBlock &border = diagram->border_and_titleblock;
|
||||
context.folio = border.folio();
|
||||
context.folio_index = diagram->folioIndex();
|
||||
context.folio_total = border.folioTotal();
|
||||
context.plant = border.plant();
|
||||
context.locmach = border.locmach();
|
||||
context.title_block_fields = border.additionalFields();
|
||||
context.project_properties = diagram->project()->projectProperties();
|
||||
if (elmt)
|
||||
{
|
||||
context.has_element = true;
|
||||
context.element_position = diagram->convertPosition(elmt->scenePos());
|
||||
context.element_prefix = elmt->getPrefix();
|
||||
}
|
||||
if (cndr)
|
||||
{
|
||||
context.has_conductor = true;
|
||||
context.wire_function = cndr->properties().m_function;
|
||||
context.wire_tension_protocol = cndr->properties().m_tension_protocol;
|
||||
context.wire_color = cndr->properties().m_wire_color;
|
||||
context.wire_section = cndr->properties().m_wire_section;
|
||||
}
|
||||
return formulaToLabel(std::move(formula), seqStruct, context);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AssignVariables::formulaToLabel
|
||||
Return the formula with its variables assigned from @p context,
|
||||
which describes a folio and the element or conductor the formula
|
||||
belongs to without needing either to be built.
|
||||
@param formula - the formula to work
|
||||
@param seqStruct - struct where is stocked int values
|
||||
(struct is passed as a reference
|
||||
and modified by this static method)
|
||||
@param context - what the variables are read from
|
||||
@return the string with variable assigned.
|
||||
*/
|
||||
QString AssignVariables::formulaToLabel(QString formula,
|
||||
sequentialNumbers &seqStruct,
|
||||
const FormulaContext &context)
|
||||
{
|
||||
AssignVariables av(std::move(formula),
|
||||
seqStruct,
|
||||
diagram,
|
||||
elmt,
|
||||
cndr);
|
||||
context);
|
||||
seqStruct = av.m_seq_struct;
|
||||
return av.m_assigned_label;
|
||||
}
|
||||
@@ -348,76 +392,54 @@ namespace autonum
|
||||
|
||||
AssignVariables::AssignVariables(const QString& formula,
|
||||
const sequentialNumbers& seqStruct,
|
||||
Diagram *diagram,
|
||||
const Element *elmt,
|
||||
const Conductor *cndr):
|
||||
m_diagram(diagram),
|
||||
const FormulaContext &context):
|
||||
m_context(context),
|
||||
m_arg_formula(formula),
|
||||
m_assigned_label(formula),
|
||||
m_seq_struct(seqStruct),
|
||||
m_element(elmt),
|
||||
m_conductor(cndr)
|
||||
m_seq_struct(seqStruct)
|
||||
{
|
||||
if (m_diagram)
|
||||
m_assigned_label.replace("%F", m_context.folio);
|
||||
m_assigned_label.replace("%f",
|
||||
QString::number(m_context.folio_index+1));
|
||||
m_assigned_label.replace("%id",
|
||||
QString::number(m_context.folio_index+1));
|
||||
m_assigned_label.replace("%total",
|
||||
QString::number(m_context.folio_total));
|
||||
m_assigned_label.replace("%M", m_context.plant);
|
||||
m_assigned_label.replace("%LM", m_context.locmach);
|
||||
|
||||
QSettings settings;
|
||||
if (m_context.has_element)
|
||||
{
|
||||
m_assigned_label.replace("%F",
|
||||
m_diagram
|
||||
-> border_and_titleblock
|
||||
.folio());
|
||||
m_assigned_label.replace("%f",
|
||||
QString::number(
|
||||
m_diagram
|
||||
->folioIndex()+1));
|
||||
m_assigned_label.replace("%id",
|
||||
QString::number(
|
||||
m_diagram
|
||||
->folioIndex()+1));
|
||||
m_assigned_label.replace("%total",
|
||||
QString::number(
|
||||
m_diagram
|
||||
->border_and_titleblock
|
||||
.folioTotal()));
|
||||
m_assigned_label.replace("%M",
|
||||
m_diagram
|
||||
-> border_and_titleblock
|
||||
.plant());
|
||||
m_assigned_label.replace("%LM",
|
||||
m_diagram
|
||||
-> border_and_titleblock
|
||||
.locmach());
|
||||
|
||||
QSettings settings;
|
||||
if (m_element)
|
||||
{
|
||||
if (settings.value("border-columns_0", true).toBool()){
|
||||
m_assigned_label.replace("%c", QString::number(m_diagram->convertPosition(m_element->scenePos()).number() - 1));
|
||||
}else{
|
||||
m_assigned_label.replace("%c", QString::number(m_diagram->convertPosition(m_element->scenePos()).number()));
|
||||
}
|
||||
m_assigned_label.replace("%l", m_diagram->convertPosition(m_element->scenePos()).letter());
|
||||
m_assigned_label.replace("%prefix", m_element->getPrefix());
|
||||
if (settings.value("border-columns_0", true).toBool()){
|
||||
m_assigned_label.replace("%c", QString::number(m_context.element_position.number() - 1));
|
||||
}else{
|
||||
m_assigned_label.replace("%c", QString::number(m_context.element_position.number()));
|
||||
}
|
||||
|
||||
if (m_conductor)
|
||||
{
|
||||
m_assigned_label.replace("%wf", cndr->properties().m_function);
|
||||
m_assigned_label.replace("%wv", cndr->properties().m_tension_protocol);
|
||||
m_assigned_label.replace("%wc", cndr->properties().m_wire_color);
|
||||
m_assigned_label.replace("%ws", cndr->properties().m_wire_section);
|
||||
}
|
||||
|
||||
assignTitleBlockVar();
|
||||
assignProjectVar();
|
||||
assignSequence();
|
||||
m_assigned_label.replace("%l", m_context.element_position.letter());
|
||||
m_assigned_label.replace("%prefix", m_context.element_prefix);
|
||||
}
|
||||
|
||||
if (m_context.has_conductor)
|
||||
{
|
||||
m_assigned_label.replace("%wf", m_context.wire_function);
|
||||
m_assigned_label.replace("%wv", m_context.wire_tension_protocol);
|
||||
m_assigned_label.replace("%wc", m_context.wire_color);
|
||||
m_assigned_label.replace("%ws", m_context.wire_section);
|
||||
}
|
||||
|
||||
assignTitleBlockVar();
|
||||
assignProjectVar();
|
||||
assignSequence();
|
||||
}
|
||||
|
||||
void AssignVariables::assignTitleBlockVar()
|
||||
{
|
||||
for (int i = 0; i < m_diagram->border_and_titleblock.additionalFields().count(); i++)
|
||||
DiagramContext fields = m_context.title_block_fields;
|
||||
for (int i = 0; i < fields.count(); i++)
|
||||
{
|
||||
QString folio_variable = m_diagram->border_and_titleblock.additionalFields().keys().at(i);
|
||||
QVariant folio_value = m_diagram->border_and_titleblock.additionalFields().operator [](folio_variable);
|
||||
QString folio_variable = fields.keys().at(i);
|
||||
QVariant folio_value = fields[folio_variable];
|
||||
|
||||
if (m_assigned_label.contains(folio_variable)) {
|
||||
m_assigned_label.replace("%{" + folio_variable + "}", folio_value.toString());
|
||||
@@ -428,10 +450,11 @@ namespace autonum
|
||||
|
||||
void AssignVariables::assignProjectVar()
|
||||
{
|
||||
for (int i = 0; i < m_diagram->project()->projectProperties().count(); i++)
|
||||
DiagramContext properties = m_context.project_properties;
|
||||
for (int i = 0; i < properties.count(); i++)
|
||||
{
|
||||
QString folio_variable = m_diagram->project()->projectProperties().keys().at(i);
|
||||
QVariant folio_value = m_diagram->project()->projectProperties().operator [](folio_variable);
|
||||
QString folio_variable = properties.keys().at(i);
|
||||
QVariant folio_value = properties[folio_variable];
|
||||
|
||||
if (m_assigned_label.contains(folio_variable)) {
|
||||
m_assigned_label.replace("%{" + folio_variable + "}", folio_value.toString());
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#ifndef ASSIGNVARIABLES_H
|
||||
#define ASSIGNVARIABLES_H
|
||||
#include "../diagramcontext.h"
|
||||
#include "../diagramposition.h"
|
||||
#include "numerotationcontext.h"
|
||||
|
||||
#include <QPointF>
|
||||
@@ -57,6 +58,36 @@ namespace autonum
|
||||
QStringList alpha;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The FormulaContext struct
|
||||
Everything a formula's variables are read from: the folio it is on
|
||||
and, for an element's or a conductor's formula, that item.
|
||||
AssignVariables::formulaToLabel() fills one from a built folio; the
|
||||
project database fills one from the project's file.
|
||||
*/
|
||||
struct FormulaContext
|
||||
{
|
||||
/// border_and_titleblock.folio(), folioIndex(), folioTotal()...
|
||||
QString folio;
|
||||
int folio_index = 0;
|
||||
int folio_total = 0;
|
||||
QString plant;
|
||||
QString locmach;
|
||||
/// the title block's additional fields and the project's properties
|
||||
DiagramContext title_block_fields;
|
||||
DiagramContext project_properties;
|
||||
/// an element's grid cell (%c, %l) and prefix
|
||||
bool has_element = false;
|
||||
DiagramPosition element_position;
|
||||
QString element_prefix;
|
||||
/// a conductor's %wf, %wv, %wc and %ws
|
||||
bool has_conductor = false;
|
||||
QString wire_function;
|
||||
QString wire_tension_protocol;
|
||||
QString wire_color;
|
||||
QString wire_section;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The AssignVariables class
|
||||
This class assign variable of a formula string.
|
||||
@@ -66,21 +97,20 @@ namespace autonum
|
||||
{
|
||||
public:
|
||||
static QString formulaToLabel (QString formula, sequentialNumbers &seqStruct, Diagram *diagram, const Element *elmt = nullptr, const Conductor *cndr = nullptr);
|
||||
static QString formulaToLabel (QString formula, sequentialNumbers &seqStruct, const FormulaContext &context);
|
||||
static QString replaceVariable (const QString &formula, const DiagramContext &dc);
|
||||
static QString genericXref (const Element *element);
|
||||
|
||||
private:
|
||||
AssignVariables(const QString& formula, const sequentialNumbers& seqStruct , Diagram *diagram, const Element *elmt = nullptr, const Conductor *cndr = nullptr);
|
||||
AssignVariables(const QString& formula, const sequentialNumbers& seqStruct, const FormulaContext &context);
|
||||
void assignTitleBlockVar();
|
||||
void assignProjectVar();
|
||||
void assignSequence();
|
||||
|
||||
Diagram *m_diagram = nullptr;
|
||||
const FormulaContext &m_context;
|
||||
QString m_arg_formula;
|
||||
QString m_assigned_label;
|
||||
sequentialNumbers m_seq_struct;
|
||||
const Element *m_element = nullptr;
|
||||
const Conductor *m_conductor = nullptr;
|
||||
};
|
||||
|
||||
void setSequentialToList(QStringList &list, NumerotationContext &nc, const QString& type);
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "sqlreadonly.h"
|
||||
|
||||
#include "../autoNum/assignvariables.h"
|
||||
#include "../borderproperties.h"
|
||||
#include "../bordertitleblock.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
#include "../elementprovider.h"
|
||||
@@ -32,8 +35,16 @@
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qet.h"
|
||||
#include "../titleblockproperties.h"
|
||||
#include "../ElementsCollection/xmlelementcollection.h"
|
||||
#include "../properties/elementdata.h"
|
||||
|
||||
#include <QLocale>
|
||||
#include <QDate>
|
||||
#include <QDomDocument>
|
||||
|
||||
#include <functional>
|
||||
#include <QMetaEnum>
|
||||
#include <QTextDocument>
|
||||
#include <QFile>
|
||||
@@ -125,6 +136,480 @@ void projectDataBase::updateDB()
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::updateDB
|
||||
updateDB() for a project just read from @p document.
|
||||
|
||||
The diagram, diagram_info, element, element_info, terminal and conductor
|
||||
tables are filled from the document itself when it carries everything
|
||||
they need -- see populateFromDocument() -- and from the built folios
|
||||
otherwise, as updateDB() does. Shapes, texts and pictures always come from
|
||||
the built folios: their boxes need the fonts and pens a folio renders with.
|
||||
|
||||
The two fills give the same tables (tst_databasefromdocument checks it on
|
||||
the shipped examples). Reading the document instead of the folios is what
|
||||
the database needs before a project can be opened without building every
|
||||
folio; see DB-FROM-XML-SCOPE.md in qelectrotech-docker.
|
||||
*/
|
||||
void projectDataBase::updateDB(const QDomDocument &document)
|
||||
{
|
||||
if (m_update_blocked || !m_content_changed) {
|
||||
updateDB();
|
||||
return;
|
||||
}
|
||||
//QET_DATABASE_FROM_FOLIOS=1 keeps the fill from the built folios:
|
||||
//a way back should the two ever disagree, and what the test that
|
||||
//says they do not compares against.
|
||||
QString why;
|
||||
if (qEnvironmentVariableIntValue("QET_DATABASE_FROM_FOLIOS") == 1) {
|
||||
why = QStringLiteral("QET_DATABASE_FROM_FOLIOS is set");
|
||||
} else if (populateFromDocument(document, &why)) {
|
||||
qInfo() << "Project database filled from the document";
|
||||
populateDrawingItemTables();
|
||||
m_content_changed = false;
|
||||
emit dataBaseUpdated();
|
||||
return;
|
||||
}
|
||||
qInfo().noquote() << "Project database filled from the folios:" << why;
|
||||
updateDB();
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct DocumentTerminal
|
||||
{
|
||||
QString uuid;
|
||||
QString name;
|
||||
bool master_label = false;
|
||||
};
|
||||
|
||||
struct DocumentDefinition
|
||||
{
|
||||
QString type;
|
||||
QString sub_type;
|
||||
QHash<QUuid, DocumentTerminal> terminals;
|
||||
};
|
||||
|
||||
struct DocumentElement
|
||||
{
|
||||
QString uuid;
|
||||
QString diagram_uuid;
|
||||
QString pos;
|
||||
QString type;
|
||||
QString sub_type;
|
||||
QVariant group;
|
||||
DiagramContext informations;
|
||||
QString label;
|
||||
QHash<QUuid, DocumentTerminal> terminals;
|
||||
};
|
||||
|
||||
//The sequential values an element or a conductor was saved with, as
|
||||
//Element::fromXml() and Conductor::fromXml() read them -- false for
|
||||
//the attributes files written before <sequentialNumbers> carry.
|
||||
bool readSequence(const QDomElement &item, autonum::sequentialNumbers *sequence)
|
||||
{
|
||||
for (const char *name : {"sequ_1", "sequf_1", "seqt_1", "seqtf_1", "seqh_1"}) {
|
||||
if (item.hasAttribute(QLatin1String(name))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
sequence->fromXml(item.firstChildElement(QStringLiteral("sequentialNumbers")));
|
||||
return true;
|
||||
}
|
||||
|
||||
struct DocumentConductor
|
||||
{
|
||||
QString uuid;
|
||||
QString diagram_uuid;
|
||||
QString element1, terminal1, element2, terminal2;
|
||||
QString text;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::populateFromDocument
|
||||
Fill the diagram, diagram_info, element, element_info, terminal and
|
||||
conductor tables from @p document, the project as read from its file,
|
||||
with the same values the built folios give -- using the same code:
|
||||
a BorderTitleBlock read from each folio's XML gives the title-block
|
||||
values and each element's grid cell, the project's embedded collection
|
||||
gives each element's definition, and the binders are shared.
|
||||
|
||||
Nothing is written unless the whole document can be read this way. It
|
||||
cannot -- and false is returned, for the caller to fill from the folios
|
||||
instead -- when a folio, element or conductor carries no saved uuid (the
|
||||
folios derive one on load), a conductor names its ends the older way,
|
||||
a folio number uses %autonum, a conductor ends on a terminal that shows
|
||||
its master's contact label, an element's definition is missing or
|
||||
not one the folios could build, two elements on a folio number their
|
||||
terminals alike, or sequential numbers are saved as the attributes
|
||||
older files carry. A file saved by a current QElectroTech carries
|
||||
everything else.
|
||||
|
||||
A label or a conductor text made from a formula is worked out again,
|
||||
as the folios do, with the same AssignVariables code: the one saved
|
||||
in the file is what the formula gave when it was saved, and a folio
|
||||
added or moved since changes it. A frozen conductor text is left to the
|
||||
folios, which work it out part-way through loading.
|
||||
@return true if the tables were filled
|
||||
*/
|
||||
bool projectDataBase::populateFromDocument(const QDomDocument &document, QString *why)
|
||||
{
|
||||
auto refuse = [why](const QString &reason) {
|
||||
if (why) *why = reason;
|
||||
return false;
|
||||
};
|
||||
if (!m_project) {
|
||||
return refuse(QStringLiteral("no project"));
|
||||
}
|
||||
|
||||
//The folios, in the order the project read them (readDiagramsXml()),
|
||||
//each with its saved uuid -- the one the built folio has.
|
||||
const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram"));
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
if (diagram_nodes.size() != diagrams.size()) {
|
||||
return refuse(QStringLiteral("the folios are not the document's"));
|
||||
}
|
||||
|
||||
const DiagramContext project_wide = m_project->projectWideProperties();
|
||||
const DiagramContext project_properties = m_project->projectProperties();
|
||||
//One border and title block read from each folio's XML in turn:
|
||||
//only what it gives is kept.
|
||||
BorderTitleBlock reader;
|
||||
QList<DiagramContext> diagram_infos;
|
||||
QList<QDate> diagram_dates;
|
||||
QList<QUuid> diagram_uuids;
|
||||
QList<DocumentElement> elements;
|
||||
QList<DocumentConductor> conductors;
|
||||
QSet<QUuid> element_uuids;
|
||||
QHash<QString, DocumentDefinition> definitions; //by element type
|
||||
|
||||
//The project's own definitions by "embed://" path, which is how an
|
||||
//element's type names them: one walk of the embedded collection
|
||||
//rather than resolving an ElementsLocation per type.
|
||||
QHash<QString, QDomElement> stored;
|
||||
std::function<void (const QDomElement &, const QString &)> walk =
|
||||
[&stored, &walk](const QDomElement &category, const QString &path) {
|
||||
for (QDomElement child = category.firstChildElement() ;
|
||||
!child.isNull() ; child = child.nextSiblingElement()) {
|
||||
const QString name = path + child.attribute(QStringLiteral("name"));
|
||||
if (child.tagName() == QLatin1String("category")) {
|
||||
walk(child, name + QLatin1Char('/'));
|
||||
} else if (child.tagName() == QLatin1String("element")) {
|
||||
stored.insert(name, child.firstChildElement(QStringLiteral("definition")));
|
||||
}
|
||||
}
|
||||
};
|
||||
if (auto collection = m_project->embeddedElementCollection()) {
|
||||
walk(collection->root(), QStringLiteral("embed://"));
|
||||
}
|
||||
|
||||
for (int i = 0 ; i < diagram_nodes.size() ; ++i)
|
||||
{
|
||||
const QDomElement diagram_xml = diagram_nodes.at(i).toElement();
|
||||
const QUuid diagram_uuid(diagram_xml.attribute(QStringLiteral("uuid")));
|
||||
if (diagram_uuid.isNull() || diagram_uuid != diagrams.at(i)->uuid()) {
|
||||
return refuse(QStringLiteral("a folio has no saved uuid"));
|
||||
}
|
||||
|
||||
BorderTitleBlock *border = &reader;
|
||||
//As a new Diagram's border starts, before initFromXml() reads it
|
||||
border->importBorder(BorderProperties());
|
||||
border->importTitleBlock(TitleBlockProperties());
|
||||
border->titleBlockFromXml(diagram_xml);
|
||||
border->borderFromXml(diagram_xml);
|
||||
if (border->folio().contains(QStringLiteral("%autonum"))) {
|
||||
return refuse(QStringLiteral("a folio number uses %autonum"));
|
||||
}
|
||||
border->setFolioData(i + 1, int(diagram_nodes.size()), QString(), project_wide);
|
||||
|
||||
//What a formula on this folio is worked out from, as
|
||||
//AssignVariables::formulaToLabel() reads it off a built folio.
|
||||
autonum::FormulaContext folio_context;
|
||||
folio_context.folio = border->folio();
|
||||
folio_context.folio_index = i;
|
||||
folio_context.folio_total = border->folioTotal();
|
||||
folio_context.plant = border->plant();
|
||||
folio_context.locmach = border->locmach();
|
||||
folio_context.title_block_fields = border->additionalFields();
|
||||
folio_context.project_properties = project_properties;
|
||||
|
||||
QHash<QUuid, int> on_this_folio; //element uuid -> index in elements
|
||||
QSet<int> terminal_ids; //the older terminal ids used so far
|
||||
for (QDomElement element_xml : QET::findInDomElement(
|
||||
diagram_xml, QStringLiteral("elements"), QStringLiteral("element")))
|
||||
{
|
||||
//Skipped by Diagram::fromXml() as well
|
||||
if (!Element::valideXml(element_xml)) {
|
||||
continue;
|
||||
}
|
||||
const QUuid uuid(element_xml.attribute(QStringLiteral("uuid")));
|
||||
if (uuid.isNull() || element_uuids.contains(uuid)) {
|
||||
return refuse(QStringLiteral("an element has no saved uuid, or shares one"));
|
||||
}
|
||||
element_uuids.insert(uuid);
|
||||
|
||||
//Element::fromXml() refuses an element whose terminals are
|
||||
//numbered like those of one read before it on the folio;
|
||||
//which of them it matches needs their geometry, so leave any
|
||||
//such folio to the folios. A current file numbers them
|
||||
//across the folio.
|
||||
QSet<int> ids;
|
||||
for (QDomElement t : QET::findInDomElement(
|
||||
element_xml, QStringLiteral("terminals"), QStringLiteral("terminal"))) {
|
||||
if (Terminal::valideXml(t)) {
|
||||
ids.insert(t.attribute(QStringLiteral("id")).toInt());
|
||||
}
|
||||
}
|
||||
if (terminal_ids.intersects(ids)) {
|
||||
return refuse(QStringLiteral("two elements on a folio number their terminals alike"));
|
||||
}
|
||||
terminal_ids.unite(ids);
|
||||
|
||||
//The definition the folio builds the element from, with the
|
||||
//checks Element::buildFromXml() refuses an element on --
|
||||
//read once per type, as many elements share one.
|
||||
const QString type = element_xml.attribute(QStringLiteral("type"));
|
||||
auto known = definitions.constFind(type);
|
||||
if (known == definitions.constEnd())
|
||||
{
|
||||
const QDomElement definition = stored.value(type);
|
||||
if (definition.isNull()) {
|
||||
return refuse(QStringLiteral("an element's definition is not in the project"));
|
||||
}
|
||||
int number;
|
||||
if (definition.tagName() != QLatin1String("definition")
|
||||
|| definition.attribute(QStringLiteral("type")) != QLatin1String("element")
|
||||
|| !QET::attributeIsAnInteger(definition, QStringLiteral("width"), &number)
|
||||
|| !QET::attributeIsAnInteger(definition, QStringLiteral("height"), &number)
|
||||
|| !QET::attributeIsAnInteger(definition, QStringLiteral("hotspot_x"), &number)
|
||||
|| !QET::attributeIsAnInteger(definition, QStringLiteral("hotspot_y"), &number)
|
||||
|| definition.firstChild().isNull()) {
|
||||
return refuse(QStringLiteral("an element's definition cannot be built"));
|
||||
}
|
||||
DocumentDefinition read;
|
||||
ElementData data;
|
||||
data.fromXml(definition);
|
||||
read.type = data.typeToString();
|
||||
read.sub_type = data.masterTypeToString();
|
||||
const QDomElement description = definition.firstChildElement(QStringLiteral("description"));
|
||||
for (QDomElement t = description.firstChildElement(QStringLiteral("terminal")) ;
|
||||
!t.isNull() ; t = t.nextSiblingElement(QStringLiteral("terminal")))
|
||||
{
|
||||
const QUuid terminal_uuid(t.attribute(QStringLiteral("uuid")));
|
||||
if (terminal_uuid.isNull()) {
|
||||
continue;
|
||||
}
|
||||
DocumentTerminal terminal;
|
||||
terminal.uuid = terminal_uuid.toString();
|
||||
terminal.name = t.attribute(QStringLiteral("name"));
|
||||
terminal.master_label = t.attribute(QStringLiteral("use_master_label")) == QLatin1String("true");
|
||||
read.terminals.insert(terminal_uuid, terminal);
|
||||
}
|
||||
known = definitions.insert(type, read);
|
||||
}
|
||||
|
||||
DocumentElement element;
|
||||
element.uuid = uuid.toString();
|
||||
element.diagram_uuid = diagram_uuid.toString();
|
||||
DiagramPosition position = border->convertPosition(
|
||||
QPointF(element_xml.attribute(QStringLiteral("x")).toDouble(),
|
||||
element_xml.attribute(QStringLiteral("y")).toDouble()));
|
||||
element.pos = position.toString();
|
||||
element.type = known->type;
|
||||
element.sub_type = known->sub_type;
|
||||
element.terminals = known->terminals;
|
||||
const QUuid group = ItemGroups::read(element_xml);
|
||||
element.group = group.isNull() ? QVariant() : QVariant(group.toString());
|
||||
element.informations.fromXml(
|
||||
element_xml.firstChildElement(QStringLiteral("elementInformations")),
|
||||
QStringLiteral("elementInformation"));
|
||||
//Element::actualLabel()
|
||||
const QString formula = element.informations.value(QStringLiteral("formula")).toString();
|
||||
if (formula.isEmpty()) {
|
||||
element.label = element.informations.value(QStringLiteral("label")).toString();
|
||||
} else {
|
||||
autonum::sequentialNumbers sequence;
|
||||
if (!readSequence(element_xml, &sequence)) {
|
||||
return refuse(QStringLiteral("an element's sequential numbers are saved the older way"));
|
||||
}
|
||||
autonum::FormulaContext context = folio_context;
|
||||
context.has_element = true;
|
||||
context.element_position = position;
|
||||
context.element_prefix = element_xml.attribute(QStringLiteral("prefix"));
|
||||
element.label = autonum::AssignVariables::formulaToLabel(formula, sequence, context);
|
||||
}
|
||||
|
||||
on_this_folio.insert(uuid, int(elements.size()));
|
||||
elements << element;
|
||||
}
|
||||
|
||||
//The conductors the folio builds, and only those: an end that is
|
||||
//not found, a conductor from a terminal to itself and a second
|
||||
//conductor between the same two terminals are all dropped on load.
|
||||
QSet<QString> joined;
|
||||
for (QDomElement conductor_xml : QET::findInDomElement(
|
||||
diagram_xml, QStringLiteral("conductors"), QStringLiteral("conductor")))
|
||||
{
|
||||
if (!Conductor::valideXml(conductor_xml)) {
|
||||
continue;
|
||||
}
|
||||
const QUuid uuid(conductor_xml.attribute(QStringLiteral("uuid")));
|
||||
if (uuid.isNull()
|
||||
|| !conductor_xml.hasAttribute(QStringLiteral("element1"))
|
||||
|| !conductor_xml.hasAttribute(QStringLiteral("element2"))) {
|
||||
return refuse(QStringLiteral("a conductor has no saved uuid, or names its ends the older way"));
|
||||
}
|
||||
QString ends[2][2];
|
||||
bool found = true;
|
||||
for (int n = 0 ; n < 2 ; ++n)
|
||||
{
|
||||
const QString index = QString::number(n + 1);
|
||||
const QUuid owner(conductor_xml.attribute(QStringLiteral("element") + index));
|
||||
const QUuid terminal(conductor_xml.attribute(QStringLiteral("terminal") + index));
|
||||
const int e = on_this_folio.value(owner, -1);
|
||||
if (e < 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
if (!elements.at(e).terminals.contains(terminal)) {
|
||||
//The folio would try the terminal's derived uuid;
|
||||
//leave that to it.
|
||||
return refuse(QStringLiteral("a conductor ends on a terminal not in its element's definition"));
|
||||
}
|
||||
const DocumentTerminal &t = elements.at(e).terminals.value(terminal);
|
||||
if (t.master_label) {
|
||||
return refuse(QStringLiteral("a conductor ends on a terminal showing its master's label"));
|
||||
}
|
||||
ends[n][0] = elements.at(e).uuid;
|
||||
ends[n][1] = t.uuid;
|
||||
}
|
||||
if (!found) {
|
||||
continue;
|
||||
}
|
||||
const QString a = ends[0][0] + ends[0][1], b = ends[1][0] + ends[1][1];
|
||||
if (a == b) {
|
||||
continue;
|
||||
}
|
||||
const QString pair = a < b ? a + b : b + a;
|
||||
if (joined.contains(pair)) {
|
||||
continue;
|
||||
}
|
||||
joined.insert(pair);
|
||||
|
||||
DocumentConductor conductor;
|
||||
conductor.uuid = uuid.toString();
|
||||
conductor.diagram_uuid = diagram_uuid.toString();
|
||||
conductor.element1 = ends[0][0];
|
||||
conductor.terminal1 = ends[0][1];
|
||||
conductor.element2 = ends[1][0];
|
||||
conductor.terminal2 = ends[1][1];
|
||||
//ConductorProperties::fromXml()'s text, without reading the
|
||||
//rest of the properties -- or, as Conductor::refreshText()
|
||||
//makes it, what its formula gives.
|
||||
const QString formula = conductor_xml.attribute(QStringLiteral("formula"));
|
||||
if (formula.isEmpty()) {
|
||||
conductor.text = conductor_xml.attribute(QStringLiteral("num"));
|
||||
} else {
|
||||
autonum::sequentialNumbers sequence;
|
||||
if (conductor_xml.attribute(QStringLiteral("freezeLabel")) == QLatin1String("true")) {
|
||||
return refuse(QStringLiteral("a conductor's text made from a formula is frozen"));
|
||||
}
|
||||
if (!readSequence(conductor_xml, &sequence)) {
|
||||
return refuse(QStringLiteral("a conductor's sequential numbers are saved the older way"));
|
||||
}
|
||||
autonum::FormulaContext context = folio_context;
|
||||
context.has_conductor = true;
|
||||
context.wire_function = conductor_xml.attribute(QStringLiteral("function"));
|
||||
context.wire_tension_protocol = conductor_xml.attribute(QStringLiteral("tension_protocol"));
|
||||
context.wire_color = conductor_xml.attribute(QStringLiteral("conductor_color"));
|
||||
context.wire_section = conductor_xml.attribute(QStringLiteral("conductor_section"));
|
||||
conductor.text = autonum::AssignVariables::formulaToLabel(formula, sequence, context);
|
||||
}
|
||||
conductors << conductor;
|
||||
}
|
||||
|
||||
diagram_uuids << diagram_uuid;
|
||||
diagram_infos << border->titleblockInformation();
|
||||
diagram_dates << border->date();
|
||||
}
|
||||
|
||||
//Everything could be read: write it.
|
||||
QSqlQuery query(m_data_base);
|
||||
for (const QString &table : {QStringLiteral("diagram"), QStringLiteral("diagram_info"),
|
||||
QStringLiteral("element"), QStringLiteral("element_info"),
|
||||
QStringLiteral("conductor"), QStringLiteral("terminal")}) {
|
||||
query.exec(QStringLiteral("DELETE FROM ") + table);
|
||||
}
|
||||
|
||||
for (int i = 0 ; i < diagram_uuids.size() ; ++i)
|
||||
{
|
||||
m_insert_diagram_query.bindValue(":uuid", diagram_uuids.at(i).toString());
|
||||
m_insert_diagram_query.bindValue(":pos", i + 1);
|
||||
if (!m_insert_diagram_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateFromDocument diagram insert error : " << m_insert_diagram_query.lastError();
|
||||
}
|
||||
bindDiagramInfoValues(m_insert_diagram_info_query, diagram_uuids.at(i),
|
||||
diagram_infos.at(i), diagram_dates.at(i));
|
||||
if (!m_insert_diagram_info_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateFromDocument diagram_info insert error : " << m_insert_diagram_info_query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
for (const DocumentElement &element : std::as_const(elements))
|
||||
{
|
||||
m_insert_elements_query.bindValue(QStringLiteral(":uuid"), element.uuid);
|
||||
m_insert_elements_query.bindValue(QStringLiteral(":diagram_uuid"), element.diagram_uuid);
|
||||
m_insert_elements_query.bindValue(QStringLiteral(":pos"), element.pos);
|
||||
m_insert_elements_query.bindValue(QStringLiteral(":type"), element.type);
|
||||
m_insert_elements_query.bindValue(QStringLiteral(":sub_type"), element.sub_type);
|
||||
m_insert_elements_query.bindValue(QStringLiteral(":group_uuid"), element.group);
|
||||
if (!m_insert_elements_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError();
|
||||
}
|
||||
bindElementInfoValues(m_insert_element_info_query, element.uuid, element.informations,
|
||||
element.label);
|
||||
if (!m_insert_element_info_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateFromDocument element_info insert error : " << m_insert_element_info_query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<QString, int> element_index;
|
||||
for (int i = 0 ; i < elements.size() ; ++i) {
|
||||
element_index.insert(elements.at(i).uuid, i);
|
||||
}
|
||||
for (const DocumentConductor &conductor : std::as_const(conductors))
|
||||
{
|
||||
for (const auto &end : {std::make_pair(conductor.element1, conductor.terminal1),
|
||||
std::make_pair(conductor.element2, conductor.terminal2)}) {
|
||||
const DocumentElement &owner = elements.at(element_index.value(end.first));
|
||||
insertTerminal(end.second, end.first,
|
||||
owner.terminals.value(QUuid(end.second)).name);
|
||||
}
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":uuid"), conductor.uuid);
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":diagram_uuid"), conductor.diagram_uuid);
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":terminal1_uuid"), conductor.terminal1);
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor.element1);
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":terminal2_uuid"), conductor.terminal2);
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor.element2);
|
||||
m_insert_conductor_query.bindValue(QStringLiteral(":text"), conductor.text);
|
||||
if (!m_insert_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateFromDocument conductor insert error : " << m_insert_conductor_query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
//While every folio is still built, their conductors are watched for
|
||||
//property changes as populateConductorTable() does -- the one walk
|
||||
//over the built folios left here.
|
||||
for (Diagram *diagram : diagrams) {
|
||||
for (Conductor *conductor : diagram->conductors()) {
|
||||
if (conductor->terminal1->parentElement() && conductor->terminal2->parentElement()) {
|
||||
watchConductor(conductor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::setUpdateBlocked
|
||||
@param blocked : whether updateDB() should skip the full rebuild
|
||||
@@ -1514,9 +1999,21 @@ void projectDataBase::populateConductorTable()
|
||||
*/
|
||||
void projectDataBase::insertTerminal(Terminal *terminal)
|
||||
{
|
||||
m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString());
|
||||
m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString());
|
||||
m_insert_terminal_query.bindValue(":name", terminal->name());
|
||||
insertTerminal(terminal->stableUuid().toString(),
|
||||
terminal->parentElement()->uuid().toString(),
|
||||
terminal->name());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::insertTerminal
|
||||
insertTerminal(Terminal *) from values rather than a live terminal.
|
||||
*/
|
||||
void projectDataBase::insertTerminal(const QString &uuid, const QString &element_uuid,
|
||||
const QString &name)
|
||||
{
|
||||
m_insert_terminal_query.bindValue(":uuid", uuid);
|
||||
m_insert_terminal_query.bindValue(":element_uuid", element_uuid);
|
||||
m_insert_terminal_query.bindValue(":name", name);
|
||||
if (!m_insert_terminal_query.exec()) {
|
||||
qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError();
|
||||
}
|
||||
@@ -1733,17 +2230,56 @@ void projectDataBase::bindElementInfoValues(QSqlQuery &query, Element *element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::bindElementInfoValues
|
||||
bindElementInfoValues(QSqlQuery &, Element *) from values: the element's
|
||||
information and the label to store for it.
|
||||
*/
|
||||
void projectDataBase::bindElementInfoValues(QSqlQuery &query, const QString &element_uuid,
|
||||
const DiagramContext &informations,
|
||||
const QString &label)
|
||||
{
|
||||
query.bindValue(QStringLiteral(":uuid"), element_uuid);
|
||||
for (const auto &key : QETInformation::elementInfoKeys()) {
|
||||
query.bindValue(QStringLiteral(":") + key,
|
||||
key == QLatin1String("label") ? label
|
||||
: informations[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
|
||||
{
|
||||
query.bindValue(":uuid", diagram->uuid());
|
||||
bindDiagramInfoValues(query, diagram->uuid(), diagram->border_and_titleblock);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::bindDiagramInfoValues
|
||||
bindDiagramInfoValues(QSqlQuery &, Diagram *) from a folio's uuid and its
|
||||
border and title block, which need not belong to a built folio.
|
||||
*/
|
||||
void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid,
|
||||
const BorderTitleBlock &border)
|
||||
{
|
||||
bindDiagramInfoValues(query, diagram_uuid, border.titleblockInformation(), border.date());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::bindDiagramInfoValues
|
||||
The same from a title block's information and date.
|
||||
*/
|
||||
void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid,
|
||||
const DiagramContext &infos, const QDate &date)
|
||||
{
|
||||
query.bindValue(":uuid", diagram_uuid);
|
||||
|
||||
auto infos = diagram->border_and_titleblock.titleblockInformation();
|
||||
for (auto key : QETInformation::diagramInfoKeys())
|
||||
{
|
||||
if (key == "date") {
|
||||
query.bindValue( ":date",
|
||||
QLocale::system().toDate(infos.value("date").toString(),
|
||||
QLocale::ShortFormat));
|
||||
//The folio's own date, not the title block's text read
|
||||
//back: that text is the locale's short format, and where
|
||||
//it has a two-digit year (en_US "M/d/yy") toDate() reads
|
||||
//2010 back as 1910.
|
||||
query.bindValue(QStringLiteral(":date"), date);
|
||||
} else {
|
||||
auto value = infos.value(key);
|
||||
auto bind = key.prepend(":");
|
||||
|
||||
@@ -33,6 +33,10 @@ class Diagram;
|
||||
class Conductor;
|
||||
class Terminal;
|
||||
class QGraphicsItem;
|
||||
class QDomDocument;
|
||||
class BorderTitleBlock;
|
||||
class DiagramContext;
|
||||
class QDate;
|
||||
|
||||
/**
|
||||
@brief The projectDataBase class
|
||||
@@ -50,6 +54,13 @@ class projectDataBase : public QObject
|
||||
virtual ~projectDataBase() override;
|
||||
|
||||
void updateDB();
|
||||
/**
|
||||
updateDB() for a project just read from @p document: the
|
||||
diagram, element, terminal and conductor tables are filled
|
||||
from the document when it carries everything they need (see
|
||||
populateFromDocument()), from the built folios otherwise.
|
||||
*/
|
||||
void updateDB(const QDomDocument &document);
|
||||
/**
|
||||
Suppress the full rebuild performed by updateDB().
|
||||
|
||||
@@ -131,18 +142,28 @@ class projectDataBase : public QObject
|
||||
void populateDiagramInfoTable();
|
||||
void populateConductorTable();
|
||||
void populateDrawingItemTables();
|
||||
bool populateFromDocument(const QDomDocument &document, QString *why = nullptr);
|
||||
bool writeDrawingItem(QObject *object);
|
||||
void flushDrawingItems();
|
||||
void forgetDrawingItem(QObject *object);
|
||||
void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram);
|
||||
void watchConductor(Conductor *conductor);
|
||||
void insertTerminal(Terminal *terminal);
|
||||
void insertTerminal(const QString &uuid, const QString &element_uuid,
|
||||
const QString &name);
|
||||
void prepareQuery();
|
||||
static QHash<QString, QString> elementInfoToString(
|
||||
Element *elmt);
|
||||
void bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram);
|
||||
static void bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid,
|
||||
const BorderTitleBlock &border);
|
||||
static void bindDiagramInfoValues(QSqlQuery &query, const QUuid &diagram_uuid,
|
||||
const DiagramContext &infos, const QDate &date);
|
||||
static void bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram);
|
||||
static void bindElementInfoValues(QSqlQuery &query, Element *element);
|
||||
static void bindElementInfoValues(QSqlQuery &query, const QString &element_uuid,
|
||||
const DiagramContext &informations,
|
||||
const QString &label);
|
||||
|
||||
private:
|
||||
QPointer<QETProject> m_project;
|
||||
|
||||
@@ -773,6 +773,18 @@ QUuid Diagram::uuid()
|
||||
return m_uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::wiresNotReconnected
|
||||
@return one line per wire of the loaded file that was left out because
|
||||
a terminal it joins could not be found, for example after the symbol's
|
||||
definition in the project was replaced by one whose terminals differ.
|
||||
Empty for a folio that loaded every wire.
|
||||
*/
|
||||
QStringList Diagram::wiresNotReconnected() const
|
||||
{
|
||||
return m_wires_not_reconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::uuidUsedByOtherDiagram
|
||||
A hand-edited or merged project file can contain two folios with the same
|
||||
@@ -1479,6 +1491,14 @@ Terminal* findTerminal(int conductor_index,
|
||||
continue;
|
||||
|
||||
return terminal;
|
||||
}
|
||||
//The uuid a project gave a terminal on opening is worked out
|
||||
//from where the terminal is in its symbol: if the symbol's
|
||||
//definition has since been replaced by one whose terminals
|
||||
//carry other uuids, the terminal at that place is still it.
|
||||
for (auto terminal: element->terminals()) {
|
||||
if (terminal->derivedUuid() == terminal_uuid)
|
||||
return terminal;
|
||||
}
|
||||
qDebug() << "Diagram::fromXml() : "
|
||||
<< terminal_index
|
||||
@@ -1837,6 +1857,8 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
}
|
||||
|
||||
// Load conductor
|
||||
if (consider_informations)
|
||||
m_wires_not_reconnected.clear();
|
||||
QList<Conductor *> added_conductors;
|
||||
for (auto f : QET::findInDomElement(root,
|
||||
QStringLiteral("conductors"),
|
||||
@@ -1849,6 +1871,33 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
Terminal* p1 = findTerminal(1, f, table_adr_id, added_elements);
|
||||
Terminal* p2 = findTerminal(2, f, table_adr_id, added_elements);
|
||||
|
||||
//Keep a trace of the wire, it will be missing from the next save
|
||||
if ((!p1 || !p2) && consider_informations)
|
||||
{
|
||||
//The symbol's label, else its name. For an end not found,
|
||||
//only the uuid form of a wire says which symbol it is on.
|
||||
auto end_label = [&f, &added_elements](const QString &index,
|
||||
Terminal *found) {
|
||||
Element *element = found ? found->parentElement() : nullptr;
|
||||
const QUuid uuid(f.attribute(QStringLiteral("element") + index));
|
||||
for (int i = 0 ; !element && !uuid.isNull()
|
||||
&& i < added_elements.size() ; ++i) {
|
||||
if (added_elements.at(i)->uuid() == uuid)
|
||||
element = added_elements.at(i);
|
||||
}
|
||||
if (!element)
|
||||
return QStringLiteral("?");
|
||||
const QString label = element->actualLabel();
|
||||
return label.isEmpty() ? element->name() : label;
|
||||
};
|
||||
QString wire = QStringLiteral("%1 - %2").arg(end_label(QStringLiteral("1"), p1),
|
||||
end_label(QStringLiteral("2"), p2));
|
||||
const QString num = f.attribute(QStringLiteral("num"));
|
||||
if (!num.isEmpty() && num != QLatin1String("_"))
|
||||
wire += QStringLiteral(" (%1)").arg(num);
|
||||
m_wires_not_reconnected << wire;
|
||||
}
|
||||
|
||||
if (p1 && p2 && p1 != p2)
|
||||
{
|
||||
Conductor *c = new Conductor(p1, p2);
|
||||
@@ -1884,6 +1933,12 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
delete c;
|
||||
}
|
||||
}
|
||||
if (consider_informations && !m_wires_not_reconnected.isEmpty()) {
|
||||
qWarning().noquote() << "Diagram::fromXml():"
|
||||
<< m_wires_not_reconnected.size()
|
||||
<< "wire(s) not loaded, a terminal they join was not found:"
|
||||
<< m_wires_not_reconnected.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
//Filling of falculatory lists
|
||||
if (content_ptr) {
|
||||
@@ -2224,6 +2279,7 @@ void Diagram::setItemGroup(QGraphicsItem *item, const QUuid &group)
|
||||
if (m_project) {
|
||||
m_project->dataBase()->itemGroupChanged(item);
|
||||
}
|
||||
emit itemGroupChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -148,7 +148,10 @@ class Diagram : public QGraphicsScene
|
||||
|
||||
bool uuidUsedByOtherDiagram(const QUuid &uuid) const;
|
||||
QUuid derivedUuid(const QDomElement &root, const QString &reason) const;
|
||||
|
||||
|
||||
//Wires of the loaded file whose ends could not be found
|
||||
QStringList m_wires_not_reconnected;
|
||||
|
||||
// METHODS
|
||||
protected:
|
||||
void drawBackground(QPainter *, const QRectF &) override;
|
||||
@@ -171,6 +174,7 @@ class Diagram : public QGraphicsScene
|
||||
void correctTextPos(Element* elmt);
|
||||
void restoreText(Element* elmt);
|
||||
QUuid uuid();
|
||||
QStringList wiresNotReconnected() const;
|
||||
void setEventInterface (DiagramEventInterface *event_interface);
|
||||
void clearEventInterface();
|
||||
|
||||
@@ -321,6 +325,10 @@ class Diagram : public QGraphicsScene
|
||||
|
||||
void diagramActivated();
|
||||
void diagramInformationChanged();
|
||||
|
||||
/// Emitted by setItemGroup(): an item joined or left a group
|
||||
/// without the selection changing (#1144)
|
||||
void itemGroupChanged();
|
||||
};
|
||||
Q_DECLARE_METATYPE(Diagram *)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "diagramcontent.h"
|
||||
|
||||
#include "diagram.h"
|
||||
#include "itemgroups.h"
|
||||
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
@@ -261,7 +262,7 @@ void DiagramContent::clear()
|
||||
*/
|
||||
int DiagramContent::removeNonMovableItems()
|
||||
{
|
||||
int count_ = 0;
|
||||
int count_ = removePinnedGroups();
|
||||
|
||||
const QList<Element *> elements_set = m_elements;
|
||||
for(Element *elmt : elements_set) {
|
||||
@@ -287,9 +288,55 @@ int DiagramContent::removeNonMovableItems()
|
||||
}
|
||||
}
|
||||
|
||||
//A wire whose two ends no longer both move is redrawn, not moved,
|
||||
//or its text would be carried off with the end that still moves
|
||||
const QList<Conductor *> conductors_to_move = m_conductors_to_move;
|
||||
for (Conductor *conductor : conductors_to_move) {
|
||||
if (!m_elements.contains(conductor->terminal1->parentElement()) ||
|
||||
!m_elements.contains(conductor->terminal2->parentElement())) {
|
||||
m_conductors_to_move.removeAll(conductor);
|
||||
if (!m_conductors_to_update.contains(conductor))
|
||||
m_conductors_to_update << conductor;
|
||||
}
|
||||
}
|
||||
|
||||
return count_;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramContent::removePinnedGroups
|
||||
A group (#1070) with a locked member does not move at all: moving the
|
||||
rest would pull the group apart around the member that stays (#1146).
|
||||
Only a locked member that is in this content pins its group.
|
||||
Called first by removeNonMovableItems(), while the locked items are
|
||||
still here to be found.
|
||||
@return the number of removed items
|
||||
*/
|
||||
int DiagramContent::removePinnedGroups()
|
||||
{
|
||||
QSet<QUuid> pinned;
|
||||
for (Element *elmt : std::as_const(m_elements))
|
||||
if (!elmt->isMovable())
|
||||
pinned << ItemGroups::groupOf(elmt);
|
||||
for (DiagramImageItem *img : std::as_const(m_images))
|
||||
if (!img->isMovable())
|
||||
pinned << ItemGroups::groupOf(img);
|
||||
for (QetShapeItem *shape : std::as_const(m_shapes))
|
||||
if (!shape->isMovable())
|
||||
pinned << ItemGroups::groupOf(shape);
|
||||
pinned.remove(QUuid());
|
||||
if (pinned.isEmpty())
|
||||
return 0;
|
||||
|
||||
auto isPinned = [&pinned](const QGraphicsItem *item) {
|
||||
return pinned.contains(ItemGroups::groupOf(item));
|
||||
};
|
||||
return int(m_elements.removeIf(isPinned)
|
||||
+ m_images.removeIf(isPinned)
|
||||
+ m_shapes.removeIf(isPinned)
|
||||
+ m_text_fields.removeIf(isPinned));
|
||||
}
|
||||
|
||||
DiagramContent &DiagramContent::operator+=(const DiagramContent &other)
|
||||
{
|
||||
for(Element *elmt : other.m_elements)
|
||||
|
||||
@@ -100,6 +100,9 @@ class DiagramContent
|
||||
DiagramContent& operator+=(const DiagramContent& other);
|
||||
bool potentialIsManaged(QList<Conductor *>conductors);
|
||||
bool hasTextEditing();
|
||||
|
||||
private:
|
||||
int removePinnedGroups();
|
||||
};
|
||||
QDebug &operator<<(QDebug, DiagramContent &);
|
||||
#endif
|
||||
|
||||
@@ -1752,7 +1752,7 @@ void DiagramView::setEventInterface(DVEventInterface *event_interface)
|
||||
{
|
||||
if (m_event_interface) delete m_event_interface;
|
||||
m_event_interface = event_interface;
|
||||
connect(m_event_interface, &DVEventInterface::finish, this, [=](){delete this->m_event_interface; this->m_event_interface = nullptr;}, Qt::QueuedConnection);
|
||||
connect(m_event_interface, &DVEventInterface::finish, this, [this](){delete this->m_event_interface; this->m_event_interface = nullptr;}, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "openelmtcommand.h"
|
||||
#include "../elementscene.h"
|
||||
#include "../graphicspart/partterminal.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QObject>
|
||||
@@ -52,6 +53,15 @@ void OpenElmtCommand::redo()
|
||||
if (m_first_redo)
|
||||
{
|
||||
m_scene->fromXml(m_document, QPointF(), true, &m_graphics_item);
|
||||
//The terminals are added to an element that may already hold
|
||||
//the same ones -- importing the same symbol twice, or two old
|
||||
//symbols whose terminals get the same derived uuid -- so they
|
||||
//get new uuids, as a paste gives them (PastePartsCommand).
|
||||
for (QGraphicsItem *qgi : std::as_const(m_graphics_item)) {
|
||||
if (qgi->type() == PartTerminal::Type) {
|
||||
static_cast<PartTerminal *>(qgi)->setNewUuid();
|
||||
}
|
||||
}
|
||||
m_scene->qgiManager().manage(m_graphics_item);
|
||||
m_first_redo = false;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "elementscene.h"
|
||||
|
||||
#include "../ElementsCollection/terminaluuids.h"
|
||||
#include "../NameList/ui/namelistdialog.h"
|
||||
#include "../NameList/ui/namelistwidget.h"
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
@@ -1196,6 +1197,13 @@ ElementContent ElementScene::loadContent(const QDomDocument &xml_document)
|
||||
if (root.tagName() != "definition" || root.attribute("type") != "element")
|
||||
return(loaded_parts);
|
||||
|
||||
//Terminals saved without a uuid get the one a project gives them
|
||||
//(see TerminalUuids::fillMissing()), not a random one: the same old
|
||||
//symbol then has the same terminal uuids wherever it is copied.
|
||||
//On a copy, the document is the caller's. A paste renews them all.
|
||||
root = root.cloneNode(true).toElement();
|
||||
TerminalUuids::fillMissingInDefinition(root);
|
||||
|
||||
//Load the graphic description of the element
|
||||
for (QDomNode node = root.firstChild() ; !node.isNull() ; node = node.nextSibling())
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ PartDynamicTextField::PartDynamicTextField(QETElementEditor *editor, QGraphicsIt
|
||||
|
||||
//Option when text is displayed in multiple line
|
||||
QTextOption option = document() -> defaultTextOption();
|
||||
option.setAlignment(Qt::AlignHCenter);
|
||||
option.setAlignment(m_alignment & Qt::AlignHorizontal_Mask);
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
document() -> setDefaultTextOption(option);
|
||||
}
|
||||
@@ -462,6 +462,10 @@ void PartDynamicTextField::setPlainText(const QString &text) {
|
||||
|
||||
void PartDynamicTextField::setAlignment(Qt::Alignment alignment) {
|
||||
m_alignment = alignment;
|
||||
//Lines of a multi-line text follow the horizontal alignment
|
||||
QTextOption option = document() -> defaultTextOption();
|
||||
option.setAlignment(m_alignment & Qt::AlignHorizontal_Mask);
|
||||
document() -> setDefaultTextOption(option);
|
||||
emit alignmentChanged(m_alignment);
|
||||
}
|
||||
|
||||
|
||||
@@ -199,18 +199,18 @@ void DynamicTextFieldEditor::setUpConnections()
|
||||
disconnectConnections();
|
||||
|
||||
//Setup the connection
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::colorChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::fontChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::taggChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textFromChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::infoNameChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::rotationChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::frameChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textWidthChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::compositeTextChanged,this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::keepVisualRotationChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::rotationPointCenterChanged, this, [=](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::colorChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::fontChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::taggChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textFromChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::infoNameChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::rotationChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::frameChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textWidthChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::compositeTextChanged,this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::keepVisualRotationChanged, this, [this](){this -> updateForm();});
|
||||
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::rotationPointCenterChanged, this, [this](){this -> updateForm();});
|
||||
|
||||
// Refresh info combo when element data changes (e.g. type switched to PLC-Slave)
|
||||
m_connection_list << connect(elementEditor()->elementScene(), &ElementScene::elementInfoChanged,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "autobreakconductor.h"
|
||||
#include "conductorautonumerotation.h"
|
||||
#include "diagram.h"
|
||||
#include "itemgroups.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
#include "qetgraphicsitem/diagramimageitem.h"
|
||||
@@ -61,6 +62,8 @@ bool ElementsMover::isReady() const
|
||||
*/
|
||||
int ElementsMover::beginMovement(Diagram *diagram, QGraphicsItem *driver_item)
|
||||
{
|
||||
m_driver_held = false;
|
||||
|
||||
// They must be no movement in progress
|
||||
if (m_movement_running) return(-1);
|
||||
|
||||
@@ -86,6 +89,17 @@ int ElementsMover::beginMovement(Diagram *diagram, QGraphicsItem *driver_item)
|
||||
m_moved_content = DiagramContent(diagram);
|
||||
m_moved_content.removeNonMovableItems();
|
||||
|
||||
//A grouped driver left out of the move belongs to a group that one
|
||||
//locked member holds in place: it stays with the group
|
||||
m_driver_held = driver_item
|
||||
&& !ItemGroups::groupOf(driver_item).isNull()
|
||||
&& !m_moved_content.items().contains(driver_item);
|
||||
if (m_driver_held && m_status_bar) {
|
||||
m_status_bar->showMessage(QObject::tr(
|
||||
"Ce groupe ne peut pas être déplacé : "
|
||||
"la position d'un de ses éléments est verrouillée."));
|
||||
}
|
||||
|
||||
//Remove element text and text group, if the parent element is selected.
|
||||
const auto element_text{m_moved_content.m_element_texts};
|
||||
for(const auto &deti : element_text) {
|
||||
@@ -109,6 +123,17 @@ int ElementsMover::beginMovement(Diagram *diagram, QGraphicsItem *driver_item)
|
||||
return(m_moved_content.count());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsMover::holds
|
||||
@return true if @a item is the item the user drags and it must not move:
|
||||
it is in a group that a locked member keeps in place (#1146). Each item
|
||||
that drives a movement asks before moving itself.
|
||||
*/
|
||||
bool ElementsMover::holds(const QGraphicsItem *item) const
|
||||
{
|
||||
return m_driver_held && item && item == m_movement_driver;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsMover::continueMovement
|
||||
Add a move to the current movement.
|
||||
@@ -255,7 +280,8 @@ void ElementsMover::endMovement()
|
||||
m_movement_running = false;
|
||||
m_moved_content.clear();
|
||||
|
||||
if (m_status_bar) {
|
||||
//Keep saying why a held group did not move
|
||||
if (m_status_bar && !m_driver_held) {
|
||||
m_status_bar->clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class ElementsMover {
|
||||
int beginMovement(Diagram *, QGraphicsItem * = nullptr);
|
||||
void continueMovement(const QPointF &);
|
||||
void endMovement();
|
||||
bool holds(const QGraphicsItem *item) const;
|
||||
|
||||
// attributes
|
||||
private:
|
||||
@@ -59,6 +60,7 @@ class ElementsMover {
|
||||
QPointF m_current_movement;
|
||||
Diagram *m_diagram{nullptr};
|
||||
QGraphicsItem *m_movement_driver{nullptr};
|
||||
bool m_driver_held{false};
|
||||
DiagramContent m_moved_content;
|
||||
QPointer<QStatusBar> m_status_bar;
|
||||
|
||||
|
||||
@@ -278,6 +278,44 @@ QString MaterialList::elementInfoKey(const QString &column, int block)
|
||||
return column + QStringLiteral("_auxiliary%1").arg(block);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::isArticleBound
|
||||
Tell whether a column describes the article itself, rather than what
|
||||
the element does or where it stands.
|
||||
|
||||
An empty cell of such a column clears the field it feeds when the
|
||||
entry is applied: an element cannot carry the manufacturer of two
|
||||
parts at once, so the value of the article picked before has to go.
|
||||
Every other column (function, comment, notes, plant, location, and
|
||||
quantity and unity, which say how many pieces this element takes and
|
||||
in which unit) is left alone by an empty cell, and so is a column the
|
||||
file does not hold at all: a file which mentions nothing about a
|
||||
field is no reason to empty it.
|
||||
@param column a column of the material file
|
||||
@return true when an empty cell of that column clears the field
|
||||
*/
|
||||
bool MaterialList::isArticleBound(const QString &column)
|
||||
{
|
||||
static const QStringList article_columns = {
|
||||
QStringLiteral("description"),
|
||||
QStringLiteral("designation"),
|
||||
QStringLiteral("manufacturer"),
|
||||
QStringLiteral("manufacturer_reference"),
|
||||
QStringLiteral("machine_manufacturer_reference"),
|
||||
QStringLiteral("supplier"),
|
||||
QStringLiteral("model"),
|
||||
QStringLiteral("category"),
|
||||
QStringLiteral("voltage_rating"),
|
||||
QStringLiteral("current_rating"),
|
||||
QStringLiteral("width"),
|
||||
QStringLiteral("height"),
|
||||
QStringLiteral("depth"),
|
||||
QStringLiteral("auxiliary")
|
||||
};
|
||||
|
||||
return article_columns.contains(column);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::translatedColumn
|
||||
@param column a column of the material file
|
||||
@@ -443,6 +481,10 @@ QChar MaterialList::detectSeparator(const QString &first_line)
|
||||
const int commas = first_line.count(QLatin1Char(','));
|
||||
|
||||
if (semicolons == 0 && tabs == 0 && commas == 0) {
|
||||
//A header holding no separator character at all is a file of a
|
||||
//single column : falling back on the semicolon is deliberate,
|
||||
//it is the one QElectroTech writes itself, and such a file
|
||||
//needs no detection anyway. Leave this alone.
|
||||
return QLatin1Char(';');
|
||||
}
|
||||
if (tabs >= semicolons && tabs >= commas && tabs > 0) {
|
||||
@@ -616,14 +658,25 @@ bool MaterialList::load(const QString &path, MaterialListData *data, QString *er
|
||||
|
||||
//The label line is followed by the machine header QElectroTech
|
||||
//writes itself : the canonical names, so that the file is read the
|
||||
//same way whatever the language it was written in.
|
||||
//same way whatever the language it was written in. That machine
|
||||
//line is the one naming the columns when it is there : the label
|
||||
//line above it may be written in another language than the one
|
||||
//running now, and would then match nothing. The label line stays
|
||||
//on disk, it is only what the user reads.
|
||||
QStringList machine_line;
|
||||
if (!rows.isEmpty() && isMachineHeaderLine(header, rows.first(), alias_map)) {
|
||||
rows.takeFirst();
|
||||
machine_line = rows.takeFirst();
|
||||
}
|
||||
|
||||
QStringList used;
|
||||
for (const QString &cell : header)
|
||||
for (int i = 0; i < header.size(); ++i)
|
||||
{
|
||||
//An empty machine cell means the column is named by the label
|
||||
//above it (a column added by hand shows up in both lines).
|
||||
QString cell = header.at(i);
|
||||
if (i < machine_line.size() && !machine_line.at(i).isEmpty()) {
|
||||
cell = machine_line.at(i);
|
||||
}
|
||||
const QString column = canonicalColumn(cell, used, alias_map);
|
||||
used.append(column);
|
||||
data->columns.append(column);
|
||||
@@ -640,6 +693,16 @@ bool MaterialList::load(const QString &path, MaterialListData *data, QString *er
|
||||
empty = false;
|
||||
}
|
||||
record.setValue(data->columns.at(i), value);
|
||||
}
|
||||
//Cells written past the header are not thrown away : they are
|
||||
//written back the way they were read, and they are content too,
|
||||
//so a line only they fill is kept as well.
|
||||
for (int i = data->columns.size(); i < row.size(); ++i)
|
||||
{
|
||||
if (!row.at(i).isEmpty()) {
|
||||
empty = false;
|
||||
}
|
||||
record.extra.append(row.at(i));
|
||||
}
|
||||
if (!empty) {
|
||||
data->records.append(record);
|
||||
@@ -673,6 +736,9 @@ bool MaterialList::writeFile(const QString &path, const MaterialListData &data,
|
||||
for (const QString &column : data.columns) {
|
||||
row.append(record.value(column));
|
||||
}
|
||||
//The cells the file holds past the header, if any, follow the
|
||||
//columns : a line wider than the header stays that wide.
|
||||
row.append(record.extra);
|
||||
rows.append(row);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
struct MaterialRecord
|
||||
{
|
||||
QMap<QString, QString> values;
|
||||
//Cells written after the last column of the header, kept as they
|
||||
//are so that appending an article never shortens a line the user
|
||||
//wrote wider than the header itself.
|
||||
QStringList extra;
|
||||
|
||||
QString value(const QString &column) const {return values.value(column);}
|
||||
void setValue(const QString &column, const QString &value) {values.insert(column, value);}
|
||||
@@ -91,6 +95,7 @@ class MaterialList
|
||||
static QStringList defaultColumns();
|
||||
static QStringList columnsForBlock(int block);
|
||||
static QString elementInfoKey(const QString &column, int block);
|
||||
static bool isArticleBound(const QString &column);
|
||||
static QString translatedColumn(const QString &column);
|
||||
static QStringList translatedHeader(const QStringList &columns);
|
||||
|
||||
|
||||
@@ -173,6 +173,11 @@ void MaterialFilterProxy::setTokens(const QStringList &tokens)
|
||||
A row is kept when each token is found in at least one of its cells :
|
||||
the words may be spread over different columns, as a search for
|
||||
"Hilfsschalter Schneider" expects.
|
||||
|
||||
Every token walks every cell of the row, which means the whole file
|
||||
on every keystroke : well within reach for a catalogue, to be
|
||||
revisited before someone imports a supplier export of tens of
|
||||
thousands of lines.
|
||||
@param source_row
|
||||
@param source_parent
|
||||
@return
|
||||
@@ -235,6 +240,11 @@ MaterialSelectionDialog::MaterialSelectionDialog(const QString &path,
|
||||
//The row numbers are the order of the file : clicking them, or the
|
||||
//corner just above them, sorts the table by them, which means the
|
||||
//table is no longer sorted at all.
|
||||
//
|
||||
//QTableView keeps that corner button to itself, so the only way to
|
||||
//reach it is the name Qt gives that private class. Nothing breaks
|
||||
//if the name ever changes : the row numbers below stay connected
|
||||
//and only this shortcut disappears.
|
||||
for (QAbstractButton *button : ui->m_table_view->findChildren<QAbstractButton *>())
|
||||
{
|
||||
if (button->inherits("QTableCornerButton")) {
|
||||
@@ -373,7 +383,7 @@ bool MaterialSelectionDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
static int wheelAmount(int delta, bool counting_items)
|
||||
{
|
||||
if (counting_items) {
|
||||
return 3 * qRound(double(delta) / QWheelEvent::DefaultDeltasPerStep);
|
||||
return 3 * qRound(static_cast<double>(delta) / static_cast<int>(QWheelEvent::DefaultDeltasPerStep));
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
@@ -511,6 +521,40 @@ MaterialRecord MaterialSelectionDialog::selectedRecord() const
|
||||
return m_model->record(source.row());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief sameEntry
|
||||
Tell whether two records describe the same article of the file.
|
||||
|
||||
The maps are not compared as they are: an entry built by the entry
|
||||
form leaves the empty columns out, load() gives every column to every
|
||||
record, and a file may hold spaces the form has trimmed. Comparing
|
||||
per column makes both shapes say the same thing. Cells the file holds
|
||||
past the header are not part of it: they say nothing about which
|
||||
article a line is.
|
||||
@param lhs
|
||||
@param rhs
|
||||
@return true when both records describe the same article
|
||||
*/
|
||||
static bool sameEntry(const MaterialRecord &lhs, const MaterialRecord &rhs)
|
||||
{
|
||||
QStringList columns = lhs.values.keys();
|
||||
for (const QString &column : rhs.values.keys())
|
||||
{
|
||||
if (!columns.contains(column)) {
|
||||
columns.append(column);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &column : columns)
|
||||
{
|
||||
if (lhs.value(column).trimmed() != rhs.value(column).trimmed()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::selectRecord
|
||||
Select the row holding that record, scrolling to it.
|
||||
@@ -525,7 +569,7 @@ bool MaterialSelectionDialog::selectRecord(const MaterialRecord &record)
|
||||
//the line which was just written.
|
||||
for (int i = records.size() - 1; i >= 0; --i)
|
||||
{
|
||||
if (records.at(i) == record)
|
||||
if (sameEntry(records.at(i), record))
|
||||
{
|
||||
row = i;
|
||||
break;
|
||||
|
||||
@@ -612,7 +612,7 @@ void QETDiagramEditor::setUpActions()
|
||||
|
||||
//Add a nomenclature item
|
||||
m_add_nomenclature = new QAction(QET::Icons::TableOfContent, tr("Ajouter une nomenclature"), this);
|
||||
connect(m_add_nomenclature, &QAction::triggered, this, [=]() {
|
||||
connect(m_add_nomenclature, &QAction::triggered, this, [this]() {
|
||||
if(this->currentDiagramView()) {
|
||||
QetGraphicsTableFactory::createAndAddNomenclature(this->currentDiagramView()->diagram());
|
||||
}
|
||||
@@ -620,14 +620,14 @@ void QETDiagramEditor::setUpActions()
|
||||
|
||||
//Add a summary item
|
||||
m_add_summary = new QAction(QET::Icons::TableOfContent, tr("Ajouter un sommaire"), this);
|
||||
connect(m_add_summary, &QAction::triggered, this, [=]() {
|
||||
connect(m_add_summary, &QAction::triggered, this, [this]() {
|
||||
if(this->currentDiagramView()) {
|
||||
QetGraphicsTableFactory::createAndAddSummary(this->currentDiagramView()->diagram());
|
||||
}
|
||||
});
|
||||
|
||||
m_terminal_strip_dialog = new QAction(QET::Icons::TerminalStrip, tr("Gestionnaire de borniers (DEV)"), this);
|
||||
connect(m_terminal_strip_dialog, &QAction::triggered, this, [=]()
|
||||
connect(m_terminal_strip_dialog, &QAction::triggered, this, [this]()
|
||||
{
|
||||
if (auto project = this->currentProject())
|
||||
{
|
||||
@@ -938,26 +938,27 @@ void QETDiagramEditor::setUpActions()
|
||||
//Align actions. No default shortcut: they are reached from the
|
||||
//Edit menu, the selection's context menu and the command search,
|
||||
//and a user can bind one if they want.
|
||||
QAction *snap_to_grid = m_align_actions_group.addAction(tr("Aligner sur la grille"));
|
||||
QAction *snap_to_grid = m_align_actions_group.addAction(QET::Icons::SnapToGrid, tr("Aligner sur la grille"));
|
||||
ShortcutManager::instance().registerAction(snap_to_grid, "diagrameditor.snap_selection_to_grid", tr("Éditeur de schémas"), QKeySequence());
|
||||
snap_to_grid->setStatusTip(tr("Remet les éléments, images et textes sélectionnés sur la grille", "status bar tip"));
|
||||
snap_to_grid->setData("snap_selection_to_grid");
|
||||
|
||||
const struct {
|
||||
const char *id;
|
||||
const QIcon &icon;
|
||||
QString text;
|
||||
QString tip;
|
||||
} align_actions[] = {
|
||||
{"align_left", tr("Aligner à gauche"), tr("Aligne les bords gauches des objets sélectionnés", "status bar tip")},
|
||||
{"align_hcenter", tr("Centrer horizontalement"), tr("Aligne les objets sélectionnés sur une même verticale, par leur point d'origine pour les éléments", "status bar tip")},
|
||||
{"align_right", tr("Aligner à droite"), tr("Aligne les bords droits des objets sélectionnés", "status bar tip")},
|
||||
{"align_top", tr("Aligner en haut"), tr("Aligne les bords supérieurs des objets sélectionnés", "status bar tip")},
|
||||
{"align_vcenter", tr("Centrer verticalement"), tr("Aligne les objets sélectionnés sur une même horizontale, par leur point d'origine pour les éléments", "status bar tip")},
|
||||
{"align_bottom", tr("Aligner en bas"), tr("Aligne les bords inférieurs des objets sélectionnés", "status bar tip")}
|
||||
{"align_left", QET::Icons::AlignLeft, tr("Aligner à gauche"), tr("Aligne les bords gauches des objets sélectionnés", "status bar tip")},
|
||||
{"align_hcenter", QET::Icons::AlignHCenter, tr("Centrer horizontalement"), tr("Aligne les objets sélectionnés sur une même verticale, par leur point d'origine pour les éléments", "status bar tip")},
|
||||
{"align_right", QET::Icons::AlignRight, tr("Aligner à droite"), tr("Aligne les bords droits des objets sélectionnés", "status bar tip")},
|
||||
{"align_top", QET::Icons::AlignTop, tr("Aligner en haut"), tr("Aligne les bords supérieurs des objets sélectionnés", "status bar tip")},
|
||||
{"align_vcenter", QET::Icons::AlignVCenter, tr("Centrer verticalement"), tr("Aligne les objets sélectionnés sur une même horizontale, par leur point d'origine pour les éléments", "status bar tip")},
|
||||
{"align_bottom", QET::Icons::AlignBottom, tr("Aligner en bas"), tr("Aligne les bords inférieurs des objets sélectionnés", "status bar tip")}
|
||||
};
|
||||
for (const auto &a : align_actions)
|
||||
{
|
||||
QAction *action = m_align_actions_group.addAction(a.text);
|
||||
QAction *action = m_align_actions_group.addAction(a.icon, a.text);
|
||||
ShortcutManager::instance().registerAction(action, QStringLiteral("diagrameditor.") + QLatin1String(a.id), tr("Éditeur de schémas"), QKeySequence());
|
||||
action->setStatusTip(a.tip);
|
||||
action->setData(QString::fromLatin1(a.id));
|
||||
@@ -1672,6 +1673,34 @@ bool QETDiagramEditor::openAndAddProject(
|
||||
);
|
||||
}
|
||||
|
||||
//Report wires left out of the load because a terminal they join
|
||||
//was not found: they would otherwise vanish on the next save
|
||||
//without the user ever being told.
|
||||
QStringList lost_wires;
|
||||
for (Diagram *diagram : project->diagrams()) {
|
||||
for (const QString &wire : diagram->wiresNotReconnected()) {
|
||||
lost_wires << tr("Folio %1 : %2").arg(diagram->folioIndex() + 1).arg(wire);
|
||||
}
|
||||
}
|
||||
if (interactive && !lost_wires.isEmpty())
|
||||
{
|
||||
QMessageBox box(QMessageBox::Warning,
|
||||
tr("Conducteurs non chargés", "message box title"),
|
||||
tr("%n conducteur(s) n'ont pas pu être reliés à leurs"
|
||||
" bornes et n'ont pas été chargés. La définition de"
|
||||
" l'élément dans le projet a probablement été remplacée"
|
||||
" par une autre dont les bornes diffèrent.\n\n"
|
||||
"Si vous enregistrez le projet, ces conducteurs"
|
||||
" disparaîtront du fichier. Fermez-le sans enregistrer pour"
|
||||
" conserver le fichier tel quel.",
|
||||
"message box content",
|
||||
lost_wires.size()),
|
||||
QMessageBox::Ok,
|
||||
this);
|
||||
box.setDetailedText(lost_wires.join(QLatin1Char('\n')));
|
||||
box.exec();
|
||||
}
|
||||
|
||||
BackupDialog backup_dialog(this);
|
||||
if (backup_dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
@@ -2179,7 +2208,7 @@ void QETDiagramEditor::alignGroupTriggered(QAction *action)
|
||||
if (mode == AlignSelectionCommand::SnapToGrid)
|
||||
message = tr("La sélection est déjà sur la grille");
|
||||
else if (command->itemCount() < 2)
|
||||
message = tr("Sélectionnez au moins deux éléments, images, textes ou groupes non verrouillés");
|
||||
message = tr("Sélectionnez au moins deux éléments, images, textes, formes ou groupes non verrouillés");
|
||||
else
|
||||
message = tr("La sélection est déjà alignée, à la grille près");
|
||||
delete command;
|
||||
@@ -2459,11 +2488,10 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
| DiagramContent::Images);
|
||||
m_depth_action_group->setEnabled(list.isEmpty()? false : true);
|
||||
|
||||
//Align actions: symbols, pictures and free texts take part.
|
||||
//Align actions: symbols, pictures, free texts and shapes take part,
|
||||
//counted the way the command counts them, a group as one.
|
||||
//Snapping needs one of them, lining them up needs two.
|
||||
const int alignable = selected_elements_count
|
||||
+ selected_image
|
||||
+ dc.count(DiagramContent::TextFields);
|
||||
const int alignable = AlignSelectionCommand::unitCount(dc);
|
||||
m_align_actions_group.setEnabled(!ro && alignable);
|
||||
const QList<QAction *> align_actions = m_align_actions_group.actions();
|
||||
for (QAction *action : align_actions.mid(1))
|
||||
@@ -3157,6 +3185,12 @@ void QETDiagramEditor::diagramWasAdded(DiagramView *dv)
|
||||
this,
|
||||
&QETDiagramEditor::selectionChanged,
|
||||
Qt::DirectConnection);
|
||||
//Grouping leaves the selection as it is, so without this Group
|
||||
//and Ungroup would keep the state from before (#1144)
|
||||
connect(dv->diagram(),
|
||||
&Diagram::itemGroupChanged,
|
||||
this,
|
||||
&QETDiagramEditor::slot_updateComplexActions);
|
||||
connect(dv, &DiagramView::modeChanged, this, &QETDiagramEditor::slot_updateModeActions);
|
||||
connect(dv, &DiagramView::elementPlacementStarted, this, &QETDiagramEditor::rememberPlacedElement);
|
||||
}
|
||||
|
||||
@@ -228,9 +228,6 @@ void Conductor::segmentsToPath()
|
||||
@param o2 Orientation de la borne 2
|
||||
*/
|
||||
void Conductor::updateConductorPath(const QPointF &p1, Qet::Orientation o1, const QPointF &p2, Qet::Orientation o2) {
|
||||
Q_UNUSED(o1);
|
||||
Q_UNUSED(o2);
|
||||
|
||||
ConductorProfile &conductor_profile = conductor_profiles[currentPathType()];
|
||||
|
||||
Q_ASSERT_X(conductor_profile.segmentsCount(QET::Both) > 1, "Conductor::priv_modifieConductor", "pas de points a modifier");
|
||||
@@ -252,6 +249,25 @@ void Conductor::updateConductorPath(const QPointF &p1, Qet::Orientation o1, cons
|
||||
qreal h_diff = (qAbs(new_rect.width()) - qAbs(profile_width) ) * getSign(profile_width);
|
||||
qreal v_diff = (qAbs(new_rect.height()) - qAbs(profile_height)) * getSign(profile_height);
|
||||
|
||||
// A profile with no segment of non-zero length along an axis has nowhere
|
||||
// to put a difference along that axis: it would be dropped and the last
|
||||
// point would join the terminal diagonally. Every straight conductor
|
||||
// loaded from a file is like this, as a zero-length segment is saved as
|
||||
// horizontal (bugtracker #105). Generate a new path instead, as when
|
||||
// there is no profile for this path type.
|
||||
const auto can_absorb = [](qreal diff, const QList<ConductorSegmentProfile *> &segments_list) {
|
||||
if (qAbs(diff) <= 0.01) return(true);
|
||||
for (ConductorSegmentProfile *csp : segments_list) {
|
||||
if (csp -> length) return(true);
|
||||
}
|
||||
return(false);
|
||||
};
|
||||
if (!can_absorb(h_diff, conductor_profile.horizontalSegments()) ||
|
||||
!can_absorb(v_diff, conductor_profile.verticalSegments())) {
|
||||
generateConductorPath(p1, o1, p2, o2);
|
||||
return;
|
||||
}
|
||||
|
||||
// applique les differences aux segments
|
||||
// apply the differences to the segments
|
||||
QMultiHash<ConductorSegmentProfile *, qreal> segments_lengths;
|
||||
|
||||
@@ -366,6 +366,12 @@ void DiagramTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
if(diagram_ && m_first_move)
|
||||
diagram_->elementsMover().beginMovement(diagram_, this);
|
||||
|
||||
if (diagram_ && diagram_->elementsMover().holds(this)) {
|
||||
m_first_move = false;
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
QPointF old_pos = pos();
|
||||
|
||||
//Set the actual pos
|
||||
|
||||
@@ -63,9 +63,17 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) :
|
||||
|
||||
//Option when text is displayed in multiple line
|
||||
QTextOption option = document()->defaultTextOption();
|
||||
option.setAlignment(Qt::AlignHCenter);
|
||||
option.setAlignment(alignment() & Qt::AlignHorizontal_Mask);
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
document()->setDefaultTextOption(option);
|
||||
|
||||
//Lines of a multi-line text follow the horizontal alignment
|
||||
connect(this, &DiagramTextItem::alignmentChanged, [this](Qt::Alignment alignment)
|
||||
{
|
||||
QTextOption option = document()->defaultTextOption();
|
||||
option.setAlignment(alignment & Qt::AlignHorizontal_Mask);
|
||||
document()->setDefaultTextOption(option);
|
||||
});
|
||||
}
|
||||
|
||||
DynamicElementTextItem::~DynamicElementTextItem()
|
||||
|
||||
@@ -697,6 +697,16 @@ Terminal *Element::parseTerminal(const QDomElement &dom_element)
|
||||
}
|
||||
|
||||
Terminal *new_terminal = new Terminal(data, this);
|
||||
//Terminals are parsed in the order of the definition, and the list
|
||||
//is not kept in that order (sort below)
|
||||
int rank = 0;
|
||||
for (Terminal *t : std::as_const(m_terminals)) {
|
||||
if (t->dock_elmt_ == new_terminal->dock_elmt_
|
||||
&& t->orientation() == new_terminal->orientation()) {
|
||||
++rank;
|
||||
}
|
||||
}
|
||||
new_terminal->setPlaceRank(rank);
|
||||
m_terminals << new_terminal;
|
||||
|
||||
connect(new_terminal, &Terminal::conductorWasAdded, this, &Element::updateConductorTexts);
|
||||
|
||||
@@ -144,6 +144,11 @@ void QetGraphicsItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
//It's the first movement, we signal it to parent diagram
|
||||
diagram()->elementsMover().beginMovement(diagram(), this);
|
||||
}
|
||||
if (diagram() && diagram()->elementsMover().holds(this)) {
|
||||
m_first_move = false;
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
//we apply the mouse movement
|
||||
QPointF old_pos = pos();
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "../undocommand/promoteshapecommand.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
#include <algorithm>
|
||||
#include <QCursor>
|
||||
#include <QMenu>
|
||||
#include <QStatusBar>
|
||||
@@ -602,6 +603,45 @@ QPainterPath QetShapeItem::outline() const
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetShapeItem::sceneOutlineRect
|
||||
@return the box around the shape as drawn, in scene coordinates:
|
||||
without the pen width, the selection margin of boundingRect() or the
|
||||
wider outline shape() gives a hovered shape.
|
||||
*/
|
||||
QRectF QetShapeItem::sceneOutlineRect() const
|
||||
{
|
||||
return mapToScene(outline()).boundingRect();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetShapeItem::setPos
|
||||
Called with the unsnapped position while the shape is dragged. Dragged
|
||||
on its own, or with other shapes only, the shape goes on the grid by the
|
||||
top-left corner of what is drawn, as Snap to grid does: its pos() cannot
|
||||
be seen, and it is off the corner whenever the shape was drawn or
|
||||
resized with Ctrl held, or rotated. Dragged together with anything
|
||||
else, it snaps by pos() as before: the whole selection follows this
|
||||
shape's movement, and correcting the corner would take the symbols off
|
||||
the grid instead.
|
||||
@param p the new position of the item
|
||||
*/
|
||||
void QetShapeItem::setPos(const QPointF &p)
|
||||
{
|
||||
const auto selection = scene() ? scene()->selectedItems() : QList<QGraphicsItem *>();
|
||||
const bool shapes_only = std::all_of(selection.cbegin(), selection.cend(),
|
||||
[](const QGraphicsItem *item) { return item->type() == QetShapeItem::Type; });
|
||||
if (!shapes_only || !isMovable()) {
|
||||
QetGraphicsItem::setPos(p);
|
||||
return;
|
||||
}
|
||||
|
||||
const QPointF corner = sceneOutlineRect().topLeft() + (p - pos());
|
||||
const QPointF snapped = p + Diagram::snapToGrid(corner) - corner;
|
||||
if (snapped != pos())
|
||||
QGraphicsItem::setPos(snapped);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetShapeItem::shape
|
||||
@return the shape of this item
|
||||
|
||||
@@ -219,6 +219,10 @@ class QetShapeItem : public QetGraphicsItem
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
QPainterPath shape() const override;
|
||||
QRectF sceneOutlineRect() const;
|
||||
|
||||
using QetGraphicsItem::setPos;
|
||||
void setPos(const QPointF &p) override;
|
||||
|
||||
protected:
|
||||
void paint(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../ElementsCollection/terminaluuids.h"
|
||||
#include "../qet.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../conductorautonumerotation.h"
|
||||
@@ -849,11 +850,11 @@ QUuid Terminal::uuid() const
|
||||
read from the definition and is not touched by moving the element on the
|
||||
folio, so the result is stable across loads, saves and folio moves, and it
|
||||
is unique within an element except where a definition genuinely declares
|
||||
two terminals at the same point -- three cases in the whole example corpus,
|
||||
and harmless, because two terminals sharing a position and orientation are
|
||||
indistinguishable in every observable respect: they merge to one terminal
|
||||
row and every conductor on either of them still resolves to the right
|
||||
element and name.
|
||||
two terminals at the same point -- three cases in the whole example
|
||||
corpus. Opening a project gives every terminal of its symbols a uuid
|
||||
(TerminalUuids::fillMissing()), the same value for all but the second
|
||||
of such a pair, so in a project this is the fallback for terminals of a
|
||||
symbol imported since it was opened.
|
||||
|
||||
Derived values are UUID v5 in a fixed namespace, so they are reproducible
|
||||
without being written to the file, and cannot collide with the v4 uuids
|
||||
@@ -866,23 +867,45 @@ QUuid Terminal::stableUuid() const
|
||||
if (!d->m_uuid.isNull()) {
|
||||
return d->m_uuid;
|
||||
}
|
||||
return derivedUuid();
|
||||
}
|
||||
|
||||
//Fixed namespace for terminal identities derived from geometry.
|
||||
static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
/**
|
||||
@brief Terminal::derivedUuid
|
||||
The identity worked out from this terminal's local position and
|
||||
orientation, whether or not it carries a uuid of its own: the value
|
||||
stableUuid() gives when it has none, and the one a project gives it
|
||||
when it is opened (TerminalUuids::fillMissing()).
|
||||
|
||||
//Position and orientation only. The name is deliberately excluded: it
|
||||
//is not stable across a save cycle -- QET rewrites a terminal named
|
||||
//"_" as unnamed, which would silently change the identity of 1421 of
|
||||
//industrial.qet's 1790 terminals on the first resave. It is also not
|
||||
//needed: keying on geometry alone produces exactly the same number of
|
||||
//collisions across the example corpus, and it means renaming a
|
||||
//terminal does not change what it is.
|
||||
const QString key = QStringLiteral("%1|%2|%3")
|
||||
.arg(d->m_pos.x(), 0, 'f', 4)
|
||||
.arg(d->m_pos.y(), 0, 'f', 4)
|
||||
.arg(static_cast<int>(d->m_orientation));
|
||||
Position and orientation only. The name is deliberately excluded: it
|
||||
is not stable across a save cycle -- QET rewrites a terminal named "_"
|
||||
as unnamed, which would silently change the identity of 1421 of
|
||||
industrial.qet's 1790 terminals on the first resave. It is also not
|
||||
needed: keying on geometry alone produces exactly the same number of
|
||||
collisions across the example corpus, and it means renaming a terminal
|
||||
does not change what it is.
|
||||
|
||||
return QUuid::createUuidV5(derived_ns, key);
|
||||
A second terminal at the same point with the same orientation gets the
|
||||
next occurrence (see setPlaceRank()), as TerminalUuids::fillMissing()
|
||||
gives it, so the two terminals of such a pair are told apart.
|
||||
*/
|
||||
QUuid Terminal::derivedUuid() const
|
||||
{
|
||||
return TerminalUuids::derived(d->m_pos.x(),
|
||||
d->m_pos.y(),
|
||||
static_cast<int>(d->m_orientation),
|
||||
m_place_rank);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Terminal::setPlaceRank
|
||||
@param rank : how many terminals of the definition, before this one in
|
||||
document order, sit at the same point with the same orientation.
|
||||
Set by Element::parseTerminal().
|
||||
*/
|
||||
void Terminal::setPlaceRank(int rank)
|
||||
{
|
||||
m_place_rank = rank;
|
||||
}
|
||||
|
||||
QString Terminal::name() const
|
||||
|
||||
@@ -76,6 +76,8 @@ class Terminal : public QGraphicsObject
|
||||
Element *parentElement () const;
|
||||
QUuid uuid () const;
|
||||
QUuid stableUuid () const;
|
||||
QUuid derivedUuid () const;
|
||||
void setPlaceRank (int rank);
|
||||
QString name () const;
|
||||
QString baseName () const;
|
||||
TerminalData::Type terminalType() const;
|
||||
@@ -142,6 +144,9 @@ class Terminal : public QGraphicsObject
|
||||
Terminal *m_previous_terminal = nullptr;
|
||||
/// Whether the mouse pointer is hovering the terminal
|
||||
bool m_hovered = false;
|
||||
/// How many terminals of the definition, before this one, sit at
|
||||
/// the same point with the same orientation; see derivedUuid()
|
||||
int m_place_rank = 0;
|
||||
/// Color used for the hover effect
|
||||
QColor m_hovered_color = Terminal::neutralColor;
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ namespace QET {
|
||||
namespace Icons {
|
||||
QIcon Add;
|
||||
QIcon AddFolio;
|
||||
QIcon AlignBottom;
|
||||
QIcon AlignHCenter;
|
||||
QIcon AlignLeft;
|
||||
QIcon AlignRight;
|
||||
QIcon AlignTop;
|
||||
QIcon AlignVCenter;
|
||||
QIcon Allowed;
|
||||
QIcon ApplicationExit;
|
||||
QIcon ArrowLeft;
|
||||
@@ -169,6 +175,7 @@ namespace QET {
|
||||
QIcon SendBackward;
|
||||
QIcon Settings;
|
||||
QIcon SinglePage;
|
||||
QIcon SnapToGrid;
|
||||
QIcon South;
|
||||
QIcon Start;
|
||||
QIcon TableOfContent;
|
||||
@@ -395,6 +402,12 @@ void QET::Icons::initIcons()
|
||||
|
||||
Add = QIcon::fromTheme("list-add");
|
||||
AddFolio = QIcon::fromTheme("folio-new");
|
||||
AlignBottom = QIcon::fromTheme("align-vertical-bottom");
|
||||
AlignHCenter = QIcon::fromTheme("align-horizontal-center");
|
||||
AlignLeft = QIcon::fromTheme("align-horizontal-left");
|
||||
AlignRight = QIcon::fromTheme("align-horizontal-right");
|
||||
AlignTop = QIcon::fromTheme("align-vertical-top");
|
||||
AlignVCenter = QIcon::fromTheme("align-vertical-center");
|
||||
Allowed = QIcon::fromTheme("user-online");
|
||||
ApplicationExit = QIcon::fromTheme("application-exit");
|
||||
ArrowLeft = QIcon::fromTheme("arrow-left");
|
||||
@@ -581,6 +594,7 @@ void QET::Icons::initIcons()
|
||||
SendBackward = QIcon::fromTheme("send_backward");
|
||||
Settings = QIcon::fromTheme("settings");
|
||||
SinglePage = QIcon::fromTheme("single_page");
|
||||
SnapToGrid = QIcon::fromTheme("snap-to-grid");
|
||||
South = QIcon::fromTheme("south");
|
||||
Start = QIcon::fromTheme("start");
|
||||
TableOfContent = QIcon::fromTheme("table-of-content");
|
||||
|
||||
@@ -30,6 +30,12 @@ namespace QET {
|
||||
// namespace
|
||||
extern QIcon Add;
|
||||
extern QIcon AddFolio;
|
||||
extern QIcon AlignBottom;
|
||||
extern QIcon AlignHCenter;
|
||||
extern QIcon AlignLeft;
|
||||
extern QIcon AlignRight;
|
||||
extern QIcon AlignTop;
|
||||
extern QIcon AlignVCenter;
|
||||
extern QIcon Allowed;
|
||||
extern QIcon ApplicationExit;
|
||||
extern QIcon ArrowLeft;
|
||||
@@ -177,6 +183,7 @@ namespace QET {
|
||||
extern QIcon SendBackward;
|
||||
extern QIcon Settings;
|
||||
extern QIcon SinglePage;
|
||||
extern QIcon SnapToGrid;
|
||||
extern QIcon South;
|
||||
extern QIcon Start;
|
||||
extern QIcon TableOfContent;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include "qetmainwindow.h"
|
||||
#include "qeticons.h"
|
||||
#include "ui/aiassistantdialog.h"
|
||||
#include "shortcutmanager.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetdiagrameditor.h"
|
||||
@@ -111,6 +112,13 @@ void QETMainWindow::initCommonActions()
|
||||
|
||||
ShortcutManager::instance().registerAction(manual_online_, "mainwindow.manual_online", tr("Général"), Qt::Key_F1);
|
||||
|
||||
connect_ai_ = new QAction(tr("Connecter un assistant IA..."), this);
|
||||
connect_ai_ -> setStatusTip(tr("Affiche la configuration qui permet à un assistant IA d'utiliser QElectroTech", "status bar tip"));
|
||||
connect(connect_ai_, &QAction::triggered, this, [this]() {
|
||||
AiAssistantDialog dialog(this);
|
||||
dialog.exec();
|
||||
});
|
||||
|
||||
youtube_ = new QAction(QET::Icons::QETVideo, tr("Chaine Youtube"), this);
|
||||
youtube_ -> setStatusTip(tr("Lance le navigateur par défaut vers la chaine Youtube de QElectroTech", "status bar tip"));
|
||||
|
||||
@@ -170,6 +178,7 @@ void QETMainWindow::initCommonMenus()
|
||||
help_menu_ -> addAction(whatsthis_action_);
|
||||
help_menu_ -> addSeparator();
|
||||
help_menu_ -> addAction(manual_online_);
|
||||
help_menu_ -> addAction(connect_ai_);
|
||||
help_menu_ -> addAction(youtube_);
|
||||
help_menu_ -> addAction(upgrade_);
|
||||
help_menu_ -> addAction(upgrade_M);
|
||||
|
||||
@@ -66,6 +66,7 @@ class QETMainWindow : public QMainWindow {
|
||||
QAction *donate_; ///< Launch browser to donate link
|
||||
QAction *about_qt_; ///< launch the "About Qt" dialog
|
||||
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
|
||||
QAction *connect_ai_; ///< Show how to connect an AI assistant (MCP server)
|
||||
QMenu *settings_menu_; ///< Settings menu
|
||||
QMenu *help_menu_; ///< Help menu
|
||||
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
|
||||
|
||||
@@ -346,7 +346,7 @@ void QETProject::init()
|
||||
{
|
||||
int ms = autosave_interval*60*1000;
|
||||
m_autosave_timer.setInterval(ms);
|
||||
connect(&m_autosave_timer, &QTimer::timeout, this, [=]()
|
||||
connect(&m_autosave_timer, &QTimer::timeout, this, [this]()
|
||||
{
|
||||
if(!this->m_file_path.isEmpty())
|
||||
this->write();
|
||||
@@ -580,7 +580,7 @@ void QETProject::setFilePath(const QString &filepath)
|
||||
m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy"));
|
||||
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
|
||||
m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm"));
|
||||
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).baseName());
|
||||
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName());
|
||||
m_project_properties.addValue("savedfilepath", filePath());
|
||||
|
||||
|
||||
@@ -1428,7 +1428,7 @@ QETResult QETProject::write()
|
||||
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
|
||||
m_project_properties.addValue("saveddate-eu", QDate::currentDate().toString("dd-MM-yyyy"));
|
||||
m_project_properties.addValue("savedtime", QDateTime::currentDateTime().toString("HH:mm"));
|
||||
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).baseName());
|
||||
m_project_properties.addValue("savedfilename", QFileInfo(filePath()).completeBaseName());
|
||||
m_project_properties.addValue("savedfilepath", filePath());
|
||||
|
||||
emit projectInformationsChanged(this);
|
||||
@@ -1880,7 +1880,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
|
||||
m_data_base.blockSignals(false);
|
||||
m_data_base.setUpdateBlocked(false);
|
||||
m_data_base.updateDB();
|
||||
m_data_base.updateDB(xml_project);
|
||||
const qint64 database_ms = phase_timer.elapsed();
|
||||
|
||||
qInfo().nospace()
|
||||
@@ -2473,14 +2473,25 @@ bool QETProject::projectWasModified()
|
||||
Indique a chaque schema du projet quel est son numero de folio et combien de
|
||||
folio le projet contient.
|
||||
*/
|
||||
/**
|
||||
@brief QETProject::projectWideProperties
|
||||
@return the project's properties as every folio's title block sees them:
|
||||
the user's project properties plus the project's title, path and file name.
|
||||
*/
|
||||
DiagramContext QETProject::projectWideProperties()
|
||||
{
|
||||
DiagramContext project_wide_properties = m_project_properties;
|
||||
project_wide_properties.addValue("projecttitle", title());
|
||||
project_wide_properties.addValue("projectpath", filePath());
|
||||
project_wide_properties.addValue("projectfilename", QFileInfo(filePath()).completeBaseName());
|
||||
return project_wide_properties;
|
||||
}
|
||||
|
||||
void QETProject::updateDiagramsFolioData()
|
||||
{
|
||||
int total_folio = m_diagrams_list.count();
|
||||
|
||||
DiagramContext project_wide_properties = m_project_properties;
|
||||
project_wide_properties.addValue("projecttitle", title());
|
||||
project_wide_properties.addValue("projectpath", filePath());
|
||||
project_wide_properties.addValue("projectfilename", QFileInfo(filePath()).baseName());
|
||||
const DiagramContext project_wide_properties = projectWideProperties();
|
||||
|
||||
for (int i = 0 ; i < total_folio ; ++ i)
|
||||
{
|
||||
|
||||
@@ -221,6 +221,7 @@ class QETProject : public QObject
|
||||
bool projectWasModified();
|
||||
bool projectOptionsWereModified();
|
||||
DiagramContext projectProperties();
|
||||
DiagramContext projectWideProperties();
|
||||
void setProjectProperties(const DiagramContext &);
|
||||
QUndoStack* undoStack() {return m_undo_stack;}
|
||||
|
||||
|
||||
@@ -739,18 +739,19 @@ QString QetScriptApi::elementName(int folioIndex, const QString &elementUuid) co
|
||||
/**
|
||||
@brief QetScriptApi::elementTerminals
|
||||
The element's terminals, in the order addConductor() indexes them: one
|
||||
entry per terminal, "<index>: <name> (<n> conductor(s))". Descriptive
|
||||
rather than structured because its only job is to let a script -- or a
|
||||
human reading a script's output -- see which index is which before
|
||||
wiring anything to it.
|
||||
entry per terminal, "<index>: <name> (<n> conductor(s)) <uuid>".
|
||||
Descriptive rather than structured because its only job is to let a
|
||||
script -- or a human reading a script's output -- see which index is
|
||||
which before wiring anything to it.
|
||||
|
||||
Indexes, not uuids, because a terminal uuid does not address a terminal
|
||||
on a folio. Terminal::uuid() comes from the catalog .elmt definition
|
||||
(see Terminal::stableUuid()), so it is empty for most of the installed
|
||||
base, and where it is not, every instance of that same element carries
|
||||
the same one -- two coils of one type placed side by side have
|
||||
byte-identical terminal uuids, which is plainly visible in the saved
|
||||
file of any project written through this API.
|
||||
The calls take the index; the uuid is what to hold instead, and
|
||||
terminalIndex() turns it back into the index. It is Terminal::
|
||||
stableUuid(): the terminal's own uuid, which every terminal of an
|
||||
opened project has (TerminalUuids::fillMissing()), or for a symbol
|
||||
imported since the project was opened the value the next opening will
|
||||
give it. It comes from the symbol's definition, so it names a terminal
|
||||
only together with its element: two coils of one type placed side by
|
||||
side have the same terminal uuids.
|
||||
|
||||
The index is the terminal's place in Element::terminals(), and that is
|
||||
@b not the order the .elmt lists them in. Element::parseTerminal()
|
||||
@@ -772,14 +773,42 @@ QStringList QetScriptApi::elementTerminals(int folioIndex, const QString &elemen
|
||||
for (int i = 0 ; i < terminals.count() ; ++i)
|
||||
{
|
||||
Terminal *t = terminals.at(i);
|
||||
list << QStringLiteral("%1: %2 (%3 conductor(s))")
|
||||
.arg(i)
|
||||
.arg(t->name().isEmpty() ? QStringLiteral("-") : t->name())
|
||||
.arg(t->conductorsCount());
|
||||
//One multi-argument arg(): chained ones would also replace a
|
||||
//"%3" or "%4" inside the terminal's name
|
||||
list << QStringLiteral("%1: %2 (%3 conductor(s)) %4")
|
||||
.arg(QString::number(i),
|
||||
t->name().isEmpty() ? QStringLiteral("-") : t->name(),
|
||||
QString::number(t->conductorsCount()),
|
||||
t->stableUuid().toString());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::terminalIndex
|
||||
The current index in elementTerminals() of the terminal of the element
|
||||
@p elementUuid whose uuid is @p terminalUuid, or -1 -- also when two of
|
||||
its terminals carry that uuid, since the index would then be a guess.
|
||||
The index is what addConductor() and the conductor calls take; unlike
|
||||
the uuid, it is undefined between two terminals at the same point.
|
||||
*/
|
||||
int QetScriptApi::terminalIndex(int folioIndex, const QString &elementUuid,
|
||||
const QString &terminalUuid) const
|
||||
{
|
||||
Element *element = findElement(folioIndex, elementUuid);
|
||||
const QUuid wanted(terminalUuid);
|
||||
if (!element || wanted.isNull()) return -1;
|
||||
|
||||
int found = -1;
|
||||
const QList<Terminal *> terminals = element->terminals();
|
||||
for (int i = 0 ; i < terminals.count() ; ++i) {
|
||||
if (terminals.at(i)->stableUuid() != wanted) continue;
|
||||
if (found >= 0) return -1;
|
||||
found = i;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const
|
||||
{
|
||||
Element *element = findElement(folioIndex, elementUuid);
|
||||
|
||||
@@ -96,14 +96,16 @@ class QetGraphicsTableItem;
|
||||
no way to connect them.
|
||||
|
||||
Terminals are addressed by their @b index in Element::terminals(),
|
||||
not by uuid, and elementTerminals() prints that indexing so a script
|
||||
can see what it is about to wire. The index is the terminal's place
|
||||
in the element's own top-to-bottom, left-to-right ordering, not the
|
||||
order its definition file lists them. Terminal uuids look like the
|
||||
obvious key and are not one: Terminal::uuid() is a property of the
|
||||
catalog .elmt definition, empty for most of the installed base and,
|
||||
where present, identical across every instance of that element -- so
|
||||
it does not distinguish one placed coil's A1 from another's.
|
||||
and elementTerminals() prints that indexing so a script can see what
|
||||
it is about to wire. The index is the terminal's place in the
|
||||
element's own top-to-bottom, left-to-right ordering, not the order
|
||||
its definition file lists them, and it is undefined between two
|
||||
terminals at the same point. What does not move is the terminal's
|
||||
uuid together with its element's: each elementTerminals() line ends
|
||||
with the terminal's uuid, and terminalIndex() turns element uuid +
|
||||
terminal uuid back into the index. A terminal uuid alone is not an
|
||||
address -- it comes from the symbol's definition, so every placed
|
||||
instance of one symbol has the same ones.
|
||||
- @b Conductor properties and @b cross-references: set a conductor's
|
||||
number, formula, colour or section (and its look: style normal/
|
||||
dashed/dashdotted, two-colour mode and second colour, dash size,
|
||||
@@ -384,6 +386,8 @@ class QetScriptApi : public QObject
|
||||
Q_INVOKABLE QStringList elementUuids(int folioIndex) const;
|
||||
Q_INVOKABLE QString elementName(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE QStringList elementTerminals(int folioIndex, const QString &elementUuid) const;
|
||||
Q_INVOKABLE int terminalIndex(int folioIndex, const QString &elementUuid,
|
||||
const QString &terminalUuid) const;
|
||||
|
||||
// -- element information, through ChangeElementInformationCommand --
|
||||
Q_INVOKABLE QString elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const;
|
||||
|
||||
@@ -181,7 +181,7 @@ bool ShortcutManager::trigger(const QString &id) const
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const QPointer<QObject> &target : qAsConst(it->targets))
|
||||
for (const QPointer<QObject> &target : std::as_const(it->targets))
|
||||
{
|
||||
if (!target) {
|
||||
continue;
|
||||
@@ -213,7 +213,7 @@ QAction *ShortcutManager::action(const QString &id, const QObject *owner) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const QPointer<QObject> &target : qAsConst(it->targets))
|
||||
for (const QPointer<QObject> &target : std::as_const(it->targets))
|
||||
{
|
||||
auto *action = qobject_cast<QAction *>(target.data());
|
||||
if (!action) {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <algorithm>
|
||||
/**
|
||||
@brief TitleBlockTemplate::TitleBlockTemplate
|
||||
Constructor
|
||||
@@ -1818,14 +1819,46 @@ QString TitleBlockTemplate::interpreteVariables(
|
||||
const QString &string,
|
||||
const DiagramContext &diagram_context) const
|
||||
{
|
||||
// A variable nobody has given a value to yet -- e.g. one just added to
|
||||
// the template, which is not in the folio's Custom tab until its
|
||||
// properties are opened (#1113) -- must render blank rather than as its
|
||||
// own name, the same as an auto-added but unset one already does (#973).
|
||||
// Collect those from the template text before substituting, so a value
|
||||
// that happens to contain "%something" is never touched.
|
||||
// A bare "%name" is unset only if no key is a prefix of it, because the
|
||||
// substitution below replaces "%key" wherever it appears.
|
||||
static const QRegularExpression rx(
|
||||
QStringLiteral("%\\{([a-z0-9_-]+)\\}|%([a-z0-9_-]+)"));
|
||||
const QStringList keys =
|
||||
diagram_context.keys(DiagramContext::DecreasingLength);
|
||||
QStringList unset;
|
||||
auto it = rx.globalMatch(string);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch m = it.next();
|
||||
const QString name = m.captured(1).isEmpty()
|
||||
? m.captured(2) : m.captured(1);
|
||||
bool known = diagram_context.contains(name);
|
||||
if (!known && m.captured(1).isEmpty()) {
|
||||
for (const QString &key : keys) {
|
||||
if (name.startsWith(key)) { known = true; break; }
|
||||
}
|
||||
}
|
||||
if (!known) unset << m.captured(0);
|
||||
}
|
||||
|
||||
QString interpreted_string = string;
|
||||
foreach (QString key,
|
||||
diagram_context.keys(DiagramContext::DecreasingLength)) {
|
||||
foreach (QString key, keys) {
|
||||
interpreted_string.replace("%{" % key % "}",
|
||||
diagram_context[key].toString());
|
||||
interpreted_string.replace("%" % key,
|
||||
diagram_context[key].toString());
|
||||
}
|
||||
std::sort(unset.begin(), unset.end(),
|
||||
[](const QString &a, const QString &b) {
|
||||
return a.length() > b.length();
|
||||
});
|
||||
for (const QString &placeholder : unset)
|
||||
interpreted_string.remove(placeholder);
|
||||
return(interpreted_string);
|
||||
}
|
||||
|
||||
@@ -1834,7 +1867,7 @@ QString TitleBlockTemplate::interpreteVariables(
|
||||
Get list of variables
|
||||
@return The list of string with variables
|
||||
*/
|
||||
QStringList TitleBlockTemplate::listOfVariables()
|
||||
QStringList TitleBlockTemplate::listOfVariables() const
|
||||
{
|
||||
QStringList list;
|
||||
// Match both the braced "%{name}" form and the bare "%name" form
|
||||
|
||||
@@ -188,7 +188,7 @@ class TitleBlockTemplate : public QObject {
|
||||
void applyCellSpan(TitleBlockCell *);
|
||||
void applyRowColNums();
|
||||
void rowColsChanged();
|
||||
QStringList listOfVariables();
|
||||
QStringList listOfVariables() const;
|
||||
|
||||
protected:
|
||||
void loadInformation(const QDomElement &);
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "aiassistantdialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFontDatabase>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
using AiAssistantSetup::Client;
|
||||
|
||||
namespace {
|
||||
const char *const GUIDE_URL =
|
||||
"https://github.com/qelectrotech/qelectrotech-source-mirror/wiki/ai_assistants";
|
||||
}
|
||||
|
||||
AiAssistantDialog::AiAssistantDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Connecter un assistant IA"));
|
||||
resize(720, 560);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
const bool windows = true;
|
||||
#else
|
||||
const bool windows = false;
|
||||
#endif
|
||||
m_paths = AiAssistantSetup::detect(QCoreApplication::applicationDirPath(),
|
||||
QCoreApplication::applicationFilePath(),
|
||||
windows);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
auto *intro = new QLabel(
|
||||
tr("Un assistant IA (Claude, GitHub Copilot, Gemini…) peut ouvrir, "
|
||||
"vérifier et modifier vos schémas grâce au serveur MCP de "
|
||||
"QElectroTech, qui fonctionne sur cet ordinateur. Copiez le texte "
|
||||
"ci-dessous dans la configuration de votre assistant. "
|
||||
"<a href=\"%1\">Guide détaillé</a>").arg(QLatin1String(GUIDE_URL)),
|
||||
this);
|
||||
intro->setWordWrap(true);
|
||||
intro->setOpenExternalLinks(true);
|
||||
layout->addWidget(intro);
|
||||
|
||||
if (m_paths.server.isEmpty()) {
|
||||
auto *missing = new QLabel(
|
||||
tr("<b>Le serveur MCP n'est pas installé avec cette version de "
|
||||
"QElectroTech.</b> Le guide explique comment l'obtenir."),
|
||||
this);
|
||||
missing->setWordWrap(true);
|
||||
layout->addWidget(missing);
|
||||
}
|
||||
|
||||
if (!m_paths.server.isEmpty()
|
||||
&& m_paths.python_status != AiAssistantSetup::PythonStatus::Found) {
|
||||
QString text;
|
||||
if (m_paths.python_status == AiAssistantSetup::PythonStatus::StoreShortcut)
|
||||
text = tr("<b>Python n'est peut-être pas installé.</b> Seul le raccourci "
|
||||
"« python » du Microsoft Store a été trouvé : sans Python, il "
|
||||
"ouvre le Store au lieu de lancer le serveur.");
|
||||
else
|
||||
text = tr("<b>Python est introuvable sur cet ordinateur</b> (commande "
|
||||
"« %1 »). Le serveur en a besoin.").arg(m_paths.python);
|
||||
if (windows)
|
||||
text += QLatin1Char(' ') + tr("Relancez l'installateur de QElectroTech et "
|
||||
"cochez « Python pour l'assistant IA », ou "
|
||||
"installez Python depuis python.org.");
|
||||
else
|
||||
text += QLatin1Char(' ') + tr("Installez Python 3 avec le gestionnaire "
|
||||
"de paquets de votre système.");
|
||||
auto *python = new QLabel(text, this);
|
||||
python->setWordWrap(true);
|
||||
layout->addWidget(python);
|
||||
}
|
||||
|
||||
auto *form = new QFormLayout();
|
||||
m_client = new QComboBox(this);
|
||||
m_client->addItem(QStringLiteral("Claude Desktop"), int(Client::ClaudeDesktop));
|
||||
m_client->addItem(QStringLiteral("Claude Code"), int(Client::ClaudeCode));
|
||||
m_client->addItem(QStringLiteral("GitHub Copilot (VS Code)"), int(Client::VsCode));
|
||||
m_client->addItem(QStringLiteral("Cursor"), int(Client::Cursor));
|
||||
m_client->addItem(QStringLiteral("Gemini CLI"), int(Client::GeminiCli));
|
||||
m_client->addItem(QStringLiteral("Codex CLI"), int(Client::CodexCli));
|
||||
m_client->addItem(QStringLiteral("LM Studio"), int(Client::LmStudio));
|
||||
form->addRow(tr("Assistant :"), m_client);
|
||||
|
||||
auto *workspace_row = new QHBoxLayout();
|
||||
m_workspace = new QLineEdit(this);
|
||||
m_workspace->setPlaceholderText(tr("Le dossier de vos schémas"));
|
||||
auto *browse = new QPushButton(tr("Parcourir…"), this);
|
||||
workspace_row->addWidget(m_workspace);
|
||||
workspace_row->addWidget(browse);
|
||||
form->addRow(tr("Dossier accessible :"), workspace_row);
|
||||
|
||||
m_allow_edit = new QCheckBox(tr("Autoriser l'assistant à modifier les schémas"), this);
|
||||
form->addRow(QString(), m_allow_edit);
|
||||
layout->addLayout(form);
|
||||
|
||||
auto *scope = new QLabel(
|
||||
tr("L'assistant ne peut lire et écrire que dans ce dossier. Sans "
|
||||
"modification autorisée, il peut seulement lire, comparer et "
|
||||
"exporter. Un assistant lit le texte des projets (repères, "
|
||||
"notes…) : un texte écrit comme une instruction peut "
|
||||
"l'influencer. Laissez les modifications désactivées pour les "
|
||||
"schémas reçus d'autres personnes."),
|
||||
this);
|
||||
scope->setWordWrap(true);
|
||||
layout->addWidget(scope);
|
||||
|
||||
m_where = new QLabel(this);
|
||||
m_where->setWordWrap(true);
|
||||
m_where->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
layout->addWidget(m_where);
|
||||
|
||||
m_text = new QPlainTextEdit(this);
|
||||
m_text->setReadOnly(true);
|
||||
m_text->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
m_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
layout->addWidget(m_text);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
m_copy = buttons->addButton(tr("Copier"), QDialogButtonBox::ActionRole);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(m_client, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &AiAssistantDialog::refresh);
|
||||
connect(m_workspace, &QLineEdit::textChanged, this, &AiAssistantDialog::refresh);
|
||||
connect(m_allow_edit, &QCheckBox::toggled, this, &AiAssistantDialog::refresh);
|
||||
connect(browse, &QPushButton::clicked, this, &AiAssistantDialog::chooseWorkspace);
|
||||
connect(m_copy, &QPushButton::clicked, this, &AiAssistantDialog::copy);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void AiAssistantDialog::refresh()
|
||||
{
|
||||
const auto client = Client(m_client->currentData().toInt());
|
||||
m_where->setText(whereItGoes(client) + QLatin1Char(' ')
|
||||
+ tr("Si le fichier contient déjà d'autres serveurs, "
|
||||
"ajoutez seulement l'entrée « qet »."));
|
||||
|
||||
const QString workspace = m_workspace->text().trimmed();
|
||||
const bool ready = !m_paths.server.isEmpty() && !workspace.isEmpty();
|
||||
m_copy->setEnabled(ready);
|
||||
if (m_paths.server.isEmpty())
|
||||
m_text->setPlainText(QString());
|
||||
else if (workspace.isEmpty())
|
||||
m_text->setPlainText(tr("Choisissez d'abord le dossier de vos schémas."));
|
||||
else
|
||||
m_text->setPlainText(AiAssistantSetup::configuration(
|
||||
client, m_paths, workspace, m_allow_edit->isChecked()));
|
||||
}
|
||||
|
||||
void AiAssistantDialog::chooseWorkspace()
|
||||
{
|
||||
const QString dir = QFileDialog::getExistingDirectory(
|
||||
this, tr("Dossier accessible à l'assistant"), m_workspace->text());
|
||||
if (!dir.isEmpty())
|
||||
m_workspace->setText(QDir::toNativeSeparators(dir));
|
||||
}
|
||||
|
||||
void AiAssistantDialog::copy()
|
||||
{
|
||||
QApplication::clipboard()->setText(m_text->toPlainText());
|
||||
m_copy->setText(tr("Copié"));
|
||||
}
|
||||
|
||||
QString AiAssistantDialog::whereItGoes(Client client) const
|
||||
{
|
||||
switch (client) {
|
||||
case Client::ClaudeDesktop:
|
||||
return tr("Dans Claude Desktop : Paramètres → Développeur → "
|
||||
"Modifier la configuration. Puis quittez et relancez Claude.");
|
||||
case Client::ClaudeCode:
|
||||
return tr("Enregistrez-le sous le nom .mcp.json dans le dossier de vos schémas.");
|
||||
case Client::VsCode:
|
||||
return tr("Enregistrez-le sous le nom .vscode/mcp.json dans le dossier "
|
||||
"ouvert dans VS Code. Copilot utilise les outils en mode agent.");
|
||||
case Client::Cursor:
|
||||
return tr("Ajoutez-le au fichier .cursor/mcp.json de votre dossier "
|
||||
"personnel.");
|
||||
case Client::GeminiCli:
|
||||
return tr("Ajoutez-le au fichier .gemini/settings.json de votre dossier "
|
||||
"personnel. Gemini CLI demande de faire confiance au dossier "
|
||||
"la première fois.");
|
||||
case Client::CodexCli:
|
||||
return tr("Ajoutez-le au fichier .codex/config.toml de votre dossier "
|
||||
"personnel.");
|
||||
case Client::LmStudio:
|
||||
return tr("Dans LM Studio : onglet Program → Install → Edit mcp.json.");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef AIASSISTANTDIALOG_H
|
||||
#define AIASSISTANTDIALOG_H
|
||||
|
||||
#include "aiassistantsetup.h"
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
@brief The AiAssistantDialog class
|
||||
Help > Connect an AI assistant: shows the configuration an assistant
|
||||
needs to start QElectroTech's MCP server, with this installation's
|
||||
paths filled in, for the user to copy into that assistant. It writes
|
||||
nothing and starts nothing.
|
||||
*/
|
||||
class AiAssistantDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AiAssistantDialog(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void chooseWorkspace();
|
||||
void copy();
|
||||
QString whereItGoes(AiAssistantSetup::Client client) const;
|
||||
|
||||
AiAssistantSetup::Paths m_paths;
|
||||
QComboBox *m_client = nullptr;
|
||||
QLineEdit *m_workspace = nullptr;
|
||||
QCheckBox *m_allow_edit = nullptr;
|
||||
QLabel *m_where = nullptr;
|
||||
QPlainTextEdit *m_text = nullptr;
|
||||
QPushButton *m_copy = nullptr;
|
||||
};
|
||||
|
||||
#endif // AIASSISTANTDIALOG_H
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "aiassistantsetup.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
The first of \a candidates that is a file, as a clean absolute path,
|
||||
or an empty string.
|
||||
*/
|
||||
QString firstFile(const QStringList &candidates)
|
||||
{
|
||||
for (const QString &c : candidates) {
|
||||
const QFileInfo info(c);
|
||||
if (info.isFile())
|
||||
return QDir::cleanPath(info.absoluteFilePath());
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJsonObject environment(const AiAssistantSetup::Paths &paths,
|
||||
const QString &workspace,
|
||||
bool allow_edit)
|
||||
{
|
||||
QJsonObject env;
|
||||
env.insert(QStringLiteral("QET_MCP_WORKSPACE"), QDir::toNativeSeparators(workspace));
|
||||
env.insert(QStringLiteral("QET_BINARY"), QDir::toNativeSeparators(paths.qet_binary));
|
||||
if (allow_edit)
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
return env;
|
||||
}
|
||||
|
||||
/// A TOML basic string: backslashes and quotes escaped.
|
||||
QString toml(const QString &s)
|
||||
{
|
||||
QString out = s;
|
||||
out.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
|
||||
out.replace(QLatin1Char('"'), QStringLiteral("\\\""));
|
||||
return QLatin1Char('"') + out + QLatin1Char('"');
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QList<AiAssistantSetup::Client> AiAssistantSetup::clients()
|
||||
{
|
||||
return {Client::ClaudeDesktop, Client::ClaudeCode, Client::VsCode,
|
||||
Client::Cursor, Client::GeminiCli, Client::CodexCli,
|
||||
Client::LmStudio};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AiAssistantSetup::detect
|
||||
Finds the server beside this QElectroTech, in the two places its
|
||||
packages put it: <root>/mcp/ beside <root>/bin/ (the Windows installers
|
||||
and portable folder) and <prefix>/share/qelectrotech/mcp/ beside
|
||||
<prefix>/bin/ (make install). On Windows the Python the installer can
|
||||
add is preferred to one on PATH.
|
||||
@param application_dir : QCoreApplication::applicationDirPath()
|
||||
@param application_file : QCoreApplication::applicationFilePath()
|
||||
@param windows : true on Windows, where Python is "python", not "python3"
|
||||
@param find_executable : looks a program up on PATH; empty means
|
||||
QStandardPaths::findExecutable(). Given in tests.
|
||||
*/
|
||||
AiAssistantSetup::Paths AiAssistantSetup::detect(const QString &application_dir,
|
||||
const QString &application_file,
|
||||
bool windows,
|
||||
const ExecutableFinder &find_executable)
|
||||
{
|
||||
const QDir bin(application_dir);
|
||||
Paths paths;
|
||||
paths.qet_binary = QDir::cleanPath(application_file);
|
||||
paths.server = firstFile({
|
||||
bin.filePath(QStringLiteral("../mcp/qet_mcp.py")),
|
||||
bin.filePath(QStringLiteral("../share/qelectrotech/mcp/qet_mcp.py"))});
|
||||
|
||||
paths.python = windows ? QStringLiteral("python") : QStringLiteral("python3");
|
||||
if (windows) {
|
||||
const QString bundled = firstFile({
|
||||
bin.filePath(QStringLiteral("../mcp/python/python.exe"))});
|
||||
if (!bundled.isEmpty()) {
|
||||
paths.python = bundled;
|
||||
paths.python_status = PythonStatus::Found;
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
||||
const ExecutableFinder find = find_executable
|
||||
? find_executable
|
||||
: ExecutableFinder([](const QString &name) {
|
||||
return QStandardPaths::findExecutable(name); });
|
||||
const QString found = find(paths.python);
|
||||
if (found.isEmpty())
|
||||
paths.python_status = PythonStatus::Missing;
|
||||
// Windows 10 and 11 put a python.exe in WindowsApps that only opens the
|
||||
// Microsoft Store. A Python installed from the Store answers from there
|
||||
// too, so this cannot tell the two apart: say which it may be.
|
||||
else if (windows && QString(found).replace(QLatin1Char('\\'), QLatin1Char('/'))
|
||||
.contains(QLatin1String("/WindowsApps/"), Qt::CaseInsensitive))
|
||||
paths.python_status = PythonStatus::StoreShortcut;
|
||||
else
|
||||
paths.python_status = PythonStatus::Found;
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AiAssistantSetup::configuration
|
||||
@return the text to paste into \a client's configuration: JSON for all
|
||||
but Codex CLI, which reads TOML. Windows paths come out with native
|
||||
separators, escaped as each format requires.
|
||||
*/
|
||||
QString AiAssistantSetup::configuration(Client client,
|
||||
const Paths &paths,
|
||||
const QString &workspace,
|
||||
bool allow_edit)
|
||||
{
|
||||
const QString python = QDir::toNativeSeparators(paths.python);
|
||||
const QString server = QDir::toNativeSeparators(paths.server);
|
||||
const QJsonObject env = environment(paths, workspace, allow_edit);
|
||||
|
||||
if (client == Client::CodexCli) {
|
||||
QString text = QStringLiteral("[mcp_servers.qet]\n");
|
||||
text += QStringLiteral("command = ") + toml(python) + QLatin1Char('\n');
|
||||
text += QStringLiteral("args = [") + toml(server) + QStringLiteral("]\n\n");
|
||||
text += QStringLiteral("[mcp_servers.qet.env]\n");
|
||||
for (auto it = env.constBegin(); it != env.constEnd(); ++it)
|
||||
text += it.key() + QStringLiteral(" = ") + toml(it.value().toString()) + QLatin1Char('\n');
|
||||
return text;
|
||||
}
|
||||
|
||||
QJsonObject server_entry;
|
||||
if (client == Client::VsCode || client == Client::Cursor)
|
||||
server_entry.insert(QStringLiteral("type"), QStringLiteral("stdio"));
|
||||
server_entry.insert(QStringLiteral("command"), python);
|
||||
server_entry.insert(QStringLiteral("args"), QJsonArray{server});
|
||||
server_entry.insert(QStringLiteral("env"), env);
|
||||
|
||||
// VS Code names the list "servers"; every other client, "mcpServers".
|
||||
const QString key = client == Client::VsCode ? QStringLiteral("servers")
|
||||
: QStringLiteral("mcpServers");
|
||||
const QJsonObject root{{key, QJsonObject{{QStringLiteral("qet"), server_entry}}}};
|
||||
return QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef AIASSISTANTSETUP_H
|
||||
#define AIASSISTANTSETUP_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <functional>
|
||||
|
||||
/**
|
||||
@brief What an AI assistant needs to start QElectroTech's MCP server
|
||||
(misc/qet-mcp), and the configuration text each assistant expects.
|
||||
|
||||
No widgets and no application object, so it can be tested alone. It
|
||||
reads the file system and nothing else: it writes nothing and starts
|
||||
nothing. AiAssistantDialog shows its result.
|
||||
*/
|
||||
namespace AiAssistantSetup
|
||||
{
|
||||
/// The assistants a configuration can be written for.
|
||||
enum class Client {
|
||||
ClaudeDesktop,
|
||||
ClaudeCode,
|
||||
VsCode,
|
||||
Cursor,
|
||||
GeminiCli,
|
||||
CodexCli,
|
||||
LmStudio
|
||||
};
|
||||
|
||||
QList<Client> clients();
|
||||
|
||||
/// Whether the Python the configuration names can actually run.
|
||||
enum class PythonStatus {
|
||||
Found,
|
||||
Missing,
|
||||
/// Only Windows' "python.exe" shortcut in WindowsApps, which opens
|
||||
/// the Microsoft Store when Python is not installed.
|
||||
StoreShortcut
|
||||
};
|
||||
|
||||
/// Where the pieces are. An empty server means none was found.
|
||||
struct Paths {
|
||||
QString server; ///< qet_mcp.py
|
||||
QString python; ///< the Python to run it with
|
||||
QString qet_binary; ///< this QElectroTech
|
||||
PythonStatus python_status = PythonStatus::Missing;
|
||||
};
|
||||
|
||||
/// Finds a program on PATH; QStandardPaths::findExecutable() by default.
|
||||
using ExecutableFinder = std::function<QString(const QString &)>;
|
||||
|
||||
Paths detect(const QString &application_dir,
|
||||
const QString &application_file,
|
||||
bool windows,
|
||||
const ExecutableFinder &find_executable = ExecutableFinder());
|
||||
|
||||
QString configuration(Client client,
|
||||
const Paths &paths,
|
||||
const QString &workspace,
|
||||
bool allow_edit);
|
||||
}
|
||||
|
||||
#endif // AIASSISTANTSETUP_H
|
||||
@@ -438,9 +438,14 @@ void ElementInfoWidget::materialFromFile(int block)
|
||||
Write a catalogue entry into the fields of one block.
|
||||
|
||||
Only the fields of that block are touched: applied to an auxiliary
|
||||
article, the entry never reaches the main article. And a cell which is
|
||||
empty in the file leaves the current value alone, so picking an entry
|
||||
describing only the order reference never wipes a comment.
|
||||
article, the entry never reaches the main article.
|
||||
|
||||
An empty cell of a column describing the article (MaterialList::
|
||||
isArticleBound) clears the field: keeping the manufacturer of the
|
||||
article picked before would describe a part which does not exist.
|
||||
An empty cell of any other column, and any column the file does not
|
||||
hold at all, leaves the field alone, so picking an entry describing
|
||||
only the order reference never wipes a comment.
|
||||
@param record the entry taken from the material file
|
||||
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
||||
*/
|
||||
@@ -459,8 +464,19 @@ void ElementInfoWidget::applyMaterialRecord(const MaterialRecord &record, int bl
|
||||
|
||||
for (const QString &column : MaterialList::columnsForBlock(block))
|
||||
{
|
||||
//A column the file does not hold says nothing about the
|
||||
//article: the field of the element stays as it is.
|
||||
if (!record.values.contains(column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString value = record.value(column);
|
||||
if (value.isEmpty()) {
|
||||
|
||||
//An empty cell only matters for the columns describing the
|
||||
//article itself, where the previous value belongs to another
|
||||
//part. For the others (function, comment, quantity...) an
|
||||
//empty cell means the file has nothing to say about them.
|
||||
if (value.isEmpty() && !MaterialList::isArticleBound(column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -478,7 +494,15 @@ void ElementInfoWidget::applyMaterialRecord(const MaterialRecord &record, int bl
|
||||
enableLiveEdit();
|
||||
}
|
||||
|
||||
apply();
|
||||
//Outside of the live edit the fields only carry the article: it
|
||||
//is the properties window which applies them when the user presses
|
||||
//"Apply" (ElementPropertiesWidget::apply() takes the undo command
|
||||
//from these very fields). Applying right away would push the change
|
||||
//on the undo stack before he has decided anything, and "Cancel"
|
||||
//would give the fields back but never the element.
|
||||
if (live_edit) {
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,7 +73,7 @@ LinkSingleElementWidget::LinkSingleElementWidget(Element *elmt,
|
||||
connect(ui->m_hide_full_masters_cb, &QCheckBox::toggled,
|
||||
this, &LinkSingleElementWidget::hideFullMastersToggled);
|
||||
|
||||
connect(m_show_qtwi, &QAction::triggered, this, [=]()
|
||||
connect(m_show_qtwi, &QAction::triggered, this, [this]()
|
||||
{
|
||||
this->on_m_tree_widget_itemDoubleClicked(this->m_qtwi_at_context_menu, 0);
|
||||
});
|
||||
@@ -83,7 +83,7 @@ LinkSingleElementWidget::LinkSingleElementWidget(Element *elmt,
|
||||
this,
|
||||
&LinkSingleElementWidget::linkTriggered);
|
||||
|
||||
connect(m_show_element, &QAction::triggered, this, [=]()
|
||||
connect(m_show_element, &QAction::triggered, this, [this]()
|
||||
{
|
||||
this->m_element->diagram()->showMe();
|
||||
this->m_element->setHighlighted(true);
|
||||
|
||||
@@ -483,13 +483,23 @@ TitleBlockTemplate *TitleBlockPropertiesWidget::templateForIndex(int index) cons
|
||||
Add to @p context every CUSTOM variable used by the currently selected
|
||||
template that is not already present, with an empty value — so the user
|
||||
only has to fill in the values instead of declaring the variables (#271).
|
||||
The standard fields (title, author, date, …) are handled by their own
|
||||
widgets and are skipped. Existing values in @p context are preserved.
|
||||
*/
|
||||
void TitleBlockPropertiesWidget::addTemplateVariables(
|
||||
DiagramContext &context, int index) const
|
||||
{
|
||||
TitleBlockTemplate *tpl = templateForIndex(index);
|
||||
addTemplateVariables(context, templateForIndex(index));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TitleBlockPropertiesWidget::addTemplateVariables
|
||||
Add to @p context every CUSTOM variable used by @p tpl that is not already
|
||||
present, with an empty value.
|
||||
The standard fields (title, author, date, …) are handled by their own
|
||||
widgets and are skipped. Existing values in @p context are preserved.
|
||||
*/
|
||||
void TitleBlockPropertiesWidget::addTemplateVariables(
|
||||
DiagramContext &context, const TitleBlockTemplate *tpl)
|
||||
{
|
||||
if (!tpl) return;
|
||||
|
||||
// Variables rendered from the dedicated standard-field widgets; they must
|
||||
|
||||
@@ -71,6 +71,10 @@ class TitleBlockPropertiesWidget : public QWidget
|
||||
void setTitleBlockTemplatesVisible(const bool &visible);
|
||||
void setReadOnly (const bool &ro);
|
||||
|
||||
static void addTemplateVariables(
|
||||
DiagramContext &context,
|
||||
const TitleBlockTemplate *tpl);
|
||||
|
||||
private:
|
||||
void addCollection (
|
||||
TitleBlockTemplatesCollection *tbt_collection);
|
||||
|
||||
@@ -30,6 +30,78 @@
|
||||
#include <QHash>
|
||||
#include <QSettings>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
One selected item that takes part: where its edges and centre
|
||||
are, and the point that goes on the grid, on the grid its own
|
||||
drag uses (divided by @a divisor, as TextGrid::snap() does).
|
||||
*/
|
||||
struct Entry {
|
||||
QGraphicsObject *item;
|
||||
Alignment::Item geometry;
|
||||
QPointF snap_point;
|
||||
qreal divisor;
|
||||
};
|
||||
|
||||
/**
|
||||
@return the items of @a dc that take part, symbols first so a
|
||||
group with a symbol in it snaps on that symbol.
|
||||
*/
|
||||
QList<Entry> entriesOf(const DiagramContent &dc, qreal text_divisor)
|
||||
{
|
||||
QList<Entry> entries;
|
||||
//A symbol's edges are its own outline, without its texts, and
|
||||
//its centre is its origin point: that is where its wires leave.
|
||||
for (Element *element : std::as_const(dc.m_elements))
|
||||
entries << Entry{element, {element->sceneBoundingRect(), element->pos()},
|
||||
element->pos(), 1};
|
||||
//A picture's edges are the picture's, without its caption
|
||||
for (DiagramImageItem *image : std::as_const(dc.m_images))
|
||||
{
|
||||
const QRectF rect = image->mapRectToScene(image->imageRect());
|
||||
entries << Entry{image, {rect, rect.center()}, image->pos(), 1};
|
||||
}
|
||||
for (IndependentTextItem *text : std::as_const(dc.m_text_fields))
|
||||
entries << Entry{text, {text->sceneBoundingRect(), text->sceneBoundingRect().center()},
|
||||
text->pos(), text_divisor};
|
||||
//A shape's edges are the shape as drawn. What goes on the grid is
|
||||
//the top-left corner of that box: a rectangle's corner, an
|
||||
//ellipse's box, a line's end. Its pos() says nothing a user can
|
||||
//see, and a rotated shape's is never on the grid anyway.
|
||||
for (QetShapeItem *shape : std::as_const(dc.m_shapes))
|
||||
{
|
||||
const QRectF rect = shape->sceneOutlineRect();
|
||||
entries << Entry{shape, {rect, rect.center()}, rect.topLeft(), 1};
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
@return @a entries sorted into the pieces that line up: a group
|
||||
(#1070) is one piece, every other item a piece of its own. Each
|
||||
piece is the list of its indexes into @a entries.
|
||||
*/
|
||||
QList<QList<int>> unitsOf(const QList<Entry> &entries)
|
||||
{
|
||||
QList<QList<int>> units;
|
||||
QHash<QUuid, int> group_units;
|
||||
for (int i = 0 ; i < entries.size() ; ++i)
|
||||
{
|
||||
const QUuid group = ItemGroups::groupOf(entries.at(i).item);
|
||||
if (group.isNull()) {
|
||||
units << QList<int>{i};
|
||||
continue;
|
||||
}
|
||||
if (!group_units.contains(group)) {
|
||||
group_units.insert(group, units.size());
|
||||
units << QList<int>();
|
||||
}
|
||||
units[group_units.value(group)] << i;
|
||||
}
|
||||
return units;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::AlignSelectionCommand
|
||||
Works out the movement of every selected item for @a mode. Nothing
|
||||
@@ -53,28 +125,9 @@ AlignSelectionCommand::AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoC
|
||||
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"), Diagram::yGrid).toInt();
|
||||
const qreal text_divisor = settings.value(TextGrid::settings_key, 1).toReal();
|
||||
|
||||
//Each kind goes where dragging it would have left it: symbols and
|
||||
//pictures on the folio grid, free texts on the text grid.
|
||||
//Shapes are left out: they are made of several points and no single
|
||||
//one of them is the obvious one to snap.
|
||||
struct Entry {
|
||||
QGraphicsObject *item;
|
||||
Alignment::Item geometry;
|
||||
qreal divisor;
|
||||
};
|
||||
QList<Entry> entries;
|
||||
//A symbol's edges are its own outline, without its texts, and its
|
||||
//centre is its origin point: that is where its wires leave.
|
||||
for (Element *element : std::as_const(dc.m_elements))
|
||||
entries << Entry{element, {element->sceneBoundingRect(), element->pos()}, 1};
|
||||
//A picture's edges are the picture's, without its caption
|
||||
for (DiagramImageItem *image : std::as_const(dc.m_images))
|
||||
{
|
||||
const QRectF rect = image->mapRectToScene(image->imageRect());
|
||||
entries << Entry{image, {rect, rect.center()}, 1};
|
||||
}
|
||||
for (IndependentTextItem *text : std::as_const(dc.m_text_fields))
|
||||
entries << Entry{text, {text->sceneBoundingRect(), text->sceneBoundingRect().center()}, text_divisor};
|
||||
//Each kind goes where dragging it would have left it: symbols,
|
||||
//pictures and shapes on the folio grid, free texts on the text grid.
|
||||
const QList<Entry> entries = entriesOf(dc, text_divisor);
|
||||
m_item_count = entries.size();
|
||||
|
||||
auto move = [this](QGraphicsObject *item, const QPointF &offset)
|
||||
@@ -86,60 +139,15 @@ AlignSelectionCommand::AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoC
|
||||
if (mode == SnapToGrid)
|
||||
{
|
||||
for (const Entry &entry : std::as_const(entries))
|
||||
move(entry.item, Alignment::gridOffset(entry.item->pos(), x_grid, y_grid, entry.divisor));
|
||||
move(entry.item, Alignment::gridOffset(entry.snap_point, x_grid, y_grid, entry.divisor));
|
||||
setText(QObject::tr("Aligner %n objet(s) sur la grille", "", childCount()));
|
||||
return;
|
||||
}
|
||||
|
||||
//A group (#1070) lines up as one piece: its edges are its members'
|
||||
//together, its centre the middle of that box, and all its members
|
||||
//move by the same amount, so the group keeps its shape. Shapes in a
|
||||
//group come along; shapes outside one are left out, as above.
|
||||
struct Unit {
|
||||
QList<QGraphicsObject *> members;
|
||||
QList<Alignment::Item> parts;
|
||||
QGraphicsObject *snap_item = nullptr; ///< lands on its grid
|
||||
qreal divisor = 1;
|
||||
};
|
||||
QList<Unit> units;
|
||||
QHash<QUuid, int> group_units;
|
||||
//Returns a reference into units, which a later call can grow:
|
||||
//use it before calling again, never keep it
|
||||
auto unitFor = [&](QGraphicsObject *item) -> Unit &
|
||||
{
|
||||
const QUuid group = ItemGroups::groupOf(item);
|
||||
if (group.isNull()) {
|
||||
units << Unit();
|
||||
return units.last();
|
||||
}
|
||||
if (!group_units.contains(group)) {
|
||||
group_units.insert(group, units.size());
|
||||
units << Unit();
|
||||
}
|
||||
return units[group_units.value(group)];
|
||||
};
|
||||
for (const Entry &entry : std::as_const(entries))
|
||||
{
|
||||
Unit &unit = unitFor(entry.item);
|
||||
unit.members << entry.item;
|
||||
unit.parts << entry.geometry;
|
||||
//Symbols come first in entries, so a group with one snaps on it
|
||||
if (!unit.snap_item) {
|
||||
unit.snap_item = entry.item;
|
||||
unit.divisor = entry.divisor;
|
||||
}
|
||||
}
|
||||
for (QetShapeItem *shape : std::as_const(dc.m_shapes))
|
||||
{
|
||||
if (ItemGroups::groupOf(shape).isNull())
|
||||
continue;
|
||||
Unit &unit = unitFor(shape);
|
||||
unit.members << shape;
|
||||
const QRectF rect = shape->sceneBoundingRect();
|
||||
unit.parts << Alignment::Item{rect, rect.center()};
|
||||
if (!unit.snap_item)
|
||||
unit.snap_item = shape;
|
||||
}
|
||||
//move by the same amount, so the group keeps its shape.
|
||||
const QList<QList<int>> units = unitsOf(entries);
|
||||
m_item_count = units.size();
|
||||
|
||||
//Lining up a single item on itself would only snap it
|
||||
@@ -158,21 +166,39 @@ AlignSelectionCommand::AlignSelectionCommand(Diagram *diagram, Mode mode, QUndoC
|
||||
}
|
||||
|
||||
QList<Alignment::Item> geometry;
|
||||
for (const Unit &unit : std::as_const(units))
|
||||
geometry << Alignment::combined(unit.parts);
|
||||
for (const QList<int> &unit : units)
|
||||
{
|
||||
QList<Alignment::Item> parts;
|
||||
for (int i : unit)
|
||||
parts << entries.at(i).geometry;
|
||||
geometry << Alignment::combined(parts);
|
||||
}
|
||||
const QList<QPointF> offsets = Alignment::alignOffsets(geometry, edge);
|
||||
|
||||
for (int i = 0 ; i < units.size() ; ++i)
|
||||
for (int u = 0 ; u < units.size() ; ++u)
|
||||
{
|
||||
const Unit &unit = units.at(i);
|
||||
const QPointF offset = Alignment::snappedOffset(unit.snap_item->pos(), offsets.at(i), edge,
|
||||
x_grid, y_grid, unit.divisor);
|
||||
for (QGraphicsObject *member : unit.members)
|
||||
move(member, offset);
|
||||
//The piece lands on the grid of its first member
|
||||
const Entry &first = entries.at(units.at(u).first());
|
||||
const QPointF offset = Alignment::snappedOffset(first.snap_point, offsets.at(u), edge,
|
||||
x_grid, y_grid, first.divisor);
|
||||
for (int i : units.at(u))
|
||||
move(entries.at(i).item, offset);
|
||||
}
|
||||
setText(QObject::tr("Aligner %n objet(s)", "", childCount()));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::unitCount
|
||||
@return the number of pieces the selection in @a dc lines up as:
|
||||
every symbol, picture, free text and shape, a group counting as one.
|
||||
Locked items are counted too, so the command can say why they did
|
||||
not move.
|
||||
*/
|
||||
int AlignSelectionCommand::unitCount(const DiagramContent &dc)
|
||||
{
|
||||
return unitsOf(entriesOf(dc, 1)).size();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AlignSelectionCommand::undo
|
||||
*/
|
||||
@@ -220,8 +246,9 @@ int AlignSelectionCommand::lockedCount() const
|
||||
}
|
||||
|
||||
/**
|
||||
@return the number of things that took part: symbols, pictures and
|
||||
free texts whose position is not locked, a group counting as one.
|
||||
@return the number of things that took part: symbols, pictures, free
|
||||
texts and shapes whose position is not locked, a group counting as
|
||||
one.
|
||||
*/
|
||||
int AlignSelectionCommand::itemCount() const
|
||||
{
|
||||
|
||||
@@ -22,11 +22,12 @@
|
||||
#include <QUndoCommand>
|
||||
|
||||
class Diagram;
|
||||
class DiagramContent;
|
||||
|
||||
/**
|
||||
@brief The AlignSelectionCommand class
|
||||
Moves each selected item by its own amount, as one undo step.
|
||||
Symbols, pictures and free texts take part; locked items are left
|
||||
Symbols, pictures, free texts and shapes take part; locked items are left
|
||||
where they are and counted, so the caller can say so.
|
||||
*/
|
||||
class AlignSelectionCommand : public QUndoCommand
|
||||
@@ -52,6 +53,8 @@ class AlignSelectionCommand : public QUndoCommand
|
||||
int lockedCount() const;
|
||||
int itemCount() const;
|
||||
|
||||
static int unitCount(const DiagramContent &dc);
|
||||
|
||||
private:
|
||||
QPointer<Diagram> m_diagram;
|
||||
int m_locked_count = 0;
|
||||
|
||||
@@ -212,6 +212,16 @@ add_test(NAME tst_scriptingsetting COMMAND tst_scriptingsetting)
|
||||
target_include_directories(tst_scriptingsetting PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_scriptingsetting PRIVATE Qt::Test Qt::Gui)
|
||||
|
||||
# AiAssistantSetup: finding the MCP server beside an installation, and the
|
||||
# configuration text for each AI assistant. Compiles the one file alone.
|
||||
add_executable(
|
||||
tst_aiassistantsetup
|
||||
tst_aiassistantsetup.cpp
|
||||
${QET_DIR}/sources/ui/aiassistantsetup.cpp)
|
||||
add_test(NAME tst_aiassistantsetup COMMAND tst_aiassistantsetup)
|
||||
target_include_directories(tst_aiassistantsetup PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_aiassistantsetup PRIVATE Qt::Test Qt::Core)
|
||||
|
||||
# QetSettings::sheetBackground() -- the sheet (folio) background the diagram
|
||||
# editor remembers between runs. Compiles qetsettings.cpp alone for the same
|
||||
# reason as the test above; Qt::Gui because the pair carries a QColor.
|
||||
@@ -343,6 +353,44 @@ if(QET_HAS_SCRIPTING)
|
||||
target_link_libraries(tst_scriptconductoruuid PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_scriptconductoruuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# The project database filled from the file as it is read holds exactly
|
||||
# what it holds filled from the built folios (QET_DATABASE_FROM_FOLIOS=1),
|
||||
# on every example saved once; an older file falls back, saying why.
|
||||
add_executable(
|
||||
tst_databasefromdocument
|
||||
tst_databasefromdocument.cpp)
|
||||
add_test(NAME tst_databasefromdocument COMMAND tst_databasefromdocument)
|
||||
add_dependencies(tst_databasefromdocument qelectrotech)
|
||||
target_link_libraries(tst_databasefromdocument PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_databasefromdocument PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# The project database keeps a folio's date with its century: 741.qet's
|
||||
# 2010-09-21, read through project_summary_view under LC_ALL=en_US.UTF-8.
|
||||
add_executable(
|
||||
tst_dbfoliodate
|
||||
tst_dbfoliodate.cpp)
|
||||
add_test(NAME tst_dbfoliodate COMMAND tst_dbfoliodate)
|
||||
add_dependencies(tst_dbfoliodate qelectrotech)
|
||||
target_link_libraries(tst_dbfoliodate PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_dbfoliodate PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# qet.elementTerminals() lists each terminal's uuid and qet.terminalIndex()
|
||||
# turns element uuid + terminal uuid back into the index the conductor
|
||||
# calls take, on examples/perceuse.qet (two terminals at one point).
|
||||
add_executable(
|
||||
tst_scriptterminaluuid
|
||||
tst_scriptterminaluuid.cpp)
|
||||
add_test(NAME tst_scriptterminaluuid COMMAND tst_scriptterminaluuid)
|
||||
add_dependencies(tst_scriptterminaluuid qelectrotech)
|
||||
target_link_libraries(tst_scriptterminaluuid PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_scriptterminaluuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
endif()
|
||||
|
||||
# A symbol saved without a uuid gets the same one on every load, and
|
||||
@@ -385,6 +433,20 @@ target_compile_definitions(tst_resaveunchanged PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Replacing a symbol's definition in a project keeps its terminal uuids, so
|
||||
# the wires saved against them still load: TerminalUuids::keep() on its own,
|
||||
# then the real binary's --info on an example whose symbols were replaced.
|
||||
add_executable(
|
||||
tst_terminaluuids
|
||||
tst_terminaluuids.cpp
|
||||
${QET_DIR}/sources/ElementsCollection/terminaluuids.cpp)
|
||||
add_test(NAME tst_terminaluuids COMMAND tst_terminaluuids)
|
||||
add_dependencies(tst_terminaluuids qelectrotech)
|
||||
target_link_libraries(tst_terminaluuids PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_terminaluuids PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# DiagramContext::fromXml() -- the two readers (QDom for projects, pugixml
|
||||
# for element definitions in the collection) give the same values: stray
|
||||
# spaces trimmed, accents kept.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "ui/aiassistantsetup.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
using AiAssistantSetup::Client;
|
||||
|
||||
class TestAiAssistantSetup : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static void touch(const QString &path)
|
||||
{
|
||||
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
}
|
||||
|
||||
static AiAssistantSetup::Paths windowsPaths()
|
||||
{
|
||||
AiAssistantSetup::Paths p;
|
||||
p.server = QStringLiteral("C:\\Program Files\\QElectroTech\\mcp\\qet_mcp.py");
|
||||
p.python = QStringLiteral("C:\\Program Files\\QElectroTech\\mcp\\python\\python.exe");
|
||||
p.qet_binary = QStringLiteral("C:\\Program Files\\QElectroTech\\bin\\QElectroTech.exe");
|
||||
return p;
|
||||
}
|
||||
|
||||
static QJsonObject entry(Client client, const AiAssistantSetup::Paths &p, bool edit)
|
||||
{
|
||||
const QByteArray text = AiAssistantSetup::configuration(
|
||||
client, p, QStringLiteral("D:\\Drawings"), edit).toUtf8();
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(text, &error);
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
qWarning() << error.errorString() << text;
|
||||
const QString key = client == Client::VsCode ? QStringLiteral("servers")
|
||||
: QStringLiteral("mcpServers");
|
||||
return doc.object().value(key).toObject().value(QStringLiteral("qet")).toObject();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void windowsInstallIsFound()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
touch(root.filePath(QStringLiteral("bin/QElectroTech.exe")));
|
||||
touch(root.filePath(QStringLiteral("mcp/qet_mcp.py")));
|
||||
touch(root.filePath(QStringLiteral("mcp/python/python.exe")));
|
||||
const auto p = AiAssistantSetup::detect(root.filePath(QStringLiteral("bin")),
|
||||
root.filePath(QStringLiteral("bin/QElectroTech.exe")), true);
|
||||
QCOMPARE(p.server, QDir::cleanPath(root.filePath(QStringLiteral("mcp/qet_mcp.py"))));
|
||||
QCOMPARE(p.python, QDir::cleanPath(root.filePath(QStringLiteral("mcp/python/python.exe"))));
|
||||
QCOMPARE(p.qet_binary, QDir::cleanPath(root.filePath(QStringLiteral("bin/QElectroTech.exe"))));
|
||||
}
|
||||
|
||||
void windowsWithoutBundledPythonUsesPath()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
touch(root.filePath(QStringLiteral("mcp/qet_mcp.py")));
|
||||
QDir().mkpath(root.filePath(QStringLiteral("bin")));
|
||||
const auto p = AiAssistantSetup::detect(root.filePath(QStringLiteral("bin")),
|
||||
root.filePath(QStringLiteral("bin/QElectroTech.exe")), true);
|
||||
QCOMPARE(p.python, QStringLiteral("python"));
|
||||
}
|
||||
|
||||
void unixInstallIsFound()
|
||||
{
|
||||
QTemporaryDir prefix;
|
||||
touch(prefix.filePath(QStringLiteral("share/qelectrotech/mcp/qet_mcp.py")));
|
||||
// A bundled-Python layout is only a Windows thing.
|
||||
touch(prefix.filePath(QStringLiteral("mcp/python/python.exe")));
|
||||
QDir().mkpath(prefix.filePath(QStringLiteral("bin")));
|
||||
const auto p = AiAssistantSetup::detect(prefix.filePath(QStringLiteral("bin")),
|
||||
prefix.filePath(QStringLiteral("bin/qelectrotech")), false);
|
||||
QCOMPARE(p.server, QDir::cleanPath(prefix.filePath(QStringLiteral("share/qelectrotech/mcp/qet_mcp.py"))));
|
||||
QCOMPARE(p.python, QStringLiteral("python3"));
|
||||
}
|
||||
|
||||
void bundledPythonIsFoundWithoutLookingOnPath()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
touch(root.filePath(QStringLiteral("mcp/qet_mcp.py")));
|
||||
touch(root.filePath(QStringLiteral("mcp/python/python.exe")));
|
||||
QDir().mkpath(root.filePath(QStringLiteral("bin")));
|
||||
bool looked = false;
|
||||
const auto p = AiAssistantSetup::detect(root.filePath(QStringLiteral("bin")),
|
||||
root.filePath(QStringLiteral("bin/QElectroTech.exe")), true,
|
||||
[&looked](const QString &) { looked = true; return QString(); });
|
||||
QCOMPARE(p.python_status, AiAssistantSetup::PythonStatus::Found);
|
||||
QVERIFY(!looked);
|
||||
}
|
||||
|
||||
void pythonStatusFromPath_data()
|
||||
{
|
||||
QTest::addColumn<bool>("windows");
|
||||
QTest::addColumn<QString>("found");
|
||||
QTest::addColumn<int>("status");
|
||||
using S = AiAssistantSetup::PythonStatus;
|
||||
QTest::newRow("windows, none") << true << QString() << int(S::Missing);
|
||||
QTest::newRow("windows, python.org") << true
|
||||
<< QStringLiteral("C:/Users/me/AppData/Local/Programs/Python/Python314/python.exe") << int(S::Found);
|
||||
QTest::newRow("windows, Store shortcut") << true
|
||||
<< QStringLiteral("C:/Users/me/AppData/Local/Microsoft/WindowsApps/python.exe") << int(S::StoreShortcut);
|
||||
QTest::newRow("windows, Store shortcut, backslashes") << true
|
||||
<< QStringLiteral("C:\\Users\\me\\AppData\\Local\\Microsoft\\WINDOWSAPPS\\python.exe") << int(S::StoreShortcut);
|
||||
QTest::newRow("linux, none") << false << QString() << int(S::Missing);
|
||||
QTest::newRow("linux, python3") << false << QStringLiteral("/usr/bin/python3") << int(S::Found);
|
||||
QTest::newRow("linux, a folder named WindowsApps") << false
|
||||
<< QStringLiteral("/opt/WindowsApps/python3") << int(S::Found);
|
||||
}
|
||||
|
||||
void pythonStatusFromPath()
|
||||
{
|
||||
QFETCH(bool, windows);
|
||||
QFETCH(QString, found);
|
||||
QFETCH(int, status);
|
||||
QTemporaryDir build;
|
||||
QString asked;
|
||||
const auto p = AiAssistantSetup::detect(build.path(),
|
||||
build.filePath(QStringLiteral("qelectrotech")), windows,
|
||||
[&](const QString &name) { asked = name; return found; });
|
||||
QCOMPARE(int(p.python_status), status);
|
||||
QCOMPARE(asked, windows ? QStringLiteral("python") : QStringLiteral("python3"));
|
||||
}
|
||||
|
||||
void noServerIsReportedAsEmpty()
|
||||
{
|
||||
QTemporaryDir build;
|
||||
const auto p = AiAssistantSetup::detect(build.path(),
|
||||
build.filePath(QStringLiteral("qelectrotech")), false);
|
||||
QVERIFY(p.server.isEmpty());
|
||||
}
|
||||
|
||||
void everyJsonClientGetsValidJson()
|
||||
{
|
||||
for (Client c : AiAssistantSetup::clients()) {
|
||||
if (c == Client::CodexCli)
|
||||
continue;
|
||||
const QJsonObject e = entry(c, windowsPaths(), false);
|
||||
QVERIFY2(!e.isEmpty(), qPrintable(QString::number(int(c))));
|
||||
// Backslashes survive: parsing the text gives the path back.
|
||||
QCOMPARE(e.value(QStringLiteral("args")).toArray().at(0).toString(), windowsPaths().server);
|
||||
QCOMPARE(e.value(QStringLiteral("command")).toString(), windowsPaths().python);
|
||||
const QJsonObject env = e.value(QStringLiteral("env")).toObject();
|
||||
QCOMPARE(env.value(QStringLiteral("QET_MCP_WORKSPACE")).toString(), QStringLiteral("D:\\Drawings"));
|
||||
QCOMPARE(env.value(QStringLiteral("QET_BINARY")).toString(), windowsPaths().qet_binary);
|
||||
const bool typed = c == Client::VsCode || c == Client::Cursor;
|
||||
QCOMPARE(e.contains(QStringLiteral("type")), typed);
|
||||
}
|
||||
}
|
||||
|
||||
void editingIsOffUnlessAllowed()
|
||||
{
|
||||
for (Client c : AiAssistantSetup::clients()) {
|
||||
const QString off = AiAssistantSetup::configuration(c, windowsPaths(), QStringLiteral("D:\\x"), false);
|
||||
const QString on = AiAssistantSetup::configuration(c, windowsPaths(), QStringLiteral("D:\\x"), true);
|
||||
QVERIFY(!off.contains(QStringLiteral("QET_ENABLE_SCRIPTING")));
|
||||
QVERIFY(on.contains(QStringLiteral("QET_ENABLE_SCRIPTING")));
|
||||
}
|
||||
}
|
||||
|
||||
void codexGetsEscapedToml()
|
||||
{
|
||||
const QString t = AiAssistantSetup::configuration(
|
||||
Client::CodexCli, windowsPaths(), QStringLiteral("D:\\Drawings"), true);
|
||||
QVERIFY(t.startsWith(QStringLiteral("[mcp_servers.qet]\n")));
|
||||
QVERIFY(t.contains(QStringLiteral("args = [\"C:\\\\Program Files\\\\QElectroTech\\\\mcp\\\\qet_mcp.py\"]")));
|
||||
QVERIFY(t.contains(QStringLiteral("[mcp_servers.qet.env]\n")));
|
||||
QVERIFY(t.contains(QStringLiteral("QET_MCP_WORKSPACE = \"D:\\\\Drawings\"")));
|
||||
QVERIFY(t.contains(QStringLiteral("QET_ENABLE_SCRIPTING = \"1\"")));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestAiAssistantSetup)
|
||||
#include "tst_aiassistantsetup.moc"
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// A project's database, filled from the file as it is read, holds exactly
|
||||
// what the same database filled from the built folios holds. Every example
|
||||
// is saved once (so that it carries the uuids a current QElectroTech
|
||||
// writes), then opened twice through the real binary's --run: once as is,
|
||||
// once with QET_DATABASE_FROM_FOLIOS=1, and the six tables compared.
|
||||
class tst_databasefromdocument : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
QProcessEnvironment env()
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment e = QProcessEnvironment::systemEnvironment();
|
||||
e.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
e.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
e.insert(QStringLiteral("HOME"), home);
|
||||
e.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
e.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
e.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
e.remove(QStringLiteral("QET_DATABASE_FROM_FOLIOS"));
|
||||
return e;
|
||||
}
|
||||
|
||||
QString run(const QStringList &args, bool from_folios = false)
|
||||
{
|
||||
QProcessEnvironment e = env();
|
||||
if (from_folios)
|
||||
e.insert(QStringLiteral("QET_DATABASE_FROM_FOLIOS"), QStringLiteral("1"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(e);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), args);
|
||||
if (!proc.waitForFinished(180000)) return {};
|
||||
return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
||||
}
|
||||
|
||||
// The six tables, each as a sorted list of its rows, and which way the
|
||||
// database was filled.
|
||||
QJsonObject dump(const QString &project, bool from_folios, QString *how)
|
||||
{
|
||||
const QString out = run({QStringLiteral("--run"), m_dir.filePath(QStringLiteral("dump.js")),
|
||||
project}, from_folios);
|
||||
QJsonObject tables;
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
if (line.contains(QStringLiteral("Project database filled")))
|
||||
*how = line.mid(line.indexOf(QStringLiteral("Project database filled")));
|
||||
const int i = line.indexOf(QStringLiteral("DUMP "));
|
||||
if (i >= 0) {
|
||||
const QJsonObject raw = QJsonDocument::fromJson(line.mid(i + 5).toUtf8()).object();
|
||||
for (auto it = raw.begin(); it != raw.end(); ++it) {
|
||||
QStringList rows;
|
||||
for (const QJsonValue &row : it.value().toArray())
|
||||
rows << QString::fromUtf8(QJsonDocument(row.toObject()).toJson(QJsonDocument::Compact));
|
||||
rows.sort();
|
||||
tables.insert(it.key(), QJsonArray::fromStringList(rows));
|
||||
}
|
||||
}
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
// tremie_vibrante.qet saved once, as a document to change
|
||||
QDomDocument resaved(const QString &saved)
|
||||
{
|
||||
run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved});
|
||||
QFile file(saved);
|
||||
QDomDocument document;
|
||||
if (file.open(QIODevice::ReadOnly))
|
||||
document.setContent(&file);
|
||||
return document;
|
||||
}
|
||||
|
||||
static bool write(const QString &path, const QDomDocument &document)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::WriteOnly | QIODevice::Truncate)
|
||||
&& file.write(document.toByteArray()) > 0;
|
||||
}
|
||||
|
||||
// Both fills of @p saved, which must come from the document, agree.
|
||||
void compareBothWays(const QString &saved)
|
||||
{
|
||||
QString how_document, how_folios;
|
||||
const QJsonObject document = dump(saved, false, &how_document);
|
||||
const QJsonObject folios = dump(saved, true, &how_folios);
|
||||
QVERIFY2(how_document == QLatin1String("Project database filled from the document"),
|
||||
qPrintable(how_document));
|
||||
QVERIFY2(how_folios.contains(QStringLiteral("QET_DATABASE_FROM_FOLIOS")), qPrintable(how_folios));
|
||||
QCOMPARE(document.keys().size(), 6);
|
||||
for (const QString &table : folios.keys()) {
|
||||
const QJsonArray a = document.value(table).toArray(), b = folios.value(table).toArray();
|
||||
QVERIFY2(a == b, qPrintable(QStringLiteral("%1: %2 rows from the document, %3 from the folios")
|
||||
.arg(table).arg(a.size()).arg(b.size())));
|
||||
}
|
||||
m_last = document;
|
||||
}
|
||||
|
||||
QJsonObject m_last; // the document's tables, from compareBothWays()
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
QFile js(m_dir.filePath(QStringLiteral("dump.js")));
|
||||
QVERIFY(js.open(QIODevice::WriteOnly));
|
||||
js.write("var out = {};\n"
|
||||
"['diagram', 'diagram_info', 'element', 'element_info', 'terminal', 'conductor']"
|
||||
".forEach(function (t) { out[t] = qet.query('SELECT * FROM ' + t); });\n"
|
||||
"qet.log('DUMP ' + JSON.stringify(out));\n");
|
||||
}
|
||||
|
||||
void sameTablesBothWays_data()
|
||||
{
|
||||
QTest::addColumn<QString>("project");
|
||||
const QDir examples(QStringLiteral(QET_EXAMPLES_DIR));
|
||||
for (const QString &f : examples.entryList({QStringLiteral("*.qet")}, QDir::Files, QDir::Name))
|
||||
QTest::newRow(f.toUtf8().constData()) << examples.filePath(f);
|
||||
}
|
||||
|
||||
void sameTablesBothWays()
|
||||
{
|
||||
QFETCH(QString, project);
|
||||
const QString saved = m_dir.filePath(QStringLiteral("saved%1.qet").arg(m_run));
|
||||
run({QStringLiteral("--resave"), project, saved});
|
||||
QVERIFY2(QFile::exists(saved), "--resave failed");
|
||||
compareBothWays(saved);
|
||||
}
|
||||
|
||||
// A label or a conductor text made from a formula is what the formula
|
||||
// gives now, not what it gave when the file was saved: every folio's
|
||||
// first element and first conductor is given a formula using each kind
|
||||
// of variable, and a saved label no formula gives.
|
||||
void formulasAreWorkedOut()
|
||||
{
|
||||
const QString saved = m_dir.filePath(QStringLiteral("formulas-saved.qet"));
|
||||
run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved});
|
||||
QFile file(saved);
|
||||
QVERIFY(file.open(QIODevice::ReadOnly));
|
||||
QDomDocument document;
|
||||
QVERIFY(document.setContent(&file));
|
||||
file.close();
|
||||
|
||||
auto property = [&document](QDomElement parent, const QString &name, const QString &value) {
|
||||
QDomElement properties = parent.firstChildElement(QStringLiteral("properties"));
|
||||
if (properties.isNull())
|
||||
properties = parent.appendChild(document.createElement(QStringLiteral("properties"))).toElement();
|
||||
QDomElement p = document.createElement(QStringLiteral("property"));
|
||||
p.setAttribute(QStringLiteral("name"), name);
|
||||
p.appendChild(document.createTextNode(value));
|
||||
properties.appendChild(p);
|
||||
};
|
||||
auto sequence = [&document](QDomElement item, const QString &unit) {
|
||||
item.removeChild(item.firstChildElement(QStringLiteral("sequentialNumbers")));
|
||||
QDomElement s = document.createElement(QStringLiteral("sequentialNumbers"));
|
||||
QDomElement u = document.createElement(QStringLiteral("unit"));
|
||||
u.appendChild(document.createTextNode(unit));
|
||||
s.appendChild(u);
|
||||
item.appendChild(s);
|
||||
};
|
||||
|
||||
property(document.documentElement(), QStringLiteral("site"), QStringLiteral("S"));
|
||||
const QDomNodeList diagrams = document.elementsByTagName(QStringLiteral("diagram"));
|
||||
QCOMPARE(diagrams.size(), 3);
|
||||
for (int i = 0 ; i < diagrams.size() ; ++i)
|
||||
{
|
||||
QDomElement diagram = diagrams.at(i).toElement();
|
||||
diagram.setAttribute(QStringLiteral("folio"), QStringLiteral("F%id"));
|
||||
diagram.setAttribute(QStringLiteral("plant"), QStringLiteral("P"));
|
||||
diagram.setAttribute(QStringLiteral("locmach"), QStringLiteral("L"));
|
||||
property(diagram, QStringLiteral("zone"), QStringLiteral("Z%1").arg(i));
|
||||
|
||||
QDomElement element = diagram.firstChildElement(QStringLiteral("elements"))
|
||||
.firstChildElement(QStringLiteral("element"));
|
||||
QVERIFY(!element.isNull());
|
||||
element.setAttribute(QStringLiteral("prefix"), QStringLiteral("X"));
|
||||
sequence(element, QStringLiteral("7"));
|
||||
QDomElement informations = element.firstChildElement(QStringLiteral("elementInformations"));
|
||||
if (informations.isNull())
|
||||
informations = element.appendChild(document.createElement(QStringLiteral("elementInformations"))).toElement();
|
||||
while (!informations.firstChild().isNull())
|
||||
informations.removeChild(informations.firstChild());
|
||||
for (const auto &info : {std::make_pair(QStringLiteral("formula"),
|
||||
QStringLiteral("K%total-%f-%F-%M-%LM-%c%l-%prefix-%{zone}-%{site}-%sequ_1")),
|
||||
std::make_pair(QStringLiteral("label"), QStringLiteral("OLD"))}) {
|
||||
QDomElement e = document.createElement(QStringLiteral("elementInformation"));
|
||||
e.setAttribute(QStringLiteral("name"), info.first);
|
||||
e.setAttribute(QStringLiteral("show"), QStringLiteral("1"));
|
||||
e.appendChild(document.createTextNode(info.second));
|
||||
informations.appendChild(e);
|
||||
}
|
||||
|
||||
QDomElement conductor = diagram.firstChildElement(QStringLiteral("conductors"))
|
||||
.firstChildElement(QStringLiteral("conductor"));
|
||||
QVERIFY(!conductor.isNull());
|
||||
conductor.setAttribute(QStringLiteral("formula"), QStringLiteral("W%total-%id-%wf-%{zone}-%sequ_1"));
|
||||
conductor.setAttribute(QStringLiteral("function"), QStringLiteral("N"));
|
||||
conductor.setAttribute(QStringLiteral("num"), QStringLiteral("OLD"));
|
||||
sequence(conductor, QStringLiteral("3"));
|
||||
}
|
||||
QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate));
|
||||
file.write(document.toByteArray());
|
||||
file.close();
|
||||
|
||||
compareBothWays(saved);
|
||||
// ...and they were worked out, not left as saved
|
||||
const QString info = QString::fromUtf8(QJsonDocument(m_last.value(QStringLiteral("element_info")).toArray())
|
||||
.toJson(QJsonDocument::Compact));
|
||||
const QString wires = QString::fromUtf8(QJsonDocument(m_last.value(QStringLiteral("conductor")).toArray())
|
||||
.toJson(QJsonDocument::Compact));
|
||||
QVERIFY2(info.contains(QStringLiteral("K3-1-F1-P-L-")), qPrintable(info.left(400)));
|
||||
QVERIFY2(info.contains(QStringLiteral("-X-Z0-S-7")), qPrintable(info.left(400)));
|
||||
QVERIFY2(wires.contains(QStringLiteral("W3-3-N-Z2-3")), qPrintable(wires.left(400)));
|
||||
QVERIFY(!info.contains(QStringLiteral("OLD")));
|
||||
QVERIFY(!wires.contains(QStringLiteral("OLD")));
|
||||
}
|
||||
|
||||
// An element the folio does not build, and so the conductors ending on
|
||||
// it, are left out the same way.
|
||||
void unbuiltElementIsLeftOut()
|
||||
{
|
||||
const QString saved = m_dir.filePath(QStringLiteral("unbuilt.qet"));
|
||||
QDomDocument document = resaved(saved);
|
||||
QDomElement conductor = document.elementsByTagName(QStringLiteral("conductor")).at(0).toElement();
|
||||
QVERIFY(!conductor.isNull());
|
||||
const QString uuid = conductor.attribute(QStringLiteral("element1"));
|
||||
const QDomNodeList elements = document.elementsByTagName(QStringLiteral("element"));
|
||||
bool found = false;
|
||||
for (int i = 0 ; i < elements.size() ; ++i) {
|
||||
QDomElement e = elements.at(i).toElement();
|
||||
if (e.attribute(QStringLiteral("uuid")) == uuid) {
|
||||
e.setAttribute(QStringLiteral("x"), QStringLiteral("nan"));
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(found);
|
||||
QVERIFY(write(saved, document));
|
||||
compareBothWays(saved);
|
||||
const QString wires = QString::fromUtf8(QJsonDocument(m_last.value(QStringLiteral("conductor")).toArray())
|
||||
.toJson(QJsonDocument::Compact));
|
||||
QVERIFY(!wires.contains(conductor.attribute(QStringLiteral("uuid"))));
|
||||
}
|
||||
|
||||
// Two elements on a folio numbering their terminals alike: which one the
|
||||
// folio then refuses depends on the terminals' geometry, so the folios
|
||||
// fill the database.
|
||||
void clashingTerminalIdsFallBack()
|
||||
{
|
||||
const QString saved = m_dir.filePath(QStringLiteral("clash.qet"));
|
||||
QDomDocument document = resaved(saved);
|
||||
const QDomElement diagram = document.elementsByTagName(QStringLiteral("diagram")).at(0).toElement();
|
||||
QDomElement first = diagram.firstChildElement(QStringLiteral("elements")).firstChildElement(QStringLiteral("element"));
|
||||
QDomElement second = first.nextSiblingElement(QStringLiteral("element"));
|
||||
const QString id = first.firstChildElement(QStringLiteral("terminals"))
|
||||
.firstChildElement(QStringLiteral("terminal")).attribute(QStringLiteral("id"));
|
||||
QDomElement terminal = second.firstChildElement(QStringLiteral("terminals"))
|
||||
.firstChildElement(QStringLiteral("terminal"));
|
||||
QVERIFY(!id.isEmpty() && !terminal.isNull());
|
||||
terminal.setAttribute(QStringLiteral("id"), id);
|
||||
QVERIFY(write(saved, document));
|
||||
QString how;
|
||||
dump(saved, false, &how);
|
||||
QCOMPARE(how, QStringLiteral("Project database filled from the folios: "
|
||||
"two elements on a folio number their terminals alike"));
|
||||
}
|
||||
|
||||
// A file whose items carry no saved uuid is filled from the folios,
|
||||
// which work those uuids out as they are built, and says why.
|
||||
void olderFileFallsBack()
|
||||
{
|
||||
QString how;
|
||||
dump(QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), false, &how);
|
||||
QCOMPARE(how, QStringLiteral("Project database filled from the folios: a folio has no saved uuid"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_databasefromdocument)
|
||||
|
||||
#include "tst_databasefromdocument.moc"
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// The project database holds each folio's own date. It used to hold the
|
||||
// title block's text read back through the locale's short format, which in a
|
||||
// locale with a two-digit year (en_US "M/d/yy") turned 2010 into 1910 -- in
|
||||
// diagram_info and in project_summary_view, which the summary table reads.
|
||||
// Runs a script through the real binary's --run on examples/741.qet, whose
|
||||
// folio is dated 20100921, with LC_ALL=en_US.UTF-8.
|
||||
class tst_dbfoliodate : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void folioDateKeepsItsCentury()
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write("qet.log('PROBE ' + JSON.stringify("
|
||||
"qet.query('SELECT date FROM project_summary_view')));\n");
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
env.insert(QStringLiteral("LC_ALL"), QStringLiteral("en_US.UTF-8"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path,
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/741.qet")});
|
||||
QVERIFY(proc.waitForFinished(120000));
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
QJsonArray rows;
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(QStringLiteral("PROBE "));
|
||||
if (i >= 0)
|
||||
rows = QJsonDocument::fromJson(line.mid(i + 6).toUtf8()).array();
|
||||
}
|
||||
QCOMPARE(rows.size(), 1);
|
||||
QCOMPARE(rows.at(0).toObject().value(QStringLiteral("date")).toString(),
|
||||
QStringLiteral("2010-09-21"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_dbfoliodate)
|
||||
|
||||
#include "tst_dbfoliodate.moc"
|
||||
@@ -0,0 +1,157 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QFile>
|
||||
#include <QDomDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
|
||||
// qet.elementTerminals() ends each line with the terminal's uuid, and
|
||||
// qet.terminalIndex(folio, element, terminalUuid) turns it back into the
|
||||
// index the conductor calls take. Runs a script through the real binary's
|
||||
// --run on perceuse.qet, which has symbols with two terminals at one point:
|
||||
// their order in the index is undefined, their uuids are not.
|
||||
class tst_scriptterminaluuid : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Run @p script on @p project in a sandbox of its own and return the
|
||||
// JSON object it logged.
|
||||
QJsonObject run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, project});
|
||||
if (!proc.waitForFinished(120000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void everyTerminalFoundByItsUuid()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var rows = [], pick = null;\n"
|
||||
"for (var f = 0; f < qet.folioCount(); f++) {\n"
|
||||
" var els = qet.elementUuids(f);\n"
|
||||
" for (var e = 0; e < els.length; e++) {\n"
|
||||
" var lines = qet.elementTerminals(f, els[e]);\n"
|
||||
" var back = lines.map(function (l) {\n"
|
||||
" return qet.terminalIndex(f, els[e], l.substring(l.lastIndexOf(' ') + 1)); });\n"
|
||||
" rows.push({lines: lines, back: back});\n"
|
||||
" if (!pick && lines.length) pick = {f: f, e: els[e], t: lines[0].split(' ').pop()};\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({rows: rows,\n"
|
||||
" unknown: qet.terminalIndex(pick.f, pick.e, '{00000000-0000-0000-0000-000000000001}'),\n"
|
||||
" junk: qet.terminalIndex(pick.f, pick.e, 'not a uuid'),\n"
|
||||
" noElement: qet.terminalIndex(pick.f, '{00000000-0000-0000-0000-000000000002}', pick.t),\n"
|
||||
" badFolio: qet.terminalIndex(99, pick.e, pick.t)}));\n"),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
|
||||
const QRegularExpression line_re(QStringLiteral(
|
||||
"^(\\d+): .* \\(\\d+ conductor\\(s\\)\\) (\\{[0-9a-f-]{36}\\})$"));
|
||||
int terminals = 0;
|
||||
for (const QJsonValue &row : r.value(QStringLiteral("rows")).toArray()) {
|
||||
const QJsonArray lines = row.toObject().value(QStringLiteral("lines")).toArray();
|
||||
const QJsonArray back = row.toObject().value(QStringLiteral("back")).toArray();
|
||||
QCOMPARE(back.size(), lines.size());
|
||||
QSet<QString> uuids;
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
const QString line = lines.at(i).toString();
|
||||
const QRegularExpressionMatch m = line_re.match(line);
|
||||
QVERIFY2(m.hasMatch(), qPrintable(line));
|
||||
QCOMPARE(m.captured(1).toInt(), i);
|
||||
QVERIFY(!QUuid(m.captured(2)).isNull());
|
||||
uuids.insert(m.captured(2));
|
||||
// the uuid finds this very terminal again
|
||||
QCOMPARE(back.at(i).toInt(), i);
|
||||
++terminals;
|
||||
}
|
||||
QCOMPARE(uuids.size(), lines.size());
|
||||
}
|
||||
QVERIFY(terminals > 200);
|
||||
|
||||
QCOMPARE(r.value(QStringLiteral("unknown")).toInt(), -1);
|
||||
QCOMPARE(r.value(QStringLiteral("junk")).toInt(), -1);
|
||||
QCOMPARE(r.value(QStringLiteral("noElement")).toInt(), -1);
|
||||
QCOMPARE(r.value(QStringLiteral("badFolio")).toInt(), -1);
|
||||
}
|
||||
|
||||
// A "%3" or "%4" in a terminal's name is listed as written, not
|
||||
// replaced by the conductor count or the uuid.
|
||||
void percentInNameKept()
|
||||
{
|
||||
QFile in(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
QVERIFY(in.open(QIODevice::ReadOnly));
|
||||
QDomDocument doc;
|
||||
QVERIFY(bool(doc.setContent(&in)));
|
||||
const QDomNodeList terminals = doc.documentElement()
|
||||
.firstChildElement(QStringLiteral("collection"))
|
||||
.elementsByTagName(QStringLiteral("terminal"));
|
||||
QVERIFY(!terminals.isEmpty());
|
||||
terminals.at(0).toElement().setAttribute(QStringLiteral("name"),
|
||||
QStringLiteral("x%3y%4"));
|
||||
const QString project = m_dir.filePath(QStringLiteral("percent.qet"));
|
||||
QFile out(project);
|
||||
QVERIFY(out.open(QIODevice::WriteOnly));
|
||||
out.write(doc.toByteArray());
|
||||
out.close();
|
||||
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var hits = [];\n"
|
||||
"for (var f = 0; f < qet.folioCount(); f++) {\n"
|
||||
" var els = qet.elementUuids(f);\n"
|
||||
" for (var e = 0; e < els.length; e++)\n"
|
||||
" qet.elementTerminals(f, els[e]).forEach(function (l) {\n"
|
||||
" if (/: x.*y.* \\(/.test(l)) hits.push(l); });\n"
|
||||
"}\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({hits: hits}));\n"), project);
|
||||
const QJsonArray hits = r.value(QStringLiteral("hits")).toArray();
|
||||
QVERIFY2(!hits.isEmpty(), "the renamed terminal was not listed");
|
||||
for (const QJsonValue &h : hits)
|
||||
QVERIFY2(h.toString().contains(QStringLiteral(": x%3y%4 (")), qPrintable(h.toString()));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_scriptterminaluuid)
|
||||
|
||||
#include "tst_scriptterminaluuid.moc"
|
||||
@@ -0,0 +1,426 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "../../sources/ElementsCollection/terminaluuids.h"
|
||||
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
|
||||
// Replacing a symbol's definition in a project must not cost the wires on
|
||||
// it. A wire is saved against the uuids of the terminals it joins and is
|
||||
// reattached on load to a terminal with that uuid, or not at all; the
|
||||
// replacement is another copy of the symbol, whose terminals usually carry
|
||||
// other uuids. TerminalUuids::keep() carries the old uuids over.
|
||||
namespace {
|
||||
|
||||
QDomElement symbol(QDomDocument &doc, const QList<QStringList> &terminals)
|
||||
{
|
||||
QDomElement element = doc.createElement(QStringLiteral("element"));
|
||||
QDomElement definition = doc.createElement(QStringLiteral("definition"));
|
||||
QDomElement description = doc.createElement(QStringLiteral("description"));
|
||||
element.appendChild(definition);
|
||||
definition.appendChild(description);
|
||||
for (const QStringList &t : terminals) {
|
||||
QDomElement terminal = doc.createElement(QStringLiteral("terminal"));
|
||||
terminal.setAttribute(QStringLiteral("x"), t.at(0));
|
||||
terminal.setAttribute(QStringLiteral("y"), t.at(1));
|
||||
terminal.setAttribute(QStringLiteral("orientation"), t.at(2));
|
||||
if (t.size() > 3)
|
||||
terminal.setAttribute(QStringLiteral("uuid"), t.at(3));
|
||||
description.appendChild(terminal);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
QStringList uuids(const QDomElement &element)
|
||||
{
|
||||
QStringList out;
|
||||
const QDomNodeList nodes = element.elementsByTagName(QStringLiteral("terminal"));
|
||||
for (int i = 0; i < nodes.size(); ++i)
|
||||
out << nodes.at(i).toElement().attribute(QStringLiteral("uuid"));
|
||||
return out;
|
||||
}
|
||||
|
||||
QList<QDomElement> embeddedSymbols(const QDomDocument &doc)
|
||||
{
|
||||
QList<QDomElement> out;
|
||||
const QDomNodeList nodes = doc.documentElement()
|
||||
.firstChildElement(QStringLiteral("collection"))
|
||||
.elementsByTagName(QStringLiteral("element"));
|
||||
for (int i = 0; i < nodes.size(); ++i)
|
||||
out << nodes.at(i).toElement();
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class tst_terminaluuids : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
// The real binary with @p args, in a sandbox of its own (so a running
|
||||
// QElectroTech cannot answer instead); false if it failed.
|
||||
bool runQet(const QStringList &args, QByteArray *out, QString *log)
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), args);
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
||||
return false;
|
||||
*log = QString::fromUtf8(proc.readAllStandardError());
|
||||
*out = proc.readAllStandardOutput();
|
||||
return true;
|
||||
}
|
||||
|
||||
// --info on @p project; returns the number of wires loaded and puts
|
||||
// what was written on stderr in @p log.
|
||||
int loadedWires(const QString &project, QString *log)
|
||||
{
|
||||
QByteArray out;
|
||||
if (!runQet({QStringLiteral("--info"), project}, &out, log))
|
||||
return -1;
|
||||
const QJsonDocument json = QJsonDocument::fromJson(out.mid(out.indexOf('{')));
|
||||
return json.object().value(QStringLiteral("conductors")).toInt(-1);
|
||||
}
|
||||
|
||||
// --resave @p in to a new file; returns its path, empty on failure.
|
||||
QString resave(const QString &in)
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("resaved%1.qet").arg(m_run));
|
||||
QByteArray stdout_;
|
||||
QString log;
|
||||
if (!runQet({QStringLiteral("--resave"), in, out}, &stdout_, &log))
|
||||
return {};
|
||||
return out;
|
||||
}
|
||||
|
||||
static QDomDocument load(const QString &path)
|
||||
{
|
||||
QDomDocument doc;
|
||||
QFile file(path);
|
||||
if (file.open(QIODevice::ReadOnly))
|
||||
doc.setContent(&file);
|
||||
return doc;
|
||||
}
|
||||
|
||||
static QByteArray bytes(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
// 2612_ats_singlephase.qet with each embedded symbol replaced by a copy
|
||||
// whose terminals carry new uuids, passed through keep() or not.
|
||||
QString replacedSymbols(bool keep_uuids)
|
||||
{
|
||||
QFile in(QStringLiteral(QET_EXAMPLES_DIR "/2612_ats_singlephase.qet"));
|
||||
if (!in.open(QIODevice::ReadOnly)) return {};
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(&in)) return {};
|
||||
for (const QDomElement &old_symbol : embeddedSymbols(doc)) {
|
||||
QDomElement new_symbol = old_symbol.cloneNode().toElement();
|
||||
const QDomNodeList terminals = new_symbol.elementsByTagName(QStringLiteral("terminal"));
|
||||
for (int i = 0; i < terminals.size(); ++i)
|
||||
terminals.at(i).toElement().setAttribute(QStringLiteral("uuid"),
|
||||
QUuid::createUuid().toString());
|
||||
if (keep_uuids)
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
old_symbol.parentNode().replaceChild(new_symbol, old_symbol);
|
||||
}
|
||||
const QString path = m_dir.filePath(keep_uuids ? QStringLiteral("kept.qet")
|
||||
: QStringLiteral("lost.qet"));
|
||||
QFile out(path);
|
||||
if (!out.open(QIODevice::WriteOnly)) return {};
|
||||
out.write(doc.toByteArray());
|
||||
return path;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void samePlaceTakesOldUuid()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n", "{a}"},
|
||||
{"20", "10", "s", "{b}"}});
|
||||
QDomElement new_symbol = symbol(doc, {{"20.0", "10", "s", "{y}"},
|
||||
{"0", "10.0", "n", "{x}"}});
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
QCOMPARE(uuids(new_symbol), (QStringList{"{b}", "{a}"}));
|
||||
}
|
||||
|
||||
void movedOrNewTerminalKeepsItsOwn()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n", "{a}"}});
|
||||
QDomElement new_symbol = symbol(doc, {{"0", "10", "e", "{x}"},
|
||||
{"0", "20", "n", "{y}"},
|
||||
{"0", "30", "n"}});
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
QCOMPARE(uuids(new_symbol), (QStringList{"{x}", "{y}", ""}));
|
||||
}
|
||||
|
||||
void oldWithoutUuidChangesNothing()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n"}});
|
||||
QDomElement new_symbol = symbol(doc, {{"0", "10", "n", "{x}"}});
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
QCOMPARE(uuids(new_symbol), (QStringList{"{x}"}));
|
||||
}
|
||||
|
||||
void twoAtOnePlaceMatchedInOrder()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QDomElement old_symbol = symbol(doc, {{"0", "10", "n", "{a}"},
|
||||
{"0", "10", "n", "{b}"}});
|
||||
QDomElement new_symbol = symbol(doc, {{"0", "10", "n", "{x}"},
|
||||
{"0", "10", "n", "{y}"},
|
||||
{"0", "10", "n", "{z}"}});
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
QCOMPARE(uuids(new_symbol), (QStringList{"{a}", "{b}", "{z}"}));
|
||||
}
|
||||
|
||||
// The new revision moved terminal {a} and put a new one where it was:
|
||||
// {a} is still {a}, and the new one is not given {a} a second time.
|
||||
void noUuidGivenTwice()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QDomElement old_symbol = symbol(doc, {{"0", "0", "n", "{a}"}});
|
||||
QDomElement new_symbol = symbol(doc, {{"0", "5", "n", "{a}"},
|
||||
{"0", "0", "n", "{c}"}});
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
QCOMPARE(uuids(new_symbol), (QStringList{"{a}", "{c}"}));
|
||||
}
|
||||
|
||||
// A terminal that already carries an old uuid is never renamed, even
|
||||
// when it moved onto the place of another old terminal.
|
||||
void movedTerminalNotRenamed()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QDomElement old_symbol = symbol(doc, {{"0", "0", "n", "{a}"},
|
||||
{"0", "5", "n", "{b}"}});
|
||||
QDomElement new_symbol = symbol(doc, {{"0", "5", "n", "{a}"},
|
||||
{"0", "9", "n", "{x}"}});
|
||||
TerminalUuids::keep(old_symbol, new_symbol);
|
||||
QCOMPARE(uuids(new_symbol), (QStringList{"{a}", "{x}"}));
|
||||
}
|
||||
|
||||
// Replacing a whole category: every symbol with a counterpart of the
|
||||
// same name, at any depth, keeps its terminal uuids.
|
||||
void keepInDirectory()
|
||||
{
|
||||
QDomDocument doc;
|
||||
auto category = [&doc](const QString &name) {
|
||||
QDomElement c = doc.createElement(QStringLiteral("category"));
|
||||
c.setAttribute(QStringLiteral("name"), name);
|
||||
return c;
|
||||
};
|
||||
auto named = [](QDomElement e, const QString &name) {
|
||||
e.setAttribute(QStringLiteral("name"), name);
|
||||
return e;
|
||||
};
|
||||
|
||||
QDomElement old_dir = category(QStringLiteral("dir"));
|
||||
QDomElement old_sub = category(QStringLiteral("sub"));
|
||||
old_dir.appendChild(named(symbol(doc, {{"0", "0", "n", "{a}"}}), "top.elmt"));
|
||||
old_dir.appendChild(old_sub);
|
||||
old_sub.appendChild(named(symbol(doc, {{"0", "0", "n", "{b}"}}), "deep.elmt"));
|
||||
|
||||
QDomElement new_dir = category(QStringLiteral("dir"));
|
||||
QDomElement new_sub = category(QStringLiteral("sub"));
|
||||
const QDomElement top = named(symbol(doc, {{"0", "0", "n", "{x}"}}), "top.elmt");
|
||||
const QDomElement other = named(symbol(doc, {{"0", "0", "n", "{y}"}}), "other.elmt");
|
||||
const QDomElement deep = named(symbol(doc, {{"0", "0", "n", "{z}"}}), "deep.elmt");
|
||||
new_dir.appendChild(top);
|
||||
new_dir.appendChild(other);
|
||||
new_dir.appendChild(new_sub);
|
||||
new_sub.appendChild(deep);
|
||||
|
||||
TerminalUuids::keepInDirectory(old_dir, new_dir);
|
||||
QCOMPARE(uuids(top), QStringList{"{a}"});
|
||||
QCOMPARE(uuids(other), QStringList{"{y}"});
|
||||
QCOMPARE(uuids(deep), QStringList{"{b}"});
|
||||
}
|
||||
|
||||
// The recipe is Terminal::stableUuid()'s, which the project database
|
||||
// and the wire uuids worked out from their ends already use: changing
|
||||
// it would change every such identity in every project.
|
||||
void derivedRecipeUnchanged()
|
||||
{
|
||||
const QUuid ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
QCOMPARE(TerminalUuids::derived(0, 10, 2),
|
||||
QUuid::createUuidV5(ns, QStringLiteral("0.0000|10.0000|2")));
|
||||
QCOMPARE(TerminalUuids::derived(-2.5, 4, 3, 1),
|
||||
QUuid::createUuidV5(ns, QStringLiteral("-2.5000|4.0000|3|1")));
|
||||
}
|
||||
|
||||
// Only terminals without a uuid get one, the derived value; two at one
|
||||
// point get distinct ones; a value already used in the symbol is never
|
||||
// given again; symbols in sub-categories are reached.
|
||||
void fillMissing()
|
||||
{
|
||||
QDomDocument doc;
|
||||
QDomElement root = doc.createElement(QStringLiteral("collection"));
|
||||
QDomElement sub = doc.createElement(QStringLiteral("category"));
|
||||
root.appendChild(sub);
|
||||
const QString taken = TerminalUuids::derived(0, 0, 0).toString();
|
||||
const QString own = QStringLiteral("{0f5d4b0c-2f7e-4a55-9a51-8c3a3e1c2d11}");
|
||||
QDomElement a = symbol(doc, {{"0", "10", "s"},
|
||||
{"5", "0", "e", own},
|
||||
{"0", "10", "s"},
|
||||
{"0", "0", "n"},
|
||||
{"9", "9", "w", taken}});
|
||||
QDomElement b = symbol(doc, {{"1", "2", "w"}});
|
||||
root.appendChild(a);
|
||||
sub.appendChild(b);
|
||||
|
||||
QCOMPARE(TerminalUuids::fillMissing(root), 4);
|
||||
QCOMPARE(uuids(a), (QStringList{
|
||||
TerminalUuids::derived(0, 10, 2).toString(),
|
||||
own,
|
||||
TerminalUuids::derived(0, 10, 2, 1).toString(),
|
||||
TerminalUuids::derived(0, 0, 0, 1).toString(),
|
||||
taken}));
|
||||
QCOMPARE(uuids(b), QStringList{TerminalUuids::derived(1, 2, 3).toString()});
|
||||
QCOMPARE(TerminalUuids::fillMissing(root), 0);
|
||||
}
|
||||
|
||||
// The element editor fills a definition read from a file with exactly
|
||||
// what a project fills the same definition with.
|
||||
void fillMissingInDefinitionMatchesProject()
|
||||
{
|
||||
QDomDocument doc;
|
||||
const QString own = QStringLiteral("{0f5d4b0c-2f7e-4a55-9a51-8c3a3e1c2d11}");
|
||||
QDomElement in_project = symbol(doc, {{"0", "10", "s"},
|
||||
{"0", "10", "s"},
|
||||
{"5", "0", "e", own},
|
||||
{"-3", "0", "w"}});
|
||||
QDomElement root = doc.createElement(QStringLiteral("collection"));
|
||||
root.appendChild(in_project);
|
||||
QDomElement definition = in_project.firstChildElement(QStringLiteral("definition"))
|
||||
.cloneNode(true).toElement();
|
||||
|
||||
QCOMPARE(TerminalUuids::fillMissingInDefinition(definition), 3);
|
||||
QCOMPARE(TerminalUuids::fillMissing(root), 3);
|
||||
QCOMPARE(uuids(definition), uuids(in_project));
|
||||
QCOMPARE(uuids(definition).at(2), own);
|
||||
QCOMPARE(uuids(definition).at(1), TerminalUuids::derived(0, 10, 2, 1).toString());
|
||||
QCOMPARE(TerminalUuids::fillMissingInDefinition(definition), 0);
|
||||
}
|
||||
|
||||
// An example whose symbols have no terminal uuids and whose wires are
|
||||
// all in the numbered form: once saved, every terminal has a uuid,
|
||||
// every wire names its ends by uuid, nothing is lost, and saving again
|
||||
// changes nothing.
|
||||
void resaveGivesEveryTerminalAUuid()
|
||||
{
|
||||
const QString original = QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet");
|
||||
QString log;
|
||||
const int wires = loadedWires(original, &log);
|
||||
QVERIFY(wires > 0);
|
||||
|
||||
const QString saved = resave(original);
|
||||
QVERIFY2(!saved.isEmpty(), "--resave failed");
|
||||
const QDomDocument doc = load(saved);
|
||||
int terminals = 0;
|
||||
for (const QDomElement &e : embeddedSymbols(doc)) {
|
||||
for (const QString &uuid : uuids(e)) {
|
||||
++terminals;
|
||||
QVERIFY2(!QUuid(uuid).isNull(), "a terminal has no uuid");
|
||||
}
|
||||
}
|
||||
QVERIFY(terminals > 0);
|
||||
const QDomNodeList conductors = doc.elementsByTagName(QStringLiteral("conductor"));
|
||||
QCOMPARE(conductors.size(), wires);
|
||||
for (int i = 0; i < conductors.size(); ++i) {
|
||||
const QDomElement c = conductors.at(i).toElement();
|
||||
QVERIFY2(c.hasAttribute(QStringLiteral("element1"))
|
||||
&& c.hasAttribute(QStringLiteral("element2")),
|
||||
"a wire is still in the numbered form");
|
||||
}
|
||||
|
||||
QCOMPARE(loadedWires(saved, &log), wires);
|
||||
QVERIFY2(!log.contains(QStringLiteral("not loaded")), "a wire was reported lost");
|
||||
const QString again = resave(saved);
|
||||
QVERIFY2(!again.isEmpty(), "second --resave failed");
|
||||
QVERIFY2(bytes(again) == bytes(saved), "the second save changed the file");
|
||||
}
|
||||
|
||||
// The uuids written on opening are derived from where each terminal
|
||||
// is: a wire saved against one still finds its terminal after the
|
||||
// symbol's definition was replaced by one with other terminal uuids.
|
||||
// perceuse.qet and industrial.qet have wires on the second of two
|
||||
// terminals at one point of a symbol, which get the next occurrence.
|
||||
void derivedUuidFoundAfterReplacement_data()
|
||||
{
|
||||
QTest::addColumn<QString>("project");
|
||||
for (const char *name : {"tremie_vibrante.qet", "perceuse.qet", "industrial.qet"})
|
||||
QTest::newRow(name) << QStringLiteral(QET_EXAMPLES_DIR "/") + QLatin1String(name);
|
||||
}
|
||||
|
||||
void derivedUuidFoundAfterReplacement()
|
||||
{
|
||||
QFETCH(QString, project);
|
||||
const QString saved = resave(project);
|
||||
QVERIFY(!saved.isEmpty());
|
||||
QString log;
|
||||
const int wires = loadedWires(saved, &log);
|
||||
QVERIFY(wires > 0);
|
||||
|
||||
QDomDocument doc = load(saved);
|
||||
for (QDomElement e : embeddedSymbols(doc)) {
|
||||
const QDomNodeList terminals = e.elementsByTagName(QStringLiteral("terminal"));
|
||||
for (int i = 0; i < terminals.size(); ++i)
|
||||
terminals.at(i).toElement().setAttribute(QStringLiteral("uuid"),
|
||||
QUuid::createUuid().toString());
|
||||
}
|
||||
const QString replaced = m_dir.filePath(QStringLiteral("replaced%1.qet").arg(m_run));
|
||||
QFile out(replaced);
|
||||
QVERIFY(out.open(QIODevice::WriteOnly));
|
||||
out.write(doc.toByteArray());
|
||||
out.close();
|
||||
|
||||
QCOMPARE(loadedWires(replaced, &log), wires);
|
||||
QVERIFY2(!log.contains(QStringLiteral("not loaded")), "a wire was reported lost");
|
||||
}
|
||||
|
||||
// The real loader, on a project whose 131 wires are saved against
|
||||
// terminal uuids: replaced symbols lose 119 of them unless keep() ran,
|
||||
// and the ones lost are reported.
|
||||
void wiresSurviveReplacedSymbols()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QString log;
|
||||
|
||||
const QString lost = replacedSymbols(false);
|
||||
QVERIFY(!lost.isEmpty());
|
||||
QCOMPARE(loadedWires(lost, &log), 12);
|
||||
QVERIFY2(log.contains(QStringLiteral("55 wire(s) not loaded"))
|
||||
&& log.contains(QStringLiteral("64 wire(s) not loaded")),
|
||||
"the lost wires were not reported");
|
||||
|
||||
const QString kept = replacedSymbols(true);
|
||||
QVERIFY(!kept.isEmpty());
|
||||
QCOMPARE(loadedWires(kept, &log), 131);
|
||||
QVERIFY2(!log.contains(QStringLiteral("not loaded")), "a wire was reported lost");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_terminaluuids)
|
||||
|
||||
#include "tst_terminaluuids.moc"
|
||||