From d6a33341ba1f150d74fd06d4c78cf9dc043cc701 Mon Sep 17 00:00:00 2001 From: Samuelopez-ansys Date: Fri, 11 Jul 2025 16:02:20 +0200 Subject: [PATCH 01/61] New extension manager --- src/ansys/aedt/core/extensions/__init__.py | 4 + .../installer/new_extension_manager.py | 198 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/ansys/aedt/core/extensions/installer/new_extension_manager.py diff --git a/src/ansys/aedt/core/extensions/__init__.py b/src/ansys/aedt/core/extensions/__init__.py index b78d8fed76c..d503890735c 100644 --- a/src/ansys/aedt/core/extensions/__init__.py +++ b/src/ansys/aedt/core/extensions/__init__.py @@ -21,3 +21,7 @@ # 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 pathlib import Path + +EXTENSIONS_PATH = Path(__file__).parent diff --git a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py new file mode 100644 index 00000000000..a75d0fde22d --- /dev/null +++ b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py @@ -0,0 +1,198 @@ +# -*- 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 dataclasses import dataclass +import os +import tkinter +from tkinter import ttk + +import PIL.Image +import PIL.ImageTk + +from ansys.aedt.core import get_pyaedt_app +from ansys.aedt.core.extensions import EXTENSIONS_PATH +from ansys.aedt.core.extensions.misc import ExtensionCommon +from ansys.aedt.core.extensions.misc import ExtensionCommonData +from ansys.aedt.core.extensions.misc import get_aedt_version +from ansys.aedt.core.extensions.misc import get_arguments +from ansys.aedt.core.extensions.misc import get_port +from ansys.aedt.core.extensions.misc import get_process_id +from ansys.aedt.core.extensions.misc import is_student +from ansys.aedt.core.internal.errors import AEDTRuntimeError + +PORT = get_port() +VERSION = get_aedt_version() +AEDT_PROCESS_ID = get_process_id() +IS_STUDENT = is_student() + +EXTENSION_TITLE = "Extension Manager" +AEDT_APPLICATIONS = [ + "Project", + "HFSS", + "Maxwell3D", + "Icepak", + "Q3D", + "Maxwell2D", + "Q2D", + "HFSS3DLayout", + "Mechanical", + "Circuit", + "EMIT", + "TwinBuilder", +] +WIDTH = 600 +HEIGHT = 250 + + +class ExtensionManager(ExtensionCommon): + """Extension for move it in AEDT.""" + + def __init__(self, withdraw: bool = False): + # Initialize the common extension class with the title and theme color + super().__init__( + EXTENSION_TITLE, + theme_color="light", + withdraw=withdraw, + add_custom_content=False, + toggle_row=4, + toggle_column=1, + ) + + # Tkinter widgets + self.right_panel = None + self.programs = None + self.default_label = None + self.images = [] + + # Trigger manually since add_extension_content requires loading expression files first + self.add_extension_content() + + self.root.minsize(WIDTH, HEIGHT) + + def add_extension_content(self): + """Add custom content to the extension UI.""" + + # Main container (horizontal layout) + container = ttk.Frame(self.root, style="PyAEDT.TFrame") + container.grid(row=1, column=0, columnspan=2, sticky="nsew") + + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + # Left panel (Programs) + left_panel = ttk.Frame(container, width=250, style="PyAEDT.TFrame") + left_panel.grid(row=0, column=0, sticky="ns") + + # Right panel (Extensions) + self.right_panel = ttk.Frame(container, style="PyAEDT.TFrame") + self.right_panel.grid(row=0, column=1, sticky="nsew") + + container.grid_rowconfigure(0, weight=1) + container.grid_columnconfigure(1, weight=1) + + # Program list + self.programs = { + "Project": ["Custom", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran"], + "HFSS": ["Custom", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran"], + "Maxwell": ["Custom", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran"], + } + + for i, name in enumerate(self.programs): + btn = ttk.Button(left_panel, text=name, command=lambda n=name: self.load_extensions(n), + style="PyAEDT.TButton") + btn.grid(row=i, column=0, sticky="ew", padx=5, pady=2) + + # Placeholder content + self.default_label = ttk.Label(self.right_panel, text="Select a category", + style="PyAEDT.TLabel", + font=("Arial", 12, "bold")) + self.default_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw") + + def load_extensions(self, category: str): + # Clear right panel + for widget in self.right_panel.winfo_children(): + widget.destroy() + + canvas = tkinter.Canvas(self.right_panel, highlightthickness=0) + scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", command=canvas.yview) + scroll_frame = ttk.Frame(canvas, style="PyAEDT.TFrame") + + scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=scroll_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + canvas.grid(row=0, column=0, sticky="nsew") + scrollbar.grid(row=0, column=1, sticky="ns") + + self.right_panel.grid_rowconfigure(0, weight=1) + self.right_panel.grid_columnconfigure(0, weight=1) + + header = ttk.Label(scroll_frame, text=f"{category} Extensions", style="PyAEDT.TLabel", + font=("Arial", 12, "bold")) + header.grid(row=0, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 10)) + + self.style.configure("PyAEDT.TFrame", relief="raised", borderwidth=1) + + options = self.programs.get(category, []) + for index, option in enumerate(options): + row = (index // 3) + 1 + col = index % 3 + + card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame", padding=10) + card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") + + if option.lower() == "custom": + icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", font=("Segoe UI Emoji", 18)) + icon.pack(pady=(0, 5)) + else: + try: + image_path = EXTENSIONS_PATH / "project" / "images" / "large" / "cloud.png" + img = PIL.Image.open(str(image_path)).resize((48, 48), PIL.Image.LANCZOS) + photo = PIL.ImageTk.PhotoImage(img) + self.images.append(photo) + icon = ttk.Label(card, image=photo, background="#ffffff") + icon.pack(pady=(0, 5)) + except Exception as e: + icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", font=("Segoe UI Emoji", 18)) + icon.pack(pady=(0, 5)) + + # Texto + label = ttk.Label(card, text=option, style="PyAEDT.TLabel", anchor="center") + label.pack() + + # Click handler para toda la tarjeta + card.bind("", lambda e, opt=option: print(f"Clicked: {opt}")) + icon.bind("", lambda e, opt=option: print(f"Clicked: {opt}")) + label.bind("", lambda e, opt=option: print(f"Clicked: {opt}")) + + # Expande columnas + for col in range(3): + scroll_frame.grid_columnconfigure(col, weight=1) + + +if __name__ == "__main__": # pragma: no cover + # Open UI + extension: ExtensionCommon = ExtensionManager(withdraw=False) + + tkinter.mainloop() From 00a037c82ac70bd62d4af5d72dd064bbb1076c45 Mon Sep 17 00:00:00 2001 From: Eduardo Blanco Date: Fri, 11 Jul 2025 17:22:57 +0200 Subject: [PATCH 02/61] FEAT: Improved theme styling in new extension manager --- .../installer/new_extension_manager.py | 174 +++++++++++++++--- src/ansys/aedt/core/extensions/misc.py | 10 + 2 files changed, 158 insertions(+), 26 deletions(-) diff --git a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py index a75d0fde22d..fcd6a9052b1 100644 --- a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py +++ b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py @@ -24,22 +24,21 @@ from dataclasses import dataclass import os +import subprocess import tkinter from tkinter import ttk import PIL.Image import PIL.ImageTk -from ansys.aedt.core import get_pyaedt_app from ansys.aedt.core.extensions import EXTENSIONS_PATH +from ansys.aedt.core.extensions.customize_automation_tab import available_toolkits +from ansys.aedt.core.extensions.customize_automation_tab import add_script_to_menu from ansys.aedt.core.extensions.misc import ExtensionCommon -from ansys.aedt.core.extensions.misc import ExtensionCommonData from ansys.aedt.core.extensions.misc import get_aedt_version -from ansys.aedt.core.extensions.misc import get_arguments from ansys.aedt.core.extensions.misc import get_port from ansys.aedt.core.extensions.misc import get_process_id from ansys.aedt.core.extensions.misc import is_student -from ansys.aedt.core.internal.errors import AEDTRuntimeError PORT = get_port() VERSION = get_aedt_version() @@ -62,7 +61,7 @@ "TwinBuilder", ] WIDTH = 600 -HEIGHT = 250 +HEIGHT = 450 class ExtensionManager(ExtensionCommon): @@ -89,6 +88,7 @@ def __init__(self, withdraw: bool = False): self.add_extension_content() self.root.minsize(WIDTH, HEIGHT) + self.root.geometry(f"{WIDTH}x{HEIGHT}") def add_extension_content(self): """Add custom content to the extension UI.""" @@ -111,12 +111,9 @@ def add_extension_content(self): container.grid_rowconfigure(0, weight=1) container.grid_columnconfigure(1, weight=1) - # Program list - self.programs = { - "Project": ["Custom", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran"], - "HFSS": ["Custom", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran"], - "Maxwell": ["Custom", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran", "Import Nastran"], - } + # Load programs and extensions dynamically + self.toolkits = available_toolkits() + self.programs = list(self.toolkits.keys()) for i, name in enumerate(self.programs): btn = ttk.Button(left_panel, text=name, command=lambda n=name: self.load_extensions(n), @@ -135,9 +132,26 @@ def load_extensions(self, category: str): widget.destroy() canvas = tkinter.Canvas(self.right_panel, highlightthickness=0) - scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", command=canvas.yview) + scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", + command=canvas.yview) scroll_frame = ttk.Frame(canvas, style="PyAEDT.TFrame") + # Apply theme to canvas + self.apply_canvas_theme(canvas) + + # Add mouse wheel scrolling + def _on_mousewheel(event): + canvas.yview_scroll(int(-1*(event.delta/120)), "units") + + def _bind_mousewheel(event): + canvas.bind_all("", _on_mousewheel) + + def _unbind_mousewheel(event): + canvas.unbind_all("") + + canvas.bind('', _bind_mousewheel) + canvas.bind('', _unbind_mousewheel) + scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=scroll_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) @@ -152,43 +166,151 @@ def load_extensions(self, category: str): font=("Arial", 12, "bold")) header.grid(row=0, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 10)) - self.style.configure("PyAEDT.TFrame", relief="raised", borderwidth=1) - - options = self.programs.get(category, []) + options = list(self.toolkits.get(category, {}).keys()) + if options: + options = ["Custom"] + options + else: + options = ["Custom"] for index, option in enumerate(options): row = (index // 3) + 1 col = index % 3 - card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame", padding=10) + card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame") card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") if option.lower() == "custom": - icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", font=("Segoe UI Emoji", 18)) + icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", + font=("Segoe UI Emoji", 18)) icon.pack(pady=(0, 5)) else: try: - image_path = EXTENSIONS_PATH / "project" / "images" / "large" / "cloud.png" - img = PIL.Image.open(str(image_path)).resize((48, 48), PIL.Image.LANCZOS) + # Try to find the icon for this specific extension + extension_info = self.toolkits[category][option] + if extension_info.get("icon"): + icon_path = (EXTENSIONS_PATH / category.lower() / + extension_info["icon"]) + else: + icon_path = (EXTENSIONS_PATH / "project" / "images" / + "large" / "cloud.png") + + # Load image and preserve transparency + img = PIL.Image.open(str(icon_path)) + img = img.resize((48, 48), PIL.Image.LANCZOS) + + # Get current theme colors for background + theme_colors = (self.theme.light if self.root.theme == "light" + else self.theme.dark) + + # Create a background with theme color for transparency + if img.mode == 'RGBA' or 'transparency' in img.info: + # Create background with theme color + bg_color = theme_colors["widget_bg"] + # Convert hex to RGB + bg_rgb = tuple(int(bg_color[i:i+2], 16) for i in (1, 3, 5)) + background = PIL.Image.new('RGBA', img.size, bg_rgb + (255,)) + # Composite the image with background + img = PIL.Image.alpha_composite(background, img.convert('RGBA')) + img = img.convert('RGB') + photo = PIL.ImageTk.PhotoImage(img) self.images.append(photo) - icon = ttk.Label(card, image=photo, background="#ffffff") + icon = ttk.Label(card, image=photo) icon.pack(pady=(0, 5)) - except Exception as e: - icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", font=("Segoe UI Emoji", 18)) + except Exception: + icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", + font=("Segoe UI Emoji", 18)) icon.pack(pady=(0, 5)) # Texto label = ttk.Label(card, text=option, style="PyAEDT.TLabel", anchor="center") label.pack() - # Click handler para toda la tarjeta - card.bind("", lambda e, opt=option: print(f"Clicked: {opt}")) - icon.bind("", lambda e, opt=option: print(f"Clicked: {opt}")) - label.bind("", lambda e, opt=option: print(f"Clicked: {opt}")) + # Click handler for launching extensions + card.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + icon.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + label.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) # Expande columnas for col in range(3): scroll_frame.grid_columnconfigure(col, weight=1) + def launch_extension(self, category: str, option: str): + """Unimplemented method to handle launching extensions.""" + return + + def handle_custom_extension(self, category: str): + """Handle custom extension installation.""" + from tkinter import filedialog, messagebox, simpledialog + + try: + # Ask for script file + script_file = filedialog.askopenfilename( + title="Select Extension Script", + filetypes=[ + ("Python files", "*.py"), + ("Executable files", "*.exe"), + ("All files", "*.*") + ] + ) + + if not script_file: + return + + # Ask for extension name + extension_name = simpledialog.askstring( + "Extension Name", + "Enter a name for this extension:" + ) + + if not extension_name: + return + + # Install the custom extension + import sys + add_script_to_menu( + name=extension_name, + script_file=script_file, + product=category, + executable_interpreter=sys.executable, + personal_lib=self.desktop.personallib, + aedt_version=self.desktop.aedt_version_id, + copy_to_personal_lib=False, + ) + + messagebox.showinfo( + "Success", + f"Extension '{extension_name}' installed successfully!" + ) + + # Refresh toolkit UI + if hasattr(self.desktop, 'odesktop'): + self.desktop.odesktop.RefreshToolkitUI() + + except Exception as e: + messagebox.showerror( + "Error", + f"Failed to install extension: {str(e)}" + ) + + def apply_canvas_theme(self, canvas): + """Apply theme to a specific canvas widget.""" + theme_colors = (self.theme.light if self.root.theme == "light" + else self.theme.dark) + canvas.configure( + background=theme_colors["pane_bg"], + highlightbackground=theme_colors["tab_border"], + highlightcolor=theme_colors["tab_border"], + ) + + def toggle_theme(self): + """Toggle between light and dark themes and update all canvases.""" + super().toggle_theme() + + # Update all canvas elements in the UI + for widget in self._ExtensionCommon__find_all_widgets(self.root, tkinter.Canvas): + self.apply_canvas_theme(widget) if __name__ == "__main__": # pragma: no cover diff --git a/src/ansys/aedt/core/extensions/misc.py b/src/ansys/aedt/core/extensions/misc.py index a0a2073541e..c99e8b38473 100644 --- a/src/ansys/aedt/core/extensions/misc.py +++ b/src/ansys/aedt/core/extensions/misc.py @@ -196,12 +196,22 @@ def __apply_theme(self, theme_color: str): """Apply a theme to the UI.""" theme_colors_dict = self.theme.light if theme_color == "light" else self.theme.dark self.root.configure(background=theme_colors_dict["widget_bg"]) + + # Apply theme to Text widgets for widget in self.__find_all_widgets(self.root, tkinter.Text): widget.configure( background=theme_colors_dict["pane_bg"], foreground=theme_colors_dict["text"], font=self.theme.default_font, ) + + # Apply theme to Canvas widgets + for widget in self.__find_all_widgets(self.root, tkinter.Canvas): + widget.configure( + background=theme_colors_dict["pane_bg"], + highlightbackground=theme_colors_dict["tab_border"], + highlightcolor=theme_colors_dict["tab_border"], + ) button_text = None if theme_color == "light": From 2bd6d0f9a869e3048328f4c8ecaf689d9e0ff7cc Mon Sep 17 00:00:00 2001 From: Samuelopez-ansys Date: Mon, 14 Jul 2025 23:17:42 +0200 Subject: [PATCH 03/61] Uninstall extension --- .../installer/new_extension_manager.py | 253 +++++++++++------- 1 file changed, 158 insertions(+), 95 deletions(-) diff --git a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py index fcd6a9052b1..16a9ba850bc 100644 --- a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py +++ b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py @@ -23,10 +23,12 @@ # SOFTWARE. from dataclasses import dataclass -import os +from pathlib import Path +import sys import subprocess import tkinter from tkinter import ttk +from tkinter import filedialog, messagebox, simpledialog import PIL.Image import PIL.ImageTk @@ -34,6 +36,7 @@ from ansys.aedt.core.extensions import EXTENSIONS_PATH from ansys.aedt.core.extensions.customize_automation_tab import available_toolkits from ansys.aedt.core.extensions.customize_automation_tab import add_script_to_menu +from ansys.aedt.core.extensions.customize_automation_tab import remove_script_from_menu from ansys.aedt.core.extensions.misc import ExtensionCommon from ansys.aedt.core.extensions.misc import get_aedt_version from ansys.aedt.core.extensions.misc import get_port @@ -60,8 +63,8 @@ "EMIT", "TwinBuilder", ] -WIDTH = 600 -HEIGHT = 450 +WIDTH = 850 +HEIGHT = 550 class ExtensionManager(ExtensionCommon): @@ -78,6 +81,10 @@ def __init__(self, withdraw: bool = False): toggle_column=1, ) + self.python_interpreter = Path(sys.executable) + self.toolkits = None + self.add_to_aedt_var = tkinter.BooleanVar(value=True) + # Tkinter widgets self.right_panel = None self.programs = None @@ -105,7 +112,7 @@ def add_extension_content(self): left_panel.grid(row=0, column=0, sticky="ns") # Right panel (Extensions) - self.right_panel = ttk.Frame(container, style="PyAEDT.TFrame") + self.right_panel = ttk.Frame(container, style="PyAEDT.TFrame", relief="solid") self.right_panel.grid(row=0, column=1, sticky="nsew") container.grid_rowconfigure(0, weight=1) @@ -113,15 +120,15 @@ def add_extension_content(self): # Load programs and extensions dynamically self.toolkits = available_toolkits() - self.programs = list(self.toolkits.keys()) + self.programs = AEDT_APPLICATIONS - for i, name in enumerate(self.programs): + for i, name in enumerate(AEDT_APPLICATIONS): btn = ttk.Button(left_panel, text=name, command=lambda n=name: self.load_extensions(n), style="PyAEDT.TButton") btn.grid(row=i, column=0, sticky="ew", padx=5, pady=2) # Placeholder content - self.default_label = ttk.Label(self.right_panel, text="Select a category", + self.default_label = ttk.Label(self.right_panel, text="Select application", style="PyAEDT.TLabel", font=("Arial", 12, "bold")) self.default_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw") @@ -132,23 +139,23 @@ def load_extensions(self, category: str): widget.destroy() canvas = tkinter.Canvas(self.right_panel, highlightthickness=0) - scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", - command=canvas.yview) - scroll_frame = ttk.Frame(canvas, style="PyAEDT.TFrame") + scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", + command=canvas.yview) + scroll_frame = ttk.Frame(canvas, style="PyAEDT.TFrame", relief="solid") # Apply theme to canvas self.apply_canvas_theme(canvas) # Add mouse wheel scrolling def _on_mousewheel(event): - canvas.yview_scroll(int(-1*(event.delta/120)), "units") - + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + def _bind_mousewheel(event): canvas.bind_all("", _on_mousewheel) - + def _unbind_mousewheel(event): canvas.unbind_all("") - + canvas.bind('', _bind_mousewheel) canvas.bind('', _unbind_mousewheel) @@ -166,138 +173,168 @@ def _unbind_mousewheel(event): font=("Arial", 12, "bold")) header.grid(row=0, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 10)) - options = list(self.toolkits.get(category, {}).keys()) - if options: - options = ["Custom"] + options + # Checkbox to add to AEDT + add_checkbox = ttk.Checkbutton( + scroll_frame, + text="Add to AEDT", + variable=self.add_to_aedt_var, + style="PyAEDT.TCheckbutton" + ) + add_checkbox.grid(row=1, column=0, sticky="w", padx=10, pady=(0, 10), columnspan=3) + + extensions = list(self.toolkits.get(category, {}).keys()) + options = {} + if extensions: + options["Custom"] = "Custom" + for extension_name in extensions: + options[extension_name] = self.toolkits[category][extension_name].get("name", extension_name) + else: - options = ["Custom"] + options["Custom"] = "Custom" for index, option in enumerate(options): - row = (index // 3) + 1 + row = (index // 3) + 2 col = index % 3 - card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame") + card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame", relief="solid") card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") if option.lower() == "custom": - icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", - font=("Segoe UI Emoji", 18)) - icon.pack(pady=(0, 5)) + icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", + font=("Segoe UI Emoji", 25)) + icon.pack(padx=10, pady=10) else: try: # Try to find the icon for this specific extension extension_info = self.toolkits[category][option] if extension_info.get("icon"): - icon_path = (EXTENSIONS_PATH / category.lower() / - extension_info["icon"]) + icon_path = (EXTENSIONS_PATH / category.lower() / + extension_info["icon"]) else: - icon_path = (EXTENSIONS_PATH / "project" / "images" / - "large" / "cloud.png") - + icon_path = (EXTENSIONS_PATH / "images" / + "large" / "pyansys.png") + # Load image and preserve transparency img = PIL.Image.open(str(icon_path)) img = img.resize((48, 48), PIL.Image.LANCZOS) - + # Get current theme colors for background - theme_colors = (self.theme.light if self.root.theme == "light" - else self.theme.dark) - + theme_colors = (self.theme.light if self.root.theme == "light" + else self.theme.dark) + # Create a background with theme color for transparency if img.mode == 'RGBA' or 'transparency' in img.info: # Create background with theme color bg_color = theme_colors["widget_bg"] # Convert hex to RGB - bg_rgb = tuple(int(bg_color[i:i+2], 16) for i in (1, 3, 5)) + bg_rgb = tuple(int(bg_color[i:i + 2], 16) for i in (1, 3, 5)) background = PIL.Image.new('RGBA', img.size, bg_rgb + (255,)) # Composite the image with background img = PIL.Image.alpha_composite(background, img.convert('RGBA')) img = img.convert('RGB') - + photo = PIL.ImageTk.PhotoImage(img) self.images.append(photo) icon = ttk.Label(card, image=photo) - icon.pack(pady=(0, 5)) + icon.pack(padx=10, pady=10) except Exception: - icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", - font=("Segoe UI Emoji", 18)) - icon.pack(pady=(0, 5)) + icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", + font=("Segoe UI Emoji", 18)) + icon.pack(padx=10, pady=10) - # Texto - label = ttk.Label(card, text=option, style="PyAEDT.TLabel", anchor="center") - label.pack() + # Text + label = ttk.Label(card, text=options[option], style="PyAEDT.TLabel", anchor="center") + label.pack(padx=10, pady=10) # Click handler for launching extensions - card.bind("", lambda e, cat=category, opt=option: - self.launch_extension(cat, opt)) - icon.bind("", lambda e, cat=category, opt=option: - self.launch_extension(cat, opt)) - label.bind("", lambda e, cat=category, opt=option: - self.launch_extension(cat, opt)) - - # Expande columnas + card.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + icon.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + label.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + + def on_right_click(event, cat=category, opt=option): + self.confirm_uninstall(cat, opt) + + card.bind("", on_right_click) + icon.bind("", on_right_click) + label.bind("", on_right_click) + + # Expand columns for col in range(3): scroll_frame.grid_columnconfigure(col, weight=1) + def launch_extension(self, category: str, option: str): """Unimplemented method to handle launching extensions.""" - return + if option.lower() == "custom": + script_file, option = self.handle_custom_extension() + icon = EXTENSIONS_PATH / "images" / "large" / "pyansys.png" + else: + script_file = EXTENSIONS_PATH / category.lower() / self.toolkits[category][option]["script"] + icon = EXTENSIONS_PATH / category.lower() / self.toolkits[category][option]["icon"] - def handle_custom_extension(self, category: str): - """Handle custom extension installation.""" - from tkinter import filedialog, messagebox, simpledialog - - try: - # Ask for script file - script_file = filedialog.askopenfilename( - title="Select Extension Script", - filetypes=[ - ("Python files", "*.py"), - ("Executable files", "*.exe"), - ("All files", "*.*") - ] - ) - - if not script_file: - return - - # Ask for extension name - extension_name = simpledialog.askstring( - "Extension Name", - "Enter a name for this extension:" - ) - - if not extension_name: - return - + if getattr(self, "add_to_aedt_var", tkinter.BooleanVar(value=True)).get(): # Install the custom extension - import sys add_script_to_menu( - name=extension_name, - script_file=script_file, - product=category, + name=option, + script_file=str(script_file), + product=category.lower(), executable_interpreter=sys.executable, personal_lib=self.desktop.personallib, aedt_version=self.desktop.aedt_version_id, copy_to_personal_lib=False, + icon_file=str(icon) ) - - messagebox.showinfo( - "Success", - f"Extension '{extension_name}' installed successfully!" - ) - + + self.desktop.logger.info(f"Extension {option} added successfully.") + # Refresh toolkit UI if hasattr(self.desktop, 'odesktop'): self.desktop.odesktop.RefreshToolkitUI() - - except Exception as e: - messagebox.showerror( - "Error", - f"Failed to install extension: {str(e)}" + + self.desktop.logger.info(f"Launching {str(script_file)}") + self.desktop.logger.info(f"Using interpreter: {str(self.python_interpreter)}") + if not script_file.is_file(): + logger.error(f"{script_file} not found.") + raise FileNotFoundError(f"{script_file} not found.") + subprocess.Popen([self.python_interpreter, str(script_file)], shell=True) + self.desktop.logger.info(f"Finished launching {script_file}.") + + def handle_custom_extension(self): + """Handle custom extension installation.""" + # Ask for script file + script_file = filedialog.askopenfilename( + title="Select Extension Script", + filetypes=[ + ("Python files", "*.py"), + ("Executable files", "*.exe"), + ("All files", "*.*") + ] + ) + + if not script_file: + script_file = EXTENSIONS_PATH / "templates" / "template_get_started.py" + else: + script_file = Path(script_file) + + extension_name = "Custom Extension" + + if getattr(self, "add_to_aedt_var", tkinter.BooleanVar(value=True)).get(): + # Ask for extension name + extension_name = simpledialog.askstring( + "Extension Name", + "Enter a name for this extension:" ) + if not extension_name: + extension_name = "Custom Extension" + + return script_file, extension_name + def apply_canvas_theme(self, canvas): """Apply theme to a specific canvas widget.""" - theme_colors = (self.theme.light if self.root.theme == "light" - else self.theme.dark) + theme_colors = (self.theme.light if self.root.theme == "light" + else self.theme.dark) canvas.configure( background=theme_colors["pane_bg"], highlightbackground=theme_colors["tab_border"], @@ -307,11 +344,37 @@ def apply_canvas_theme(self, canvas): def toggle_theme(self): """Toggle between light and dark themes and update all canvases.""" super().toggle_theme() - + # Update all canvas elements in the UI for widget in self._ExtensionCommon__find_all_widgets(self.root, tkinter.Canvas): self.apply_canvas_theme(widget) + def confirm_uninstall(self, category, option): + if option.lower() == "custom": + # Ask for extension name + option = simpledialog.askstring( + "Extension Name", + "Extension name to uninstall:" + ) + + if not option: + messagebox.showinfo("Information", "Wrong extension name.") + return + + answer = messagebox.askyesno( + "Uninstall extension", + f"Do you want to uninstall '{option}' in {category}?" + ) + + if answer: + try: + remove_script_from_menu(desktop_object=self.desktop, name=option, product=category) + # Refresh toolkit UI + if hasattr(self.desktop, 'odesktop'): + self.desktop.odesktop.RefreshToolkitUI() + except Exception: + messagebox.showerror("Error", f"Extension could not be uninstalled") + if __name__ == "__main__": # pragma: no cover # Open UI From 5094e8e7c6fc382babbbf26c087bb174d682b62d Mon Sep 17 00:00:00 2001 From: Samuelopez-ansys Date: Tue, 15 Jul 2025 08:41:30 +0200 Subject: [PATCH 04/61] Add unit test --- codecov.yml | 3 +- .../extensions/icepak/toolkits_catalog.toml | 2 +- .../extensions/installer/extension_manager.py | 602 ++++++++++-------- .../installer/new_extension_manager.py | 383 ----------- .../unit/extensions/test_extension_manager.py | 104 +++ 5 files changed, 437 insertions(+), 657 deletions(-) delete mode 100644 src/ansys/aedt/core/extensions/installer/new_extension_manager.py create mode 100644 tests/unit/extensions/test_extension_manager.py diff --git a/codecov.yml b/codecov.yml index 05ba2e94749..38a10083fe3 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,7 +2,8 @@ ignore: - "src/ansys/aedt/core/rpc/*.py" - "src/ansys/aedt/core/misc/*.py" - "src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_utils.py" - - "src/ansys/aedt/core/extensions/installer" + - "src/ansys/aedt/core/extensions/installer/*" + - "!src/ansys/aedt/core/extensions/installer/extension_manager.py" - "src/ansys/aedt/core/extensions/templates" - "src/ansys/aedt/core/common_rpc.py" - "src/ansys/aedt/core/internal/grpc_plugin_dll_class.py" diff --git a/src/ansys/aedt/core/extensions/icepak/toolkits_catalog.toml b/src/ansys/aedt/core/extensions/icepak/toolkits_catalog.toml index ca181a3c6ce..cfa37bd6a68 100644 --- a/src/ansys/aedt/core/extensions/icepak/toolkits_catalog.toml +++ b/src/ansys/aedt/core/extensions/icepak/toolkits_catalog.toml @@ -1,5 +1,5 @@ [CreatePowerMap] name = "Create Power Map from CSV file" script = "power_map_from_csv.py" -icon = "images/large/push.png" +icon = "images/large/power_map.png" template = "run_pyaedt_toolkit_script" \ No newline at end of file diff --git a/src/ansys/aedt/core/extensions/installer/extension_manager.py b/src/ansys/aedt/core/extensions/installer/extension_manager.py index 2c831fa87c2..c4fcae5f891 100644 --- a/src/ansys/aedt/core/extensions/installer/extension_manager.py +++ b/src/ansys/aedt/core/extensions/installer/extension_manager.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# 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 @@ -21,320 +22,377 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import os +from dataclasses import dataclass +from pathlib import Path import sys -import tkinter as tk -from tkinter import filedialog +import subprocess +import tkinter from tkinter import ttk +from tkinter import filedialog, messagebox, simpledialog import webbrowser import PIL.Image import PIL.ImageTk -from ansys.aedt.core import Desktop -import ansys.aedt.core.extensions -from ansys.aedt.core.extensions.customize_automation_tab import add_script_to_menu +from ansys.aedt.core.extensions import EXTENSIONS_PATH from ansys.aedt.core.extensions.customize_automation_tab import available_toolkits +from ansys.aedt.core.extensions.customize_automation_tab import add_script_to_menu from ansys.aedt.core.extensions.customize_automation_tab import remove_script_from_menu +from ansys.aedt.core.extensions.misc import ExtensionCommon from ansys.aedt.core.extensions.misc import get_aedt_version from ansys.aedt.core.extensions.misc import get_port from ansys.aedt.core.extensions.misc import get_process_id from ansys.aedt.core.extensions.misc import is_student -import ansys.aedt.core.extensions.templates -port = get_port() -version = get_aedt_version() -aedt_process_id = get_process_id() -student_version = is_student() +PORT = get_port() +VERSION = get_aedt_version() +AEDT_PROCESS_ID = get_process_id() +IS_STUDENT = is_student() -# Set Python version based on AEDT version -python_version = "3.10" if version > "2023.1" else "3.7" +EXTENSION_TITLE = "Extension Manager" +AEDT_APPLICATIONS = [ + "Project", + "HFSS", + "Maxwell3D", + "Icepak", + "Q3D", + "Maxwell2D", + "Q2D", + "HFSS3DLayout", + "Mechanical", + "Circuit", + "EMIT", + "TwinBuilder", +] +WIDTH = 850 +HEIGHT = 550 + + +class ExtensionManager(ExtensionCommon): + """Extension for move it in AEDT.""" + + def __init__(self, withdraw: bool = False): + # Initialize the common extension class with the title and theme color + super().__init__( + EXTENSION_TITLE, + theme_color="light", + withdraw=withdraw, + add_custom_content=False, + toggle_row=4, + toggle_column=1, + ) -VENV_DIR_PREFIX = ".pyaedt_env" + self.python_interpreter = Path(sys.executable) + self.toolkits = None + self.add_to_aedt_var = tkinter.BooleanVar(value=True) + # Tkinter widgets + self.right_panel = None + self.programs = None + self.default_label = None + self.images = [] -def create_toolkit_page(frame, internal_toolkits): - """Create page to display toolkit on.""" - # Available toolkits + # Trigger manually since add_extension_content requires loading expression files first + self.add_extension_content() - def action_get_script_path(): - if input_file["state"] != "disabled": - file_selected = filedialog.askopenfilename(title="Select the script file") - input_file.insert(0, file_selected) + self.root.minsize(WIDTH, HEIGHT) + self.root.geometry(f"{WIDTH}x{HEIGHT}") - toolkits = ["Custom"] + internal_toolkits + def add_extension_content(self): + """Add custom content to the extension.""" - max_length = max(len(item) for item in toolkits) + 1 + # Main container (horizontal layout) + container = ttk.Frame(self.root, style="PyAEDT.TFrame") + container.grid(row=1, column=0, columnspan=2, sticky="nsew") - # Combobox with available toolkit options - toolkits_combo_label = tk.Label(frame, text="Extension:", width=max_length) - toolkits_combo_label.grid(row=2, column=0, padx=5, pady=5) + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) - toolkits_combo = ttk.Combobox( - frame, values=list(filter(lambda x: x != "", toolkits)), state="readonly", width=max_length - ) - toolkits_combo.set("Custom") - toolkits_combo.grid(row=2, column=1, padx=5, pady=5) + # Left panel (Programs) + left_panel = ttk.Frame(container, width=250, style="PyAEDT.TFrame") + left_panel.grid(row=0, column=0, sticky="ns") - # Create entry box for directory path + # Right panel (Extensions) + self.right_panel = ttk.Frame(container, style="PyAEDT.TFrame", relief="solid") + self.right_panel.grid(row=0, column=1, sticky="nsew") - input_file_label = tk.Button(frame, text="Enter script path:", command=action_get_script_path) - input_file_label.grid(row=3, column=0, padx=5, pady=5) - input_file = tk.Entry(frame) - input_file.grid(row=3, column=1, padx=5, pady=5) + container.grid_rowconfigure(0, weight=1) + container.grid_columnconfigure(1, weight=1) - toolkit_name_label = tk.Label(frame, text="Enter toolkit name:") - toolkit_name_label.grid(row=4, column=0, padx=5, pady=5) - toolkit_name = tk.Entry(frame) - toolkit_name.grid(row=4, column=1, padx=5, pady=5) + # Load programs and extensions dynamically + self.toolkits = available_toolkits() + self.programs = AEDT_APPLICATIONS - # Install button - install_button = tk.Button(frame, text="Install", bg="green", fg="white", padx=20, pady=5) - install_button.grid(row=5, column=0, padx=5, pady=5, sticky="nsew") - uninstall_button = tk.Button(frame, text="Uninstall", bg="red", fg="white", padx=20, pady=5) - uninstall_button.grid(row=5, column=1, padx=5, pady=5, sticky="nsew") + for i, name in enumerate(AEDT_APPLICATIONS): + btn = ttk.Button(left_panel, text=name, command=lambda n=name: self.load_extensions(n), + style="PyAEDT.TButton") + btn.grid(row=i, column=0, sticky="ew", padx=5, pady=2) - def update_page(event=None): - selected_toolkit = toolkits_combo.get() + # Placeholder content + self.default_label = ttk.Label(self.right_panel, text="Select application", + style="PyAEDT.TLabel", + font=("Arial", 12, "bold")) + self.default_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw") - if selected_toolkit == "Custom": - install_button.config(text="Install") - uninstall_button.config(state="normal") - toolkits_combo_label.config(text="Extension") - else: - toolkits_combo_label.config(text="Toolkit") - - if selected_toolkit == "Custom": - toolkit_name.config(state="normal") - input_file_label.config(text="Enter script path:") - input_file.config(state="normal") - elif selected_toolkit != "Custom": - input_file.delete(0, tk.END) - input_file.insert(0, "") - toolkit_name.delete(0, tk.END) - toolkit_name.insert(0, "") - input_file.config(state="disabled") - toolkit_name.config(state="disabled") - - toolkits_combo.bind("<>", update_page) - - update_page() - - return install_button, uninstall_button, input_file, toolkits_combo, toolkit_name - - -def open_window(window, window_name, internal_toolkits): - """Open a window.""" - if not hasattr(window, "opened"): - window.opened = True - window.title(window_name) - install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = create_toolkit_page( - window, internal_toolkits - ) - root.minsize(500, 250) - return install_button, uninstall_button, input_file, toolkits_combo, toolkit_name - else: - window.deiconify() + def load_extensions(self, category: str): + """Load application extensions.""" + # Clear right panel + for widget in self.right_panel.winfo_children(): + widget.destroy() + canvas = tkinter.Canvas(self.right_panel, highlightthickness=0) + scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", + command=canvas.yview) + scroll_frame = ttk.Frame(canvas, style="PyAEDT.TFrame", relief="solid") -def __get_command_function( - is_install, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button -): - return lambda: button_is_clicked( - is_install, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button - ) + # Apply theme to canvas + self.apply_canvas_theme(canvas) + # Add mouse wheel scrolling + def _on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") -def toolkit_window(toolkit_level="Project"): - """Create interactive toolkit window.""" - toolkit_window_var = tk.Toplevel(root) + def _bind_mousewheel(event): + canvas.bind_all("", _on_mousewheel) - toolkits = available_toolkits() + def _unbind_mousewheel(event): + canvas.unbind_all("") - if toolkit_level not in toolkits: - install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = open_window( - toolkit_window_var, toolkit_level, [] - ) - else: - install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = open_window( - toolkit_window_var, toolkit_level, list(toolkits[toolkit_level].keys()) - ) - toolkit_window_var.minsize(250, 150) - - install_command = __get_command_function( - True, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button - ) - uninstall_command = __get_command_function( - False, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button - ) - - install_button.configure(command=install_command) - uninstall_button.configure(command=uninstall_command) - - -def button_is_clicked( - install_action, toolkit_level, input_file, combo_toolkits, toolkit_name, install_button, uninstall_button -): - """Set up a button for installing and uninstalling the toolkit.""" - file = input_file.get() - selected_toolkit_name = combo_toolkits.get() - name = toolkit_name.get() - - desktop = Desktop( - version=version, - port=port, - new_desktop=False, - non_graphical=False, - close_on_exit=False, - student_version=student_version, - aedt_process_id=aedt_process_id, - ) - - desktop.odesktop.CloseAllWindows() - - toolkits = available_toolkits() - selected_toolkit_info = {} - icon = None - url = None - if toolkit_level in toolkits and selected_toolkit_name in toolkits[toolkit_level]: - selected_toolkit_info = toolkits[toolkit_level][selected_toolkit_name] - product_path = os.path.join(os.path.dirname(ansys.aedt.core.extensions.__file__), toolkit_level.lower()) - name = selected_toolkit_info.get("name") - if selected_toolkit_info.get("script", None): - file = os.path.abspath(os.path.join(product_path, selected_toolkit_info.get("script"))) - if selected_toolkit_info.get("url", None): - url = selected_toolkit_info.get("url") - if selected_toolkit_info.get("icon", None): - icon = os.path.abspath(os.path.join(product_path, selected_toolkit_info.get("icon"))) - - valid_name = name is not None and not os.path.isdir(name) - - valid_file = False - if not file: - valid_file = True - elif os.path.isfile(file): - valid_file = True - - if url and install_action: - try: - webbrowser.open(url) - except Exception as e: # pragma: no cover - desktop.logger.error("Error launching browser for %s: %s", name, str(e)) - desktop.logger.error(f"There was an error launching a browser. Please open the following link: {url}.") - - elif valid_name and valid_file: - if install_action: - if not file: - name = "Template" - file = os.path.join( - os.path.dirname(ansys.aedt.core.extensions.templates.__file__), "template_get_started.py" - ) - - is_exe = False - if os.path.basename(file).split(".")[1] == "exe": - exe_path = os.path.dirname(file) - is_exe = True - name = os.path.basename(file).split(".")[0] - - internal_path = os.path.join(exe_path, "_internal", "assets") - if os.path.isdir(internal_path): - for icon_file in os.listdir(internal_path): - if icon_file.lower().endswith(".png"): - icon = os.path.abspath(os.path.join(internal_path, icon_file)) - break - - desktop.logger.info("Install {}".format(name)) - - executable_interpreter = sys.executable - - if os.path.isfile(executable_interpreter): - template_file = "run_pyaedt_toolkit_script" - if is_exe: - template_file = "run_pyaedt_toolkit_executable" - if selected_toolkit_info: - template_file = selected_toolkit_info.get("template") - add_script_to_menu( - name=name, - script_file=file, - product=toolkit_level, - icon_file=icon, - executable_interpreter=executable_interpreter, - personal_lib=desktop.personallib, - aedt_version=desktop.aedt_version_id, - template_file=template_file, - copy_to_personal_lib=False, - ) - desktop.logger.info(f"{name} installed") - else: - desktop.logger.info("PyAEDT environment is not installed.") - elif not name: - desktop.logger.error("Enter a name for the toolkit to uninstall.") - else: - desktop.logger.info(f"Uninstall {name}.") - remove_script_from_menu(desktop_object=desktop, name=name, product=toolkit_level) - else: - desktop.logger.error("Python file not found or wrong name.") + canvas.bind('', _bind_mousewheel) + canvas.bind('', _unbind_mousewheel) - desktop.odesktop.CloseAllWindows() - desktop.odesktop.RefreshToolkitUI() - desktop.release_desktop(False, False) + scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=scroll_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + canvas.grid(row=0, column=0, sticky="nsew") + scrollbar.grid(row=0, column=1, sticky="ns") -root = tk.Tk() -root.title("Extension Manager") + self.right_panel.grid_rowconfigure(0, weight=1) + self.right_panel.grid_columnconfigure(0, weight=1) -# Load the logo for the main window -icon_path = os.path.join(os.path.dirname(ansys.aedt.core.extensions.__file__), "images", "large", "logo.png") -im = PIL.Image.open(icon_path) -photo = PIL.ImageTk.PhotoImage(im) + header = ttk.Label(scroll_frame, text=f"{category} Extensions", style="PyAEDT.TLabel", + font=("Arial", 12, "bold")) + header.grid(row=0, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 10)) -# Set the icon for the main window -root.iconphoto(True, photo) + # Checkbox to add to AEDT + add_checkbox = ttk.Checkbutton( + scroll_frame, + text="Add to AEDT", + variable=self.add_to_aedt_var, + style="PyAEDT.TCheckbutton" + ) + add_checkbox.grid(row=1, column=0, sticky="w", padx=10, pady=(0, 10), columnspan=3) -# Configure style for ttk buttons -style = ttk.Style() -style.configure("Toolbutton.TButton", padding=6, font=("Helvetica", 10)) + extensions = list(self.toolkits.get(category, {}).keys()) + options = {} + if extensions: + options["Custom"] = "Custom" + for extension_name in extensions: + options[extension_name] = self.toolkits[category][extension_name].get("name", extension_name) -toolkit_levels = [ - "Project", - "", - "", - "", - "HFSS", - "Maxwell3D", - "Icepak", - "Q3D", - "Maxwell2D", - "Q2D", - "HFSS3DLayout", - "Mechanical", - "Circuit", - "EMIT", - "TwinBuilder", - "", -] + else: + options["Custom"] = "Custom" + for index, option in enumerate(options): + row = (index // 3) + 2 + col = index % 3 + + card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame", relief="solid") + card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") + + if option.lower() == "custom": + icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", + font=("Segoe UI Emoji", 25)) + icon.pack(padx=10, pady=10) + else: + try: + # Try to find the icon for this specific extension + extension_info = self.toolkits[category][option] + if extension_info.get("icon"): + icon_path = (EXTENSIONS_PATH / category.lower() / + extension_info["icon"]) + else: + icon_path = (EXTENSIONS_PATH / "images" / + "large" / "pyansys.png") + + # Load image and preserve transparency + img = PIL.Image.open(str(icon_path)) + img = img.resize((48, 48), PIL.Image.LANCZOS) + + # Get current theme colors for background + theme_colors = (self.theme.light if self.root.theme == "light" + else self.theme.dark) + + # Create a background with theme color for transparency + if img.mode == 'RGBA' or 'transparency' in img.info: + # Create background with theme color + bg_color = theme_colors["widget_bg"] + # Convert hex to RGB + bg_rgb = tuple(int(bg_color[i:i + 2], 16) for i in (1, 3, 5)) + background = PIL.Image.new('RGBA', img.size, bg_rgb + (255,)) + # Composite the image with background + img = PIL.Image.alpha_composite(background, img.convert('RGBA')) + img = img.convert('RGB') + + photo = PIL.ImageTk.PhotoImage(img) + self.images.append(photo) + icon = ttk.Label(card, image=photo) + icon.pack(padx=10, pady=10) + except Exception: + icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", + font=("Segoe UI Emoji", 18)) + icon.pack(padx=10, pady=10) + + # Text + label = ttk.Label(card, text=options[option], style="PyAEDT.TLabel", anchor="center") + label.pack(padx=10, pady=10) + + # Click handler for launching extensions + card.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + icon.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + label.bind("", lambda e, cat=category, opt=option: + self.launch_extension(cat, opt)) + + def on_right_click(event, cat=category, opt=option): + self.confirm_uninstall(cat, opt) + + card.bind("", on_right_click) + icon.bind("", on_right_click) + label.bind("", on_right_click) + + # Expand columns + for col in range(3): + scroll_frame.grid_columnconfigure(col, weight=1) + + def launch_extension(self, category: str, option: str): + """Launch extension.""" + if option.lower() == "custom": + script_file, option = self.handle_custom_extension() + icon = EXTENSIONS_PATH / "images" / "large" / "pyansys.png" + else: + if self.toolkits[category][option].get("script", None): + script_file = EXTENSIONS_PATH / category.lower() / self.toolkits[category][option]["script"] + icon = EXTENSIONS_PATH / category.lower() / self.toolkits[category][option]["icon"] + elif self.toolkits[category][option].get("url", None): + url = self.toolkits[category][option]["url"] + try: + webbrowser.open(str(url)) + except Exception as e: # pragma: no cover + desktop.logger.error("Error launching browser for %s: %s", name, str(e)) + desktop.logger.error( + f"There was an error launching a browser. Please open the following link: {url}.") + return + else: # pragma: no cover + messagebox.showinfo("Error", "Wrong extension.") + return + + if getattr(self, "add_to_aedt_var", tkinter.BooleanVar(value=True)).get(): + # Install the custom extension + add_script_to_menu( + name=option, + script_file=str(script_file), + product=category.lower(), + executable_interpreter=sys.executable, + personal_lib=self.desktop.personallib, + aedt_version=self.desktop.aedt_version_id, + copy_to_personal_lib=False, + icon_file=str(icon) + ) + + self.desktop.logger.info(f"Extension {option} added successfully.") + + # Refresh toolkit UI + if hasattr(self.desktop, 'odesktop'): + self.desktop.odesktop.RefreshToolkitUI() + + self.desktop.logger.info(f"Launching {str(script_file)}") + self.desktop.logger.info(f"Using interpreter: {str(self.python_interpreter)}") + if not script_file.is_file(): + logger.error(f"{script_file} not found.") + raise FileNotFoundError(f"{script_file} not found.") + subprocess.Popen([self.python_interpreter, str(script_file)], shell=True) + self.desktop.logger.info(f"Finished launching {script_file}.") + + def handle_custom_extension(self): + """Handle custom extension installation.""" + # Ask for script file + script_file = filedialog.askopenfilename( + title="Select Extension Script", + filetypes=[ + ("Python files", "*.py"), + ("Executable files", "*.exe"), + ("All files", "*.*") + ] + ) -window_width, window_height = 500, 250 -screen_width = root.winfo_screenwidth() -screen_height = root.winfo_screenheight() -x_position = (screen_width - window_width) // 2 -y_position = (screen_height - window_height) // 2 - -root.geometry(f"{window_width}x{window_height}+{x_position}+{y_position}") - -# Create buttons in a 4x4 grid, centered -for i, level in enumerate(toolkit_levels): - row_num = i // 4 - col_num = i % 4 - if level: - toolkit_button = ttk.Button( - root, - text=level, - command=lambda toolkit_level=level: toolkit_window(toolkit_level), - style="Toolbutton.TButton", + if not script_file: + script_file = EXTENSIONS_PATH / "templates" / "template_get_started.py" + else: + script_file = Path(script_file) + + extension_name = "Custom Extension" + + if getattr(self, "add_to_aedt_var", tkinter.BooleanVar(value=True)).get(): + # Ask for extension name + extension_name = simpledialog.askstring( + "Extension Name", + "Enter a name for this extension:" + ) + + if not extension_name: + extension_name = "Custom Extension" + + return script_file, extension_name + + def apply_canvas_theme(self, canvas): + """Apply theme to a specific canvas widget.""" + theme_colors = (self.theme.light if self.root.theme == "light" + else self.theme.dark) + canvas.configure( + background=theme_colors["pane_bg"], + highlightbackground=theme_colors["tab_border"], + highlightcolor=theme_colors["tab_border"], + ) + + def toggle_theme(self): + """Toggle between light and dark themes and update all canvases.""" + super().toggle_theme() + + # Update all canvas elements in the UI + for widget in self._ExtensionCommon__find_all_widgets(self.root, tkinter.Canvas): + self.apply_canvas_theme(widget) + + def confirm_uninstall(self, category, option): + if option.lower() == "custom": + # Ask for extension name + option = simpledialog.askstring( + "Extension Name", + "Extension name to uninstall:" + ) + + if not option: + messagebox.showinfo("Information", "Wrong extension name.") + return + + answer = messagebox.askyesno( + "Uninstall extension", + f"Do you want to uninstall '{option}' in {category}?" ) - toolkit_button.grid(row=row_num, column=col_num, padx=10, pady=10) -root.minsize(window_width, window_height) + if answer: + try: + remove_script_from_menu(desktop_object=self.desktop, name=option, product=category) + # Refresh toolkit UI + if hasattr(self.desktop, 'odesktop'): + self.desktop.odesktop.RefreshToolkitUI() + except Exception: + messagebox.showerror("Error", f"Extension could not be uninstalled") + + +if __name__ == "__main__": # pragma: no cover + # Open UI + extension: ExtensionCommon = ExtensionManager(withdraw=False) -root.mainloop() + tkinter.mainloop() diff --git a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py b/src/ansys/aedt/core/extensions/installer/new_extension_manager.py deleted file mode 100644 index 16a9ba850bc..00000000000 --- a/src/ansys/aedt/core/extensions/installer/new_extension_manager.py +++ /dev/null @@ -1,383 +0,0 @@ -# -*- 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 dataclasses import dataclass -from pathlib import Path -import sys -import subprocess -import tkinter -from tkinter import ttk -from tkinter import filedialog, messagebox, simpledialog - -import PIL.Image -import PIL.ImageTk - -from ansys.aedt.core.extensions import EXTENSIONS_PATH -from ansys.aedt.core.extensions.customize_automation_tab import available_toolkits -from ansys.aedt.core.extensions.customize_automation_tab import add_script_to_menu -from ansys.aedt.core.extensions.customize_automation_tab import remove_script_from_menu -from ansys.aedt.core.extensions.misc import ExtensionCommon -from ansys.aedt.core.extensions.misc import get_aedt_version -from ansys.aedt.core.extensions.misc import get_port -from ansys.aedt.core.extensions.misc import get_process_id -from ansys.aedt.core.extensions.misc import is_student - -PORT = get_port() -VERSION = get_aedt_version() -AEDT_PROCESS_ID = get_process_id() -IS_STUDENT = is_student() - -EXTENSION_TITLE = "Extension Manager" -AEDT_APPLICATIONS = [ - "Project", - "HFSS", - "Maxwell3D", - "Icepak", - "Q3D", - "Maxwell2D", - "Q2D", - "HFSS3DLayout", - "Mechanical", - "Circuit", - "EMIT", - "TwinBuilder", -] -WIDTH = 850 -HEIGHT = 550 - - -class ExtensionManager(ExtensionCommon): - """Extension for move it in AEDT.""" - - def __init__(self, withdraw: bool = False): - # Initialize the common extension class with the title and theme color - super().__init__( - EXTENSION_TITLE, - theme_color="light", - withdraw=withdraw, - add_custom_content=False, - toggle_row=4, - toggle_column=1, - ) - - self.python_interpreter = Path(sys.executable) - self.toolkits = None - self.add_to_aedt_var = tkinter.BooleanVar(value=True) - - # Tkinter widgets - self.right_panel = None - self.programs = None - self.default_label = None - self.images = [] - - # Trigger manually since add_extension_content requires loading expression files first - self.add_extension_content() - - self.root.minsize(WIDTH, HEIGHT) - self.root.geometry(f"{WIDTH}x{HEIGHT}") - - def add_extension_content(self): - """Add custom content to the extension UI.""" - - # Main container (horizontal layout) - container = ttk.Frame(self.root, style="PyAEDT.TFrame") - container.grid(row=1, column=0, columnspan=2, sticky="nsew") - - self.root.grid_rowconfigure(1, weight=1) - self.root.grid_columnconfigure(0, weight=1) - - # Left panel (Programs) - left_panel = ttk.Frame(container, width=250, style="PyAEDT.TFrame") - left_panel.grid(row=0, column=0, sticky="ns") - - # Right panel (Extensions) - self.right_panel = ttk.Frame(container, style="PyAEDT.TFrame", relief="solid") - self.right_panel.grid(row=0, column=1, sticky="nsew") - - container.grid_rowconfigure(0, weight=1) - container.grid_columnconfigure(1, weight=1) - - # Load programs and extensions dynamically - self.toolkits = available_toolkits() - self.programs = AEDT_APPLICATIONS - - for i, name in enumerate(AEDT_APPLICATIONS): - btn = ttk.Button(left_panel, text=name, command=lambda n=name: self.load_extensions(n), - style="PyAEDT.TButton") - btn.grid(row=i, column=0, sticky="ew", padx=5, pady=2) - - # Placeholder content - self.default_label = ttk.Label(self.right_panel, text="Select application", - style="PyAEDT.TLabel", - font=("Arial", 12, "bold")) - self.default_label.grid(row=0, column=0, padx=20, pady=20, sticky="nw") - - def load_extensions(self, category: str): - # Clear right panel - for widget in self.right_panel.winfo_children(): - widget.destroy() - - canvas = tkinter.Canvas(self.right_panel, highlightthickness=0) - scrollbar = ttk.Scrollbar(self.right_panel, orient="vertical", - command=canvas.yview) - scroll_frame = ttk.Frame(canvas, style="PyAEDT.TFrame", relief="solid") - - # Apply theme to canvas - self.apply_canvas_theme(canvas) - - # Add mouse wheel scrolling - def _on_mousewheel(event): - canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") - - def _bind_mousewheel(event): - canvas.bind_all("", _on_mousewheel) - - def _unbind_mousewheel(event): - canvas.unbind_all("") - - canvas.bind('', _bind_mousewheel) - canvas.bind('', _unbind_mousewheel) - - scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) - canvas.create_window((0, 0), window=scroll_frame, anchor="nw") - canvas.configure(yscrollcommand=scrollbar.set) - - canvas.grid(row=0, column=0, sticky="nsew") - scrollbar.grid(row=0, column=1, sticky="ns") - - self.right_panel.grid_rowconfigure(0, weight=1) - self.right_panel.grid_columnconfigure(0, weight=1) - - header = ttk.Label(scroll_frame, text=f"{category} Extensions", style="PyAEDT.TLabel", - font=("Arial", 12, "bold")) - header.grid(row=0, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 10)) - - # Checkbox to add to AEDT - add_checkbox = ttk.Checkbutton( - scroll_frame, - text="Add to AEDT", - variable=self.add_to_aedt_var, - style="PyAEDT.TCheckbutton" - ) - add_checkbox.grid(row=1, column=0, sticky="w", padx=10, pady=(0, 10), columnspan=3) - - extensions = list(self.toolkits.get(category, {}).keys()) - options = {} - if extensions: - options["Custom"] = "Custom" - for extension_name in extensions: - options[extension_name] = self.toolkits[category][extension_name].get("name", extension_name) - - else: - options["Custom"] = "Custom" - for index, option in enumerate(options): - row = (index // 3) + 2 - col = index % 3 - - card = ttk.Frame(scroll_frame, style="PyAEDT.TFrame", relief="solid") - card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") - - if option.lower() == "custom": - icon = ttk.Label(card, text="🧩", style="PyAEDT.TLabel", - font=("Segoe UI Emoji", 25)) - icon.pack(padx=10, pady=10) - else: - try: - # Try to find the icon for this specific extension - extension_info = self.toolkits[category][option] - if extension_info.get("icon"): - icon_path = (EXTENSIONS_PATH / category.lower() / - extension_info["icon"]) - else: - icon_path = (EXTENSIONS_PATH / "images" / - "large" / "pyansys.png") - - # Load image and preserve transparency - img = PIL.Image.open(str(icon_path)) - img = img.resize((48, 48), PIL.Image.LANCZOS) - - # Get current theme colors for background - theme_colors = (self.theme.light if self.root.theme == "light" - else self.theme.dark) - - # Create a background with theme color for transparency - if img.mode == 'RGBA' or 'transparency' in img.info: - # Create background with theme color - bg_color = theme_colors["widget_bg"] - # Convert hex to RGB - bg_rgb = tuple(int(bg_color[i:i + 2], 16) for i in (1, 3, 5)) - background = PIL.Image.new('RGBA', img.size, bg_rgb + (255,)) - # Composite the image with background - img = PIL.Image.alpha_composite(background, img.convert('RGBA')) - img = img.convert('RGB') - - photo = PIL.ImageTk.PhotoImage(img) - self.images.append(photo) - icon = ttk.Label(card, image=photo) - icon.pack(padx=10, pady=10) - except Exception: - icon = ttk.Label(card, text="❓", style="PyAEDT.TLabel", - font=("Segoe UI Emoji", 18)) - icon.pack(padx=10, pady=10) - - # Text - label = ttk.Label(card, text=options[option], style="PyAEDT.TLabel", anchor="center") - label.pack(padx=10, pady=10) - - # Click handler for launching extensions - card.bind("", lambda e, cat=category, opt=option: - self.launch_extension(cat, opt)) - icon.bind("", lambda e, cat=category, opt=option: - self.launch_extension(cat, opt)) - label.bind("", lambda e, cat=category, opt=option: - self.launch_extension(cat, opt)) - - def on_right_click(event, cat=category, opt=option): - self.confirm_uninstall(cat, opt) - - card.bind("", on_right_click) - icon.bind("", on_right_click) - label.bind("", on_right_click) - - # Expand columns - for col in range(3): - scroll_frame.grid_columnconfigure(col, weight=1) - - def launch_extension(self, category: str, option: str): - """Unimplemented method to handle launching extensions.""" - if option.lower() == "custom": - script_file, option = self.handle_custom_extension() - icon = EXTENSIONS_PATH / "images" / "large" / "pyansys.png" - else: - script_file = EXTENSIONS_PATH / category.lower() / self.toolkits[category][option]["script"] - icon = EXTENSIONS_PATH / category.lower() / self.toolkits[category][option]["icon"] - - if getattr(self, "add_to_aedt_var", tkinter.BooleanVar(value=True)).get(): - # Install the custom extension - add_script_to_menu( - name=option, - script_file=str(script_file), - product=category.lower(), - executable_interpreter=sys.executable, - personal_lib=self.desktop.personallib, - aedt_version=self.desktop.aedt_version_id, - copy_to_personal_lib=False, - icon_file=str(icon) - ) - - self.desktop.logger.info(f"Extension {option} added successfully.") - - # Refresh toolkit UI - if hasattr(self.desktop, 'odesktop'): - self.desktop.odesktop.RefreshToolkitUI() - - self.desktop.logger.info(f"Launching {str(script_file)}") - self.desktop.logger.info(f"Using interpreter: {str(self.python_interpreter)}") - if not script_file.is_file(): - logger.error(f"{script_file} not found.") - raise FileNotFoundError(f"{script_file} not found.") - subprocess.Popen([self.python_interpreter, str(script_file)], shell=True) - self.desktop.logger.info(f"Finished launching {script_file}.") - - def handle_custom_extension(self): - """Handle custom extension installation.""" - # Ask for script file - script_file = filedialog.askopenfilename( - title="Select Extension Script", - filetypes=[ - ("Python files", "*.py"), - ("Executable files", "*.exe"), - ("All files", "*.*") - ] - ) - - if not script_file: - script_file = EXTENSIONS_PATH / "templates" / "template_get_started.py" - else: - script_file = Path(script_file) - - extension_name = "Custom Extension" - - if getattr(self, "add_to_aedt_var", tkinter.BooleanVar(value=True)).get(): - # Ask for extension name - extension_name = simpledialog.askstring( - "Extension Name", - "Enter a name for this extension:" - ) - - if not extension_name: - extension_name = "Custom Extension" - - return script_file, extension_name - - def apply_canvas_theme(self, canvas): - """Apply theme to a specific canvas widget.""" - theme_colors = (self.theme.light if self.root.theme == "light" - else self.theme.dark) - canvas.configure( - background=theme_colors["pane_bg"], - highlightbackground=theme_colors["tab_border"], - highlightcolor=theme_colors["tab_border"], - ) - - def toggle_theme(self): - """Toggle between light and dark themes and update all canvases.""" - super().toggle_theme() - - # Update all canvas elements in the UI - for widget in self._ExtensionCommon__find_all_widgets(self.root, tkinter.Canvas): - self.apply_canvas_theme(widget) - - def confirm_uninstall(self, category, option): - if option.lower() == "custom": - # Ask for extension name - option = simpledialog.askstring( - "Extension Name", - "Extension name to uninstall:" - ) - - if not option: - messagebox.showinfo("Information", "Wrong extension name.") - return - - answer = messagebox.askyesno( - "Uninstall extension", - f"Do you want to uninstall '{option}' in {category}?" - ) - - if answer: - try: - remove_script_from_menu(desktop_object=self.desktop, name=option, product=category) - # Refresh toolkit UI - if hasattr(self.desktop, 'odesktop'): - self.desktop.odesktop.RefreshToolkitUI() - except Exception: - messagebox.showerror("Error", f"Extension could not be uninstalled") - - -if __name__ == "__main__": # pragma: no cover - # Open UI - extension: ExtensionCommon = ExtensionManager(withdraw=False) - - tkinter.mainloop() diff --git a/tests/unit/extensions/test_extension_manager.py b/tests/unit/extensions/test_extension_manager.py new file mode 100644 index 00000000000..253c1aa7f7b --- /dev/null +++ b/tests/unit/extensions/test_extension_manager.py @@ -0,0 +1,104 @@ +# -*- 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 tkinter +from unittest.mock import MagicMock +from unittest.mock import PropertyMock +from unittest.mock import patch + +import pytest + +from ansys.aedt.core.extensions.installer.extension_manager import ExtensionManager +from ansys.aedt.core.extensions.misc import ExtensionCommon + + +@pytest.fixture +def mock_aedt_app(): + """Fixture que crea una aplicación AEDT falsa.""" + mock_desktop = MagicMock() + mock_desktop.aedt_version_id = "2025.1" + mock_desktop.personallib = "/tmp/personal" + mock_desktop.logger = MagicMock() + mock_desktop.odesktop = MagicMock() + + with patch.object(ExtensionCommon, "desktop", new_callable=PropertyMock) as mock_desktop_property: + mock_desktop_property.return_value = mock_desktop + yield mock_desktop + + +@patch("ansys.aedt.core.extensions.misc.Desktop") +@patch("ansys.aedt.core.extensions.customize_automation_tab.available_toolkits") +def test_extension_manager_init(mock_toolkits, mock_desktop, mock_aedt_app): + """Extension manager initialization.""" + mock_desktop.return_value = MagicMock() + mock_toolkits.return_value = {"HFSS": {"MyExt": {"name": "My Extension", "script": "dummy.py", "icon": None}}} + + extension = ExtensionManager(withdraw=True) + + assert extension.root.title() == "Extension Manager" + assert extension.root.theme == "light" + assert extension.toolkits is not None + + extension.root.destroy() + + +@patch("ansys.aedt.core.extensions.misc.Desktop") +@patch("ansys.aedt.core.extensions.customize_automation_tab.available_toolkits") +def test_extension_manager_load_extensions(mock_toolkits, mock_desktop, mock_aedt_app): + """Load one category.""" + mock_desktop.return_value = MagicMock() + mock_toolkits.return_value = {"HFSS": {"MyExt": {"name": "My Extension", "script": "dummy.py", "icon": None}}} + + extension = ExtensionManager(withdraw=True) + extension.load_extensions("HFSS") + + canvas = next(w for w in extension.right_panel.winfo_children() if isinstance(w, tkinter.Canvas)) + + scroll_frame = canvas.winfo_children()[0].children.values() + found = any(isinstance(w, tkinter.ttk.Label) and "HFSS Extensions" in w.cget("text") for w in scroll_frame) + assert found + + extension.root.destroy() + + +@patch("ansys.aedt.core.extensions.misc.Desktop") +@patch("ansys.aedt.core.extensions.customize_automation_tab.available_toolkits") +def test_extension_manager_custom_extension_cancel(mock_toolkits, mock_desktop, mock_aedt_app): + mock_desktop.return_value = MagicMock() + mock_toolkits.return_value = {"HFSS": {}} + + extension = ExtensionManager(withdraw=True) + + # Checkbox + extension.add_to_aedt_var.set(True) + + with ( + patch("tkinter.filedialog.askopenfilename", return_value=""), + patch("tkinter.simpledialog.askstring", return_value=None), + ): + script_file, name = extension.handle_custom_extension() + assert "template_get_started.py" in str(script_file) + assert name == "Custom Extension" + + extension.root.destroy() From 5f0d3ae4bf55e4aacce714655c2eda216f1da167 Mon Sep 17 00:00:00 2001 From: Samuelopez-ansys Date: Tue, 15 Jul 2025 08:53:38 +0200 Subject: [PATCH 05/61] Add documentation --- doc/source/Getting_started/Installation.rst | 9 ++++++--- doc/source/Resources/my_custom_extension.png | Bin 69537 -> 0 bytes doc/source/Resources/toolkit_manager_1.png | Bin 86957 -> 166373 bytes doc/source/Resources/toolkit_manager_2.png | Bin 5105 -> 10919 bytes doc/source/Resources/toolkit_manager_3.png | Bin 7582 -> 0 bytes doc/source/User_guide/extensions.rst | 16 +++++----------- 6 files changed, 11 insertions(+), 14 deletions(-) delete mode 100644 doc/source/Resources/my_custom_extension.png delete mode 100644 doc/source/Resources/toolkit_manager_3.png diff --git a/doc/source/Getting_started/Installation.rst b/doc/source/Getting_started/Installation.rst index 7add2c97946..3e91bb91673 100644 --- a/doc/source/Getting_started/Installation.rst +++ b/doc/source/Getting_started/Installation.rst @@ -49,7 +49,7 @@ If you have installation problems, visit :ref:`Troubleshooting`. Extension manager ~~~~~~~~~~~~~~~~~ -The user can install or uninstall automated workflows using the extension manager. +The user can launch automated workflows and add them to the AEDT Automation tab using the extension manager. There are three options: - **Pre-installed extensions** already available in the PyAEDT library. @@ -64,13 +64,16 @@ See `Extension Manager `_. diff --git a/doc/source/Resources/my_custom_extension.png b/doc/source/Resources/my_custom_extension.png deleted file mode 100644 index 90ece84f848735537a8c3572392b271492bad002..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69537 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

ebe|fYn-TbQ06e86BRz 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