Skip to content

Commit e3a419d

Browse files
SMoraisAnsyspyansys-ci-botMaxJPRey
authored
REFACTOR: Extension architecture using common class (#6238)
Co-authored-by: pyansys-ci-bot <[email protected]> Co-authored-by: Maxime Rey <[email protected]>
1 parent 629efaa commit e3a419d

File tree

8 files changed

+626
-235
lines changed

8 files changed

+626
-235
lines changed

doc/changelog.d/6238.miscellaneous.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Extension architecture using common class

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

Lines changed: 206 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,44 +24,233 @@
2424

2525
"""Miscellaneous Methods for PyAEDT workflows."""
2626

27+
from __future__ import annotations
28+
29+
from abc import abstractmethod
2730
import argparse
2831
import os
2932
from pathlib import Path
3033
import sys
31-
34+
import tkinter
35+
from tkinter import ttk
36+
from tkinter.messagebox import showerror
37+
from typing import List
38+
from typing import Optional
39+
from typing import Tuple
40+
from typing import Type
41+
from typing import Union
42+
43+
import PIL.Image
44+
import PIL.ImageTk
45+
46+
import ansys.aedt.core.extensions
3247
from ansys.aedt.core.internal.aedt_versions import aedt_versions
3348

49+
NO_ACTIVE_PROJECT = "No active project"
50+
MOON = "\u2600"
51+
SUN = "\u263d"
52+
3453

3554
def get_process_id():
3655
"""Get process ID from environment variable."""
37-
aedt_process_id = None
38-
if os.getenv("PYAEDT_SCRIPT_PROCESS_ID", None): # pragma: no cover
39-
aedt_process_id = int(os.getenv("PYAEDT_SCRIPT_PROCESS_ID"))
40-
return aedt_process_id
56+
value = os.getenv("PYAEDT_SCRIPT_PROCESS_ID")
57+
return int(value) if value is not None else None
4158

4259

4360
def get_port():
4461
"""Get gRPC port from environment variable."""
45-
port = 0
46-
if "PYAEDT_SCRIPT_PORT" in os.environ:
47-
port = int(os.environ["PYAEDT_SCRIPT_PORT"])
48-
return port
62+
res = int(os.getenv("PYAEDT_SCRIPT_PORT", 0))
63+
return res
4964

5065

5166
def get_aedt_version():
5267
"""Get AEDT release from environment variable."""
53-
version = aedt_versions.current_version
54-
if "PYAEDT_SCRIPT_VERSION" in os.environ:
55-
version = os.environ["PYAEDT_SCRIPT_VERSION"]
56-
return version
68+
res = os.getenv("PYAEDT_SCRIPT_VERSION", aedt_versions.current_version)
69+
return res
5770

5871

5972
def is_student():
6073
"""Get if AEDT student is opened from environment variable."""
61-
student_version = False
62-
if "PYAEDT_STUDENT_VERSION" in os.environ: # pragma: no cover
63-
student_version = False if os.environ["PYAEDT_STUDENT_VERSION"] == "False" else True
64-
return student_version
74+
res = os.getenv("PYAEDT_STUDENT_VERSION", "False") != "False"
75+
return res
76+
77+
78+
class ExtensionCommon:
79+
def __init__(
80+
self,
81+
title: str,
82+
theme_color: str = "light",
83+
withdraw: bool = False,
84+
add_custom_content: bool = True,
85+
toggle_row: Optional[int] = None,
86+
toggle_column: Optional[int] = None,
87+
):
88+
"""Create and initialize a themed Tkinter UI window.
89+
90+
This function creates a Tkinter root window, applies a theme, sets the
91+
application icon, and configures error handling behavior. It also allows for
92+
optional withdrawal of the window, i.e. keeping it hidden.
93+
94+
Parameters:
95+
----------
96+
title : str
97+
The title of the main window.
98+
theme_color: str, optional
99+
The theme color to apply to the UI. Options are "light" or "dark". Default is "light".
100+
withdraw : bool, optional
101+
If True, the main window is hidden. Default is ``False``.
102+
add_custom_content : bool, optional
103+
If True, the method `add_extension_content` is called to add custom content to the UI.
104+
toggle_row : int, optional
105+
The row index where the toggle button will be placed.
106+
toggle_column : int, optional
107+
The column index where the toggle button will be placed.
108+
"""
109+
if theme_color not in ["light", "dark"]:
110+
raise ValueError(f"Invalid theme color: {theme_color}. Use 'light' or 'dark'.")
111+
112+
self.root = self.__init_root(title, withdraw)
113+
self.style = ttk.Style()
114+
self.theme = ExtensionTheme()
115+
self.__apply_theme(theme_color)
116+
if toggle_row is not None and toggle_column is not None:
117+
self.add_toggle_theme_button(toggle_row=toggle_row, toggle_column=toggle_column)
118+
if add_custom_content:
119+
self.add_extension_content()
120+
121+
def add_toggle_theme_button(self, toggle_row, toggle_column):
122+
"""Create a button to toggle between light and dark themes."""
123+
button_frame = ttk.Frame(
124+
self.root, style="PyAEDT.TFrame", relief=tkinter.SUNKEN, borderwidth=2, name="theme_button_frame"
125+
)
126+
button_frame.grid(row=toggle_row, column=toggle_column, sticky="e", padx=10, pady=10)
127+
change_theme_button = ttk.Button(
128+
button_frame,
129+
width=20,
130+
text=SUN,
131+
command=self.toggle_theme,
132+
style="PyAEDT.TButton",
133+
name="theme_toggle_button",
134+
)
135+
change_theme_button.grid(row=0, column=0)
136+
137+
def toggle_theme(self):
138+
"""Toggle between light and dark themes."""
139+
if self.root.theme == "light":
140+
self.__apply_theme("dark")
141+
elif self.root.theme == "dark":
142+
self.__apply_theme("light")
143+
else:
144+
raise ValueError(f"Unknown theme: {self.root.theme}. Use 'light' or 'dark'.")
145+
146+
def __init_root(self, title: str, withdraw: bool) -> tkinter.Tk:
147+
"""Initialize the Tkinter root window with error handling and icon."""
148+
149+
def report_callback_exception(self, exc, val, tb):
150+
"""Custom exception showing an error message."""
151+
showerror("Error", message=f"{val} \n {tb}")
152+
153+
def report_callback_exception_withdraw(self, exc, val, tb):
154+
"""Custom exception that raises the error without showing a message box."""
155+
raise val
156+
157+
if withdraw:
158+
tkinter.Tk.report_callback_exception = report_callback_exception_withdraw
159+
else:
160+
tkinter.Tk.report_callback_exception = report_callback_exception
161+
162+
root = tkinter.Tk()
163+
root.title(title)
164+
if withdraw:
165+
root.withdraw()
166+
167+
# Load and set the logo for the main window
168+
if not withdraw:
169+
icon_path = Path(ansys.aedt.core.extensions.__path__[0]) / "images" / "large" / "logo.png"
170+
im = PIL.Image.open(icon_path)
171+
photo = PIL.ImageTk.PhotoImage(im, master=root)
172+
root.iconphoto(True, photo)
173+
174+
return root
175+
176+
def __apply_theme(self, theme_color: str):
177+
"""Apply a theme to the UI."""
178+
theme_colors_dict = self.theme.light if theme_color == "light" else self.theme.dark
179+
self.root.configure(background=theme_colors_dict["widget_bg"])
180+
for widget in self.__find_all_widgets(self.root, tkinter.Text):
181+
widget.configure(
182+
background=theme_colors_dict["pane_bg"],
183+
foreground=theme_colors_dict["text"],
184+
font=self.theme.default_font,
185+
)
186+
187+
button_text = None
188+
if theme_color == "light":
189+
self.theme.apply_light_theme(self.style)
190+
self.root.theme = "light"
191+
button_text = SUN
192+
else:
193+
self.theme.apply_dark_theme(self.style)
194+
self.root.theme = "dark"
195+
button_text = MOON
196+
197+
try:
198+
self.change_theme_button.config(text=button_text)
199+
except KeyError:
200+
# Handle the case where the button is not yet created
201+
pass
202+
203+
def __find_all_widgets(
204+
self, widget: tkinter.Widget, widget_classes: Union[Type[tkinter.Widget], Tuple[Type[tkinter.Widget], ...]]
205+
) -> List[tkinter.Widget]:
206+
"""Return a list of all widgets of given type(s) in the widget hierarchy."""
207+
res = []
208+
if isinstance(widget, widget_classes):
209+
res.append(widget)
210+
for child in widget.winfo_children():
211+
res.extend(self.__find_all_widgets(child, widget_classes))
212+
return res
213+
214+
@property
215+
def change_theme_button(self) -> tkinter.Widget:
216+
"""Return the theme toggle button."""
217+
res = self.root.nametowidget("theme_button_frame.theme_toggle_button")
218+
return res
219+
220+
@property
221+
def browse_button(self) -> tkinter.Widget:
222+
"""Return the browse button."""
223+
res = self.root.nametowidget("browse_button")
224+
return res
225+
226+
@property
227+
def desktop(self) -> ansys.aedt.core.Desktop:
228+
res = ansys.aedt.core.Desktop(
229+
new_desktop=False,
230+
version=get_aedt_version(),
231+
port=get_port(),
232+
aedt_process_id=get_process_id(),
233+
student_version=is_student(),
234+
)
235+
return res
236+
237+
@property
238+
def active_project_name(self) -> str:
239+
"""Return the name of the active project."""
240+
res = NO_ACTIVE_PROJECT
241+
active_project = self.desktop.active_project()
242+
if active_project:
243+
res = active_project.GetName()
244+
return res
245+
246+
@abstractmethod
247+
def add_extension_content(self):
248+
"""Add content to the extension UI.
249+
250+
This method should be implemented by subclasses to add specific content
251+
to the extension UI.
252+
"""
253+
raise NotImplementedError("Subclasses must implement this method.")
65254

66255

67256
def create_default_ui(title, withdraw=False):

0 commit comments

Comments
 (0)