Repository: Nerogar/OneTrainer
Pull request #716: Get OT icon working in all windows.
Author: login: O-J1
Created: 2025-03-03T07:00:15Z | Merged: 2025-03-22T18:46:12Z

I hate that this is the only solution but CTK has a known bug around this (another one :D) https://github.com/TomSchimansky/CustomTkinter/discussions/1099

Additionally CTK dev admitted the following here: https://github.com/TomSchimansky/CustomTkinter/discussions/2343

> " dont have any time left to work on this project at the moment. However this will change in 5 weeks" 

(this was in April 2024 lol)

> "I thought, and still think, that this project is not suited very well for collaborative development. There are so many workarounds and hacks used to make this library work "

> "The makeshift nature of this project is also what kept me from working further on the project because I saw no future for it because of all the workarounds and the disappointing performance"

My changes were just a couple of lines in each file + a new helper function in `ui_utils.py` to add the icon to the context menus (params, timestep etc etc), also wanted Ruff to stop shouting at me at me so I ran sorting and formatted.

If someone knows better solution that:
1. Doesnt introduce any additional delay
2. Doesnt cause the icon to potential strobe if CTK lags.

Then it would be much appreciated. The way I went with prevents strobing for the most part, unsure about delay.

![image-1](https://github.com/user-attachments/assets/32e1ecb8-4c82-4a07-b137-2d4d9752d0eb)



Diff:
diff --git a/modules/ui/CaptionUI.py b/modules/ui/CaptionUI.py
index d27aaffb0..857a65309 100644
--- a/modules/ui/CaptionUI.py
+++ b/modules/ui/CaptionUI.py
@@ -16,7 +16,7 @@
 from modules.util import path_util
 from modules.util.torch_util import default_device
 from modules.util.ui import components
-from modules.util.ui.ui_utils import bind_mousewheel
+from modules.util.ui.ui_utils import bind_mousewheel, set_window_icon
 from modules.util.ui.UIState import UIState
 
 import torch
@@ -49,6 +49,7 @@ def __init__(
         self.title("OneTrainer")
         self.geometry("1280x980")
         self.resizable(False, False)
+        set_window_icon(self)
         self.wait_visibility()
         self.focus_set()
 
@@ -108,6 +109,7 @@ def __init__(
         self.content_column(self.bottom_frame)
 
         self.load_directory()
+        self.after(150, lambda: set_window_icon(self))
 
     def top_bar(self, master):
         top_frame = ctk.CTkFrame(master)
diff --git a/modules/ui/ConceptWindow.py b/modules/ui/ConceptWindow.py
index aadde8d05..492de513b 100644
--- a/modules/ui/ConceptWindow.py
+++ b/modules/ui/ConceptWindow.py
@@ -6,20 +6,25 @@
 from modules.util.config.ConceptConfig import ConceptConfig
 from modules.util.enum.BalancingStrategy import BalancingStrategy
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 from mgds.LoadingPipeline import LoadingPipeline
 from mgds.OutputPipelineModule import OutputPipelineModule
 from mgds.PipelineModule import PipelineModule
 from mgds.pipelineModules.RandomBrightness import RandomBrightness
-from mgds.pipelineModules.RandomCircularMaskShrink import RandomCircularMaskShrink
+from mgds.pipelineModules.RandomCircularMaskShrink import (
+    RandomCircularMaskShrink,
+)
 from mgds.pipelineModules.RandomContrast import RandomContrast
 from mgds.pipelineModules.RandomFlip import RandomFlip
 from mgds.pipelineModules.RandomHue import RandomHue
 from mgds.pipelineModules.RandomMaskRotateCrop import RandomMaskRotateCrop
 from mgds.pipelineModules.RandomRotate import RandomRotate
 from mgds.pipelineModules.RandomSaturation import RandomSaturation
-from mgds.pipelineModuleTypes.RandomAccessPipelineModule import RandomAccessPipelineModule
+from mgds.pipelineModuleTypes.RandomAccessPipelineModule import (
+    RandomAccessPipelineModule,
+)
 
 import torch
 from torchvision.transforms import functional
@@ -71,6 +76,7 @@ def __init__(
         self.title("Concept")
         self.geometry("800x700")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
@@ -86,6 +92,7 @@ def __init__(
         self.text_augmentation_tab = self.__text_augmentation_tab(tabview.add("text augmentation"))
 
         components.button(self, 1, 0, "ok", self.__ok)
+        self.after(150, lambda: set_window_icon(self))
 
     def __general_tab(self, master, concept: ConceptConfig):
         frame = ctk.CTkScrollableFrame(master, fg_color="transparent")
diff --git a/modules/ui/ConvertModelUI.py b/modules/ui/ConvertModelUI.py
index 5417aa60c..9d8c33575 100644
--- a/modules/ui/ConvertModelUI.py
+++ b/modules/ui/ConvertModelUI.py
@@ -11,6 +11,7 @@
 from modules.util.ModelNames import EmbeddingName, ModelNames
 from modules.util.torch_util import torch_gc
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 import customtkinter as ctk
@@ -24,6 +25,7 @@ def __init__(self, parent, *args, **kwargs):
         self.title("Convert models")
         self.geometry("550x350")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.focus_set()
 
@@ -40,6 +42,7 @@ def __init__(self, parent, *args, **kwargs):
         self.main_frame(self.frame)
 
         self.frame.pack(fill="both", expand=True)
+        self.after(150, lambda: set_window_icon(self))
 
     def main_frame(self, master):
         # model type
diff --git a/modules/ui/GenerateCaptionsWindow.py b/modules/ui/GenerateCaptionsWindow.py
index 349e2fdad..e7a49dc0b 100644
--- a/modules/ui/GenerateCaptionsWindow.py
+++ b/modules/ui/GenerateCaptionsWindow.py
@@ -1,5 +1,7 @@
 from tkinter import filedialog
 
+from modules.util.ui.ui_utils import set_window_icon
+
 import customtkinter as ctk
 
 
@@ -22,6 +24,7 @@ def __init__(self, parent, path, parent_include_subdirectories, *args, **kwargs)
         self.title("Batch generate captions")
         self.geometry("360x360")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
@@ -82,6 +85,7 @@ def __init__(self, parent, path, parent_include_subdirectories, *args, **kwargs)
         self.create_captions_button.grid(row=8, column=0, columnspan=2, sticky="w", padx=5, pady=5)
 
         self.frame.pack(fill="both", expand=True)
+        self.after(150, lambda: set_window_icon(self))
 
     def browse_for_path(self, entry_box):
         # get the path from the user
diff --git a/modules/ui/GenerateMasksWindow.py b/modules/ui/GenerateMasksWindow.py
index d1ac730ca..572a1ee5e 100644
--- a/modules/ui/GenerateMasksWindow.py
+++ b/modules/ui/GenerateMasksWindow.py
@@ -1,5 +1,7 @@
 from tkinter import filedialog
 
+from modules.util.ui.ui_utils import set_window_icon
+
 import customtkinter as ctk
 
 
@@ -22,6 +24,7 @@ def __init__(self, parent, path, parent_include_subdirectories, *args, **kwargs)
         self.title("Batch generate masks")
         self.geometry("360x430")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
@@ -96,6 +99,7 @@ def __init__(self, parent, path, parent_include_subdirectories, *args, **kwargs)
         self.create_masks_button.grid(row=10, column=0, columnspan=2, sticky="w", padx=5, pady=5)
 
         self.frame.pack(fill="both", expand=True)
+        self.after(150, lambda: set_window_icon(self))
 
     def browse_for_path(self, entry_box):
         # get the path from the user
diff --git a/modules/ui/OffloadingWindow.py b/modules/ui/OffloadingWindow.py
index d257e9bb1..26f88f69a 100644
--- a/modules/ui/OffloadingWindow.py
+++ b/modules/ui/OffloadingWindow.py
@@ -1,6 +1,9 @@
 from modules.util.config.TrainConfig import TrainConfig
-from modules.util.enum.GradientCheckpointingMethod import GradientCheckpointingMethod
+from modules.util.enum.GradientCheckpointingMethod import (
+    GradientCheckpointingMethod,
+)
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 import customtkinter as ctk
@@ -24,6 +27,7 @@ def __init__(
         self.title("Offloading")
         self.geometry("800x400")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
@@ -38,6 +42,7 @@ def __init__(
         frame.grid(row=0, column=0, sticky='nsew')
 
         components.button(self, 1, 0, "ok", self.__ok)
+        self.after(150, lambda: set_window_icon(self))
 
     def __content_frame(self, master):
         frame = ctk.CTkScrollableFrame(master, fg_color="transparent")
diff --git a/modules/ui/OptimizerParamsWindow.py b/modules/ui/OptimizerParamsWindow.py
index 5bd3d6009..4989365ed 100644
--- a/modules/ui/OptimizerParamsWindow.py
+++ b/modules/ui/OptimizerParamsWindow.py
@@ -10,6 +10,7 @@
     update_optimizer_config,
 )
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 
 import customtkinter as ctk
 
@@ -34,6 +35,7 @@ def __init__(
         self.title("Optimizer Settings")
         self.geometry("800x500")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
@@ -53,6 +55,7 @@ def __init__(
 
         components.button(self, 1, 0, "ok", command=self.on_window_close)
         self.main_frame(self.frame)
+        self.after(150, lambda: set_window_icon(self))
 
     def main_frame(self, master):
         # Optimizer
diff --git a/modules/ui/ProfilingWindow.py b/modules/ui/ProfilingWindow.py
index 403650230..4041d9596 100644
--- a/modules/ui/ProfilingWindow.py
+++ b/modules/ui/ProfilingWindow.py
@@ -1,6 +1,7 @@
 import faulthandler
 
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 
 import customtkinter as ctk
 from scalene import scalene_profiler
@@ -14,6 +15,7 @@ def __init__(self, parent, *args, **kwargs):
         self.title("Profiling")
         self.geometry("512x512")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.focus_set()
 
@@ -34,6 +36,7 @@ def __init__(self, parent, *args, **kwargs):
 
         self.protocol("WM_DELETE_WINDOW", self.withdraw)
         self.withdraw()
+        self.after(200, lambda: set_window_icon(self))
 
     def _dump_stack(self):
         with open('stacks.txt', 'w') as f:
diff --git a/modules/ui/SampleParamsWindow.py b/modules/ui/SampleParamsWindow.py
index 54ec5bb0a..e64788870 100644
--- a/modules/ui/SampleParamsWindow.py
+++ b/modules/ui/SampleParamsWindow.py
@@ -1,6 +1,7 @@
 from modules.ui.SampleFrame import SampleFrame
 from modules.util.config.SampleConfig import SampleConfig
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 import customtkinter as ctk
@@ -17,6 +18,7 @@ def __init__(self, parent, sample: SampleConfig, ui_state: UIState, *args, **kwa
         self.geometry("800x500")
         self.resizable(True, True)
         self.wait_visibility()
+        self.wait_visibility()
         self.grab_set()
         self.focus_set()
 
@@ -28,6 +30,7 @@ def __init__(self, parent, sample: SampleConfig, ui_state: UIState, *args, **kwa
         frame.grid(row=0, column=0, padx=0, pady=0, sticky="nsew")
 
         components.button(self, 1, 0, "ok", self.__ok)
+        self.after(150, lambda: set_window_icon(self))
 
     def __ok(self):
         self.destroy()
diff --git a/modules/ui/SampleWindow.py b/modules/ui/SampleWindow.py
index 263bb25d2..4a9303b39 100644
--- a/modules/ui/SampleWindow.py
+++ b/modules/ui/SampleWindow.py
@@ -2,7 +2,10 @@
 import os
 
 from modules.model.BaseModel import BaseModel
-from modules.modelSampler.BaseModelSampler import BaseModelSampler, ModelSamplerOutput
+from modules.modelSampler.BaseModelSampler import (
+    BaseModelSampler,
+    ModelSamplerOutput,
+)
 from modules.ui.SampleFrame import SampleFrame
 from modules.util import create
 from modules.util.callbacks.TrainCallbacks import TrainCallbacks
@@ -14,6 +17,7 @@
 from modules.util.enum.TrainingMethod import TrainingMethod
 from modules.util.time_util import get_string_timestamp
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 import torch
@@ -60,6 +64,7 @@ def __init__(
         self.title("Sample")
         self.geometry("1200x800")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.focus_set()
 
@@ -87,6 +92,7 @@ def __init__(
 
         self.progress = components.progress(self, 2, 0)
         components.button(self, 3, 0, "sample", self.__sample)
+        self.after(150, lambda: set_window_icon(self))
 
     def __load_model(self) -> BaseModel:
         model_loader = create.create_model_loader(
diff --git a/modules/ui/SchedulerParamsWindow.py b/modules/ui/SchedulerParamsWindow.py
index b91a36bc0..396eae650 100644
--- a/modules/ui/SchedulerParamsWindow.py
+++ b/modules/ui/SchedulerParamsWindow.py
@@ -2,6 +2,7 @@
 from modules.util.config.TrainConfig import TrainConfig
 from modules.util.enum.LearningRateScheduler import LearningRateScheduler
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 import customtkinter as ctk
@@ -83,6 +84,7 @@ def __init__(self, parent, train_config: TrainConfig, ui_state, *args, **kwargs)
         self.title("Learning Rate Scheduler Settings")
         self.geometry("800x400")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
@@ -100,6 +102,7 @@ def __init__(self, parent, train_config: TrainConfig, ui_state, *args, **kwargs)
 
         components.button(self, 1, 0, "ok", command=self.on_window_close)
         self.main_frame(self.frame)
+        self.after(150, lambda: set_window_icon(self))
 
     def main_frame(self, master):
         if self.train_config.learning_rate_scheduler is LearningRateScheduler.CUSTOM:
diff --git a/modules/ui/TimestepDistributionWindow.py b/modules/ui/TimestepDistributionWindow.py
index 30b6bda11..89868eca1 100644
--- a/modules/ui/TimestepDistributionWindow.py
+++ b/modules/ui/TimestepDistributionWindow.py
@@ -1,9 +1,12 @@
 import random
 
-from modules.modelSetup.mixin.ModelSetupNoiseMixin import ModelSetupNoiseMixin
+from modules.modelSetup.mixin.ModelSetupNoiseMixin import (
+    ModelSetupNoiseMixin,
+)
 from modules.util.config.TrainConfig import TrainConfig
 from modules.util.enum.TimestepDistribution import TimestepDistribution
 from modules.util.ui import components
+from modules.util.ui.ui_utils import set_window_icon
 from modules.util.ui.UIState import UIState
 
 import torch
@@ -85,6 +88,7 @@ def __init__(
         self.geometry("900x600")
         self.resizable(True, True)
         self.wait_visibility()
+        set_window_icon(self)
         self.grab_set()
         self.focus_set()
 
@@ -98,6 +102,7 @@ def __init__(
         frame.grid(row=0, column=0, sticky='nsew')
 
         components.button(self, 1, 0, "ok", self.__ok)
+        self.after(150, lambda: set_window_icon(self))
 
     def __content_frame(self, master):
         frame = ctk.CTkScrollableFrame(master, fg_color="transparent")
diff --git a/modules/ui/TrainUI.py b/modules/ui/TrainUI.py
index 843870566..432bafd98 100644
--- a/modules/ui/TrainUI.py
+++ b/modules/ui/TrainUI.py
@@ -4,7 +4,7 @@
 import webbrowser
 from collections.abc import Callable
 from pathlib import Path
-from tkinter import filedialog
+from tkinter import PhotoImage, filedialog
 
 from modules.trainer.CloudTrainer import CloudTrainer
 from modules.trainer.GenericTrainer import GenericTrainer
@@ -52,6 +52,15 @@ def __init__(self):
         super().__init__()
 
         self.title("OneTrainer")
+        try:
+            # Windows attempt
+            self.iconbitmap("resources/icons/icon.ico")
+        except Exception as e:
+            print("Error using iconbitmap:", e)
+
+        # Load a PNG icon to set the global icon for future toplevels apparently
+        self._icon_photo = PhotoImage(file="resources/icons/icon.png")
+        self.wm_iconphoto(True, self._icon_photo)
         self.geometry("1100x740")
 
         # more efficient version of ctk.set_appearance_mode("System"), which retrieves the system theme on each main loop iteration
diff --git a/modules/util/ui/ui_utils.py b/modules/util/ui/ui_utils.py
index b5678f962..99cf81f0c 100644
--- a/modules/util/ui/ui_utils.py
+++ b/modules/util/ui/ui_utils.py
@@ -1,13 +1,18 @@
+import platform
 import sys
+import tkinter as tk
 from collections.abc import Callable
+from pathlib import Path
 from tkinter import EventType
 from typing import Any
 
+from customtkinter import CTk, CTkToplevel
+
 
 def bind_mousewheel(
-        widget: Any,
-        whitelist: set[Any] | None,
-        callback: Callable[[int, Any], None],
+    widget: Any,
+    whitelist: set[Any] | None,
+    callback: Callable[[int, Any], None],
 ):
     assert whitelist is None or isinstance(whitelist, set)
 
@@ -24,7 +29,9 @@ def process_mousewheel(raw_event):
         if raw_event.type == EventType.MouseWheel:  # Windows and Mac.
             # Positive sign means scroll up, negative sign means scroll down.
             # Windows uses a multiple of 120. Macs use the raw number of steps.
-            delta = raw_event.delta if is_mac else int(raw_event.delta / 120)
+            delta = (
+                raw_event.delta if is_mac else int(raw_event.delta / 120)
+            )
         elif raw_event.type == EventType.ButtonPress:  # Linux.
             # Button 4 means scroll up. Button 5 means scroll down.
             # NOTE: Tk only supports binding mouse buttons 1, 2 and 3. The 4/5
@@ -40,3 +47,46 @@ def process_mousewheel(raw_event):
     if sys.platform == "linux":
         widget.bind("<Button-4>", process_mousewheel)
         widget.bind("<Button-5>", process_mousewheel)
+
+def set_window_icon(window: tk.Tk | tk.Toplevel | CTk | CTkToplevel) -> None:
+    """Set the application window icon based on the current platform
+
+    Args:
+        window: The window object (Tk, Toplevel, CTk, CTkToplevel) to set the icon for
+    """
+    # Check if window is a valid window object that can have an icon
+    if (
+        not hasattr(window, "winfo_toplevel")
+        and not hasattr(window, "iconbitmap")
+        and not hasattr(window, "iconphoto")
+    ):
+        # Not a window that can have icons
+        return
+
+    icon_dir = Path("resources/icons")
+
+    try:
+        if platform.system() == "Windows":
+            ico_path = icon_dir / "icon.ico"
+            if ico_path.exists():
+                # For windows, use the toplevel window if this is a frame
+                if hasattr(window, "winfo_toplevel") and not hasattr(
+                    window, "iconbitmap"
+                ):
+                    window = window.winfo_toplevel()
+                window.iconbitmap(str(ico_path))
+        elif platform.system() == "Linux":
+            png_path = icon_dir / "icon.png"
+            if png_path.exists():
+                icon_img = tk.PhotoImage(file=str(png_path))
+                # For Linux, use the toplevel window if this is a frame
+                if hasattr(window, "winfo_toplevel") and not hasattr(
+                    window, "iconphoto"
+                ):
+                    window = window.winfo_toplevel()
+                window.iconphoto(True, icon_img)
+        elif platform.system() == "Darwin":  # macOS
+            # macOS is a rabbit hole sadly for icons, so we'll just pass
+            pass
+    except Exception as e:
+        print(f"Failed to set window icon: {e}")
diff --git a/resources/icons/icon.ico b/resources/icons/icon.ico
new file mode 100644
index 000000000..673a225b3
Binary files /dev/null and b/resources/icons/icon.ico differ


Reviews:
user: login: Nerogar
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-08T14:17:38Z
user: login: Nerogar
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-08T14:21:26Z
user: login: Nerogar
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-08T14:28:06Z
user: login: O-J1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T08:29:28Z
user: login: O-J1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T08:30:16Z
user: login: O-J1
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-09T09:34:25Z

Review comments:
diff_hunk: @@ -22,80 +26,161 @@ def __init__(self, parent, path, parent_include_subdirectories, *args, **kwargs)
         self.title("Batch generate masks")
         self.geometry("360x430")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
 
         self.mode_var = ctk.StringVar(self, "Create if absent")
-        self.modes = ["Replace all masks", "Create if absent", "Add to existing", "Subtract from existing", "Blend with existing"]
+        self.modes = [
path: modules/ui/GenerateMasksWindow.py
user: login: Nerogar
body: Can you revert the formatting changes in this file? I've tried to keep each component on one line were it makes sense, even if that doesn't fit in the 120 character limit. The same happened in some of the other files
created_at: 2025-03-08T14:17:38Z
author_association: OWNER
diff_hunk: @@ -40,3 +45,48 @@ def process_mousewheel(raw_event):
     if sys.platform == "linux":
         widget.bind("<Button-4>", process_mousewheel)
         widget.bind("<Button-5>", process_mousewheel)
+
+
+def set_window_icon(window: Any) -> None:
path: modules/util/ui/ui_utils.py
user: login: Nerogar
body: Shouldn't the type hint be `CTkToplevel`? Or are there any situations where another component is passed in?
created_at: 2025-03-08T14:21:26Z
author_association: OWNER
diff_hunk: @@ -40,3 +45,48 @@ def process_mousewheel(raw_event):
     if sys.platform == "linux":
         widget.bind("<Button-4>", process_mousewheel)
         widget.bind("<Button-5>", process_mousewheel)
+
+
+def set_window_icon(window: Any) -> None:
+    """Set the application window icon based on the current platform
+
+    Args:
+        window: The window object (Tk, Toplevel, CTk, CTkToplevel) to set the icon for
+    """
+    # Check if window is a valid window object that can have an icon
+    if (
+        not hasattr(window, "winfo_toplevel")
+        and not hasattr(window, "iconbitmap")
+        and not hasattr(window, "iconphoto")
+    ):
+        # Not a window that can have icons
+        return
+    icon_dir = (
path: modules/util/ui/ui_utils.py
user: login: Nerogar
body: The current working directory is always assumed to be the repository root directory. So this can be simplified to `icon_dir = Path("resources/icons")`. That also removes a dependency on the file location.
created_at: 2025-03-08T14:28:06Z
author_association: OWNER
diff_hunk: @@ -40,3 +45,48 @@ def process_mousewheel(raw_event):
     if sys.platform == "linux":
         widget.bind("<Button-4>", process_mousewheel)
         widget.bind("<Button-5>", process_mousewheel)
+
+
+def set_window_icon(window: Any) -> None:
+    """Set the application window icon based on the current platform
+
+    Args:
+        window: The window object (Tk, Toplevel, CTk, CTkToplevel) to set the icon for
+    """
+    # Check if window is a valid window object that can have an icon
+    if (
+        not hasattr(window, "winfo_toplevel")
+        and not hasattr(window, "iconbitmap")
+        and not hasattr(window, "iconphoto")
+    ):
+        # Not a window that can have icons
+        return
+    icon_dir = (
path: modules/util/ui/ui_utils.py
user: login: O-J1
body: Done
created_at: 2025-03-09T08:29:28Z
author_association: COLLABORATOR
diff_hunk: @@ -40,3 +45,48 @@ def process_mousewheel(raw_event):
     if sys.platform == "linux":
         widget.bind("<Button-4>", process_mousewheel)
         widget.bind("<Button-5>", process_mousewheel)
+
+
+def set_window_icon(window: Any) -> None:
path: modules/util/ui/ui_utils.py
user: login: O-J1
body: Ive gone and set it to WindowType, which is `WindowType = tk.Tk | tk.Toplevel | CTk | CTkToplevel | Any`
created_at: 2025-03-09T08:30:16Z
author_association: COLLABORATOR
diff_hunk: @@ -22,80 +26,161 @@ def __init__(self, parent, path, parent_include_subdirectories, *args, **kwargs)
         self.title("Batch generate masks")
         self.geometry("360x430")
         self.resizable(True, True)
+        set_window_icon(self)
         self.wait_visibility()
         self.grab_set()
         self.focus_set()
 
         self.mode_var = ctk.StringVar(self, "Create if absent")
-        self.modes = ["Replace all masks", "Create if absent", "Add to existing", "Subtract from existing", "Blend with existing"]
+        self.modes = [
path: modules/ui/GenerateMasksWindow.py
user: login: O-J1
body: Done, reverted all formatting, kept only import sort (Ruff) and readded my changes.
created_at: 2025-03-09T09:34:25Z
author_association: COLLABORATOR

PR comments:
user: login: Arcitec
created_at: 2025-03-03T11:11:09Z
body: Good discovery about the CustomTkInter author's message. It has been obvious for a long time that it's a very hacky and abandoned library, and he mentions that "after some time I don't understand the hacks myself anymore either". I doubt that he will ever resume work on CTk, so #584 is inevitable some day (it's the only non-hacky, modern looking UI library).

Anyway, the trick looks fine.

I was thinking about the code formatting issue while reading the changes. As you can see, when you saved these files and applied some personal code formatter, it changed a lot of other unrelated lines.

The OneTrainer codebase has been in need of an official code formatter for a while now. And since we already use ruff as linter, we could enable "ruff fmt" too. That way we have one project (ruff) handling both the code style issues and ensuring that we all use the same code formatting.

I remember that our ruff config file explicitly disables the formatter and runs ruff in "linter only" mode. So it's mostly a matter of enabling the formatter too, running it once on every existing file in the project so that all old files become fixed, and then we can continue from there without running into formatting issues anymore.

The current project situation without a formatter means that people have to manually format their code to avoid changing lots of unrelated lines.

@Nerogar what do you say? Is it time to start looking into enabling ruff fmt?
author_association: CONTRIBUTOR
user: login: O-J1
created_at: 2025-03-03T11:30:24Z
body: Also if this is merged, please `squash and merge` if thats acceptable. My commit message got a bit colored due to my frustration with CTK late at night
author_association: COLLABORATOR
user: login: O-J1
created_at: 2025-03-09T10:27:18Z
body: Addressed all your review comments 👍
author_association: COLLABORATOR