Skip to content

Commit 2acb028

Browse files
Added plot style context managers to all plot methods (#208)
2 parents c449aac + 1afa3b0 commit 2acb028

27 files changed

Lines changed: 1280 additions & 572 deletions

Python_Engine/Python/src/python_toolkit/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,3 @@
1818
if os.name == "nt":
1919
# override "HOME" in case this is set to something other than default for windows
2020
os.environ["HOME"] = (Path("C:/Users/") / getpass.getuser()).as_posix()
21-
22-
23-
# set plotting style for modules within this toolkit
24-
plt.style.use(BHOM_DIRECTORY / "bhom.mplstyle")

Python_Engine/Python/src/python_toolkit/bhom/bhom.mplstyle renamed to Python_Engine/Python/src/python_toolkit/bhom.mplstyle

File renamed without changes.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Default matplotlib settings for this toolkit.
2+
3+
# Text
4+
text.color: white
5+
6+
# Set custom colors. All colors are in web style hex format.
7+
axes.prop_cycle: cycler('color', ['702F8A', 'E63187', '00A9E0', 'FFCF04', '6CC24E', 'EB671C', '00A499', 'D50032', '24135F', '6D104E', '006DA8', 'D06A13', '5D822D', 'F0AC1B', '1C3660', 'BC204B', '8F72B0', 'FCD16D', '8DB9CA', 'EE7837', 'AFC1A2', 'B72B77', 'A0D2C9', 'E6484D'])
8+
9+
# Face settings
10+
axes.facecolor: black
11+
axes.edgecolor: white
12+
13+
# Style spines
14+
axes.linewidth: 0.8
15+
axes.spines.top: False
16+
axes.spines.left: True
17+
axes.spines.right: False
18+
axes.spines.bottom: True
19+
20+
# Set line styling for line plots
21+
lines.linewidth: 1
22+
lines.solid_capstyle: round
23+
lines.dash_capstyle: round
24+
25+
# Grid style
26+
axes.axisbelow: True
27+
axes.grid: true
28+
axes.grid.axis: both
29+
grid.color: 958B82
30+
grid.linestyle: --
31+
grid.linewidth: 0.5
32+
33+
# Setting font sizes and spacing
34+
axes.labelsize: medium
35+
axes.labelweight: semibold
36+
axes.labelcolor: white
37+
axes.ymargin: 0.1
38+
font.family: sans-serif
39+
font.sans-serif: Segoe UI
40+
font.size: 10
41+
xtick.labelsize: medium
42+
xtick.labelcolor: white
43+
xtick.major.pad: 3.5
44+
ytick.labelsize: medium
45+
ytick.labelcolor: white
46+
ytick.major.pad: 3.5
47+
48+
# date formatter
49+
date.autoformatter.day: %b-%d
50+
date.autoformatter.hour: %b-%d %H
51+
date.autoformatter.microsecond: %M:%S.%f
52+
date.autoformatter.minute: %d %H:%M
53+
date.autoformatter.month: %b
54+
date.autoformatter.second: %H:%M:%S
55+
date.autoformatter.year: %Y
56+
57+
# Title
58+
axes.titlelocation: left
59+
axes.titlepad: 6
60+
axes.titlesize: large
61+
axes.titleweight: bold
62+
63+
# Remove major and minor ticks except for on the x-axis.
64+
xtick.color: white
65+
xtick.major.size: 3
66+
xtick.minor.size: 2
67+
ytick.color: white
68+
ytick.major.size: 3
69+
ytick.minor.size: 2
70+
71+
# Set spacing for figure and also DPI.
72+
figure.subplot.left: 0.08
73+
figure.subplot.right: 0.95
74+
figure.subplot.bottom: 0.07
75+
figure.figsize: 12, 5
76+
figure.dpi: 150
77+
figure.facecolor: black
78+
79+
# Properties for saving the figure. Ensure a high DPI when saving so we have a good resolution.
80+
savefig.dpi: 300
81+
savefig.facecolor: black
82+
savefig.bbox: tight
83+
savefig.pad_inches: 0.2
84+
85+
# Legend Styling
86+
legend.framealpha: 0
87+
legend.frameon: False
88+
legend.facecolor: inherit

Python_Engine/Python/src/python_toolkit/bhom_tkinter/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
WarningBox,
2121
)
2222

23+
from .theming import (
24+
TclTheme,
25+
ThemeManager,
26+
LIGHT,
27+
DARK,
28+
)
29+
2330
__all__ = [
2431
"BHoMBaseWidget",
2532
"PackingOptions",
@@ -38,4 +45,6 @@
3845
"LandingPage",
3946
"ProcessingWindow",
4047
"WarningBox",
48+
"TclTheme",
49+
"ThemeManager"
4150
]

Python_Engine/Python/src/python_toolkit/bhom_tkinter/bhom_base_window.py

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from python_toolkit.bhom_tkinter.widgets._widgets_base import BHoMBaseWidget
2525
from python_toolkit.bhom_tkinter.widgets.button import Button
2626
import python_toolkit
27-
from theming.theme import ThemeManager
27+
from python_toolkit.bhom_tkinter.theming.theme import ThemeManager
2828

2929
class BHoMBaseWindow(tk.Tk):
3030
"""
@@ -44,13 +44,15 @@ def __init__(
4444
show_submit: bool = True,
4545
submit_text: str = "Submit",
4646
submit_command: Optional[Callable] = None,
47+
close_on_submit: bool = True,
4748
show_close: bool = True,
4849
close_text: str = "Close",
4950
close_command: Optional[Callable] = None,
5051
on_close_window: Optional[Callable] = None,
5152
theme_mode:str = "auto",
52-
widgets: List[BHoMBaseWidget] = [],
53+
widgets: Optional[List[BHoMBaseWidget]] = None,
5354
top_most: bool = True,
55+
fullscreen: bool = False,
5456
buttons_side: Literal["left", "right"] = "right",
5557
grid_dimensions: Optional[tuple[int, int]] = None,
5658
**kwargs
@@ -77,6 +79,7 @@ def __init__(
7779
on_close_window (callable, optional): Command when X is pressed.
7880
theme_path (Path, optional): Path to custom TCL theme file. If None, uses default style.tcl.
7981
theme_mode (str): Theme mode - "light", "dark", or "auto" to detect from system (default: "auto").
82+
fullscreen (bool): Whether the window starts in fullscreen mode (default: False).
8083
buttons_side (str): Side for buttons - "left" or "right" (default: "right").
8184
grid_dimensions (tuple[int, int], optional): If provided, configures content area with specified rows and columns for grid layout.
8285
**kwargs
@@ -91,7 +94,10 @@ def __init__(
9194
if self.top_most:
9295
self.attributes("-topmost", True)
9396

94-
self.widgets = widgets
97+
self.fullscreen = fullscreen
98+
99+
# Avoid sharing widget instances across windows/runs.
100+
self.widgets = list(widgets) if widgets is not None else []
95101

96102
# Hide window during setup to prevent flash
97103
self.withdraw()
@@ -107,6 +113,7 @@ def __init__(
107113
self.fixed_height = height
108114
self.center_on_screen = center_on_screen
109115
self.submit_command = submit_command
116+
self.close_on_submit = close_on_submit
110117
self.close_command = close_command
111118
self.result = None
112119
self._is_exiting = False
@@ -120,6 +127,7 @@ def __init__(
120127
self._auto_fit_height = height is None
121128
self._post_show_size_applied = False
122129
self.grid_dimensions = grid_dimensions
130+
self._cached_widget_values: dict[str, object] = {}
123131

124132
# Handle window close (X button)
125133
self.protocol("WM_DELETE_WINDOW", lambda: self._on_close_window(on_close_window))
@@ -337,9 +345,13 @@ def _build_banner(self, parent: ttk.Frame, title: str, logo_path: Optional[Path]
337345
from PIL import Image, ImageTk
338346
img = Image.open(logo_path)
339347
img.thumbnail((80, 80), Image.Resampling.LANCZOS)
340-
self.logo_image = ImageTk.PhotoImage(img)
348+
# Bind image to this root explicitly to avoid stale image handles
349+
# when previous runs failed and tore down a different Tk interpreter.
350+
self.logo_image = ImageTk.PhotoImage(img, master=self)
341351
logo_label = Label(logo_container, image=self.logo_image)
342352
logo_label.pack(fill=tk.BOTH, expand=True)
353+
except tk.TclError:
354+
pass
343355
except ImportError:
344356
pass # PIL not available, skip logo
345357

@@ -459,6 +471,13 @@ def _apply_sizing(self) -> None:
459471
else:
460472
final_height = max(self.min_height, required_height)
461473

474+
# Fullscreen overrides normal sizing/positioning
475+
if self.fullscreen:
476+
self.attributes("-fullscreen", True)
477+
self.after(0, self._show_window_with_styling)
478+
self._is_resizing = False
479+
return
480+
462481
# Position
463482
if self.center_on_screen and not self._has_been_shown:
464483
screen_width = self.winfo_screenwidth()
@@ -567,11 +586,30 @@ def _exit(self, result: str, callback: Optional[Callable] = None) -> None:
567586
except Exception as ex:
568587
print(f"Warning: Exit callback raised an exception: {ex}")
569588
finally:
589+
# Capture values while widgets still exist so `get()` remains usable
590+
# after root teardown.
591+
self._cached_widget_values = self._collect_widget_values()
570592
self.destroy_root()
571593

572594
def _on_submit(self) -> None:
573595
"""Handle submit button click."""
574-
self._exit("submit", self.submit_command)
596+
if self.close_on_submit:
597+
self._exit("submit", self.submit_command)
598+
return
599+
600+
self.result = "submit"
601+
try:
602+
if self.submit_command:
603+
self.submit_command()
604+
except tk.TclError as ex:
605+
message = str(ex).lower()
606+
if not ("image" in message and "doesn't exist" in message):
607+
print(f"Warning: Exit callback raised an exception: {ex}")
608+
except Exception as ex:
609+
print(f"Warning: Exit callback raised an exception: {ex}")
610+
finally:
611+
self._cached_widget_values = self._collect_widget_values()
612+
575613

576614
def _on_close(self) -> None:
577615
"""Handle close button click."""
@@ -581,6 +619,30 @@ def _on_close_window(self, callback: Optional[Callable]) -> None:
581619
"""Handle window X button click."""
582620
self._exit("window_closed", callback)
583621

622+
def get(self):
623+
try:
624+
if not self.winfo_exists():
625+
return dict(self._cached_widget_values)
626+
except Exception:
627+
return dict(self._cached_widget_values)
628+
629+
widget_values = self._collect_widget_values()
630+
self._cached_widget_values = dict(widget_values)
631+
return widget_values
632+
633+
def _collect_widget_values(self) -> dict[str, object]:
634+
"""Collect values from all registered widgets."""
635+
widget_values: dict[str, object] = {}
636+
637+
for widget in self.widgets:
638+
639+
if hasattr(widget, "get"):
640+
try:
641+
widget_values[widget.id] = widget.get()
642+
except Exception as ex:
643+
print(f"Warning: Failed to get value from widget {widget}: {ex}")
644+
return widget_values
645+
584646

585647
if __name__ == "__main__":
586648

@@ -591,11 +653,12 @@ def _on_close_window(self, callback: Optional[Callable]) -> None:
591653

592654
test = BHoMBaseWindow(
593655
title="Test Window",
594-
theme_mode="auto",
656+
theme_mode="light",
595657
)
596658

597659
test.widgets.append(Label(test.content_frame, text="Hello, World!"))
598660
test.widgets.append(Button(test.content_frame, text="Click Me", command=lambda: print("Button Clicked!"), helper_text="This is a button.", item_title="Button Widget Title"))
599661

600662
test.build()
601663
test.mainloop()
664+
print(test.get())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .theme import TclTheme, ThemeManager, LIGHT, DARK

Python_Engine/Python/src/python_toolkit/bhom_tkinter/theming/bhom_dark_theme.tcl

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,41 @@ namespace eval ttk::theme::bhom_dark {
385385
active $colors(-hover-bg) \
386386
disabled $colors(-disabled-bg)]
387387

388-
# Radiobutton - sleek hover effect with bold font
388+
# Larger checkbutton variant used by CheckboxSelection widget
389+
ttk::style layout Checkbox.TCheckbutton {
390+
Checkbutton.padding -sticky nswe -children {
391+
Checkbutton.indicator -side left -sticky {}
392+
Checkbutton.label -side left -sticky w
393+
}
394+
}
395+
396+
ttk::style configure Checkbox.TCheckbutton \
397+
-font {{Segoe UI} 11} \
398+
-padding {6 8} \
399+
-indicatormargin {0 0 10 0} \
400+
-indicatorrelief flat \
401+
-indicatorsize 18 \
402+
-borderwidth 0 \
403+
-relief flat \
404+
-focusthickness 0 \
405+
-indicatorcolor $colors(-inputbg) \
406+
-indicatorbackground $colors(-inputbg)
407+
408+
ttk::style map Checkbox.TCheckbutton \
409+
-background [list \
410+
active $colors(-bg) \
411+
selected $colors(-bg)] \
412+
-foreground [list \
413+
active $colors(-primary) \
414+
disabled $colors(-disabled-fg)] \
415+
-indicatorbackground [list \
416+
selected $colors(-primary) \
417+
active $colors(-inputbg) \
418+
disabled $colors(-disabled-bg)] \
419+
-indicatorcolor [list \
420+
selected $colors(-primary) \
421+
active $colors(-inputbg) \
422+
disabled $colors(-disabled-bg)]
389423
ttk::style configure TRadiobutton \
390424
-background $colors(-bg) \
391425
-foreground $colors(-fg) \
@@ -410,6 +444,51 @@ namespace eval ttk::theme::bhom_dark {
410444
active $colors(-hover-bg) \
411445
disabled $colors(-disabled-bg)]
412446

447+
# Larger radiobutton variant used by RadioSelection widget
448+
ttk::style layout Radio.TRadiobutton {
449+
Radiobutton.padding -sticky nswe -children {
450+
Radiobutton.indicator -side left -sticky {}
451+
Radiobutton.label -side left -sticky w
452+
}
453+
}
454+
455+
ttk::style configure Radio.TRadiobutton \
456+
-font {{Segoe UI} 11} \
457+
-padding {6 8} \
458+
-indicatormargin {0 0 10 0} \
459+
-indicatorsize 15 \
460+
-borderwidth 0 \
461+
-relief flat \
462+
-focusthickness 0 \
463+
-indicatorbackground $colors(-inputbg) \
464+
-indicatorforeground $colors(-inputbg) \
465+
-upperbordercolor $colors(-border) \
466+
-lowerbordercolor $colors(-border)
467+
468+
ttk::style map Radio.TRadiobutton \
469+
-background [list \
470+
active $colors(-bg) \
471+
selected $colors(-bg)] \
472+
-foreground [list \
473+
active $colors(-primary) \
474+
disabled $colors(-disabled-fg)] \
475+
-indicatorbackground [list \
476+
selected $colors(-primary) \
477+
active $colors(-inputbg) \
478+
disabled $colors(-disabled-bg)] \
479+
-indicatorforeground [list \
480+
selected $colors(-primary) \
481+
active $colors(-inputbg) \
482+
disabled $colors(-disabled-bg)] \
483+
-upperbordercolor [list \
484+
selected $colors(-primary) \
485+
active $colors(-border) \
486+
disabled $colors(-disabled-bg)] \
487+
-lowerbordercolor [list \
488+
selected $colors(-primary) \
489+
active $colors(-border) \
490+
disabled $colors(-disabled-bg)]
491+
413492
# Scrollbar - minimal sleek design without arrows
414493
ttk::style configure TScrollbar \
415494
-background $colors(-border) \

0 commit comments

Comments
 (0)