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)
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 tosystem_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.
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.
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.
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.
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.
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
oros.path
.
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.
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.
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.
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.