gui_common

This module defines the GUIUtils class, a collection of static helper methods designed to simplify the creation and management of common Tkinter GUI elements used across the application's views.

It provides standardized ways to build widgets like labeled entries, buttons, and frames, as well as utility functions for error display, icon handling, safe variable access, and window positioning.

  1"""
  2This module defines the GUIUtils class, a collection of static helper methods
  3designed to simplify the creation and management of common Tkinter GUI elements
  4used across the application's views.
  5
  6It provides standardized ways to build widgets like labeled entries, buttons,
  7and frames, as well as utility functions for error display, icon handling,
  8safe variable access, and window positioning.
  9"""
 10
 11import tkinter as tk
 12
 13from tkinter import PhotoImage
 14import platform
 15import os
 16from tkinter import messagebox
 17import logging
 18from typing import Callable
 19
 20import system_config
 21
 22logger = logging.getLogger()
 23
 24
 25class GUIUtils:
 26    """
 27    Provides static utility methods for common GUI-related tasks in Tkinter.
 28
 29    This class bundles functions frequently needed when building Tkinter interfaces.
 30    Designed to aid in code reuse and consistency across different view (GUI) components. Since all
 31    methods are static, this class is not intended to be instantiated; its methods
 32    should be called directly using the class name (e.g., `GUIUtils.create_button(...)`).
 33
 34    Attributes
 35    ----------
 36    None (all methods are static).
 37
 38    Methods
 39    -------
 40    - `create_labeled_entry`(...)
 41        Creates a standard Lable/Entry combination component packed in a shared Frame.
 42    - `create_basic_frame`(...)
 43        Creates a basic Frame widget with grid expansion configuration.
 44    - `create_button`(...)
 45        Creates a standard Button widget within a Frame.
 46    - `display_error`(...)
 47        Shows a error message box to the user.
 48    - `create_timer`(...)
 49        Creates a timer label wiget.
 50    - `get_window_icon_path`() -> *could be moved to `system_config` module later.*
 51        Determines and returns the OS-specific path for the application icon.
 52    - `set_program_icon`(...)
 53        Sets the icon for a given Tkinter window based on the OS.
 54    - `safe_tkinter_get`(...)
 55        Safely retrieves the value from a Tkinter variable, handling potential errors when stored value is "" or nothing.
 56    - `center_window`(...)
 57        Positions a Tkinter window in the center of the screen.
 58    - `askyesno`(...)
 59        Displays a standard yes/no confirmation dialog box.
 60    """
 61
 62    @staticmethod
 63    def create_labeled_entry(
 64        parent: tk.Frame,
 65        label_text: str,
 66        text_var: tk.IntVar | tk.StringVar,
 67        row: int,
 68        column: int,
 69    ) -> tuple[tk.Frame, tk.Label, tk.Entry]:
 70        """
 71        Creates a standard UI component consisting of a Frame containing a Label
 72        positioned above an Entry widget. Configures grid weighting for resizing.
 73
 74        Parameters
 75        ----------
 76        - **parent** (*tk.Frame*): The parent widget where this component will be placed.
 77        - **label_text** (*str*): The text to display in the Label widget.
 78        - **text_var** (*tk.IntVar OR tk.StringVar*): The Tkinter variable linked to the Entry widget's content.
 79        - **row** (*int*): The grid row within the parent widget for this component's frame.
 80        - **column** (*int*): The grid column within the parent widget for this component's frame.
 81
 82        Returns
 83        -------
 84        - *tuple[tk.Frame, tk.Label, tk.Entry]*: A tuple containing the created created Frame, Label, and Entry widgets.
 85
 86        Raises
 87        ------
 88        - *Exception*: Propagates any exceptions that occur during widget creation, after logging the error.
 89        """
 90        try:
 91            frame = tk.Frame(parent)
 92            frame.grid(row=row, column=column, padx=5, sticky="nsew")
 93            frame.grid_columnconfigure(0, weight=1)
 94            frame.grid_rowconfigure(0, weight=1)
 95            frame.grid_rowconfigure(1, weight=1)
 96
 97            label = tk.Label(
 98                frame,
 99                text=label_text,
100                bg="light blue",
101                font=("Helvetica", 20),
102                highlightthickness=1,
103                highlightbackground="dark blue",
104            )
105            label.grid(row=0, pady=5)
106
107            entry = tk.Entry(
108                frame,
109                textvariable=text_var,
110                font=("Helvetica", 24),
111                highlightthickness=1,
112                highlightbackground="black",
113            )
114            entry.grid(row=1, sticky="nsew", pady=5)
115
116            logger.info(f"Labeled entry '{label_text}' created.")
117            return frame, label, entry
118        except Exception as e:
119            logger.error(f"Error creating labeled entry '{label_text}': {e}")
120            raise
121
122    @staticmethod
123    def create_basic_frame(
124        parent: tk.Frame | tk.Tk, row: int, column: int, rows: int, cols: int
125    ) -> tk.Frame:
126        """
127        Creates a basic tk.Frame widget with optional highlighting and grid configuration.
128
129        Parameters
130        ----------
131        - **parent** (*tk.Frame OR tk.Tk, tk.Toplevel]*): The parent widget.
132        - **row** (*int*): The grid row for the frame in the parent.
133        - **column** (*int*): The grid column for the frame in the parent.
134        - **rows** (*int, optional*): Number of internal rows to configure with weight=1 / expansion.
135        - **cols** (*int, optional*): Number of internal columns to configure with weight=1 / expansion.
136
137        Returns
138        -------
139        - *tk.Frame*: The created and configured Frame widget.
140
141        Raises
142        ------
143        - *Exception*: Propagates any exceptions during frame creation, after logging.
144        """
145        try:
146            frame = tk.Frame(parent, highlightthickness=1, highlightbackground="black")
147            frame.grid(row=row, column=column, padx=5, pady=5, sticky="nsew")
148            for i in range(rows):
149                frame.grid_rowconfigure(i, weight=1)
150            for i in range(cols):
151                frame.grid_columnconfigure(i, weight=1)
152
153            return frame
154        except Exception as e:
155            logger.error(f"Error creating status frame: {e}")
156            raise
157
158    @staticmethod
159    def create_button(
160        parent: tk.Frame | tk.Toplevel,
161        button_text: str,
162        command: Callable,
163        bg: str,
164        row: int,
165        column: int,
166    ) -> tuple[tk.Frame, tk.Button]:
167        """
168        Creates a tk.Button widget housed within its own tk.Frame for layout control.
169
170        The Frame allows the button to expand/contract cleanly within its grid cell.
171
172        Parameters
173        ----------
174        - **parent** (*Union[tk.Frame, tk.Tk, tk.Toplevel]*): The parent widget.
175        - **button_text** (*str*): The text displayed on the button.
176        - **command** (*Callable / Function*): The function or method to call when the button is clicked.
177        - **bg** (*str*): The background color of the button (Tkinter colors e.g., "green", "light grey").
178        - **row** (*int*): The grid row for the button's frame in the parent.
179        - **column** (*int*): The grid column for the button's frame in the parent.
180
181        Returns
182        -------
183        - *tuple[tk.Frame, tk.Button]*: A tuple containing the created Frame and Button widgets.
184
185        Raises
186        ------
187        - *Exception*: Propagates any exceptions during widget creation, after logging.
188        """
189        try:
190            frame = tk.Frame(parent, highlightthickness=1, highlightbackground="black")
191            frame.grid(row=row, column=column, padx=5, pady=5, sticky="nsew")
192
193            button = tk.Button(
194                frame, text=button_text, command=command, bg=bg, font=("Helvetica", 24)
195            )
196            button.grid(row=0, sticky="nsew", ipadx=10, ipady=10)
197
198            frame.grid_columnconfigure(0, weight=1)
199            frame.grid_rowconfigure(0, weight=1)
200            logger.info(f"Button '{button_text}' created.")
201            return frame, button
202        except Exception as e:
203            logger.error(f"Error creating button '{button_text}': {e}")
204            raise
205
206    @staticmethod
207    def display_error(error: str, message: str) -> None:
208        """
209        Displays an error message box to the user.
210
211        Parameters
212        ----------
213        - **error_title** (*str*): The title for the error message box window.
214        - **message** (*str*): The error message content to display.
215
216        Raises
217        ------
218        - *Exception*: Propagates any exceptions from messagebox, after logging.
219        """
220        try:
221            messagebox.showinfo(error, message)
222            logger.error(f"Error displayed: {error} - {message}")
223        except Exception as e:
224            logger.error(f"Error displaying error message: {e}")
225            raise
226
227    @staticmethod
228    def create_timer(
229        parent: tk.Frame | tk.Tk | tk.Toplevel,
230        timer_name: str,
231        initial_text: str,
232        row: int,
233        column: int,
234    ):
235        """
236        Creates a UI component for displaying a tkinter Label component serving as timer, consists of a Frame
237        containing a timer name Label and a time display Label.
238
239        Parameters
240        ----------
241        - **parent** (*tk.Frame OR tk.Tk OR tk.Toplevel]*): The parent widget.
242        - **timer_name** (*str*): The descriptive name for the timer (e.g., "Session Time").
243        - **initial_text** (*str*): The initial text to display in the time label (e.g., "00:00:00").
244        - **row** (*int*): The grid row for the timer's frame in the parent.
245        - **column** (*int*): The grid column for the timer's frame in the parent.
246
247        Returns
248        -------
249        - *Tuple[tk.Frame, tk.Label, tk.Label]*: A tuple containing the Frame, the name Label,
250          and the time display Label.
251
252        Raises
253        ------
254        - *Exception*: Propagates any exceptions during widget creation, after logging.
255        """
256        try:
257            frame = tk.Frame(
258                parent, highlightthickness=1, highlightbackground="dark blue"
259            )
260            frame.grid(row=row, column=column, padx=10, pady=5, sticky="nsw")
261
262            label = tk.Label(
263                frame, text=timer_name, bg="light blue", font=("Helvetica", 24)
264            )
265            label.grid(row=0, column=0)
266
267            time_label = tk.Label(
268                frame, text=initial_text, bg="light blue", font=("Helvetica", 24)
269            )
270            time_label.grid(row=0, column=1)
271
272            logger.info(f"Timer '{timer_name}' created.")
273            return frame, label, time_label
274        except Exception as e:
275            logger.error(f"Error creating timer '{timer_name}': {e}")
276            raise
277
278    @staticmethod
279    def get_window_icon_path() -> str:
280        """
281        Determines the correct application icon file path based on the operating system.
282
283        Relies on `system_config.get_assets_path()` to find the assets directory.
284        Uses '.ico' for Windows and '.png' for other systems (Linux, macOS).
285
286        Returns
287        -------
288        - *str*: The absolute path to the appropriate icon file.
289
290        Raises
291        ------
292        - *FileNotFoundError*: If the determined icon file does not exist.
293        - *Exception*: Propagates exceptions from `system_config` or `os.path`.
294        """
295        # get the absolute path of the assets directory
296        base_path = system_config.get_assets_path()
297
298        # Determine the operating system
299        os_name = platform.system()
300
301        # Select the appropriate icon file based on the operating system
302        if os_name == "Windows":
303            # Windows expects .ico
304            icon_filename = "rat.ico"
305        else:
306            # Linux and macOS use .png
307            icon_filename = "rat.png"
308
309        return os.path.join(base_path, icon_filename)
310
311    @staticmethod
312    def set_program_icon(window: tk.Tk | tk.Toplevel, icon_path: str) -> None:
313        """
314        Sets the program icon for a given Tkinter window (Tk or Toplevel).
315
316        Uses the appropriate method based on the operating system (`iconbitmap` for
317        Windows, `iconphoto` for others).
318
319        Parameters
320        ----------
321        - **window** (*tk.Tk OR tk.Toplevel*): The Tkinter window object whose icon should be set.
322        - **icon_path** (*str*): The absolute path to the icon file ('.ico' for Windows, '.png'/''.gif' otherwise).
323
324        Raises
325        ------
326        - *Exception*: Propagates any exceptions during icon setting, after logging.
327        """
328        os_name = platform.system()
329        if os_name == "Windows":
330            window.iconbitmap(icon_path)
331        else:
332            # Use .png or .gif file directly
333            photo = PhotoImage(file=icon_path)
334            window.tk.call("wm", "iconphoto", window._w, photo)  # type: ignore
335
336    @staticmethod
337    def safe_tkinter_get(var: tk.StringVar | tk.IntVar) -> str | int | None:
338        """
339        Safely retrieves the value from a Tkinter variable using .get().
340        .get() method fails on tk int values
341        when the value is "" or empty (when emptying a entries contents and filling it
342        with something else), this avoids that by returning none instead if the val is == "".
343
344        Specifically handles the `tk.TclError` that occurs when trying to `.get()` an
345        empty `tk.IntVar` or `tk.DoubleVar` (often happens when an Entry linked
346        to it is cleared by the user). Returns `None` in case of this error.
347
348        Parameters
349        ----------
350        - **var** (*str OR int or None*): The Tkinter variable to get the value from.
351
352        Raises
353        ------
354        - *Exception*: Propagates any non-TclError exceptions during the `.get()` call.
355        """
356        try:
357            return var.get()
358        except tk.TclError:
359            return None
360
361    @staticmethod
362    def center_window(window: tk.Tk | tk.Toplevel) -> None:
363        """
364        Centers a Tkinter window on the screen based on screen dimensions. Specifically, window has maximum of 80 percent of screen
365        width and height.
366
367        Forces an update of the window's pending tasks (`update_idletasks`) to ensure
368        its requested size (`winfo_reqwidth`, `winfo_reqheight`) is accurate before calculating
369        the centering position. Sets the window's geometry string.
370
371        Parameters
372        ----------
373        - **window** (*tk.Tk OR tk.Toplevel*): The window object to center.
374
375        Raises
376        ------
377        - *Exception*: Propagates any exceptions during geometry calculation or setting, after logging.
378        """
379        try:
380            # Get the screen dimensions
381            window_width = window.winfo_reqwidth()
382            window_height = window.winfo_reqheight()
383
384            screen_width = window.winfo_screenwidth()
385            screen_height = window.winfo_screenheight()
386
387            max_width = int(window_width * 0.80)
388            max_height = int(window_height * 0.80)
389
390            # Calculate the x and y coordinates to center the window
391            x = (screen_width - max_width) // 2
392            y = (screen_height - max_height) // 2
393
394            # Set the window's position
395            window.geometry(f"{max_width}x{max_height}+{x}+{y}")
396
397            logger.info("Experiment control window re-sized centered.")
398        except Exception as e:
399            logger.error(f"Error centering experiment control window: {e}")
400            raise
401
402    @staticmethod
403    def askyesno(window_title: str, message: str) -> bool:
404        """
405        Displays a standard modal confirmation dialog box with 'Yes' and 'No' buttons.
406
407        Wraps `tkinter.messagebox.askyesno`.
408
409        Parameters
410        ----------
411        - **window_title** (*str*): The title for the confirmation dialog window.
412        - **message** (*str*): The question or message to display to the user.
413
414        Returns
415        -------
416        - *bool*: `True` if the user clicks 'Yes', `False` if the user clicks 'No' or closes the dialog.
417
418        Raises
419        ------
420        - *Exception*: Propagates any exceptions from messagebox, after logging.
421        """
422        return messagebox.askyesno(title=window_title, message=message)
logger = <RootLogger root (INFO)>
class GUIUtils:
 26class GUIUtils:
 27    """
 28    Provides static utility methods for common GUI-related tasks in Tkinter.
 29
 30    This class bundles functions frequently needed when building Tkinter interfaces.
 31    Designed to aid in code reuse and consistency across different view (GUI) components. Since all
 32    methods are static, this class is not intended to be instantiated; its methods
 33    should be called directly using the class name (e.g., `GUIUtils.create_button(...)`).
 34
 35    Attributes
 36    ----------
 37    None (all methods are static).
 38
 39    Methods
 40    -------
 41    - `create_labeled_entry`(...)
 42        Creates a standard Lable/Entry combination component packed in a shared Frame.
 43    - `create_basic_frame`(...)
 44        Creates a basic Frame widget with grid expansion configuration.
 45    - `create_button`(...)
 46        Creates a standard Button widget within a Frame.
 47    - `display_error`(...)
 48        Shows a error message box to the user.
 49    - `create_timer`(...)
 50        Creates a timer label wiget.
 51    - `get_window_icon_path`() -> *could be moved to `system_config` module later.*
 52        Determines and returns the OS-specific path for the application icon.
 53    - `set_program_icon`(...)
 54        Sets the icon for a given Tkinter window based on the OS.
 55    - `safe_tkinter_get`(...)
 56        Safely retrieves the value from a Tkinter variable, handling potential errors when stored value is "" or nothing.
 57    - `center_window`(...)
 58        Positions a Tkinter window in the center of the screen.
 59    - `askyesno`(...)
 60        Displays a standard yes/no confirmation dialog box.
 61    """
 62
 63    @staticmethod
 64    def create_labeled_entry(
 65        parent: tk.Frame,
 66        label_text: str,
 67        text_var: tk.IntVar | tk.StringVar,
 68        row: int,
 69        column: int,
 70    ) -> tuple[tk.Frame, tk.Label, tk.Entry]:
 71        """
 72        Creates a standard UI component consisting of a Frame containing a Label
 73        positioned above an Entry widget. Configures grid weighting for resizing.
 74
 75        Parameters
 76        ----------
 77        - **parent** (*tk.Frame*): The parent widget where this component will be placed.
 78        - **label_text** (*str*): The text to display in the Label widget.
 79        - **text_var** (*tk.IntVar OR tk.StringVar*): The Tkinter variable linked to the Entry widget's content.
 80        - **row** (*int*): The grid row within the parent widget for this component's frame.
 81        - **column** (*int*): The grid column within the parent widget for this component's frame.
 82
 83        Returns
 84        -------
 85        - *tuple[tk.Frame, tk.Label, tk.Entry]*: A tuple containing the created created Frame, Label, and Entry widgets.
 86
 87        Raises
 88        ------
 89        - *Exception*: Propagates any exceptions that occur during widget creation, after logging the error.
 90        """
 91        try:
 92            frame = tk.Frame(parent)
 93            frame.grid(row=row, column=column, padx=5, sticky="nsew")
 94            frame.grid_columnconfigure(0, weight=1)
 95            frame.grid_rowconfigure(0, weight=1)
 96            frame.grid_rowconfigure(1, weight=1)
 97
 98            label = tk.Label(
 99                frame,
100                text=label_text,
101                bg="light blue",
102                font=("Helvetica", 20),
103                highlightthickness=1,
104                highlightbackground="dark blue",
105            )
106            label.grid(row=0, pady=5)
107
108            entry = tk.Entry(
109                frame,
110                textvariable=text_var,
111                font=("Helvetica", 24),
112                highlightthickness=1,
113                highlightbackground="black",
114            )
115            entry.grid(row=1, sticky="nsew", pady=5)
116
117            logger.info(f"Labeled entry '{label_text}' created.")
118            return frame, label, entry
119        except Exception as e:
120            logger.error(f"Error creating labeled entry '{label_text}': {e}")
121            raise
122
123    @staticmethod
124    def create_basic_frame(
125        parent: tk.Frame | tk.Tk, row: int, column: int, rows: int, cols: int
126    ) -> tk.Frame:
127        """
128        Creates a basic tk.Frame widget with optional highlighting and grid configuration.
129
130        Parameters
131        ----------
132        - **parent** (*tk.Frame OR tk.Tk, tk.Toplevel]*): The parent widget.
133        - **row** (*int*): The grid row for the frame in the parent.
134        - **column** (*int*): The grid column for the frame in the parent.
135        - **rows** (*int, optional*): Number of internal rows to configure with weight=1 / expansion.
136        - **cols** (*int, optional*): Number of internal columns to configure with weight=1 / expansion.
137
138        Returns
139        -------
140        - *tk.Frame*: The created and configured Frame widget.
141
142        Raises
143        ------
144        - *Exception*: Propagates any exceptions during frame creation, after logging.
145        """
146        try:
147            frame = tk.Frame(parent, highlightthickness=1, highlightbackground="black")
148            frame.grid(row=row, column=column, padx=5, pady=5, sticky="nsew")
149            for i in range(rows):
150                frame.grid_rowconfigure(i, weight=1)
151            for i in range(cols):
152                frame.grid_columnconfigure(i, weight=1)
153
154            return frame
155        except Exception as e:
156            logger.error(f"Error creating status frame: {e}")
157            raise
158
159    @staticmethod
160    def create_button(
161        parent: tk.Frame | tk.Toplevel,
162        button_text: str,
163        command: Callable,
164        bg: str,
165        row: int,
166        column: int,
167    ) -> tuple[tk.Frame, tk.Button]:
168        """
169        Creates a tk.Button widget housed within its own tk.Frame for layout control.
170
171        The Frame allows the button to expand/contract cleanly within its grid cell.
172
173        Parameters
174        ----------
175        - **parent** (*Union[tk.Frame, tk.Tk, tk.Toplevel]*): The parent widget.
176        - **button_text** (*str*): The text displayed on the button.
177        - **command** (*Callable / Function*): The function or method to call when the button is clicked.
178        - **bg** (*str*): The background color of the button (Tkinter colors e.g., "green", "light grey").
179        - **row** (*int*): The grid row for the button's frame in the parent.
180        - **column** (*int*): The grid column for the button's frame in the parent.
181
182        Returns
183        -------
184        - *tuple[tk.Frame, tk.Button]*: A tuple containing the created Frame and Button widgets.
185
186        Raises
187        ------
188        - *Exception*: Propagates any exceptions during widget creation, after logging.
189        """
190        try:
191            frame = tk.Frame(parent, highlightthickness=1, highlightbackground="black")
192            frame.grid(row=row, column=column, padx=5, pady=5, sticky="nsew")
193
194            button = tk.Button(
195                frame, text=button_text, command=command, bg=bg, font=("Helvetica", 24)
196            )
197            button.grid(row=0, sticky="nsew", ipadx=10, ipady=10)
198
199            frame.grid_columnconfigure(0, weight=1)
200            frame.grid_rowconfigure(0, weight=1)
201            logger.info(f"Button '{button_text}' created.")
202            return frame, button
203        except Exception as e:
204            logger.error(f"Error creating button '{button_text}': {e}")
205            raise
206
207    @staticmethod
208    def display_error(error: str, message: str) -> None:
209        """
210        Displays an error message box to the user.
211
212        Parameters
213        ----------
214        - **error_title** (*str*): The title for the error message box window.
215        - **message** (*str*): The error message content to display.
216
217        Raises
218        ------
219        - *Exception*: Propagates any exceptions from messagebox, after logging.
220        """
221        try:
222            messagebox.showinfo(error, message)
223            logger.error(f"Error displayed: {error} - {message}")
224        except Exception as e:
225            logger.error(f"Error displaying error message: {e}")
226            raise
227
228    @staticmethod
229    def create_timer(
230        parent: tk.Frame | tk.Tk | tk.Toplevel,
231        timer_name: str,
232        initial_text: str,
233        row: int,
234        column: int,
235    ):
236        """
237        Creates a UI component for displaying a tkinter Label component serving as timer, consists of a Frame
238        containing a timer name Label and a time display Label.
239
240        Parameters
241        ----------
242        - **parent** (*tk.Frame OR tk.Tk OR tk.Toplevel]*): The parent widget.
243        - **timer_name** (*str*): The descriptive name for the timer (e.g., "Session Time").
244        - **initial_text** (*str*): The initial text to display in the time label (e.g., "00:00:00").
245        - **row** (*int*): The grid row for the timer's frame in the parent.
246        - **column** (*int*): The grid column for the timer's frame in the parent.
247
248        Returns
249        -------
250        - *Tuple[tk.Frame, tk.Label, tk.Label]*: A tuple containing the Frame, the name Label,
251          and the time display Label.
252
253        Raises
254        ------
255        - *Exception*: Propagates any exceptions during widget creation, after logging.
256        """
257        try:
258            frame = tk.Frame(
259                parent, highlightthickness=1, highlightbackground="dark blue"
260            )
261            frame.grid(row=row, column=column, padx=10, pady=5, sticky="nsw")
262
263            label = tk.Label(
264                frame, text=timer_name, bg="light blue", font=("Helvetica", 24)
265            )
266            label.grid(row=0, column=0)
267
268            time_label = tk.Label(
269                frame, text=initial_text, bg="light blue", font=("Helvetica", 24)
270            )
271            time_label.grid(row=0, column=1)
272
273            logger.info(f"Timer '{timer_name}' created.")
274            return frame, label, time_label
275        except Exception as e:
276            logger.error(f"Error creating timer '{timer_name}': {e}")
277            raise
278
279    @staticmethod
280    def get_window_icon_path() -> str:
281        """
282        Determines the correct application icon file path based on the operating system.
283
284        Relies on `system_config.get_assets_path()` to find the assets directory.
285        Uses '.ico' for Windows and '.png' for other systems (Linux, macOS).
286
287        Returns
288        -------
289        - *str*: The absolute path to the appropriate icon file.
290
291        Raises
292        ------
293        - *FileNotFoundError*: If the determined icon file does not exist.
294        - *Exception*: Propagates exceptions from `system_config` or `os.path`.
295        """
296        # get the absolute path of the assets directory
297        base_path = system_config.get_assets_path()
298
299        # Determine the operating system
300        os_name = platform.system()
301
302        # Select the appropriate icon file based on the operating system
303        if os_name == "Windows":
304            # Windows expects .ico
305            icon_filename = "rat.ico"
306        else:
307            # Linux and macOS use .png
308            icon_filename = "rat.png"
309
310        return os.path.join(base_path, icon_filename)
311
312    @staticmethod
313    def set_program_icon(window: tk.Tk | tk.Toplevel, icon_path: str) -> None:
314        """
315        Sets the program icon for a given Tkinter window (Tk or Toplevel).
316
317        Uses the appropriate method based on the operating system (`iconbitmap` for
318        Windows, `iconphoto` for others).
319
320        Parameters
321        ----------
322        - **window** (*tk.Tk OR tk.Toplevel*): The Tkinter window object whose icon should be set.
323        - **icon_path** (*str*): The absolute path to the icon file ('.ico' for Windows, '.png'/''.gif' otherwise).
324
325        Raises
326        ------
327        - *Exception*: Propagates any exceptions during icon setting, after logging.
328        """
329        os_name = platform.system()
330        if os_name == "Windows":
331            window.iconbitmap(icon_path)
332        else:
333            # Use .png or .gif file directly
334            photo = PhotoImage(file=icon_path)
335            window.tk.call("wm", "iconphoto", window._w, photo)  # type: ignore
336
337    @staticmethod
338    def safe_tkinter_get(var: tk.StringVar | tk.IntVar) -> str | int | None:
339        """
340        Safely retrieves the value from a Tkinter variable using .get().
341        .get() method fails on tk int values
342        when the value is "" or empty (when emptying a entries contents and filling it
343        with something else), this avoids that by returning none instead if the val is == "".
344
345        Specifically handles the `tk.TclError` that occurs when trying to `.get()` an
346        empty `tk.IntVar` or `tk.DoubleVar` (often happens when an Entry linked
347        to it is cleared by the user). Returns `None` in case of this error.
348
349        Parameters
350        ----------
351        - **var** (*str OR int or None*): The Tkinter variable to get the value from.
352
353        Raises
354        ------
355        - *Exception*: Propagates any non-TclError exceptions during the `.get()` call.
356        """
357        try:
358            return var.get()
359        except tk.TclError:
360            return None
361
362    @staticmethod
363    def center_window(window: tk.Tk | tk.Toplevel) -> None:
364        """
365        Centers a Tkinter window on the screen based on screen dimensions. Specifically, window has maximum of 80 percent of screen
366        width and height.
367
368        Forces an update of the window's pending tasks (`update_idletasks`) to ensure
369        its requested size (`winfo_reqwidth`, `winfo_reqheight`) is accurate before calculating
370        the centering position. Sets the window's geometry string.
371
372        Parameters
373        ----------
374        - **window** (*tk.Tk OR tk.Toplevel*): The window object to center.
375
376        Raises
377        ------
378        - *Exception*: Propagates any exceptions during geometry calculation or setting, after logging.
379        """
380        try:
381            # Get the screen dimensions
382            window_width = window.winfo_reqwidth()
383            window_height = window.winfo_reqheight()
384
385            screen_width = window.winfo_screenwidth()
386            screen_height = window.winfo_screenheight()
387
388            max_width = int(window_width * 0.80)
389            max_height = int(window_height * 0.80)
390
391            # Calculate the x and y coordinates to center the window
392            x = (screen_width - max_width) // 2
393            y = (screen_height - max_height) // 2
394
395            # Set the window's position
396            window.geometry(f"{max_width}x{max_height}+{x}+{y}")
397
398            logger.info("Experiment control window re-sized centered.")
399        except Exception as e:
400            logger.error(f"Error centering experiment control window: {e}")
401            raise
402
403    @staticmethod
404    def askyesno(window_title: str, message: str) -> bool:
405        """
406        Displays a standard modal confirmation dialog box with 'Yes' and 'No' buttons.
407
408        Wraps `tkinter.messagebox.askyesno`.
409
410        Parameters
411        ----------
412        - **window_title** (*str*): The title for the confirmation dialog window.
413        - **message** (*str*): The question or message to display to the user.
414
415        Returns
416        -------
417        - *bool*: `True` if the user clicks 'Yes', `False` if the user clicks 'No' or closes the dialog.
418
419        Raises
420        ------
421        - *Exception*: Propagates any exceptions from messagebox, after logging.
422        """
423        return messagebox.askyesno(title=window_title, message=message)

Provides static utility methods for common GUI-related tasks in Tkinter.

This class bundles functions frequently needed when building Tkinter interfaces. Designed to aid in code reuse and consistency across different view (GUI) components. Since all methods are static, this class is not intended to be instantiated; its methods should be called directly using the class name (e.g., GUIUtils.create_button(...)).

Attributes

None (all methods are static).

Methods

  • create_labeled_entry(...) Creates a standard Lable/Entry combination component packed in a shared Frame.
  • create_basic_frame(...) Creates a basic Frame widget with grid expansion configuration.
  • create_button(...) Creates a standard Button widget within a Frame.
  • display_error(...) Shows a error message box to the user.
  • create_timer(...) Creates a timer label wiget.
  • get_window_icon_path() -> could be moved to system_config module later. Determines and returns the OS-specific path for the application icon.
  • set_program_icon(...) Sets the icon for a given Tkinter window based on the OS.
  • safe_tkinter_get(...) Safely retrieves the value from a Tkinter variable, handling potential errors when stored value is "" or nothing.
  • center_window(...) Positions a Tkinter window in the center of the screen.
  • askyesno(...) Displays a standard yes/no confirmation dialog box.
@staticmethod
def create_labeled_entry( parent: tkinter.Frame, label_text: str, text_var: tkinter.IntVar | tkinter.StringVar, row: int, column: int) -> tuple[tkinter.Frame, tkinter.Label, tkinter.Entry]:
 63    @staticmethod
 64    def create_labeled_entry(
 65        parent: tk.Frame,
 66        label_text: str,
 67        text_var: tk.IntVar | tk.StringVar,
 68        row: int,
 69        column: int,
 70    ) -> tuple[tk.Frame, tk.Label, tk.Entry]:
 71        """
 72        Creates a standard UI component consisting of a Frame containing a Label
 73        positioned above an Entry widget. Configures grid weighting for resizing.
 74
 75        Parameters
 76        ----------
 77        - **parent** (*tk.Frame*): The parent widget where this component will be placed.
 78        - **label_text** (*str*): The text to display in the Label widget.
 79        - **text_var** (*tk.IntVar OR tk.StringVar*): The Tkinter variable linked to the Entry widget's content.
 80        - **row** (*int*): The grid row within the parent widget for this component's frame.
 81        - **column** (*int*): The grid column within the parent widget for this component's frame.
 82
 83        Returns
 84        -------
 85        - *tuple[tk.Frame, tk.Label, tk.Entry]*: A tuple containing the created created Frame, Label, and Entry widgets.
 86
 87        Raises
 88        ------
 89        - *Exception*: Propagates any exceptions that occur during widget creation, after logging the error.
 90        """
 91        try:
 92            frame = tk.Frame(parent)
 93            frame.grid(row=row, column=column, padx=5, sticky="nsew")
 94            frame.grid_columnconfigure(0, weight=1)
 95            frame.grid_rowconfigure(0, weight=1)
 96            frame.grid_rowconfigure(1, weight=1)
 97
 98            label = tk.Label(
 99                frame,
100                text=label_text,
101                bg="light blue",
102                font=("Helvetica", 20),
103                highlightthickness=1,
104                highlightbackground="dark blue",
105            )
106            label.grid(row=0, pady=5)
107
108            entry = tk.Entry(
109                frame,
110                textvariable=text_var,
111                font=("Helvetica", 24),
112                highlightthickness=1,
113                highlightbackground="black",
114            )
115            entry.grid(row=1, sticky="nsew", pady=5)
116
117            logger.info(f"Labeled entry '{label_text}' created.")
118            return frame, label, entry
119        except Exception as e:
120            logger.error(f"Error creating labeled entry '{label_text}': {e}")
121            raise

Creates a standard UI component consisting of a Frame containing a Label positioned above an Entry widget. Configures grid weighting for resizing.

Parameters

  • parent (tk.Frame): The parent widget where this component will be placed.
  • label_text (str): The text to display in the Label widget.
  • text_var (tk.IntVar OR tk.StringVar): The Tkinter variable linked to the Entry widget's content.
  • row (int): The grid row within the parent widget for this component's frame.
  • column (int): The grid column within the parent widget for this component's frame.

Returns

  • tuple[tk.Frame, tk.Label, tk.Entry]: A tuple containing the created created Frame, Label, and Entry widgets.

Raises

  • Exception: Propagates any exceptions that occur during widget creation, after logging the error.
@staticmethod
def create_basic_frame( parent: tkinter.Frame | tkinter.Tk, row: int, column: int, rows: int, cols: int) -> tkinter.Frame:
123    @staticmethod
124    def create_basic_frame(
125        parent: tk.Frame | tk.Tk, row: int, column: int, rows: int, cols: int
126    ) -> tk.Frame:
127        """
128        Creates a basic tk.Frame widget with optional highlighting and grid configuration.
129
130        Parameters
131        ----------
132        - **parent** (*tk.Frame OR tk.Tk, tk.Toplevel]*): The parent widget.
133        - **row** (*int*): The grid row for the frame in the parent.
134        - **column** (*int*): The grid column for the frame in the parent.
135        - **rows** (*int, optional*): Number of internal rows to configure with weight=1 / expansion.
136        - **cols** (*int, optional*): Number of internal columns to configure with weight=1 / expansion.
137
138        Returns
139        -------
140        - *tk.Frame*: The created and configured Frame widget.
141
142        Raises
143        ------
144        - *Exception*: Propagates any exceptions during frame creation, after logging.
145        """
146        try:
147            frame = tk.Frame(parent, highlightthickness=1, highlightbackground="black")
148            frame.grid(row=row, column=column, padx=5, pady=5, sticky="nsew")
149            for i in range(rows):
150                frame.grid_rowconfigure(i, weight=1)
151            for i in range(cols):
152                frame.grid_columnconfigure(i, weight=1)
153
154            return frame
155        except Exception as e:
156            logger.error(f"Error creating status frame: {e}")
157            raise

Creates a basic tk.Frame widget with optional highlighting and grid configuration.

Parameters

  • parent (tk.Frame OR tk.Tk, tk.Toplevel]): The parent widget.
  • row (int): The grid row for the frame in the parent.
  • column (int): The grid column for the frame in the parent.
  • rows (int, optional): Number of internal rows to configure with weight=1 / expansion.
  • cols (int, optional): Number of internal columns to configure with weight=1 / expansion.

Returns

  • tk.Frame: The created and configured Frame widget.

Raises

  • Exception: Propagates any exceptions during frame creation, after logging.
@staticmethod
def create_button( parent: tkinter.Frame | tkinter.Toplevel, button_text: str, command: Callable, bg: str, row: int, column: int) -> tuple[tkinter.Frame, tkinter.Button]:
159    @staticmethod
160    def create_button(
161        parent: tk.Frame | tk.Toplevel,
162        button_text: str,
163        command: Callable,
164        bg: str,
165        row: int,
166        column: int,
167    ) -> tuple[tk.Frame, tk.Button]:
168        """
169        Creates a tk.Button widget housed within its own tk.Frame for layout control.
170
171        The Frame allows the button to expand/contract cleanly within its grid cell.
172
173        Parameters
174        ----------
175        - **parent** (*Union[tk.Frame, tk.Tk, tk.Toplevel]*): The parent widget.
176        - **button_text** (*str*): The text displayed on the button.
177        - **command** (*Callable / Function*): The function or method to call when the button is clicked.
178        - **bg** (*str*): The background color of the button (Tkinter colors e.g., "green", "light grey").
179        - **row** (*int*): The grid row for the button's frame in the parent.
180        - **column** (*int*): The grid column for the button's frame in the parent.
181
182        Returns
183        -------
184        - *tuple[tk.Frame, tk.Button]*: A tuple containing the created Frame and Button widgets.
185
186        Raises
187        ------
188        - *Exception*: Propagates any exceptions during widget creation, after logging.
189        """
190        try:
191            frame = tk.Frame(parent, highlightthickness=1, highlightbackground="black")
192            frame.grid(row=row, column=column, padx=5, pady=5, sticky="nsew")
193
194            button = tk.Button(
195                frame, text=button_text, command=command, bg=bg, font=("Helvetica", 24)
196            )
197            button.grid(row=0, sticky="nsew", ipadx=10, ipady=10)
198
199            frame.grid_columnconfigure(0, weight=1)
200            frame.grid_rowconfigure(0, weight=1)
201            logger.info(f"Button '{button_text}' created.")
202            return frame, button
203        except Exception as e:
204            logger.error(f"Error creating button '{button_text}': {e}")
205            raise

Creates a tk.Button widget housed within its own tk.Frame for layout control.

The Frame allows the button to expand/contract cleanly within its grid cell.

Parameters

  • parent (Union[tk.Frame, tk.Tk, tk.Toplevel]): The parent widget.
  • button_text (str): The text displayed on the button.
  • command (Callable / Function): The function or method to call when the button is clicked.
  • bg (str): The background color of the button (Tkinter colors e.g., "green", "light grey").
  • row (int): The grid row for the button's frame in the parent.
  • column (int): The grid column for the button's frame in the parent.

Returns

  • tuple[tk.Frame, tk.Button]: A tuple containing the created Frame and Button widgets.

Raises

  • Exception: Propagates any exceptions during widget creation, after logging.
@staticmethod
def display_error(error: str, message: str) -> None:
207    @staticmethod
208    def display_error(error: str, message: str) -> None:
209        """
210        Displays an error message box to the user.
211
212        Parameters
213        ----------
214        - **error_title** (*str*): The title for the error message box window.
215        - **message** (*str*): The error message content to display.
216
217        Raises
218        ------
219        - *Exception*: Propagates any exceptions from messagebox, after logging.
220        """
221        try:
222            messagebox.showinfo(error, message)
223            logger.error(f"Error displayed: {error} - {message}")
224        except Exception as e:
225            logger.error(f"Error displaying error message: {e}")
226            raise

Displays an error message box to the user.

Parameters

  • error_title (str): The title for the error message box window.
  • message (str): The error message content to display.

Raises

  • Exception: Propagates any exceptions from messagebox, after logging.
@staticmethod
def create_timer( parent: tkinter.Frame | tkinter.Tk | tkinter.Toplevel, timer_name: str, initial_text: str, row: int, column: int):
228    @staticmethod
229    def create_timer(
230        parent: tk.Frame | tk.Tk | tk.Toplevel,
231        timer_name: str,
232        initial_text: str,
233        row: int,
234        column: int,
235    ):
236        """
237        Creates a UI component for displaying a tkinter Label component serving as timer, consists of a Frame
238        containing a timer name Label and a time display Label.
239
240        Parameters
241        ----------
242        - **parent** (*tk.Frame OR tk.Tk OR tk.Toplevel]*): The parent widget.
243        - **timer_name** (*str*): The descriptive name for the timer (e.g., "Session Time").
244        - **initial_text** (*str*): The initial text to display in the time label (e.g., "00:00:00").
245        - **row** (*int*): The grid row for the timer's frame in the parent.
246        - **column** (*int*): The grid column for the timer's frame in the parent.
247
248        Returns
249        -------
250        - *Tuple[tk.Frame, tk.Label, tk.Label]*: A tuple containing the Frame, the name Label,
251          and the time display Label.
252
253        Raises
254        ------
255        - *Exception*: Propagates any exceptions during widget creation, after logging.
256        """
257        try:
258            frame = tk.Frame(
259                parent, highlightthickness=1, highlightbackground="dark blue"
260            )
261            frame.grid(row=row, column=column, padx=10, pady=5, sticky="nsw")
262
263            label = tk.Label(
264                frame, text=timer_name, bg="light blue", font=("Helvetica", 24)
265            )
266            label.grid(row=0, column=0)
267
268            time_label = tk.Label(
269                frame, text=initial_text, bg="light blue", font=("Helvetica", 24)
270            )
271            time_label.grid(row=0, column=1)
272
273            logger.info(f"Timer '{timer_name}' created.")
274            return frame, label, time_label
275        except Exception as e:
276            logger.error(f"Error creating timer '{timer_name}': {e}")
277            raise

Creates a UI component for displaying a tkinter Label component serving as timer, consists of a Frame containing a timer name Label and a time display Label.

Parameters

  • parent (tk.Frame OR tk.Tk OR tk.Toplevel]): The parent widget.
  • timer_name (str): The descriptive name for the timer (e.g., "Session Time").
  • initial_text (str): The initial text to display in the time label (e.g., "00:00:00").
  • row (int): The grid row for the timer's frame in the parent.
  • column (int): The grid column for the timer's frame in the parent.

Returns

  • Tuple[tk.Frame, tk.Label, tk.Label]: A tuple containing the Frame, the name Label, and the time display Label.

Raises

  • Exception: Propagates any exceptions during widget creation, after logging.
@staticmethod
def get_window_icon_path() -> str:
279    @staticmethod
280    def get_window_icon_path() -> str:
281        """
282        Determines the correct application icon file path based on the operating system.
283
284        Relies on `system_config.get_assets_path()` to find the assets directory.
285        Uses '.ico' for Windows and '.png' for other systems (Linux, macOS).
286
287        Returns
288        -------
289        - *str*: The absolute path to the appropriate icon file.
290
291        Raises
292        ------
293        - *FileNotFoundError*: If the determined icon file does not exist.
294        - *Exception*: Propagates exceptions from `system_config` or `os.path`.
295        """
296        # get the absolute path of the assets directory
297        base_path = system_config.get_assets_path()
298
299        # Determine the operating system
300        os_name = platform.system()
301
302        # Select the appropriate icon file based on the operating system
303        if os_name == "Windows":
304            # Windows expects .ico
305            icon_filename = "rat.ico"
306        else:
307            # Linux and macOS use .png
308            icon_filename = "rat.png"
309
310        return os.path.join(base_path, icon_filename)

Determines the correct application icon file path based on the operating system.

Relies on system_config.get_assets_path() to find the assets directory. Uses '.ico' for Windows and '.png' for other systems (Linux, macOS).

Returns

  • str: The absolute path to the appropriate icon file.

Raises

  • FileNotFoundError: If the determined icon file does not exist.
  • Exception: Propagates exceptions from system_config or os.path.
@staticmethod
def set_program_icon(window: tkinter.Tk | tkinter.Toplevel, icon_path: str) -> None:
312    @staticmethod
313    def set_program_icon(window: tk.Tk | tk.Toplevel, icon_path: str) -> None:
314        """
315        Sets the program icon for a given Tkinter window (Tk or Toplevel).
316
317        Uses the appropriate method based on the operating system (`iconbitmap` for
318        Windows, `iconphoto` for others).
319
320        Parameters
321        ----------
322        - **window** (*tk.Tk OR tk.Toplevel*): The Tkinter window object whose icon should be set.
323        - **icon_path** (*str*): The absolute path to the icon file ('.ico' for Windows, '.png'/''.gif' otherwise).
324
325        Raises
326        ------
327        - *Exception*: Propagates any exceptions during icon setting, after logging.
328        """
329        os_name = platform.system()
330        if os_name == "Windows":
331            window.iconbitmap(icon_path)
332        else:
333            # Use .png or .gif file directly
334            photo = PhotoImage(file=icon_path)
335            window.tk.call("wm", "iconphoto", window._w, photo)  # type: ignore

Sets the program icon for a given Tkinter window (Tk or Toplevel).

Uses the appropriate method based on the operating system (iconbitmap for Windows, iconphoto for others).

Parameters

  • window (tk.Tk OR tk.Toplevel): The Tkinter window object whose icon should be set.
  • icon_path (str): The absolute path to the icon file ('.ico' for Windows, '.png'/''.gif' otherwise).

Raises

  • Exception: Propagates any exceptions during icon setting, after logging.
@staticmethod
def safe_tkinter_get(var: tkinter.StringVar | tkinter.IntVar) -> str | int | None:
337    @staticmethod
338    def safe_tkinter_get(var: tk.StringVar | tk.IntVar) -> str | int | None:
339        """
340        Safely retrieves the value from a Tkinter variable using .get().
341        .get() method fails on tk int values
342        when the value is "" or empty (when emptying a entries contents and filling it
343        with something else), this avoids that by returning none instead if the val is == "".
344
345        Specifically handles the `tk.TclError` that occurs when trying to `.get()` an
346        empty `tk.IntVar` or `tk.DoubleVar` (often happens when an Entry linked
347        to it is cleared by the user). Returns `None` in case of this error.
348
349        Parameters
350        ----------
351        - **var** (*str OR int or None*): The Tkinter variable to get the value from.
352
353        Raises
354        ------
355        - *Exception*: Propagates any non-TclError exceptions during the `.get()` call.
356        """
357        try:
358            return var.get()
359        except tk.TclError:
360            return None

Safely retrieves the value from a Tkinter variable using .get(). .get() method fails on tk int values when the value is "" or empty (when emptying a entries contents and filling it with something else), this avoids that by returning none instead if the val is == "".

Specifically handles the tk.TclError that occurs when trying to .get() an empty tk.IntVar or tk.DoubleVar (often happens when an Entry linked to it is cleared by the user). Returns None in case of this error.

Parameters

  • var (str OR int or None): The Tkinter variable to get the value from.

Raises

  • Exception: Propagates any non-TclError exceptions during the .get() call.
@staticmethod
def center_window(window: tkinter.Tk | tkinter.Toplevel) -> None:
362    @staticmethod
363    def center_window(window: tk.Tk | tk.Toplevel) -> None:
364        """
365        Centers a Tkinter window on the screen based on screen dimensions. Specifically, window has maximum of 80 percent of screen
366        width and height.
367
368        Forces an update of the window's pending tasks (`update_idletasks`) to ensure
369        its requested size (`winfo_reqwidth`, `winfo_reqheight`) is accurate before calculating
370        the centering position. Sets the window's geometry string.
371
372        Parameters
373        ----------
374        - **window** (*tk.Tk OR tk.Toplevel*): The window object to center.
375
376        Raises
377        ------
378        - *Exception*: Propagates any exceptions during geometry calculation or setting, after logging.
379        """
380        try:
381            # Get the screen dimensions
382            window_width = window.winfo_reqwidth()
383            window_height = window.winfo_reqheight()
384
385            screen_width = window.winfo_screenwidth()
386            screen_height = window.winfo_screenheight()
387
388            max_width = int(window_width * 0.80)
389            max_height = int(window_height * 0.80)
390
391            # Calculate the x and y coordinates to center the window
392            x = (screen_width - max_width) // 2
393            y = (screen_height - max_height) // 2
394
395            # Set the window's position
396            window.geometry(f"{max_width}x{max_height}+{x}+{y}")
397
398            logger.info("Experiment control window re-sized centered.")
399        except Exception as e:
400            logger.error(f"Error centering experiment control window: {e}")
401            raise

Centers a Tkinter window on the screen based on screen dimensions. Specifically, window has maximum of 80 percent of screen width and height.

Forces an update of the window's pending tasks (update_idletasks) to ensure its requested size (winfo_reqwidth, winfo_reqheight) is accurate before calculating the centering position. Sets the window's geometry string.

Parameters

  • window (tk.Tk OR tk.Toplevel): The window object to center.

Raises

  • Exception: Propagates any exceptions during geometry calculation or setting, after logging.
@staticmethod
def askyesno(window_title: str, message: str) -> bool:
403    @staticmethod
404    def askyesno(window_title: str, message: str) -> bool:
405        """
406        Displays a standard modal confirmation dialog box with 'Yes' and 'No' buttons.
407
408        Wraps `tkinter.messagebox.askyesno`.
409
410        Parameters
411        ----------
412        - **window_title** (*str*): The title for the confirmation dialog window.
413        - **message** (*str*): The question or message to display to the user.
414
415        Returns
416        -------
417        - *bool*: `True` if the user clicks 'Yes', `False` if the user clicks 'No' or closes the dialog.
418
419        Raises
420        ------
421        - *Exception*: Propagates any exceptions from messagebox, after logging.
422        """
423        return messagebox.askyesno(title=window_title, message=message)

Displays a standard modal confirmation dialog box with 'Yes' and 'No' buttons.

Wraps tkinter.messagebox.askyesno.

Parameters

  • window_title (str): The title for the confirmation dialog window.
  • message (str): The question or message to display to the user.

Returns

  • bool: True if the user clicks 'Yes', False if the user clicks 'No' or closes the dialog.

Raises

  • Exception: Propagates any exceptions from messagebox, after logging.