spacr 1.0.7__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spacr/gui_utils.py CHANGED
@@ -9,7 +9,7 @@ import psutil
9
9
  from PIL import Image, ImageTk
10
10
  from screeninfo import get_monitors
11
11
 
12
- from .gui_elements import AnnotateApp, spacrEntry, spacrCheck, spacrCombo
12
+ from .gui_elements import spacrEntry, spacrCheck, spacrCombo
13
13
 
14
14
  try:
15
15
  ctypes.windll.shcore.SetProcessDpiAwareness(True)
@@ -18,7 +18,20 @@ except AttributeError:
18
18
 
19
19
  def initialize_cuda():
20
20
  """
21
- Initializes CUDA in the main process by performing a simple GPU operation.
21
+ Initializes CUDA for the main process if a compatible GPU is available.
22
+
23
+ This function checks if CUDA is available on the system. If it is, it allocates
24
+ a small tensor on the GPU to ensure that CUDA is properly initialized. A message
25
+ is printed to indicate whether CUDA was successfully initialized or if it is not
26
+ available.
27
+
28
+ Note:
29
+ This function is intended to be used in environments where CUDA-enabled GPUs
30
+ are present and PyTorch is installed.
31
+
32
+ Prints:
33
+ - "CUDA initialized in the main process." if CUDA is available and initialized.
34
+ - "CUDA is not available." if no compatible GPU is detected.
22
35
  """
23
36
  if torch.cuda.is_available():
24
37
  # Allocate a small tensor on the GPU
@@ -28,6 +41,27 @@ def initialize_cuda():
28
41
  print("CUDA is not available.")
29
42
 
30
43
  def set_high_priority(process):
44
+ """
45
+ Sets the priority of a given process to high.
46
+
47
+ On Windows systems, the process priority is set to HIGH_PRIORITY_CLASS.
48
+ On Unix-like systems, the process priority is adjusted to a higher level
49
+ by setting its niceness value to -10.
50
+
51
+ Args:
52
+ process (psutil.Process): The process whose priority is to be adjusted.
53
+
54
+ Raises:
55
+ psutil.AccessDenied: If the current user does not have permission to change
56
+ the priority of the process.
57
+ psutil.NoSuchProcess: If the specified process does not exist.
58
+ Exception: For any other errors encountered during the operation.
59
+
60
+ Notes:
61
+ - This function requires the `psutil` library to interact with system processes.
62
+ - Adjusting process priority may require elevated privileges depending on the
63
+ operating system and user permissions.
64
+ """
31
65
  try:
32
66
  p = psutil.Process(process.pid)
33
67
  if os.name == 'nt': # Windows
@@ -43,10 +77,49 @@ def set_high_priority(process):
43
77
  print(f"Failed to set high priority for process {process.pid}: {e}")
44
78
 
45
79
  def set_cpu_affinity(process):
80
+ """
81
+ Set the CPU affinity for a given process to use all available CPUs.
82
+
83
+ This function modifies the CPU affinity of the specified process, allowing
84
+ it to run on all CPUs available on the system.
85
+
86
+ Args:
87
+ process (psutil.Process): A psutil.Process object representing the process
88
+ whose CPU affinity is to be set.
89
+
90
+ Raises:
91
+ psutil.NoSuchProcess: If the process does not exist.
92
+ psutil.AccessDenied: If the process cannot be accessed due to insufficient permissions.
93
+ psutil.ZombieProcess: If the process is a zombie process.
94
+ """
46
95
  p = psutil.Process(process.pid)
47
96
  p.cpu_affinity(list(range(os.cpu_count())))
48
97
 
49
98
  def proceed_with_app(root, app_name, app_func):
99
+ """
100
+ Prepares the application window to load a new app by clearing the current
101
+ content frame and initializing the specified app.
102
+
103
+ Args:
104
+ root (tk.Tk or tk.Toplevel): The root window or parent container that
105
+ contains the content frame.
106
+ app_name (str): The name of the application to be loaded (not used in
107
+ the current implementation but could be useful for logging or
108
+ debugging purposes).
109
+ app_func (callable): A function that initializes the new application
110
+ within the content frame. It should accept the content frame as
111
+ its only argument.
112
+
113
+ Behavior:
114
+ - Destroys all widgets in the `content_frame` attribute of `root`
115
+ (if it exists).
116
+ - Calls `app_func` with `root.content_frame` to initialize the new
117
+ application.
118
+
119
+ Note:
120
+ Ensure that `root` has an attribute `content_frame` that is a valid
121
+ tkinter container (e.g., a `tk.Frame`) before calling this function.
122
+ """
50
123
  # Clear the current content frame
51
124
  if hasattr(root, 'content_frame'):
52
125
  for widget in root.content_frame.winfo_children():
@@ -59,6 +132,31 @@ def proceed_with_app(root, app_name, app_func):
59
132
  app_func(root.content_frame)
60
133
 
61
134
  def load_app(root, app_name, app_func):
135
+ """
136
+ Load a new application into the GUI framework.
137
+
138
+ This function handles the transition between applications in the GUI by
139
+ clearing the current canvas, canceling scheduled tasks, and invoking
140
+ exit functionality for specific applications if necessary.
141
+
142
+ Args:
143
+ root: The root object of the GUI, which contains the canvas,
144
+ after_tasks, and other application state.
145
+ app_name (str): The name of the application to load.
146
+ app_func (callable): The function to initialize the new application.
147
+
148
+ Behavior:
149
+ - Clears the current canvas if it exists.
150
+ - Cancels all scheduled `after` tasks associated with the root object.
151
+ - If the current application has an exit function and the new app is
152
+ not "Annotate" or "make_masks", the exit function is invoked before
153
+ proceeding to the new application.
154
+ - Proceeds to load the new application using the provided `app_func`.
155
+
156
+ Note:
157
+ The `proceed_with_app` function is used internally to finalize the
158
+ transition to the new application.
159
+ """
62
160
  # Clear the canvas if it exists
63
161
  if root.canvas is not None:
64
162
  root.clear_frame(root.canvas)
@@ -184,139 +282,83 @@ def create_input_field(frame, label_text, row, var_type='entry', options=None, d
184
282
 
185
283
  def process_stdout_stderr(q):
186
284
  """
187
- Redirect stdout and stderr to the queue q.
285
+ Redirects the standard output (stdout) and standard error (stderr) streams
286
+ to a queue for processing.
287
+
288
+ This function replaces the default `sys.stdout` and `sys.stderr` with
289
+ instances of `WriteToQueue`, which write all output to the provided queue.
290
+
291
+ :param q: A queue object where the redirected output will be stored.
292
+ :type q: queue.Queue
188
293
  """
189
294
  sys.stdout = WriteToQueue(q)
190
295
  sys.stderr = WriteToQueue(q)
191
296
 
192
297
  class WriteToQueue(io.TextIOBase):
193
298
  """
194
- A custom file-like class that writes any output to a given queue.
195
- This can be used to redirect stdout and stderr.
299
+ A file-like object that redirects writes to a queue.
300
+
301
+ :param q: The queue to write output to.
302
+ :type q: queue.Queue
196
303
  """
197
304
  def __init__(self, q):
198
305
  self.q = q
199
306
  def write(self, msg):
307
+ """
308
+ Write string to stream.
309
+
310
+ :param msg: The string message to write.
311
+ :type msg: str
312
+ :returns: Number of characters written.
313
+ :rtype: int
314
+ """
200
315
  if msg.strip(): # Avoid empty messages
201
316
  self.q.put(msg)
202
317
  def flush(self):
318
+ """
319
+ Flush write buffers, if applicable.
320
+
321
+ This is a no-op in this implementation.
322
+ """
203
323
  pass
204
324
 
205
325
  def cancel_after_tasks(frame):
326
+ """
327
+ Cancels all scheduled 'after' tasks associated with a given frame.
328
+
329
+ This function checks if the provided frame object has an attribute
330
+ named 'after_tasks', which is expected to be a list of task IDs
331
+ scheduled using the `after` method (e.g., in a Tkinter application).
332
+ If such tasks exist, it cancels each of them using the `after_cancel`
333
+ method and then clears the list.
334
+
335
+ Args:
336
+ frame: An object (typically a Tkinter widget) that may have an
337
+ 'after_tasks' attribute containing scheduled task IDs.
338
+
339
+ Raises:
340
+ AttributeError: If the frame does not have the required methods
341
+ (`after_cancel` or `after_tasks` attribute).
342
+ """
206
343
  if hasattr(frame, 'after_tasks'):
207
344
  for task in frame.after_tasks:
208
345
  frame.after_cancel(task)
209
346
  frame.after_tasks.clear()
210
347
 
211
- def annotate(settings):
212
- from .settings import set_annotate_default_settings
213
- settings = set_annotate_default_settings(settings)
214
- src = settings['src']
215
-
216
- db = os.path.join(src, 'measurements/measurements.db')
217
- conn = sqlite3.connect(db)
218
- c = conn.cursor()
219
- c.execute('PRAGMA table_info(png_list)')
220
- cols = c.fetchall()
221
- if settings['annotation_column'] not in [col[1] for col in cols]:
222
- c.execute(f"ALTER TABLE png_list ADD COLUMN {settings['annotation_column']} integer")
223
- conn.commit()
224
- conn.close()
225
-
226
- root = tk.Tk()
227
-
228
- root.geometry(f"{root.winfo_screenwidth()}x{root.winfo_screenheight()}")
229
-
230
- db_path = os.path.join(settings['src'], 'measurements/measurements.db')
231
-
232
- app = AnnotateApp(root,
233
- db_path=db_path,
234
- src=settings['src'],
235
- image_type=settings['image_type'],
236
- channels=settings['channels'],
237
- image_size=settings['img_size'],
238
- annotation_column=settings['annotation_column'],
239
- normalize=settings['normalize'],
240
- percentiles=settings['percentiles'],
241
- measurement=settings['measurement'],
242
- threshold=settings['threshold'],
243
- normalize_channels=settings['normalize_channels'])
244
-
245
- app.load_images()
246
- root.mainloop()
247
-
248
- def generate_annotate_fields(frame):
249
- from .settings import set_annotate_default_settings
250
- from .gui_elements import set_dark_style
251
-
252
- style_out = set_dark_style(ttk.Style())
253
- font_loader = style_out['font_loader']
254
- font_size = style_out['font_size'] - 2
255
-
256
- vars_dict = {}
257
- settings = set_annotate_default_settings(settings={})
258
-
259
- for setting in settings:
260
- vars_dict[setting] = {
261
- 'entry': ttk.Entry(frame),
262
- 'value': settings[setting]
263
- }
264
-
265
- # Arrange input fields and labels
266
- for row, (name, data) in enumerate(vars_dict.items()):
267
- tk.Label(
268
- frame,
269
- text=f"{name.replace('_', ' ').capitalize()}:",
270
- bg=style_out['bg_color'],
271
- fg=style_out['fg_color'],
272
- font=font_loader.get_font(size=font_size)
273
- ).grid(row=row, column=0)
274
-
275
- value = data['value']
276
- if isinstance(value, list):
277
- string_value = ','.join(map(str, value))
278
- elif isinstance(value, (int, float, bool)):
279
- string_value = str(value)
280
- elif value is None:
281
- string_value = ''
282
- else:
283
- string_value = value
284
-
285
- data['entry'].insert(0, string_value)
286
- data['entry'].grid(row=row, column=1)
287
-
288
- return vars_dict
348
+ def load_next_app(root):
349
+ """
350
+ Loads the next application by invoking the function stored in the `next_app_func`
351
+ attribute of the provided `root` object. If the current root window has been
352
+ destroyed, a new root window is initialized before invoking the next application.
289
353
 
290
- def run_annotate_app(vars_dict, parent_frame):
291
- settings = {key: data['entry'].get() for key, data in vars_dict.items()}
292
- settings['channels'] = settings['channels'].split(',')
293
- settings['img_size'] = list(map(int, settings['img_size'].split(','))) # Convert string to list of integers
294
- settings['percentiles'] = list(map(int, settings['percentiles'].split(','))) # Convert string to list of integers
295
- settings['normalize'] = settings['normalize'].lower() == 'true'
296
- settings['normalize_channels'] = settings['channels'].split(',')
297
- settings['rows'] = int(settings['rows'])
298
- settings['columns'] = int(settings['columns'])
299
- settings['measurement'] = settings['measurement'].split(',')
300
- settings['threshold'] = None if settings['threshold'].lower() == 'none' else int(settings['threshold'])
301
-
302
- # Clear previous content instead of destroying the root
303
- if hasattr(parent_frame, 'winfo_children'):
304
- for widget in parent_frame.winfo_children():
305
- widget.destroy()
306
-
307
- # Start the annotate application in the same root window
308
- annotate_app(parent_frame, settings)
309
-
310
- # Global list to keep references to PhotoImage objects
311
- global_image_refs = []
312
-
313
- def annotate_app(parent_frame, settings):
314
- global global_image_refs
315
- global_image_refs.clear()
316
- root = parent_frame.winfo_toplevel()
317
- annotate_with_image_refs(settings, root, lambda: load_next_app(root))
354
+ Args:
355
+ root (tk.Tk): The current root window object, which contains the attributes
356
+ `next_app_func` (a callable for the next application) and
357
+ `next_app_args` (a tuple of arguments to pass to the callable).
318
358
 
319
- def load_next_app(root):
359
+ Raises:
360
+ tk.TclError: If the root window does not exist and needs to be reinitialized.
361
+ """
320
362
  # Get the next app function and arguments
321
363
  next_app_func = root.next_app_func
322
364
  next_app_args = root.next_app_args
@@ -335,38 +377,30 @@ def load_next_app(root):
335
377
  new_root.title("SpaCr Application")
336
378
  next_app_func(new_root, *next_app_args)
337
379
 
338
- def annotate_with_image_refs(settings, root, shutdown_callback):
339
- from .settings import set_annotate_default_settings
340
-
341
- settings = set_annotate_default_settings(settings)
342
- src = settings['src']
343
-
344
- db = os.path.join(src, 'measurements/measurements.db')
345
- conn = sqlite3.connect(db)
346
- c = conn.cursor()
347
- c.execute('PRAGMA table_info(png_list)')
348
- cols = c.fetchall()
349
- if settings['annotation_column'] not in [col[1] for col in cols]:
350
- c.execute(f"ALTER TABLE png_list ADD COLUMN {settings['annotation_column']} integer")
351
- conn.commit()
352
- conn.close()
353
-
354
- screen_width = root.winfo_screenwidth()
355
- screen_height = root.winfo_screenheight()
356
- root.geometry(f"{screen_width}x{screen_height}")
380
+ def convert_settings_dict_for_gui(settings):
381
+ """
382
+ Convert a dictionary of settings into a format suitable for GUI rendering.
357
383
 
358
- app = AnnotateApp(root, db, src, image_type=settings['image_type'], channels=settings['channels'], image_size=settings['img_size'], annotation_column=settings['annotation_column'], normalize=settings['normalize'], percentiles=settings['percentiles'], measurement=settings['measurement'], threshold=settings['threshold'], normalize_channels=settings['normalize_channels'], outline=settings['outline'], outline_threshold_factor=settings['outline_threshold_factor'], outline_sigma=settings['outline_sigma'])
384
+ Each key in the input dictionary is mapped to a tuple of the form:
385
+ (input_type, options, default_value), where:
386
+
387
+ - input_type (str): The type of GUI element. One of:
388
+ * 'combo' for dropdown menus
389
+ * 'check' for checkboxes
390
+ * 'entry' for entry fields
391
+ - options (list or None): A list of selectable options for 'combo' types, or None for other types.
392
+ - default_value: The current or default value to be displayed in the GUI.
359
393
 
360
- # Set the canvas background to black
361
- root.configure(bg='black')
394
+ Special keys are mapped to pre-defined configurations with known option sets
395
+ (e.g., 'metadata_type', 'channels', 'model_type').
362
396
 
363
- # Store the shutdown function and next app details in the root
364
- root.current_app_exit_func = lambda: [app.shutdown(), shutdown_callback()]
397
+ :param settings: Dictionary where keys are setting names and values are their current values.
398
+ :type settings: dict
365
399
 
366
- # Call load_images after setting up the root window
367
- app.load_images()
400
+ :return: Dictionary mapping setting names to tuples for GUI rendering.
401
+ :rtype: dict
402
+ """
368
403
 
369
- def convert_settings_dict_for_gui(settings):
370
404
  from torchvision import models as torch_models
371
405
  torchvision_models = [name for name, obj in torch_models.__dict__.items() if callable(obj)]
372
406
  chans = ['0', '1', '2', '3', '4', '5', '6', '7', '8', None]
@@ -419,10 +453,21 @@ def convert_settings_dict_for_gui(settings):
419
453
 
420
454
  return variables
421
455
 
422
-
423
456
  def spacrFigShow(fig_queue=None):
424
457
  """
425
- Replacement for plt.show() that queues figures instead of displaying them.
458
+ Displays the current matplotlib figure or adds it to a queue.
459
+
460
+ This function retrieves the current matplotlib figure using `plt.gcf()`.
461
+ If a `fig_queue` is provided, the figure is added to the queue.
462
+ Otherwise, the figure is displayed using the `show()` method.
463
+ After the figure is either queued or displayed, it is closed using `plt.close()`.
464
+
465
+ Args:
466
+ fig_queue (queue.Queue, optional): A queue to store the figure.
467
+ If None, the figure is displayed instead.
468
+
469
+ Returns:
470
+ None
426
471
  """
427
472
  fig = plt.gcf()
428
473
  if fig_queue:
@@ -436,7 +481,7 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
436
481
  """
437
482
  Wraps the run_multiple_simulations function to integrate with GUI processes.
438
483
 
439
- Parameters:
484
+ Args:
440
485
  - settings: dict, The settings for the run_multiple_simulations function.
441
486
  - q: multiprocessing.Queue, Queue for logging messages to the GUI.
442
487
  - fig_queue: multiprocessing.Queue, Queue for sending figures to the GUI.
@@ -461,8 +506,45 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
461
506
  plt.show = original_show
462
507
 
463
508
  def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
464
-
465
- from .core import generate_image_umap, preprocess_generate_masks
509
+ """
510
+ Executes a specified processing function in the GUI context based on `settings_type`.
511
+
512
+ This function selects and runs one of the core `spaCR` processing functions
513
+ (e.g., segmentation, measurement, classification, barcode mapping) based on the
514
+ provided `settings_type` string. It wraps the execution with a logging mechanism
515
+ to redirect stdout/stderr to the GUI console and handles exceptions cleanly.
516
+
517
+ Args
518
+ ----------
519
+ settings_type : str
520
+ A string indicating which processing function to execute. Supported values include:
521
+ 'mask', 'measure', 'classify', 'train_cellpose', 'ml_analyze', 'cellpose_masks',
522
+ 'cellpose_all', 'map_barcodes', 'regression', 'recruitment', 'analyze_plaques', 'convert'.
523
+
524
+ settings : dict
525
+ A dictionary of parameters required by the selected function.
526
+
527
+ q : multiprocessing.Queue
528
+ Queue for redirecting standard output and errors to the GUI console.
529
+
530
+ fig_queue : multiprocessing.Queue
531
+ Queue used to transfer figures (e.g., plots) from the worker process to the GUI.
532
+
533
+ stop_requested : multiprocessing.Value
534
+ A shared value to signal whether execution has completed or was interrupted.
535
+
536
+ Raises
537
+ ------
538
+ ValueError
539
+ If an invalid `settings_type` is provided.
540
+
541
+ Notes
542
+ -----
543
+ - Redirects stdout/stderr to the GUI using `process_stdout_stderr`.
544
+ - Catches and reports any exceptions to the GUI queue.
545
+ - Sets `stop_requested.value = 1` when the task finishes (whether successful or not).
546
+ """
547
+ from .core import preprocess_generate_masks
466
548
  from .spacr_cellpose import identify_masks_finetune, check_cellpose_models
467
549
  from .submodules import analyze_recruitment
468
550
  from .ml import generate_ml_scores, perform_regression
@@ -506,9 +588,6 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
506
588
  elif settings_type == 'recruitment':
507
589
  function = analyze_recruitment
508
590
  imports = 1
509
- elif settings_type == 'umap':
510
- function = generate_image_umap
511
- imports = 1
512
591
  elif settings_type == 'analyze_plaques':
513
592
  function = analyze_plaques
514
593
  imports = 1
@@ -529,7 +608,7 @@ def hide_all_settings(vars_dict, categories):
529
608
  """
530
609
  Function to initially hide all settings in the GUI.
531
610
 
532
- Parameters:
611
+ Args:
533
612
  - categories: dict, The categories of settings with their corresponding settings.
534
613
  - vars_dict: dict, The dictionary containing the settings and their corresponding widgets.
535
614
  """
@@ -551,6 +630,32 @@ def hide_all_settings(vars_dict, categories):
551
630
  return vars_dict
552
631
 
553
632
  def setup_frame(parent_frame):
633
+ """
634
+ Set up the main GUI layout within the given parent frame.
635
+
636
+ This function initializes a dark-themed, resizable GUI layout using `PanedWindow`
637
+ containers. It organizes the layout into left-hand settings, central vertical content,
638
+ and bottom horizontal panels. It also sets initial sash positions and layout weights.
639
+
640
+ Args
641
+ ----------
642
+ parent_frame : tk.Frame
643
+ The parent Tkinter frame to populate with the GUI layout.
644
+
645
+ Returns
646
+ -------
647
+ tuple
648
+ A tuple containing:
649
+ - parent_frame (tk.Frame): The modified parent frame with the layout initialized.
650
+ - vertical_container (tk.PanedWindow): Top container in the right-hand area for main content.
651
+ - horizontal_container (tk.PanedWindow): Bottom container for additional widgets.
652
+ - settings_container (tk.PanedWindow): Left-hand container for GUI settings.
653
+
654
+ Notes
655
+ -----
656
+ - Uses `set_dark_style` and `set_element_size` from `gui_elements` to theme and size widgets.
657
+ - Dynamically positions the sash between the left and right panes to 25% of the screen width.
658
+ """
554
659
  from .gui_elements import set_dark_style, set_element_size
555
660
 
556
661
  style = ttk.Style(parent_frame)
@@ -599,8 +704,29 @@ def setup_frame(parent_frame):
599
704
 
600
705
  return parent_frame, vertical_container, horizontal_container, settings_container
601
706
 
602
-
603
707
  def download_hug_dataset(q, vars_dict):
708
+ """
709
+ Downloads a dataset and settings files from the Hugging Face Hub and updates the provided variables dictionary.
710
+
711
+ Args:
712
+ q (queue.Queue): A queue object used for logging messages during the download process.
713
+ vars_dict (dict): A dictionary containing variables to be updated. If 'src' is present in the dictionary,
714
+ the third element of 'src' will be updated with the downloaded dataset path.
715
+
716
+ The function performs the following steps:
717
+ 1. Downloads a dataset from the Hugging Face Hub using the specified repository ID and subfolder.
718
+ 2. Updates the 'src' variable in `vars_dict` with the local path of the downloaded dataset, if applicable.
719
+ 3. Logs the dataset download status to the provided queue.
720
+ 4. Downloads settings files from another repository on the Hugging Face Hub.
721
+ 5. Logs the settings download status to the provided queue.
722
+
723
+ Notes:
724
+ - The dataset is downloaded to a local directory under the user's home directory named "datasets".
725
+ - Any exceptions during the download process are caught and logged to the queue.
726
+
727
+ Raises:
728
+ None: All exceptions are handled internally and logged to the queue.
729
+ """
604
730
  dataset_repo_id = "einarolafsson/toxo_mito"
605
731
  settings_repo_id = "einarolafsson/spacr_settings"
606
732
  dataset_subfolder = "plate1"
@@ -771,6 +897,31 @@ def display_gif_in_plot_frame(gif_path, parent_frame):
771
897
  update_frame(0)
772
898
 
773
899
  def display_media_in_plot_frame(media_path, parent_frame):
900
+ """
901
+ Display a media file (MP4, AVI, or GIF) in a Tkinter frame, playing it on repeat
902
+ while fully filling the frame and maintaining the aspect ratio.
903
+
904
+ Args:
905
+ media_path (str): The file path to the media file (MP4, AVI, or GIF).
906
+ parent_frame (tk.Frame): The Tkinter frame where the media will be displayed.
907
+
908
+ Behavior:
909
+ - For MP4 and AVI files:
910
+ - Uses OpenCV to read and play the video.
911
+ - Resizes and crops the video to fully fill the parent frame while maintaining aspect ratio.
912
+ - Plays the video on repeat.
913
+ - For GIF files:
914
+ - Uses PIL to read and play the GIF.
915
+ - Resizes and crops the GIF to fully fill the parent frame while maintaining aspect ratio.
916
+ - Plays the GIF on repeat.
917
+
918
+ Raises:
919
+ ValueError: If the file format is not supported (only MP4, AVI, and GIF are supported).
920
+
921
+ Notes:
922
+ - The function clears any existing widgets in the parent frame before displaying the media.
923
+ - The parent frame is configured to expand and adapt to the media's aspect ratio.
924
+ """
774
925
  """Display an MP4, AVI, or GIF and play it on repeat in the parent_frame, fully filling the frame while maintaining aspect ratio."""
775
926
  # Clear parent_frame if it contains any previous widgets
776
927
  for widget in parent_frame.winfo_children():
@@ -915,8 +1066,7 @@ def display_media_in_plot_frame(media_path, parent_frame):
915
1066
  else:
916
1067
  raise ValueError("Unsupported file format. Only .mp4, .avi, and .gif are supported.")
917
1068
 
918
- def print_widget_structure(widget, indent=0):
919
- """Recursively print the widget structure."""
1069
+ def print_widget_structure(widget, indent=0):
920
1070
  # Print the widget's name and class
921
1071
  print(" " * indent + f"{widget}: {widget.winfo_class()}")
922
1072