Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 12 additions & 9 deletions Python_Engine/Python/src/python_toolkit/bhom/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,18 @@ def wrapper(*args, **kwargs) -> Any:
exec_metadata["Errors"].extend(convert_exc_info_to_bhom_error(sys.exc_info()))
raise exc
finally:
log_file = BHOM_LOG_FOLDER / f"Usage_{function.__module__.split('.')[0]}_{datetime.now().strftime('%Y%m%d')}.log"

if ANALYTICS_LOGGER.handlers[0].baseFilename != str(log_file):
ANALYTICS_LOGGER.handlers[0].close()
ANALYTICS_LOGGER.handlers[0].baseFilename = str(log_file)

ANALYTICS_LOGGER.info(
json.dumps(exec_metadata, default=str, indent=None)
)
try:
log_file = BHOM_LOG_FOLDER / f"Usage_{function.__module__.split('.')[0]}_{datetime.now().strftime('%Y%m%d')}.log"

if ANALYTICS_LOGGER.handlers[0].baseFilename != str(log_file):
ANALYTICS_LOGGER.handlers[0].close()
ANALYTICS_LOGGER.handlers[0].baseFilename = str(log_file)

ANALYTICS_LOGGER.info(
json.dumps(exec_metadata, default=str, indent=None)
)
except Exception:
pass

return result

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,10 @@ def refresh_sizing(self) -> None:
"""Recalculate and apply window sizing (useful after adding widgets)."""
self._apply_sizing()

def close(self) -> None:
"""Close and destroy the window. Override in subclasses for custom close behaviour."""
self.destroy_root()

def destroy_root(self) -> None:
"""Safely terminate and destroy the Tk root window."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(
custom_validation: Optional[Callable[[object], Tuple[bool, Optional[str], Optional[Literal['info', 'warning', 'error']]]]] = None,
disable_validation: bool = False,
build_options: BuildOptions = PackingOptions(),
on_change: Optional[Callable] = None,
**kwargs):
"""
Initialize the widget base.
Expand All @@ -46,6 +47,10 @@ def __init__(
Receives the current widget value and must return
`(is_valid, message, severity)`.
disable_validation: When `True`, all validation returns valid.
on_change: Optional callback invoked whenever the widget value
changes. The current value is passed as the single argument.
Supported by all widgets; supplements widget-specific ``command``
parameters where those exist.
**kwargs: Additional Frame options
"""
super().__init__(parent, **kwargs)
Expand All @@ -57,6 +62,7 @@ def __init__(
self.fill_extents = self._normalise_bool(fill_extents)
self.custom_validation = custom_validation
self.disable_validation = bool(disable_validation)
self.on_change: Optional[Callable] = on_change

if id is None:
self.id = str(uuid4())
Expand Down Expand Up @@ -212,10 +218,11 @@ def align_child_text(self, widget: tk.Widget, alignment: Optional[Literal['left'
if alignment is not None:
self.alignment = self._normalise_alignment(alignment)

self._apply_text_alignment(widget)

if alignment is not None:
self.alignment = previous_alignment
try:
self._apply_text_alignment(widget)
finally:
if alignment is not None:
self.alignment = previous_alignment

def set_alignment(self, alignment: Literal['left', 'center', 'right']) -> None:
"""Set widget-wide alignment and refresh built-in labels.
Expand Down Expand Up @@ -247,6 +254,21 @@ def set_fill_extents(self, fill_extents: bool) -> None:
label.pack_configure(anchor=self._pack_anchor)
self._apply_text_alignment(label)

def _fire_on_change(self, value: object) -> None:
"""Invoke the ``on_change`` callback with the current widget value.

This is called internally by each widget subclass whenever its value
changes. It is safe to call even when ``on_change`` is ``None``.

Args:
value: The current widget value to pass to the callback.
"""
if self.on_change is not None:
try:
self.on_change(value)
except Exception:
pass

@abstractmethod
def get(self):
"""Get the current value of the widget."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(
super().__init__(parent, **kwargs)

self._user_command = command
self._click_count = 0
self.button = ttk.Button(
self.content_frame,
text=text,
Expand All @@ -40,20 +41,21 @@ def __init__(

def _on_click(self):
"""Internal click handler increments counter and calls user callback."""
self._click_count += 1
if self._user_command:
try:
self._user_command()
except Exception as e:
CONSOLE_LOGGER.error(f"Unhandled exception when trying to perform custom command: {e}", exc_info=True)


def get(self):
"""Return None as nothing to get."""
return None
"""Return the number of times the button has been clicked."""
return self._click_count

def set(self, value):
"""No set method."""
pass
"""Update the button label text when passed a string."""
if isinstance(value, str):
self.button.configure(text=value)

def validate(self) -> tuple[bool, Optional[str], Optional[Literal['info', 'warning', 'error']]]:
"""Button has no user-editable state, so validation is always valid unless overridden."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from typing import Optional, List, Callable, Tuple, Literal

from python_toolkit.bhom_tkinter.widgets._widgets_base import BHoMBaseWidget
from python_toolkit.bhom_tkinter.widgets.button import Button

class CheckboxSelection(BHoMBaseWidget):
"""A reusable checkbox selection widget built from a list of fields, allowing multiple selections."""
Expand Down Expand Up @@ -51,7 +50,7 @@ def __init__(

# Sub-frame for checkbox controls
self.buttons_frame = ttk.Frame(self.content_frame)
self.buttons_frame.pack(side="top", fill="x", expand=True)
self.buttons_frame.pack(side="top", anchor=self._pack_anchor)

self._build_buttons()

Expand Down Expand Up @@ -98,6 +97,7 @@ def _on_select_field(self, field):
"""Handle checkbox selection change."""
if self.command:
self.command(self.get())
self._fire_on_change(self.get())

def get(self) -> List[str]:
"""Return a list of currently selected values.
Expand All @@ -123,20 +123,23 @@ def select_all(self):
var.set(True)
if self.command:
self.command(self.get())
self._fire_on_change(self.get())

def deselect_all(self):
"""Deselect all checkboxes."""
for var in self.value_vars.values():
var.set(False)
if self.command:
self.command(self.get())
self._fire_on_change(self.get())

def toggle_all(self):
"""Toggle all checkbox states."""
for var in self.value_vars.values():
var.set(not var.get())
if self.command:
self.command(self.get())
self._fire_on_change(self.get())

def set_fields(self, fields: List[str], defaults: Optional[List[str]] = None):
"""Replace the available fields and rebuild the widget.
Expand All @@ -151,14 +154,6 @@ def set_fields(self, fields: List[str], defaults: Optional[List[str]] = None):
if defaults:
self.set(defaults)

def pack(self, **kwargs):
"""Pack the widget with the given options.

Args:
**kwargs: Pack geometry manager options.
"""
super().pack(**kwargs)

def validate(self) -> tuple[bool, Optional[str], Optional[Literal['info', 'warning', 'error']]]:
"""Validate the current selection against min/max constraints.

Expand Down
Loading