Skip to content

REFACTOR: Extension architecture using common class #6238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e164176
REFACTOR: Add extension common class
SMoraisAnsys Jun 6, 2025
aec2d0e
REFACTOR: Extension template use common extension
SMoraisAnsys Jun 6, 2025
c4d5707
REFACTOR: Remove duplicate and add constants
SMoraisAnsys Jun 6, 2025
81fb647
TESTS: Add tests for ExtensionCommon
SMoraisAnsys Jun 6, 2025
e661b62
REFACTOR: Add desktop property
SMoraisAnsys Jun 6, 2025
8f3b01c
FIX: Argument typo
SMoraisAnsys Jun 6, 2025
4d39d0a
chore: adding changelog file 6238.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Jun 6, 2025
a1e0462
FIX: Ensure image is associated to right context
SMoraisAnsys Jun 6, 2025
c9d42b5
TESTS: Add tkinter teadown to ensure safe tests
SMoraisAnsys Jun 6, 2025
8baa02c
Update src/ansys/aedt/core/extensions/misc.py
SMoraisAnsys Jun 10, 2025
5c0c136
TESTS: Add unit tests
SMoraisAnsys Jun 10, 2025
407d07c
REFACTOR: Extension architecture
SMoraisAnsys Jun 10, 2025
3e82a02
TESTS: Add system tests
SMoraisAnsys Jun 10, 2025
9457b36
Merge branch 'refactor/extension-architecture' of github.com:ansys/py…
SMoraisAnsys Jun 10, 2025
17db24b
TESTS: Add system test for template
SMoraisAnsys Jun 10, 2025
ab296fb
Merge branch 'main' into refactor/extension-architecture
SMoraisAnsys Jun 10, 2025
43f4e1f
CHORE: cleanup
SMoraisAnsys Jun 10, 2025
5039a9d
Merge branch 'refactor/extension-architecture' of github.com:ansys/py…
SMoraisAnsys Jun 10, 2025
ef5c8c1
CHORE: cleanup
SMoraisAnsys Jun 10, 2025
49cbdec
FIX: Codacy feedback
SMoraisAnsys Jun 11, 2025
dd03647
Merge branch 'main' into refactor/extension-architecture
SMoraisAnsys Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/6238.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extension architecture using common class
231 changes: 213 additions & 18 deletions src/ansys/aedt/core/extensions/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,239 @@

"""Miscellaneous Methods for PyAEDT workflows."""

from __future__ import annotations

from abc import abstractmethod
import argparse
import os
from pathlib import Path
import sys

import tkinter
from tkinter import ttk
from tkinter.messagebox import showerror
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union

import PIL.Image
import PIL.ImageTk

import ansys.aedt.core.extensions
from ansys.aedt.core.internal.aedt_versions import aedt_versions

NO_ACTIVE_PROJECT = "No active project"
SUN = "\u263d"
MOON = "\u2600"


def get_process_id():
"""Get process ID from environment variable."""
aedt_process_id = None
if os.getenv("PYAEDT_SCRIPT_PROCESS_ID", None): # pragma: no cover
aedt_process_id = int(os.getenv("PYAEDT_SCRIPT_PROCESS_ID"))
return aedt_process_id
value = os.getenv("PYAEDT_SCRIPT_PROCESS_ID")
return int(value) if value is not None else None


def get_port():
"""Get gRPC port from environment variable."""
port = 0
if "PYAEDT_SCRIPT_PORT" in os.environ:
port = int(os.environ["PYAEDT_SCRIPT_PORT"])
return port
res = int(os.getenv("PYAEDT_SCRIPT_PORT", 0))
return res


def get_aedt_version():
"""Get AEDT release from environment variable."""
version = aedt_versions.current_version
if "PYAEDT_SCRIPT_VERSION" in os.environ:
version = os.environ["PYAEDT_SCRIPT_VERSION"]
return version
res = os.getenv("PYAEDT_SCRIPT_VERSION", aedt_versions.current_version)
return res


def is_student():
"""Get if AEDT student is opened from environment variable."""
student_version = False
if "PYAEDT_STUDENT_VERSION" in os.environ: # pragma: no cover
student_version = False if os.environ["PYAEDT_STUDENT_VERSION"] == "False" else True
return student_version
res = os.getenv("PYAEDT_STUDENT_VERSION", "False") != "False"
return res


class ExtensionCommon:
def __init__(
self,
title: str,
theme_color: str = "light",
withdraw: bool = False,
add_custom_content: bool = True,
toggle_row: Optional[int] = None,
toggle_column: Optional[int] = None,
):
"""Create and initialize a themed Tkinter UI window.

This function creates a Tkinter root window, applies a theme, sets the
application icon, and configures error handling behavior. It also allows for
optional withdrawal of the window, i.e. keeping it hidden.

Parameters:
----------
title : str
The title of the main window.
theme_color: str, optional
The theme color to apply to the UI. Options are "light" or "dark". Default is "light".
withdraw : bool, optional
If True, the main window is hidden. Default is False.
add_custom_content : bool, optional
If True, the method `add_extension_content` is called to add custom content to the UI.
toggle_row : int, optional
The row index where the toggle button will be placed.
toggle_column : int, optional
The column index where the toggle button will be placed.
"""
self.root = self.__init_root(title, withdraw)
self.style = ttk.Style()
self.theme = self.__init_theme(theme_color)
if toggle_row is not None and toggle_column is not None:
self.add_toggle_theme_button(toggle_row=toggle_row, toggle_column=toggle_column)
if add_custom_content:
self.add_extension_content()

def add_toggle_theme_button(self, toggle_row, toggle_column):
"""Create a button to toggle between light and dark themes."""
button_frame = ttk.Frame(
self.root, style="PyAEDT.TFrame", relief=tkinter.SUNKEN, borderwidth=2, name="theme_button_frame"
)
button_frame.grid(row=toggle_row, column=toggle_column, sticky="e", padx=10, pady=10)
change_theme_button = ttk.Button(
button_frame,
width=20,
text=SUN,
command=self.toggle_theme,
style="PyAEDT.TButton",
name="theme_toggle_button",
)
change_theme_button.grid(row=0, column=0)

def toggle_theme(self):
"""Toggle between light and dark themes."""
if self.root.theme == "light":
self.__apply_dark_theme()
self.root.theme = "dark"
else:
self.__apply_light_theme()
self.root.theme = "light"

def __init_root(self, title: str, withdraw: bool) -> tkinter.Tk:
"""Initialize the Tkinter root window with error handling and icon."""

def report_callback_exception(self, exc, val, tb):
"""Custom exception showing an error message."""
showerror("Error", message=str(val))

Check warning on line 147 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L147

Added line #L147 was not covered by tests

def report_callback_exception_withdraw(self, exc, val, tb):
"""Custom exception that raises the error without showing a message box."""
raise val

Check warning on line 151 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L151

Added line #L151 was not covered by tests

if withdraw:
tkinter.Tk.report_callback_exception = report_callback_exception_withdraw
else:
tkinter.Tk.report_callback_exception = report_callback_exception

Check warning on line 156 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L156

Added line #L156 was not covered by tests

root = tkinter.Tk()
root.title(title)
if withdraw:
root.withdraw()

# Load and set the logo for the main window
if not withdraw:
icon_path = Path(ansys.aedt.core.extensions.__path__[0]) / "images" / "large" / "logo.png"
im = PIL.Image.open(icon_path)
photo = PIL.ImageTk.PhotoImage(im, master=root)
root.iconphoto(True, photo)

Check warning on line 168 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L165-L168

Added lines #L165 - L168 were not covered by tests

return root

def __init_theme(self, theme_color: str) -> ExtensionTheme:
"""Initialize the theme for the UI."""
from ansys.aedt.core.extensions.misc import ExtensionTheme

theme = ExtensionTheme()
if theme_color == "light":
theme.apply_light_theme(self.style)
self.root.configure(bg=theme.light["widget_bg"])
self.root.theme = "light"
elif theme_color == "dark":
theme.apply_dark_theme(self.style)
self.root.configure(bg=theme.dark["widget_bg"])
self.root.theme = "dark"
else:
raise ValueError(f"Unknown theme: {theme}. Use 'light' or 'dark'.")
return theme

def __apply_light_theme(self):
"""Apply the light theme on the UI."""
self.root.configure(bg=self.theme.light["widget_bg"])
for widget in self.__find_all_widgets(self.root, tkinter.Text):
widget.configure(

Check warning on line 193 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L193

Added line #L193 was not covered by tests
bg=self.theme.light["pane_bg"], foreground=self.theme.light["text"], font=self.theme.default_font
)
self.theme.apply_light_theme(self.style)
self.change_theme_button.config(text=SUN)

def __apply_dark_theme(self):
"""Apply the dark theme on the UI."""
self.root.configure(bg=self.theme.dark["widget_bg"])
for widget in self.__find_all_widgets(self.root, tkinter.Text):
widget.configure(

Check warning on line 203 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L203

Added line #L203 was not covered by tests
bg=self.theme.dark["pane_bg"], foreground=self.theme.dark["text"], font=self.theme.default_font
)
self.theme.apply_dark_theme(self.style)
self.change_theme_button.config(text=MOON)

def __find_all_widgets(
self, widget: tkinter.Widget, widget_classes: Union[Type[tkinter.Widget], Tuple[Type[tkinter.Widget], ...]]
) -> List[tkinter.Widget]:
"""Return a list of all widgets of given type(s) in the widget hierarchy."""
res = []
if isinstance(widget, widget_classes):
res.append(widget)

Check warning on line 215 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L215

Added line #L215 was not covered by tests
for child in widget.winfo_children():
res.extend(self.__find_all_widgets(child, widget_classes))
return res

@property
def change_theme_button(self) -> tkinter.Widget:
"""Return the theme toggle button."""
res = self.root.nametowidget("theme_button_frame.theme_toggle_button")
return res

@property
def browse_button(self) -> tkinter.Widget:
"""Return the browse button."""
res = self.root.nametowidget("browse_button")
return res

Check warning on line 230 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L230

Added line #L230 was not covered by tests

@property
def desktop(self) -> ansys.aedt.core.Desktop:
res = ansys.aedt.core.Desktop(
new_desktop_session=False,
version=get_aedt_version(),
port=get_port(),
aedt_process_id=get_process_id(),
student_version=is_student(),
)
return res

@property
def active_project_name(self) -> str:
"""Return the name of the active project."""
res = NO_ACTIVE_PROJECT
active_project = self.desktop.active_project()
if active_project:
res = active_project.GetName()

Check warning on line 249 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L249

Added line #L249 was not covered by tests
return res

@abstractmethod
def add_extension_content(self):
"""Add content to the extension UI.

This method should be implemented by subclasses to add specific content
to the extension UI.
"""
raise NotImplementedError("Subclasses must implement this method.")

Check warning on line 259 in src/ansys/aedt/core/extensions/misc.py

View check run for this annotation

Codecov / codecov/patch

src/ansys/aedt/core/extensions/misc.py#L259

Added line #L259 was not covered by tests


def create_default_ui(title, withdraw=False):
Expand Down Expand Up @@ -96,7 +291,7 @@
# Load the logo for the main window
icon_path = Path(ansys.aedt.core.extensions.__path__[0]) / "images" / "large" / "logo.png"
im = PIL.Image.open(icon_path)
photo = PIL.ImageTk.PhotoImage(im)
photo = PIL.ImageTk.PhotoImage(im, master=root)

# Set the icon for the main window
root.iconphoto(True, photo)
Expand Down
Loading
Loading