mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 14:14:15 +02:00
Merge remote-tracking branch 'upstream/master' into feat/database-from-document
# Conflicts: # sources/qetproject.cpp
This commit is contained in:
@@ -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/"
|
||||
|
||||
@@ -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}。"
|
||||
|
||||
@@ -714,6 +714,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
|
||||
|
||||
@@ -82,6 +82,32 @@ Register it with an MCP client, for example:
|
||||
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
|
||||
|
||||
+66
-21
@@ -884,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:
|
||||
@@ -925,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)
|
||||
@@ -943,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)
|
||||
@@ -3207,12 +3240,24 @@ def _env_paths(name: str) -> list:
|
||||
return out
|
||||
|
||||
|
||||
def _installed_prefix() -> Path | None:
|
||||
"""The install prefix when this script is <prefix>/share/qelectrotech/mcp/."""
|
||||
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" and here.parent.name == "qelectrotech" \
|
||||
and here.parent.parent.name == "share":
|
||||
return here.parent.parent.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
|
||||
|
||||
|
||||
@@ -3226,10 +3271,12 @@ def resolve_binary() -> Path | None:
|
||||
env = os.environ.get("QET_BINARY", "").strip()
|
||||
if env:
|
||||
return Path(env).expanduser().resolve()
|
||||
prefix = _installed_prefix()
|
||||
if prefix is not None:
|
||||
for name in ("qelectrotech", "qelectrotech.exe"):
|
||||
cand = prefix / "bin" / name
|
||||
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")
|
||||
@@ -3238,11 +3285,9 @@ def resolve_binary() -> Path | None:
|
||||
|
||||
def default_elements_dir() -> Path | None:
|
||||
"""The element collection of the install this script ships in, if any."""
|
||||
prefix = _installed_prefix()
|
||||
if prefix is not None:
|
||||
coll = prefix / "share" / "qelectrotech" / "elements"
|
||||
if coll.is_dir():
|
||||
return coll.resolve()
|
||||
install = _installation()
|
||||
if install is not None and install[1].is_dir():
|
||||
return install[1].resolve()
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -2562,6 +2562,82 @@ class BinaryPolicy(unittest.TestCase):
|
||||
self.assertEqual(args["elements_dir"],
|
||||
str((prefix / "share" / "qelectrotech" / "elements").resolve()))
|
||||
|
||||
def installed_at(self, root: Path, mcp: Path):
|
||||
"""Import a copy of the server placed at mcp/qet_mcp.py."""
|
||||
import importlib.util
|
||||
mcp.mkdir(parents=True)
|
||||
shutil.copy2(HERE / "qet_mcp.py", mcp / "qet_mcp.py")
|
||||
spec = importlib.util.spec_from_file_location(f"qet_mcp_{root.name}", mcp / "qet_mcp.py")
|
||||
inst = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(inst)
|
||||
return inst
|
||||
|
||||
def test_the_windows_layout_is_found_too(self):
|
||||
"""The Windows installers and the portable folder put the server in
|
||||
<root>/mcp, the program in <root>/bin as QElectroTech.exe, and the
|
||||
collection in <root>/elements."""
|
||||
root = Path(self.tmp.name) / "QElectroTech"
|
||||
(root / "elements").mkdir(parents=True)
|
||||
exe = fake_qet(root / "bin", "QElectroTech.exe")
|
||||
inst = self.installed_at(root, root / "mcp")
|
||||
del os.environ["QET_BINARY"]
|
||||
args = {"project": str(self.root / "ok.qet")}
|
||||
inst.enforce_path_policy("qet_query", args)
|
||||
self.assertEqual(args["binary"], str(exe.resolve()))
|
||||
self.assertEqual(args["elements_dir"], str((root / "elements").resolve()))
|
||||
|
||||
def test_a_copy_saved_anywhere_is_not_an_install(self):
|
||||
"""A downloaded copy sitting beside some bin/ folder must not go
|
||||
looking for QElectroTech there."""
|
||||
home = Path(self.tmp.name) / "home"
|
||||
fake_qet(home / "bin")
|
||||
inst = self.installed_at(home, home / "Downloads")
|
||||
self.assertIsNone(inst._installation())
|
||||
|
||||
def test_a_folder_named_mcp_is_not_an_install(self):
|
||||
"""Without a bin/ beside it, an mcp/ folder is just a folder."""
|
||||
inst = self.installed_at(Path(self.tmp.name) / "x", Path(self.tmp.name) / "x" / "mcp")
|
||||
self.assertIsNone(inst._installation())
|
||||
|
||||
|
||||
class LaunchExecutable(unittest.TestCase):
|
||||
"""Windows cannot run a lone copy of QElectroTech (F065): its DLLs sit
|
||||
beside the original. Everywhere else the private copy stays."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
base = Path(self.tmp.name)
|
||||
self.src = base / "bin" / "qelectrotech"
|
||||
self.src.parent.mkdir()
|
||||
self.src.write_text("#!/bin/sh\nexit 0\n")
|
||||
self.sandbox = base / "sandbox"
|
||||
self.sandbox.mkdir()
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_windows_runs_the_original(self):
|
||||
self.assertEqual(m._launch_executable(self.src, self.sandbox, True), self.src)
|
||||
self.assertEqual(list(self.sandbox.iterdir()), [], "nothing is copied on Windows")
|
||||
|
||||
def test_windows_keeps_its_own_qt_platform(self):
|
||||
"""The Windows packages have no offscreen plugin; asking for it
|
||||
leaves QElectroTech stuck at a message box."""
|
||||
env = m._launch_env({"PATH": "x"}, self.sandbox, True)
|
||||
self.assertNotIn("QT_QPA_PLATFORM", env)
|
||||
self.assertEqual(env["PATH"], "x")
|
||||
|
||||
def test_elsewhere_runs_offscreen(self):
|
||||
env = m._launch_env({}, self.sandbox, False)
|
||||
self.assertEqual(env["QT_QPA_PLATFORM"], "offscreen")
|
||||
self.assertEqual(env["HOME"], str(self.sandbox))
|
||||
|
||||
def test_elsewhere_runs_a_private_copy(self):
|
||||
exe = m._launch_executable(self.src, self.sandbox, False)
|
||||
self.assertEqual(exe.parent, self.sandbox)
|
||||
self.assertNotEqual(exe, self.src)
|
||||
self.assertEqual(exe.read_text(), self.src.read_text())
|
||||
|
||||
|
||||
class ScriptingDisabledHint(unittest.TestCase):
|
||||
"""QElectroTech may refuse to run scripts at all, and says so in French.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -2483,7 +2483,7 @@ 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()).baseName());
|
||||
project_wide_properties.addValue("projectfilename", QFileInfo(filePath()).completeBaseName());
|
||||
return project_wide_properties;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user