diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 5e07429a14a..2bd399e3bc3 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -150,6 +150,53 @@ jobs: python -c "import ansys.aedt.core; from ansys.aedt.core import __version__" + + build-application-windows: + name: "Build SAM Bot - Windows" + runs-on: windows-latest + needs: [pr-title] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Install Dependencies + run: pip install .[freeze] + + - name: Extract version + run: | + python src/ansys/aedt/core/extensions/pyaedt_bot/installer/extract_version.py + + - name: Freeze application + run: pyinstaller src/ansys/aedt/core/extensions/pyaedt_bot/installer/frozen.spec + + - name: Install NSIS + run: choco install nsis -y + + - name: Run NSIS + shell: pwsh + if: always() + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + $setupPath = "src/ansys/aedt/core/extensions/pyaedt_bot/installer/setup.nsi" + + if (!(Test-Path -Path $setupPath)) { + Write-Error "NSIS script not found at $setupPath" + } + & makensis $setupPath + +# - name: List output +# run: ls -R dist + + - uses: actions/upload-artifact@v4 + with: + name: SAM-Bot-Installer-windows + path: dist/pyaedt_bot/SAM-Bot-Installer-windows.exe + unit-tests: name: Running unit tests needs: [smoke-tests] diff --git a/codecov.yml b/codecov.yml index 05ba2e94749..4fcf6777674 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,6 +4,7 @@ ignore: - "src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_utils.py" - "src/ansys/aedt/core/extensions/installer" - "src/ansys/aedt/core/extensions/templates" + - "src/ansys/aedt/core/extensions/pyaedt_bot" - "src/ansys/aedt/core/common_rpc.py" - "src/ansys/aedt/core/internal/grpc_plugin_dll_class.py" - "src/ansys/aedt/core/edb.py" diff --git a/doc/changelog.d/6145.added.md b/doc/changelog.d/6145.added.md new file mode 100644 index 00000000000..395269c030d --- /dev/null +++ b/doc/changelog.d/6145.added.md @@ -0,0 +1 @@ +SAM Bot \ No newline at end of file diff --git a/doc/source/Resources/pyaedt_installer_from_aedt.py b/doc/source/Resources/pyaedt_installer_from_aedt.py index b4eb277dfdb..e80df1f483b 100644 --- a/doc/source/Resources/pyaedt_installer_from_aedt.py +++ b/doc/source/Resources/pyaedt_installer_from_aedt.py @@ -37,7 +37,6 @@ is_linux = os.name == "posix" is_windows = not is_linux - VENV_DIR_PREFIX = ".pyaedt_env" """ @@ -52,7 +51,6 @@ else: VENV_DIR = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX) - DISCLAIMER = ( "This script will download and install certain third-party software and/or " "open-source software (collectively, 'Third-Party Software'). Such Third-Party " @@ -274,9 +272,11 @@ def install_pyaedt(): subprocess.run([str(python_exe), "-m", "pip", "install", "--upgrade", "pip"], check=True) # nosec subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "wheel"], check=True) # nosec if args.version <= "231": - subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]=='0.9.0'"], check=True) # nosec + subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]=='0.9.0'"], + check=True) # nosec subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "jupyterlab"], check=True) # nosec - subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipython", "-U"], check=True) # nosec + subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipython", "-U"], + check=True) # nosec subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipyvtklink"], check=True) # nosec else: subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]"], check=True) # nosec @@ -308,7 +308,8 @@ def install_pyaedt(): if args.version <= "231": subprocess.run([str(pip_exe), "pip=1000", "install", "pyaedt[all]=='0.9.0'"], check=True) # nosec subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "jupyterlab"], check=True) # nosec - subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipython", "-U"], check=True) # nosec + subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipython", "-U"], + check=True) # nosec subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "ipyvtklink"], check=True) # nosec else: subprocess.run([str(pip_exe), "--default-timeout=1000", "install", "pyaedt[all]"], check=True) # nosec diff --git a/doc/source/User_guide/extensions.rst b/doc/source/User_guide/extensions.rst index e989e0d8352..c6d2b1d09a5 100644 --- a/doc/source/User_guide/extensions.rst +++ b/doc/source/User_guide/extensions.rst @@ -13,6 +13,7 @@ The following sections provide further clarification. You can launch extensions in standalone mode from the console or a Python script. + Pre-installed extensions ------------------------ @@ -151,15 +152,6 @@ They are small automated workflows with a simple GUI. Shielding effectiveness automated workflow HFSS. - - .. grid-item-card:: Point cloud generator - :link: pyaedt_extensions_doc/hfss/point_cloud_generator - :link-type: doc - :margin: 2 2 0 0 - - Generate object and surface conforming point clouds. - - .. grid-item-card:: Move it :link: pyaedt_extensions_doc/hfss/move_it :link-type: doc @@ -200,6 +192,14 @@ They are small automated workflows with a simple GUI. Import different schematic files (ACS, SP, CIR, QCV) into Circuit. + .. grid-item-card:: Circuit configuration + :link: pyaedt_extensions_doc/circuit/circuit_configuration + :link-type: doc + :margin: 2 2 0 0 + + Apply simulation configuration to a Circuit design. + + Twin Builder extensions ~~~~~~~~~~~~~~~~~~~~~~~ @@ -330,3 +330,45 @@ The Python script requires a common initial part to define the port and the vers aedtapp.modeler.create_sphere([0, 0, 0], 20) app.release_desktop(False, False) + + +SAM Bot +------- + +SAM Bot (Smart AEDT Manager) is a standalone utility that provides a floating GUI for launching PyAEDT-based automation scripts quickly and easily. +It enables a drag-and-drop experience and a right-click context menu for executing extensions defined in a TOML configuration file. + +**Key features:** + +- Launch PyAEDT extensions directly without opening AEDT manually. +- Use a TOML file to define and organize multiple automation scripts. +- Drag the bot across the screen or dock it in a convenient location. +- Access the context menu by right-clicking on the bot icon. + +.. image:: ../_static/sam_bot_example.png + :width: 300 + :alt: SAM Bot Interface + +**Installation:** + +You can download and install SAM Bot by visiting the following link: + +`Download SAM Bot `_ + +After downloading the `.exe` file, you can run it directly. +To customize the configuration, set the `PYAEDT_BOT_CONFIG` environment variable to the path of your TOML file. + +**Example TOML file:** + +.. code:: toml + + interpreter = "Your_Python_Interpreter" + + [Project] + extension_1 = { name = "Import Nastran/STL", script = "project/import_nastran.py" } + + [NewSection] + extension_1 = { name = "Custom extension", script = "custom_script.py" } + extension_2 = { name = "Choke Designer", script = "hfss/choke_designer.py" } + +**Note:** SAM Bot is compatible with Python 3.10 and is typically used with a virtual environment managed by PyAEDT. diff --git a/doc/source/User_guide/pyaedt_extensions_doc/circuit/index.rst b/doc/source/User_guide/pyaedt_extensions_doc/circuit/index.rst index 8195221c5d3..3f58bebd911 100644 --- a/doc/source/User_guide/pyaedt_extensions_doc/circuit/index.rst +++ b/doc/source/User_guide/pyaedt_extensions_doc/circuit/index.rst @@ -9,3 +9,11 @@ Circuit extensions :margin: 2 2 0 0 Import different schematic files (ACS, SP, CIR, QCV) into Circuit. + + + .. grid-item-card:: Circuit configuration + :link: circuit_configuration + :link-type: doc + :margin: 2 2 0 0 + + Apply simulation configuration to a Circuit design. diff --git a/doc/source/User_guide/pyaedt_extensions_doc/hfss/index.rst b/doc/source/User_guide/pyaedt_extensions_doc/hfss/index.rst index 7a9d9bbfb86..54d9f8845ae 100644 --- a/doc/source/User_guide/pyaedt_extensions_doc/hfss/index.rst +++ b/doc/source/User_guide/pyaedt_extensions_doc/hfss/index.rst @@ -17,7 +17,6 @@ HFSS extensions Edit a source from file data in HFSS. - .. grid-item-card:: Shielding effectiveness :link: shielding :link-type: doc @@ -25,7 +24,6 @@ HFSS extensions Shielding effectiveness workflow in HFSS. - .. grid-item-card:: Move it :link: move_it :link-type: doc diff --git a/doc/source/_static/sam_bot_example.png b/doc/source/_static/sam_bot_example.png new file mode 100644 index 00000000000..8adbebbab7d Binary files /dev/null and b/doc/source/_static/sam_bot_example.png differ diff --git a/doc/styles/config/vocabularies/ANSYS/accept.txt b/doc/styles/config/vocabularies/ANSYS/accept.txt index 4817b2a5578..68797d4f5e3 100644 --- a/doc/styles/config/vocabularies/ANSYS/accept.txt +++ b/doc/styles/config/vocabularies/ANSYS/accept.txt @@ -21,6 +21,7 @@ COM interface [Cc]omponents Conda CPython +datas DesignXploration docstring [Dd]ocstrings diff --git a/pyproject.toml b/pyproject.toml index ff639ce718c..ceb3f26cadf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,10 @@ examples = [ "plotly>=6.0,<6.1", "scikit-rf>=0.30.0,<1.8", ] +freeze = [ + "pyaedt[all]", + "pyinstaller" +] [tool.setuptools.dynamic] version = {attr = "ansys.aedt.core.__version__"} diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/assets/bot.ico b/src/ansys/aedt/core/extensions/pyaedt_bot/assets/bot.ico new file mode 100644 index 00000000000..a3db9f2f06a Binary files /dev/null and b/src/ansys/aedt/core/extensions/pyaedt_bot/assets/bot.ico differ diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/assets/bot.png b/src/ansys/aedt/core/extensions/pyaedt_bot/assets/bot.png new file mode 100644 index 00000000000..62815904b90 Binary files /dev/null and b/src/ansys/aedt/core/extensions/pyaedt_bot/assets/bot.png differ diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/assets/config.toml b/src/ansys/aedt/core/extensions/pyaedt_bot/assets/config.toml new file mode 100644 index 00000000000..b4e41fa1c38 --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/assets/config.toml @@ -0,0 +1,46 @@ +interpreter = "" + +[Project] +extension_1 = { name = "Import Nastran/STL", script = "project/import_nastran.py" } +extension_2 = { name = "Generate report", script = "circuit/create_report.py" } +extension_3 = { name = "Configure layout", script = "circuit/configure_edb.py" } +extension_4 = { name = "Advanced Fields Calculator", script = "circuit/advanced_fields_calculator.py" } +extension_5 = { name = "Point Cloud Generator", script = "circuit/points_cloud.py" } + +[Circuit] +extension_1 = { name = "Import Schematic", script = "circuit/import_schematic.py" } +extension_2 = { name = "Circuit Configuration", script = "circuit/circuit_configuration.py" } + +[HFSS] +extension_1 = { name = "Choke Designer", script = "hfss/choke_designer.py" } +extension_2 = { name = "Shielding Effectiveness", script = "hfss/shielding_effectiveness.py" } +extension_3 = { name = "Move IT", script = "hfss/move_it.py" } +extension_4 = { name = "Push from Transient", script = "hfss/push_excitation_from_file.py" } + +[HFSS3DLayout] +extension_1 = { name = "Export to 3D", script = "hfss3dlayout/export_to_3d.py" } +extension_2 = { name = "Push from Transient", script = "hfss3dlayout/push_excitation_from_file_3dl.py" } +extension_3 = { name = "Export Layout info", script = "hfss3dlayout/export_layout.py" } +extension_4 = { name = "Advanced Cutout", script = "hfss3dlayout/export_layout.py" } +extension_5 = { name = "Parametrize Layout", script = "hfss3dlayout/parametrize_edb.py" } +extension_6 = { name = "Arbitrary Waveports", script = "hfss3dlayout/generate_arbitrary_wave_ports.py" } +extension_7 = { name = "Via merging", script = "hfss3dlayout/via_clustering_extension.py" } +extension_8 = { name = "Post Layout Design", script = "hfss3dlayout/post_layout_design_toolkit.py" } + +[Maxwell3D] +extension_1 = { name = "Fields distribution", script = "maxwell3d/fields_distribution.py" } + +[Maxwell2D] +extension_1 = { name = "Fields distribution", script = "maxwell3d/fields_distribution.py" } + +[Icepak] +extension_1 = { name = "Create Power map", script = "icepak/power_map_from_csv.py" } + +[TwinBuilder] +extension_1 = { name = "Export to Circuit", script = "twinbuilder/convert_to_circuit.py" } + +[EMIT] + +[Q3D] + +[Q2D] diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/installer/extract_version.py b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/extract_version.py new file mode 100644 index 00000000000..b58c2e27a0d --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/extract_version.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import re + +try: + THIS_PATH = os.path.dirname(__file__) +except NameError: + THIS_PATH = os.getcwd() + +with open("src/ansys/aedt/core/__init__.py", "r") as f: + content = f.read() + +match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) +if match: + with open(os.path.join(THIS_PATH, "VERSION"), "w") as v: + v.write(match.group(1)) diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/installer/frozen.spec b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/frozen.spec new file mode 100644 index 00000000000..6c87f31bfc3 --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/frozen.spec @@ -0,0 +1,79 @@ +import glob +import os +import sys +from PyInstaller.utils.hooks import collect_all, collect_submodules, copy_metadata +from PyInstaller import __main__ + +block_cipher = None + +# Path where this script is located +try: + THIS_PATH = os.path.dirname(__file__) +except NameError: + THIS_PATH = os.getcwd() + +OUT_PATH = 'pyaedt_bot' +APP_NAME = 'SAM Bot' + +# Update paths based on the folder structure +CODE_PATH = os.path.join(THIS_PATH, "src", "ansys", "aedt", "core", "extensions", "pyaedt_bot") +INSTALLER_PATH = os.path.join(CODE_PATH, 'installer') +ASSETS_PATH = os.path.join(CODE_PATH, 'assets') +ICON_FILE = os.path.join(ASSETS_PATH, 'bot.ico') +HOOKS_DIR = os.path.join(INSTALLER_PATH, 'hooks') + +# Path to the main entry file (pyaedt_bot.py) +main_py = os.path.join(CODE_PATH, 'pyaedt_bot.py') + +if not os.path.isfile(main_py): + raise FileNotFoundError(f'Unable to locate main entrypoint at {main_py}') + +# Additional data files to include +added_files = [ + (os.path.join(ASSETS_PATH, 'bot.png'), 'assets'), + (os.path.join(ASSETS_PATH, 'bot.ico'), 'assets'), + (os.path.join(ASSETS_PATH, 'config.toml'), 'assets'), + (os.path.join(INSTALLER_PATH, 'VERSION'), '.'), +] + +# Copy required package metadata +added_files += copy_metadata('ansys-tools-visualization_interface') + +# PyInstaller analysis +a = Analysis([main_py], + pathex=[], + binaries=[], + datas=added_files, + hiddenimports=[], + hookspath=[HOOKS_DIR], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) + +# Create the Python archive +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +# Create the executable +exe = EXE(pyz, + a.scripts, + [], + exclude_binaries=True, + name=APP_NAME, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + icon=ICON_FILE) + +# Collect all necessary files into the final output folder +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name=OUT_PATH) diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/installer/hooks/hook-ansys.aedt.core.py b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/hooks/hook-ansys.aedt.core.py new file mode 100644 index 00000000000..c5e59b983f1 --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/hooks/hook-ansys.aedt.core.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files("ansys.aedt.core") diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/installer/hooks/hook-ansys.tools.visualization_interface.py b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/hooks/hook-ansys.tools.visualization_interface.py new file mode 100644 index 00000000000..93df05cbe6b --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/hooks/hook-ansys.tools.visualization_interface.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from PyInstaller.utils.hooks import collect_all + +datas, binaries, hiddenimports = collect_all("ansys.tools.visualization_interface") diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/installer/setup.nsi b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/setup.nsi new file mode 100644 index 00000000000..639fba7285d --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/setup.nsi @@ -0,0 +1,87 @@ +; NSIS script for SAM Bot installer + +!define PRODUCT_NAME "SAM Bot" +!define OUTFILE_NAME "SAM-Bot-Installer-windows.exe" + +; Define relative root paths from this script's location +!define ROOT_DIR "..\..\..\..\..\..\.." +!define DIST_DIR "${ROOT_DIR}\dist\pyaedt_bot" +!define ICON_FILE "${DIST_DIR}\_internal\assets\bot.ico" +!define LICENSE_FILE "${ROOT_DIR}\LICENSE" +!define VERSION_FILE "VERSION" +!define /file PRODUCT_VERSION "${VERSION_FILE}" + +Name "${PRODUCT_NAME}" +OutFile "${DIST_DIR}\${OUTFILE_NAME}" +VIProductVersion "${PRODUCT_VERSION}" + +!define MULTIUSER_EXECUTIONLEVEL Highest +!define MULTIUSER_MUI +!define MULTIUSER_INSTALLMODE_COMMANDLINE +!include MultiUser.nsh +!include MUI2.nsh +!include InstallOptions.nsh + +!define MUI_PAGE_CUSTOMFUNCTION_PRE oneclickpre +!insertmacro MULTIUSER_PAGE_INSTALLMODE +!insertmacro MUI_PAGE_LICENSE "${LICENSE_FILE}" +!insertmacro MUI_PAGE_INSTFILES +!include "uninstall.nsi" + +Function CreateDesktopShortCut + CreateShortCut "$desktop\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" +FunctionEnd + +!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" +!define MUI_FINISHPAGE_SHOWREADME +!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut" +!define MUI_FINISHPAGE_SHOWREADME_FUNCTION "CreateDesktopShortCut" +!insertmacro MUI_PAGE_FINISH + +Function .onInit + !insertmacro MULTIUSER_INIT +FunctionEnd + +Function un.onInit + !insertmacro MULTIUSER_UNINIT +FunctionEnd + +Section "${PRODUCT_NAME}" SEC01 + SetOutPath "$PROGRAMFILES64\ANSYS Inc\${PRODUCT_NAME}" + + File /r "${DIST_DIR}\*" + + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" + + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$\"$INSTDIR\${PRODUCT_NAME}.exe$\"" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "ANSYS Inc" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Version" "${PRODUCT_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${PRODUCT_VERSION}" + + WriteUninstaller "$INSTDIR\uninstall.exe" +SectionEnd + +Section "Uninstall" SEC02 + Delete "$PROGRAMFILES64\${PRODUCT_NAME}\*.*" + RMDir "$PROGRAMFILES64\${PRODUCT_NAME}" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" + RMDir "$SMPROGRAMS\${PRODUCT_NAME}" + Delete "$desktop\${PRODUCT_NAME}.lnk" +SectionEnd + +Icon "${ICON_FILE}" +InstallDir "$PROGRAMFILES64\ANSYS Inc\${PRODUCT_NAME}" + +InstProgressFlags smooth +Function oneclickpre + !insertmacro MUI_HEADER_TEXT "Installing ${PRODUCT_NAME}" "Please wait while the installation completes." + HideWindow +FunctionEnd + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_LANGUAGE English diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/installer/uninstall.nsi b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/uninstall.nsi new file mode 100644 index 00000000000..180d4895567 --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/installer/uninstall.nsi @@ -0,0 +1,42 @@ +Var DeleteUserData +Var DeleteLogs + +Section "Uninstall" + MessageBox MB_YESNO|MB_ICONQUESTION "Do you want to uninstall ${PRODUCT_NAME} ${PRODUCT_VERSION}?" IDYES checkDeleteUserData + Abort + +checkDeleteUserData: + MessageBox MB_YESNO|MB_ICONQUESTION "Delete application user data?" IDYES deleteUserData + StrCpy $DeleteUserData 0 + Goto checkDeleteLogs + +deleteUserData: + StrCpy $DeleteUserData 1 + +checkDeleteLogs: + MessageBox MB_YESNO|MB_ICONQUESTION "Delete logs?" IDYES deleteLogs + StrCpy $DeleteLogs 0 + Goto doneAsking + +deleteLogs: + StrCpy $DeleteLogs 1 + +doneAsking: + + ${If} $DeleteUserData == 1 + RMDir /r "$PROFILE\.${PRODUCT_NAME}" + ${EndIf} + + ${If} $DeleteLogs == 1 + RMDir /r "$PROFILE\.${PRODUCT_NAME}_logs" + ${EndIf} + + Delete "$INSTDIR\*.*" + RMDir /r "$INSTDIR" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" + RMDir "$SMPROGRAMS\${PRODUCT_NAME}" + Delete "$desktop\${PRODUCT_NAME}.lnk" + + MessageBox MB_OK|MB_ICONINFORMATION "${PRODUCT_NAME} has been uninstalled." +SectionEnd \ No newline at end of file diff --git a/src/ansys/aedt/core/extensions/pyaedt_bot/pyaedt_bot.py b/src/ansys/aedt/core/extensions/pyaedt_bot/pyaedt_bot.py new file mode 100644 index 00000000000..285eb26ef88 --- /dev/null +++ b/src/ansys/aedt/core/extensions/pyaedt_bot/pyaedt_bot.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +from pathlib import Path +import subprocess +import sys +import tkinter as tk +from tkinter import Menu + +from ansys.aedt.core.aedt_logger import pyaedt_logger as logger +from ansys.aedt.core.generic.file_utils import read_toml + + +def get_base_dir(): + """Determine the base directory for resources depending on whether running as PyInstaller executable or as a + script.""" + if getattr(sys, "frozen", False): + # Running from PyInstaller bundle + return Path(sys._MEIPASS) + else: + # Running from source + return Path(__file__).resolve().parent + + +VENV_DIR_PREFIX = ".pyaedt_env" + + +class PyAEDTBot(tk.Tk): + def __init__(self): + super().__init__() + + self.base_dir = get_base_dir() + + # Load icon for the bot + icon_path = self.base_dir / "assets" / "bot.png" + self.icon_image = tk.PhotoImage(file=icon_path) + + # Load config file from environment or default + self.config_path = os.environ.get("PyAEDT_BOT_CONFIG") + if self.config_path is None or not Path(self.config_path).is_file(): + self.config_path = self.base_dir / "assets" / "config.toml" + else: + self.config_path = Path(self.config_path) + + if self.config_path.is_file(): + self.config = read_toml(self.config_path) + else: + raise FileNotFoundError(f"{self.config_path} not found.") + + default_interpreter = Path(os.environ["APPDATA"]) / VENV_DIR_PREFIX / "3_10" / "Scripts" / "python.exe" + + self.python_interpreter = Path(sys.executable) + + if "python.exe" not in str(self.python_interpreter): + self.python_interpreter = default_interpreter + + local_interpreter = self.config.get("interpreter", None) + if local_interpreter: + self.python_interpreter = Path(local_interpreter) + + self.extensions_dir = ( + self.python_interpreter.parent.parent / "Lib" / "site-packages" / "ansys" / "aedt" / "core" / "extensions" + ) + + # Window setup + self.overrideredirect(True) + self.geometry("+100+100") + self.configure(bg="white") + self.attributes("-topmost", True) + self.wm_attributes("-transparentcolor", "white") + + self._offset_x = 0 + self._offset_y = 0 + + # Display icon as draggable bot + self.icon_label = tk.Label(self, image=self.icon_image, bg="white", bd=0) + self.icon_label.pack() + self.icon_label.bind("", self.start_move) + self.icon_label.bind("", self.move_icon) + + # Right-click context menu + self.menu = Menu(self, tearoff=0) + self.load_menu() + self.icon_label.bind("", self.show_menu) + + def start_move(self, event): + """Start moving the window.""" + self._offset_x = event.x + self._offset_y = event.y + + def move_icon(self, event): + """Move the icon according to mouse motion.""" + x = self.winfo_pointerx() - self._offset_x + y = self.winfo_pointery() - self._offset_y + self.geometry(f"+{x}+{y}") + + def show_menu(self, event): + """Display the context menu on right click.""" + self.menu.tk_popup(event.x_root, event.y_root) + + def load_menu(self): + """Load the context menu from the TOML configuration.""" + + for extension_type, extensions in self.config.items(): + if isinstance(extensions, dict): + submenu = Menu(self.menu, tearoff=0) + for key, extension_data in extensions.items(): + name = extension_data.get("name", key) + script = extension_data.get("script") + if not script: + continue + + script_path = Path(script) + if not script_path.is_file(): + script_path = self.extensions_dir / script_path + + submenu.add_command(label=name, command=lambda s=script_path: self.launch_extension(s)) + self.menu.add_cascade(label=extension_type, menu=submenu) + + self.menu.add_separator() + self.menu.add_command(label="Help", command=self.show_help) + self.menu.add_command(label="Quit", command=self.destroy) + + def show_help(self): + """Display a help message describing the bot.""" + help_window = tk.Toplevel(self) + help_window.title("About SAM Bot") + help_window.configure(bg="white") + help_window.resizable(False, False) + + # Set the custom icon (top-left corner) + help_window.iconphoto(False, self.icon_image) + + message = ( + "SAM Bot - Smart AEDT Manager\n\n" + "SAM Bot provides a floating menu for launching PyAEDT automation scripts with ease.\n\n" + "Features:\n" + "- Right-click the bot icon to access configured extensions.\n" + "- Drag the bot by clicking and holding the icon.\n" + "- Customize panels using the 'PYAEDT_BOT_CONFIG' environment variable.\n" + "- Edit the TOML configuration file to define your own extensions.\n" + "- You can also use a custom Python virtual environment (Python 3.10 required).\n\n" + f"You can use this configuration file as a template: {self.config_path}." + ) + + # Text label + label = tk.Label(help_window, text=message, justify="left", bg="white", font=("Segoe UI", 10), padx=10, pady=10) + label.pack() + + # Close button + close_button = tk.Button(help_window, text="Close", command=help_window.destroy) + close_button.pack(pady=(0, 10)) + + def launch_extension(self, script_path): + """Launch an extension script using the Python interpreter.""" + logger.info(f"Launching {script_path}") + logger.info(f"Using interpreter: {self.python_interpreter}") + if not script_path.is_file(): + logger.error(f"{script_path} not found.") + raise FileNotFoundError(f"{script_path} not found.") + subprocess.Popen([self.python_interpreter, str(script_path)], shell=True) + logger.info(f"Finished launching {script_path}.") + + +if __name__ == "__main__": + app = PyAEDTBot() + app.mainloop() diff --git a/src/ansys/aedt/core/extensions/templates/template_get_started.py b/src/ansys/aedt/core/extensions/templates/template_get_started.py index 60c6a8e9c41..6a5e973e525 100644 --- a/src/ansys/aedt/core/extensions/templates/template_get_started.py +++ b/src/ansys/aedt/core/extensions/templates/template_get_started.py @@ -55,7 +55,7 @@ def frontend(): app = ansys.aedt.core.Desktop( new_desktop=False, - specified_version=version, + version=version, port=port, aedt_process_id=aedt_process_id, student_version=is_student,