Skip to content

Stylesheet Management System

Unified stylesheet management with automatic theme translation. Write stylesheets once (light mode), get all themes automatically.

Theme Persistence

The StylesheetManager automatically persists the user's theme selection to settings. When the application restarts, the previously selected theme is restored.

from src.shared_services.rendering.stylesheets.api import set_theme, get_theme

# Set theme - automatically saved to settings
set_theme("dark")

# On next app start, the manager loads "dark" from settings
manager = StylesheetManager.instance()
print(manager.get_theme())  # "dark"

The theme is stored in appearance/theme using legacy values (wm/dm) for backward compatibility with the old ViewStyleManager.

To set theme without persisting (useful for previews):

manager.set_theme("dark", persist=False)

Prerequisites

Before using the stylesheet system with PathDef, ensure the path management system is initialized at application startup:

# In main.py or application entry point
from src.shared_services.path_management.api import initialize_paths

def main():
    initialize_paths()  # Must be called before any PathDef usage
    # ... rest of app startup

Quick Start

from src.shared_services.rendering.stylesheets.api import (
    StylesheetManager,
    load_stylesheet,
)
from src.shared_services.path_management.api import get_path_str

# Option 1: Register widget with PathDef stylesheets (recommended)
from mymodule.constants.paths import Stylesheets

manager = StylesheetManager.instance()
manager.register(self, [Stylesheets.MainView, Stylesheets.Components])

# Option 2: Load stylesheet directly using PathDef
css = load_stylesheet(Stylesheets.MainView)
widget.setStyleSheet(css)

# Switch theme - all registered widgets update automatically
manager.set_theme("dark")

Defining Stylesheet Paths with PathDef

Define stylesheets using PathDef in your module's constants/paths.py. PathDef paths must be relative to the data root and use valid prefixes.

Valid Path Prefixes

Prefix PathType Description
.app_data/ REPLACEABLE Application resources, replaced on updates
.app_temp/ REPLACEABLE Temporary files
persistent_data/ PROTECTED User data, preserved across updates
app_logs/ PROTECTED Log files

Module-Specific Stylesheet Constants

Create a constants/paths.py in your module:

# modules/plant_design/constants/paths.py
from typing import Final
from src.shared_services.path_management.path_types import PathDef, PathType


class Stylesheets:
    """Stylesheet paths for the Plant Design module."""

    StartupView: Final = PathDef(
        ".app_data/stylesheets/PlantDesign/StartUp/startup_view.qss",
        PathType.REPLACEABLE,
        description="Plant Design startup view stylesheet",
    )

    ProjectListItem: Final = PathDef(
        ".app_data/stylesheets/PlantDesign/StartUp/project_list_item.qss",
        PathType.REPLACEABLE,
        description="Project list item stylesheet",
    )

    MainView: Final = PathDef(
        ".app_data/stylesheets/PlantDesign/Main/main_view.qss",
        PathType.REPLACEABLE,
        description="Main plant design view stylesheet",
    )

Shared/Global Stylesheet Constants

For stylesheets used across multiple modules, define in:

# src/shared_services/constants/paths.py
from typing import Final
from src.shared_services.path_management.path_types import PathDef, PathType


class Stylesheets:
    """Shared stylesheet paths used across multiple modules."""

    Globals: Final = PathDef(
        ".app_data/stylesheets/globals.qss",
        PathType.REPLACEABLE,
        description="Global application stylesheet",
    )

    MessageBoxes: Final = PathDef(
        ".app_data/stylesheets/MessageBoxes/loading_dialog.qss",
        PathType.REPLACEABLE,
        description="Loading dialog stylesheet",
    )

Stylesheet File Location

Stylesheets using PathDef must be placed in the data directory structure:

data/
  .app_data/
    stylesheets/
      globals.qss
      PlantDesign/
        StartUp/
          startup_view.qss
          project_list_item.qss
        Main/
          main_view.qss
      SoftwareHub/
        home_view.qss
        settings_view.qss
      MessageBoxes/
        loading_dialog.qss

Using the StylesheetManager

Registering Widgets

from src.shared_services.rendering.stylesheets.api import StylesheetManager
from mymodule.constants.paths import Stylesheets


class MyView(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setObjectName("MyView")

        # Register for automatic theme updates
        manager = StylesheetManager.instance()
        manager.register(self, [Stylesheets.MainView, Stylesheets.Components])

Loading Stylesheets Directly

from src.shared_services.rendering.stylesheets.api import load_stylesheet
from mymodule.constants.paths import Stylesheets

# Load with current theme
css = load_stylesheet(Stylesheets.MainView)
widget.setStyleSheet(css)

# Load with specific theme
dark_css = load_stylesheet(Stylesheets.MainView, theme="dark")

Theme Switching

from src.shared_services.rendering.stylesheets.api import set_theme, get_theme

# Switch theme (updates all registered widgets automatically)
set_theme("dark")

# Get current theme
current = get_theme()  # "dark"

Theme Change Callbacks

def on_theme_change(new_theme: str):
    # Handle theme change (update icons, etc.)
    pass

manager = StylesheetManager.instance()
manager.on_theme_change(on_theme_change)

Migration from Legacy String Paths

The system supports gradual migration from the old ViewStyleManager approach.

New Way (StylesheetManager with PathDef)

from src.shared_services.rendering.stylesheets.api import (
    StylesheetManager,
    load_stylesheet,
)
from modules.plant_design.constants.paths import Stylesheets

manager = StylesheetManager.instance()
manager.register(self, [Stylesheets.StartupView, Stylesheets.ProjectListItem])

css = load_stylesheet(Stylesheets.StartupView)

Migration Steps

  1. Create PathDef constants in your module's constants/paths.py
  2. Move stylesheet files from QssHandler/Stylesheets/WM/ to data/.app_data/stylesheets/
  3. Update imports to use PathDef constants
  4. Replace registration calls from register_view_styles() to register()

Temporary Coexistence

During migration, both systems can coexist: - Old code using string paths continues to work - New code uses PathDef constants - Both resolve stylesheets and apply theme translation

API Reference

StylesheetManager

manager = StylesheetManager.instance()

# Register widget with PathDef stylesheets
manager.register(widget, [Stylesheets.MainView, Stylesheets.ListItem])

# Set theme (updates all widgets)
manager.set_theme("dark")

# Get current theme
theme = manager.get_theme()

# Unregister widget
manager.unregister(widget)

# Manual refresh (after modifying stylesheets on disk)
manager.refresh_all()

# Register theme change callback
manager.on_theme_change(callback)

Loading Functions

from src.shared_services.rendering.stylesheets.api import (
    load_stylesheet,
    load_stylesheet_intelligent,  # Legacy compatibility
)

# Load with PathDef (recommended)
css = load_stylesheet(Stylesheets.MainView)

# Load with specific theme
css = load_stylesheet(Stylesheets.MainView, theme="dark")

# Legacy: Handle paths with /DM/ or /WM/ markers
css = load_stylesheet_intelligent("QssHandler/Stylesheets/WM/view.qss")

Theme Functions

from src.shared_services.rendering.stylesheets.api import set_theme, get_theme

set_theme("dark")
current = get_theme()  # "dark"

ThemeTranslator

For direct stylesheet translation without file loading:

from src.shared_services.rendering.stylesheets.api import ThemeTranslator

# Translate CSS string from light to dark
dark_css = ThemeTranslator.translate(
    light_css,
    source_theme="light",
    target_theme="dark"
)

# Check if stylesheet uses light theme colors
is_light = ThemeTranslator.is_source_theme(css, "light")

Cache Management

from src.shared_services.rendering.stylesheets.api import (
    StylesheetCache,
    clear_stylesheet_cache,
)

# Check cache size
count = StylesheetCache.size()

# Clear everything
clear_stylesheet_cache()

# Clear specific theme
StylesheetCache.clear_theme("dark")

How It Works

PathDef Constant
     |
     v
get_path_str() --> Absolute file path
     |
     v
load_stylesheet()
     |
     +---> StylesheetCache.get(path, theme)
     |           |
     |           +--> Cache hit? Return cached
     |
     +---> Read file from disk
     |
     +---> ThemeTranslator.translate() (if theme != source)
     |           |
     |           +--> ColorSystem mappings
     |           +--> Hex, rgb(), rgba() conversion
     |
     +---> Resolve SVG paths
     |
     +---> StylesheetCache.put(path, theme, result)
     |
     v
Translated Stylesheet

Key Benefits

Before (Old System) After (New System)
Maintain 2 folders (DM + WM) Single source folder
Manual theme translation Automatic runtime translation
String paths scattered in code Centralized PathDef constants
No path validation Compile-time path validation
Theme sync issues possible Always in sync

Stylesheet Writing Guidelines

  1. Write for light mode only - The translator handles dark mode
  2. Use ColorSystem colors - They translate correctly between themes
  3. Avoid hardcoded dark colors - Use semantic colors instead
Purpose Color ColorSystem Key
Background #FFFFFF background
Surface #F8FAFC surface_1
Text #0F172A text_primary
Secondary text #64748B text_secondary
Border #E2E8F0 border
Primary accent #2563EB primary

Example Stylesheet

/* Write once for light mode */
QWidget {
    background-color: #FFFFFF;
    color: #0F172A;
}

QPushButton {
    background-color: #F8FAFC;
    border: 1px solid #E2E8F0;
    color: #0F172A;
}

QPushButton:hover {
    background-color: #F1F5F9;
}

This automatically becomes:

/* Auto-translated to dark mode */
QWidget {
    background-color: #1E1E1E;
    color: #D4D4D4;
}

QPushButton {
    background-color: #252526;
    border: 1px solid #3C3C3C;
    color: #D4D4D4;
}

QPushButton:hover {
    background-color: #2D2D2D;
}

Files

File Purpose
api.py Public API exports
stylesheet_manager.py Main manager singleton
theme_translator.py Runtime color translation
cache.py LRU stylesheet cache

Integration with Icon System

The stylesheet system works alongside the icon rendering system:

from src.shared_services.rendering.icons.api import IconColors, render_svg
from src.shared_services.rendering.stylesheets.api import set_theme

# Both use the same ColorSystem
set_theme("dark")  # Updates stylesheets AND icon colors

For coordinated theme switching across all rendering systems, use the icon registry's set_theme() which can trigger stylesheet updates via callbacks.

API Reference

src.shared_services.rendering.stylesheets.api

Unified Stylesheet System - Public API.

This module provides the public API for the stylesheet management system. Import from here for all stylesheet-related functionality.

USAGE

from src.shared_services.rendering.stylesheets.api import ( StylesheetManager, load_stylesheet, get_stylesheet_manager, ) from mymodule.constants.paths import Stylesheets

manager = StylesheetManager.instance() manager.register(self, [Stylesheets.MainView, Stylesheets.Components])

Option 2: Load stylesheet directly using PathDef

css = load_stylesheet(Stylesheets.MainView) widget.setStyleSheet(css)

Switch theme (updates all registered widgets)

manager.set_theme("dark")

MIGRATION FROM OLD SYSTEM:

Old way (ViewStyleManager):
    from QssHandler.load_qss import ViewStyleManager, get_dynamic_sheet_intelligent

    style_manager = ViewStyleManager.instance()
    style_manager.register_view_styles(self, ["view.qss"], module_folder="Module")
    css = get_dynamic_sheet_intelligent(path)

New way (StylesheetManager):
    from src.shared_services.rendering.stylesheets.api import (
        StylesheetManager,
        load_stylesheet,
    )
    from mymodule.constants.paths import Stylesheets

    manager = StylesheetManager.instance()
    manager.register(self, [Stylesheets.MainView])  # Using PathDef
    css = load_stylesheet(Stylesheets.MainView)

QUICK REFERENCE:

StylesheetManager.instance()
    Get the singleton manager for widget registration.

StylesheetManager.register(widget, stylesheets)
    Register a widget with PathDef stylesheets.

StylesheetManager.set_theme(theme)
    Switch theme and update all registered widgets.

load_stylesheet(stylesheet, theme)
    Load and translate a stylesheet (PathDef or path string).

get_color(key)
    Get a QColor from ColorSystem for the active theme.

ThemeTranslator.translate(css, source, target)
    Translate stylesheet colors between themes.

StylesheetCache

LRU cache for translated stylesheets.

This cache stores translated stylesheet strings to avoid redundant translations. It uses an OrderedDict for LRU eviction when the cache reaches its maximum size.

The cache is keyed by (stylesheet_path, theme) tuples, allowing the same source stylesheet to be cached for multiple themes simultaneously.

Example

Basic usage::

# Check cache
cached = StylesheetCache.get("/path/to/style.qss", "dark")
if cached is None:
    # Translate and cache
    translated = translate(source)
    StylesheetCache.put("/path/to/style.qss", "dark", translated)
Note

Call StylesheetCache.clear() when switching themes to ensure fresh translations with new color mappings.

Source code in src\shared_services\rendering\stylesheets\cache.py
class StylesheetCache:
    """
    LRU cache for translated stylesheets.

    This cache stores translated stylesheet strings to avoid
    redundant translations. It uses an OrderedDict for LRU
    eviction when the cache reaches its maximum size.

    The cache is keyed by (stylesheet_path, theme) tuples,
    allowing the same source stylesheet to be cached for
    multiple themes simultaneously.

    Example:
        Basic usage::

            # Check cache
            cached = StylesheetCache.get("/path/to/style.qss", "dark")
            if cached is None:
                # Translate and cache
                translated = translate(source)
                StylesheetCache.put("/path/to/style.qss", "dark", translated)

    Note:
        Call StylesheetCache.clear() when switching themes to
        ensure fresh translations with new color mappings.
    """

    _cache: OrderedDict = OrderedDict()
    _max_size: int = 200  # Max cached stylesheets

    @classmethod
    def get(cls, path: str, theme: str) -> Optional[str]:
        """
        Get a cached stylesheet.

        Args:
            path: Path to the source stylesheet file.
            theme: Theme name (e.g., "light", "dark").

        Returns:
            Cached stylesheet string, or None if not cached.
        """
        key: CacheKey = (path, theme)
        if key in cls._cache:
            # Move to end (most recently used)
            cls._cache.move_to_end(key)
            return cls._cache[key]
        return None

    @classmethod
    def put(cls, path: str, theme: str, stylesheet: str) -> None:
        """
        Store a translated stylesheet in the cache.

        Args:
            path: Path to the source stylesheet file.
            theme: Theme name (e.g., "light", "dark").
            stylesheet: Translated stylesheet string.
        """
        key: CacheKey = (path, theme)

        # Remove if already exists (will re-add at end)
        if key in cls._cache:
            del cls._cache[key]

        # Add to cache
        cls._cache[key] = stylesheet

        # Evict oldest if over limit
        while len(cls._cache) > cls._max_size:
            cls._cache.popitem(last=False)

    @classmethod
    def clear(cls) -> None:
        """
        Clear all cached stylesheets.

        Call this when switching themes or when stylesheets
        may have changed on disk.
        """
        cls._cache.clear()

    @classmethod
    def clear_theme(cls, theme: str) -> None:
        """
        Clear cached stylesheets for a specific theme.

        Args:
            theme: Theme name to clear.
        """
        keys_to_remove = [key for key in cls._cache if key[1] == theme]
        for key in keys_to_remove:
            del cls._cache[key]

    @classmethod
    def size(cls) -> int:
        """
        Get the current number of cached stylesheets.

        Returns:
            Number of cached entries.
        """
        return len(cls._cache)

    @classmethod
    def set_max_size(cls, max_size: int) -> None:
        """
        Set the maximum cache size.

        Args:
            max_size: Maximum number of stylesheets to cache.
        """
        cls._max_size = max_size
        # Evict if necessary
        while len(cls._cache) > cls._max_size:
            cls._cache.popitem(last=False)
clear() classmethod

Clear all cached stylesheets.

Call this when switching themes or when stylesheets may have changed on disk.

Source code in src\shared_services\rendering\stylesheets\cache.py
@classmethod
def clear(cls) -> None:
    """
    Clear all cached stylesheets.

    Call this when switching themes or when stylesheets
    may have changed on disk.
    """
    cls._cache.clear()
clear_theme(theme) classmethod

Clear cached stylesheets for a specific theme.

Parameters:

Name Type Description Default
theme str

Theme name to clear.

required
Source code in src\shared_services\rendering\stylesheets\cache.py
@classmethod
def clear_theme(cls, theme: str) -> None:
    """
    Clear cached stylesheets for a specific theme.

    Args:
        theme: Theme name to clear.
    """
    keys_to_remove = [key for key in cls._cache if key[1] == theme]
    for key in keys_to_remove:
        del cls._cache[key]
get(path, theme) classmethod

Get a cached stylesheet.

Parameters:

Name Type Description Default
path str

Path to the source stylesheet file.

required
theme str

Theme name (e.g., "light", "dark").

required

Returns:

Type Description
Optional[str]

Cached stylesheet string, or None if not cached.

Source code in src\shared_services\rendering\stylesheets\cache.py
@classmethod
def get(cls, path: str, theme: str) -> Optional[str]:
    """
    Get a cached stylesheet.

    Args:
        path: Path to the source stylesheet file.
        theme: Theme name (e.g., "light", "dark").

    Returns:
        Cached stylesheet string, or None if not cached.
    """
    key: CacheKey = (path, theme)
    if key in cls._cache:
        # Move to end (most recently used)
        cls._cache.move_to_end(key)
        return cls._cache[key]
    return None
put(path, theme, stylesheet) classmethod

Store a translated stylesheet in the cache.

Parameters:

Name Type Description Default
path str

Path to the source stylesheet file.

required
theme str

Theme name (e.g., "light", "dark").

required
stylesheet str

Translated stylesheet string.

required
Source code in src\shared_services\rendering\stylesheets\cache.py
@classmethod
def put(cls, path: str, theme: str, stylesheet: str) -> None:
    """
    Store a translated stylesheet in the cache.

    Args:
        path: Path to the source stylesheet file.
        theme: Theme name (e.g., "light", "dark").
        stylesheet: Translated stylesheet string.
    """
    key: CacheKey = (path, theme)

    # Remove if already exists (will re-add at end)
    if key in cls._cache:
        del cls._cache[key]

    # Add to cache
    cls._cache[key] = stylesheet

    # Evict oldest if over limit
    while len(cls._cache) > cls._max_size:
        cls._cache.popitem(last=False)
set_max_size(max_size) classmethod

Set the maximum cache size.

Parameters:

Name Type Description Default
max_size int

Maximum number of stylesheets to cache.

required
Source code in src\shared_services\rendering\stylesheets\cache.py
@classmethod
def set_max_size(cls, max_size: int) -> None:
    """
    Set the maximum cache size.

    Args:
        max_size: Maximum number of stylesheets to cache.
    """
    cls._max_size = max_size
    # Evict if necessary
    while len(cls._cache) > cls._max_size:
        cls._cache.popitem(last=False)
size() classmethod

Get the current number of cached stylesheets.

Returns:

Type Description
int

Number of cached entries.

Source code in src\shared_services\rendering\stylesheets\cache.py
@classmethod
def size(cls) -> int:
    """
    Get the current number of cached stylesheets.

    Returns:
        Number of cached entries.
    """
    return len(cls._cache)

StylesheetManager

Manages stylesheet loading and theme translation for widgets.

This singleton class provides: - Widget registration for automatic stylesheet updates - Single-source stylesheet loading (no duplicate DM/WM folders) - Runtime theme translation using ColorSystem - Stylesheet caching for performance - SVG path resolution in QSS files

The manager reads stylesheets from a single source location and translates them on-the-fly based on the current theme. This eliminates the need to maintain parallel DM/WM folder structures.

Example

Basic usage::

manager = StylesheetManager.instance()

# Register widget (in view __init__)
manager.register(
    self,
    ["startup_view.qss", "list_item.qss"],
    module="PlantDesign/StartUp"
)

# Later, switch theme
manager.set_theme("dark")  # All widgets update automatically
Note

This class is designed as a drop-in replacement for the legacy ViewStyleManager. The API is intentionally similar for easy migration.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
class StylesheetManager:
    """
    Manages stylesheet loading and theme translation for widgets.

    This singleton class provides:
    - Widget registration for automatic stylesheet updates
    - Single-source stylesheet loading (no duplicate DM/WM folders)
    - Runtime theme translation using ColorSystem
    - Stylesheet caching for performance
    - SVG path resolution in QSS files

    The manager reads stylesheets from a single source location and
    translates them on-the-fly based on the current theme. This
    eliminates the need to maintain parallel DM/WM folder structures.

    Example:
        Basic usage::

            manager = StylesheetManager.instance()

            # Register widget (in view __init__)
            manager.register(
                self,
                ["startup_view.qss", "list_item.qss"],
                module="PlantDesign/StartUp"
            )

            # Later, switch theme
            manager.set_theme("dark")  # All widgets update automatically

    Note:
        This class is designed as a drop-in replacement for the
        legacy ViewStyleManager. The API is intentionally similar
        for easy migration.
    """

    _instance: Optional["StylesheetManager"] = None

    # Default paths - can be configured
    DEFAULT_SOURCE_THEME = "light"
    DEFAULT_STYLESHEET_BASE = "QssHandler/Stylesheets/WM"  # Source folder (light mode)

    def __init__(self) -> None:
        """Initialize the stylesheet manager."""
        self._registered: List[RegisteredWidget] = []
        self._current_theme: str = self._load_saved_theme()
        self._stylesheet_base: str = self.DEFAULT_STYLESHEET_BASE
        self._source_theme: str = self.DEFAULT_SOURCE_THEME
        self._project_root: Optional[Path] = None
        self._theme_change_callbacks: List[Callable[[str], None]] = []

    @staticmethod
    def _load_saved_theme() -> str:
        """
        Load the saved theme from settings.

        Returns:
            Theme name ("light" or "dark"), defaults to "light".
        """
        saved_value = app_settings.get(_THEME_SETTINGS_KEY, "wm")
        return _SETTINGS_TO_THEME.get(saved_value, "light")
    @staticmethod
    def _save_theme(theme: str) -> None:
        """
        Save the theme to settings.

        Args:
            theme: Theme name ("light" or "dark").
        """
        settings_value = _THEME_TO_SETTINGS.get(theme, "wm")
        app_settings.set(_THEME_SETTINGS_KEY, settings_value)

    @classmethod
    def instance(cls) -> "StylesheetManager":
        """
        Get the singleton instance.

        Returns:
            The shared StylesheetManager instance.
        """
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @classmethod
    def reset_instance(cls) -> None:
        """
        Reset the singleton instance.

        Useful for testing or reinitializing the manager.
        """
        cls._instance = None
        StylesheetCache.clear()

    def apply_initial_theme(self) -> None:
        """
        Apply the saved theme on application startup.

        Call this method after the QApplication is created to apply
        the palette and global styles. This should be called once
        during application initialization.

        Example::

            app = QApplication(sys.argv)
            StylesheetManager.instance().apply_initial_theme()
        """
        theme = self._current_theme

        # Apply QPalette for native widgets and Windows frame
        palette = create_palette(theme)
        self._apply_palette(palette)

        # Apply global context menu styles
        self._apply_global_styles(theme)

        # Sync icon system with theme
        self._sync_icon_theme(theme)

    def configure(
        self,
        stylesheet_base: Optional[str] = None,
        source_theme: Optional[str] = None,
        project_root: Optional[Union[str, Path]] = None,
    ) -> None:
        """
        Configure the stylesheet manager.

        Args:
            stylesheet_base: Base directory for source stylesheets.
            source_theme: Theme of the source stylesheets.
            project_root: Project root directory for path resolution.

        Example::

            manager.configure(
                stylesheet_base="src/stylesheets",
                source_theme="light",
                project_root="/path/to/project"
            )
        """
        if stylesheet_base is not None:
            self._stylesheet_base = stylesheet_base
        if source_theme is not None:
            self._source_theme = source_theme
        if project_root is not None:
            self._project_root = Path(project_root)

    def get_project_root(self) -> Path:
        """
        Get the project root directory.

        Returns:
            Path to the project root.
        """
        if self._project_root is None:
            # Auto-detect from current file location
            current_file = Path(__file__).resolve()
            # Navigate up to find project root (contains 'src' folder)
            for parent in current_file.parents:
                if (parent / "src").exists() and (parent / "QssHandler").exists():
                    self._project_root = parent
                    break
            if self._project_root is None:
                self._project_root = Path.cwd()
        return self._project_root

    def register(
        self,
        widget: QWidget,
        stylesheets: List[StylesheetSpec],
        module: Optional[str] = None,
    ) -> None:
        """
        Register a widget for stylesheet management.

        The widget will have its stylesheet applied immediately and
        will be automatically updated when the theme changes.

        Args:
            widget: The Qt widget to style.
            stylesheets: List of PathDef objects or path strings.
            module: Optional module subdirectory (legacy support for string paths).

        Example:
            Using PathDef (recommended)::

                from mymodule.constants.paths import Stylesheets

                manager.register(self, [Stylesheets.MainView, Stylesheets.ListItem])

            Using string paths (legacy)::

                manager.register(
                    self,
                    ["startup_view.qss", "list_item.qss"],
                    module="PlantDesign/StartUp"
                )
        """
        widget_ref = weakref.ref(widget)

        # Resolve stylesheet paths
        resolved_paths = self._resolve_stylesheets(stylesheets, module)

        entry = RegisteredWidget(
            widget_ref=widget_ref,
            stylesheets=stylesheets,
            resolved_paths=resolved_paths,
        )

        self._registered.append(entry)

        # Apply stylesheet immediately
        self._apply_stylesheets(widget, resolved_paths)

    def unregister(self, widget: QWidget) -> bool:
        """
        Unregister a widget from stylesheet management.

        Args:
            widget: The widget to unregister.

        Returns:
            True if the widget was found and unregistered.
        """
        initial_count = len(self._registered)
        self._registered = [
            entry
            for entry in self._registered
            if entry.widget_ref() is not widget
        ]
        return len(self._registered) < initial_count

    def set_theme(self, theme: str, persist: bool = True) -> None:
        """
        Switch theme and update all registered widgets.

        This method performs a complete theme switch:
        1. Updates the QPalette for native Qt widgets and Windows frame
        2. Clears stylesheet cache and re-applies all stylesheets
        3. Applies global context menu styles
        4. Syncs the icon system
        5. Refreshes special widgets (QListWidget, QTextBrowser)

        The theme selection is persisted to settings by default.

        Args:
            theme: Theme name ("light" or "dark").
            persist: If True, save the theme to settings for
                persistence across application restarts.

        Example::

            manager.set_theme("dark")
        """
        if theme == self._current_theme:
            return

        self._current_theme = theme

        # Apply QPalette for native widgets and Windows frame
        palette = create_palette(theme)
        self._apply_palette(palette)

        # Clear cache and update registered widgets
        StylesheetCache.clear()
        self._update_all_widgets()

        # Apply global context menu styles
        self._apply_global_styles(theme)

        if persist:
            self._save_theme(theme)

        # Sync icon system with new theme
        self._sync_icon_theme(theme)

        # Refresh special widgets
        self._refresh_special_widgets(palette)

        # Notify callbacks
        for callback in self._theme_change_callbacks:
            try:
                callback(theme)
            except Exception:
                pass
    @staticmethod
    def _sync_icon_theme(theme: str) -> None:
        """
        Synchronize the icon rendering system with the current theme.

        Args:
            theme: Theme name ("light" or "dark").
        """
        try:
            from src.shared_services.rendering.icons.icon_registry import IconRegistry
            IconRegistry.instance().set_theme(theme)
        except ImportError:
            pass
    @staticmethod
    def _apply_palette(palette: QPalette) -> None:
        """
        Apply a palette to the application.

        Args:
            palette: The QPalette to apply.
        """
        app = QApplication.instance()
        if app:
            app.setPalette(palette)
    @staticmethod
    def _apply_global_styles(theme: str) -> None:
        """
        Apply global styles to the application.

        Args:
            theme: Theme name ("light" or "dark").
        """
        app = QApplication.instance()
        if not app:
            return

        # Clear any previous global stylesheet to prevent bleeding.
        # Context menus use FluentContextMenu (custom-painted, no QSS needed).
        # Individual widget stylesheets are handled by register().
        app.setStyleSheet("")

    @staticmethod
    def _refresh_special_widgets(palette: QPalette) -> None:
        """
        Refresh special widgets that need manual palette updates.

        Some widgets like QListWidget and QTextBrowser need their
        palette explicitly refreshed after a theme change.

        Args:
            palette: The current theme palette.
        """
        for top_widget in QApplication.topLevelWidgets():
            # Refresh QListWidget items
            for list_widget in top_widget.findChildren(QListWidget):
                list_widget.setPalette(palette)
                for i in range(list_widget.count()):
                    item = list_widget.item(i)
                    if item:
                        item.setForeground(QBrush())

            # Refresh QTextBrowser widgets
            for text_browser in top_widget.findChildren(QTextBrowser):
                text_browser.setPalette(palette)

    def get_theme(self) -> str:
        """
        Get the current theme.

        Returns:
            Current theme name.
        """
        return self._current_theme

    def on_theme_change(self, callback: Callable[[str], None]) -> None:
        """
        Register a callback for theme changes.

        Args:
            callback: Function to call with new theme name.
        """
        self._theme_change_callbacks.append(callback)

    def load_stylesheet(
        self,
        stylesheet: StylesheetSpec,
        theme: Optional[str] = None,
    ) -> str:
        """
        Load and translate a stylesheet.

        This is the main function for loading stylesheets. It:
        1. Resolves PathDef or path string to absolute path
        2. Reads the source stylesheet file
        3. Translates colors if needed for the target theme
        4. Resolves SVG paths in the stylesheet
        5. Caches the result

        Args:
            stylesheet: PathDef object or path string to the stylesheet.
            theme: Target theme (default: current theme).

        Returns:
            Translated stylesheet string.

        Example:
            Using PathDef::

                from mymodule.constants.paths import Stylesheets
                css = manager.load_stylesheet(Stylesheets.MainView)

            Using string path::

                css = manager.load_stylesheet("views/main.qss")
        """
        theme = theme or self._current_theme

        # Resolve PathDef or string to path
        resolved_path = self._resolve_spec(stylesheet)

        # Check cache
        cached = StylesheetCache.get(resolved_path, theme)
        if cached is not None:
            return cached

        # Load source stylesheet
        content = self._read_stylesheet_file(resolved_path)
        if content is None:
            return ""

        # Translate if needed
        if theme != self._source_theme:
            content = ThemeTranslator.translate(
                content,
                source_theme=self._source_theme,
                target_theme=theme,
            )

        # Resolve image paths (SVG, PNG, etc.) and replace {{ICON_VARIANT}} placeholder
        content = self._resolve_image_paths(content, resolved_path)

        # Cache and return
        StylesheetCache.put(resolved_path, theme, content)
        return content

    def refresh_widget(self, widget: QWidget) -> None:
        """
        Refresh stylesheet for a specific widget.

        Args:
            widget: Widget to refresh.
        """
        for entry in self._registered:
            if entry.widget_ref() is widget:
                self._apply_stylesheets(widget, entry.resolved_paths)
                break

    def refresh_all(self) -> None:
        """
        Refresh all registered widgets.

        Call this after modifying stylesheets on disk.
        """
        StylesheetCache.clear()
        self._update_all_widgets()

    def cleanup(self) -> None:
        """
        Remove references to deleted widgets.

        Called automatically during updates, but can be
        called manually to free memory.
        """
        self._registered = [
            entry
            for entry in self._registered
            if entry.widget_ref() is not None
            and self._is_widget_valid(entry.widget_ref())
        ]

    def get_registered_count(self) -> int:
        """
        Get the number of registered widgets.

        Returns:
            Number of registered entries.
        """
        return len(self._registered)

    # =========================================================================
    # PRIVATE METHODS
    # =========================================================================

    def _resolve_spec(self, spec: StylesheetSpec) -> str:
        """
        Resolve a stylesheet specification to an absolute path.

        Args:
            spec: PathDef, Path, or string path.

        Returns:
            Resolved absolute path string.
        """
        # Handle PathDef objects
        if isinstance(spec, PathDef):
            return get_path_str(spec)

        # Handle Path objects
        if isinstance(spec, Path):
            if spec.is_absolute():
                return str(spec)
            return self._resolve_single_path(str(spec))

        # Handle string paths
        return self._resolve_single_path(str(spec))

    def _resolve_stylesheets(
        self,
        stylesheets: List[StylesheetSpec],
        module: Optional[str] = None,
    ) -> List[str]:
        """
        Resolve a list of stylesheet specifications to paths.

        Args:
            stylesheets: List of PathDef objects or path strings.
            module: Optional module subdirectory (for legacy string paths).

        Returns:
            List of resolved absolute path strings.
        """
        resolved = []

        for spec in stylesheets:
            # PathDef objects are resolved directly
            if isinstance(spec, PathDef):
                resolved.append(get_path_str(spec))
            elif isinstance(spec, Path):
                resolved.append(str(spec) if spec.is_absolute() else self._resolve_single_path(str(spec)))
            else:
                # String path - use legacy resolution with module
                path = self._resolve_string_path(str(spec), module)
                if path:
                    resolved.append(path)

        return resolved

    def _resolve_string_path(self, path: str, module: Optional[str] = None) -> Optional[str]:
        """
        Resolve a string stylesheet path (legacy support).

        Args:
            path: Stylesheet filename or relative path.
            module: Optional module subdirectory.

        Returns:
            Resolved path or None if not found.
        """
        project_root = self.get_project_root()
        base_path = project_root / self._stylesheet_base

        if module:
            # Try module-specific path first
            full_path = base_path / module / path
            if full_path.exists():
                return str(full_path)

        # Try base path
        full_path = base_path / path
        if full_path.exists():
            return str(full_path)

        # Try as absolute or project-relative
        return self._resolve_single_path(path)

    def _resolve_single_path(self, path: str) -> str:
        """Resolve a single stylesheet path."""
        path_obj = Path(path)

        # If absolute and exists, use as-is
        if path_obj.is_absolute() and path_obj.exists():
            return str(path_obj)

        # Try relative to project root
        project_root = self.get_project_root()
        full_path = project_root / path
        if full_path.exists():
            return str(full_path)

        # Try relative to stylesheet base
        base_path = project_root / self._stylesheet_base
        full_path = base_path / path
        if full_path.exists():
            return str(full_path)

        # Return original (may fail later)
        return path
    @staticmethod
    def _read_stylesheet_file(path: str) -> Optional[str]:
        """Read a stylesheet file with encoding fallback."""
        try:
            with open(path, "r", encoding="utf-8") as f:
                return f.read()
        except UnicodeDecodeError:
            try:
                with open(path, "r", encoding="latin-1") as f:
                    return f.read()
            except Exception:
                return None
        except FileNotFoundError:
            return None
        except Exception:
            return None

    def _resolve_image_paths(self, content: str, stylesheet_path: str) -> str:
        """
        Resolve relative image paths in stylesheet to absolute paths.

        Handles url() references in QSS files for SVGs, PNGs, and other images.
        Uses the path management system for proper support when compiled.

        Theme-aware image switching:
            Use {{ICON_VARIANT}} placeholder in image names to automatically
            switch between light and dark icon variants based on theme.

            Example in QSS:
                url(images/dropdown_{{ICON_VARIANT}}.png)

            This resolves to:
                - Light theme: dropdown_dark.png (dark icon on light background)
                - Dark theme: dropdown_light.png (light icon on dark background)

        The images should be placed in: data/.app_data/stylesheets/images/
        """
        stylesheet_dir = Path(stylesheet_path).parent

        # Get the central stylesheet images directory using path management
        # This works correctly both in development and when compiled
        from src.shared_services.constants.paths import StylesheetAssets

        try:
            stylesheet_images_dir = Path(get_path_str(StylesheetAssets.ImagesDir))
        except Exception:
            # Fallback if path management not initialized
            stylesheet_images_dir = self.get_project_root() / "data" / ".app_data" / "stylesheets" / "images"

        # Determine icon variant based on current theme
        # Light theme -> dark icons (for contrast)
        # Dark theme -> light icons (for contrast)
        icon_variant = "dark" if self._current_theme in ("light", "soft") else "light"

        # Replace {{ICON_VARIANT}} placeholder with theme-appropriate value
        content = content.replace("{{ICON_VARIANT}}", icon_variant)

        def resolve_url(match: re.Match) -> str:
            url_content = match.group(1)

            # Skip if already absolute or data URI
            if url_content.startswith(("http", "data:", "/")):
                return match.group(0)

            # Clean up the path
            image_path = url_content.strip("'\"")
            image_name = Path(image_path).name

            # Try to find the image in multiple locations (priority order)
            candidates = [
                # 1. Central stylesheet images directory (recommended)
                #    For paths like "images/dropdown.png" -> look for just the filename
                stylesheet_images_dir / image_name,
                # 2. Central stylesheet images with subdirectory
                #    For paths like "images/dropdown.png" -> preserve subdirectory
                stylesheet_images_dir / image_path.replace("images/", ""),
                # 3. Relative to stylesheet directory
                stylesheet_dir / image_path,
                # 4. In widget_icons subfolder of stylesheet directory
                stylesheet_dir / "widget_icons" / image_path,
            ]

            for candidate in candidates:
                if candidate.exists():
                    # Return with forward slashes for QSS compatibility
                    return f'url("{candidate.as_posix()}")'

            # Return original if not found
            return match.group(0)

        # Match url() patterns
        pattern = re.compile(r'url\s*\(\s*([^)]+)\s*\)', re.IGNORECASE)
        return pattern.sub(resolve_url, content)

    def _apply_stylesheets(self, widget: QWidget, paths: List[str]) -> None:
        """Apply stylesheets to a widget."""
        try:
            combined = []
            for path in paths:
                css = self.load_stylesheet(path)
                if css:
                    combined.append(css)

            if combined:
                widget.setStyleSheet("\n".join(combined))
        except RuntimeError:
            # Widget may have been deleted
            pass

    def _update_all_widgets(self) -> None:
        """Update all registered widgets with current theme."""
        valid_entries: List[RegisteredWidget] = []

        for entry in self._registered:
            widget = entry.widget_ref()

            if widget is not None and self._is_widget_valid(entry.widget_ref):
                valid_entries.append(entry)
                self._apply_stylesheets(widget, entry.resolved_paths)

        # Keep only valid entries
        self._registered = valid_entries
    @staticmethod
    def _is_widget_valid(widget_ref: weakref.ref) -> bool:
        """Check if a widget reference is still valid."""
        widget = widget_ref()
        if widget is None:
            return False

        try:
            widget.isVisible()
            return True
        except RuntimeError:
            return False
__init__()

Initialize the stylesheet manager.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def __init__(self) -> None:
    """Initialize the stylesheet manager."""
    self._registered: List[RegisteredWidget] = []
    self._current_theme: str = self._load_saved_theme()
    self._stylesheet_base: str = self.DEFAULT_STYLESHEET_BASE
    self._source_theme: str = self.DEFAULT_SOURCE_THEME
    self._project_root: Optional[Path] = None
    self._theme_change_callbacks: List[Callable[[str], None]] = []
apply_initial_theme()

Apply the saved theme on application startup.

Call this method after the QApplication is created to apply the palette and global styles. This should be called once during application initialization.

Example::

app = QApplication(sys.argv)
StylesheetManager.instance().apply_initial_theme()
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def apply_initial_theme(self) -> None:
    """
    Apply the saved theme on application startup.

    Call this method after the QApplication is created to apply
    the palette and global styles. This should be called once
    during application initialization.

    Example::

        app = QApplication(sys.argv)
        StylesheetManager.instance().apply_initial_theme()
    """
    theme = self._current_theme

    # Apply QPalette for native widgets and Windows frame
    palette = create_palette(theme)
    self._apply_palette(palette)

    # Apply global context menu styles
    self._apply_global_styles(theme)

    # Sync icon system with theme
    self._sync_icon_theme(theme)
cleanup()

Remove references to deleted widgets.

Called automatically during updates, but can be called manually to free memory.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def cleanup(self) -> None:
    """
    Remove references to deleted widgets.

    Called automatically during updates, but can be
    called manually to free memory.
    """
    self._registered = [
        entry
        for entry in self._registered
        if entry.widget_ref() is not None
        and self._is_widget_valid(entry.widget_ref())
    ]
configure(stylesheet_base=None, source_theme=None, project_root=None)

Configure the stylesheet manager.

Parameters:

Name Type Description Default
stylesheet_base Optional[str]

Base directory for source stylesheets.

None
source_theme Optional[str]

Theme of the source stylesheets.

None
project_root Optional[Union[str, Path]]

Project root directory for path resolution.

None

Example::

manager.configure(
    stylesheet_base="src/stylesheets",
    source_theme="light",
    project_root="/path/to/project"
)
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def configure(
    self,
    stylesheet_base: Optional[str] = None,
    source_theme: Optional[str] = None,
    project_root: Optional[Union[str, Path]] = None,
) -> None:
    """
    Configure the stylesheet manager.

    Args:
        stylesheet_base: Base directory for source stylesheets.
        source_theme: Theme of the source stylesheets.
        project_root: Project root directory for path resolution.

    Example::

        manager.configure(
            stylesheet_base="src/stylesheets",
            source_theme="light",
            project_root="/path/to/project"
        )
    """
    if stylesheet_base is not None:
        self._stylesheet_base = stylesheet_base
    if source_theme is not None:
        self._source_theme = source_theme
    if project_root is not None:
        self._project_root = Path(project_root)
get_project_root()

Get the project root directory.

Returns:

Type Description
Path

Path to the project root.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def get_project_root(self) -> Path:
    """
    Get the project root directory.

    Returns:
        Path to the project root.
    """
    if self._project_root is None:
        # Auto-detect from current file location
        current_file = Path(__file__).resolve()
        # Navigate up to find project root (contains 'src' folder)
        for parent in current_file.parents:
            if (parent / "src").exists() and (parent / "QssHandler").exists():
                self._project_root = parent
                break
        if self._project_root is None:
            self._project_root = Path.cwd()
    return self._project_root
get_registered_count()

Get the number of registered widgets.

Returns:

Type Description
int

Number of registered entries.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def get_registered_count(self) -> int:
    """
    Get the number of registered widgets.

    Returns:
        Number of registered entries.
    """
    return len(self._registered)
get_theme()

Get the current theme.

Returns:

Type Description
str

Current theme name.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def get_theme(self) -> str:
    """
    Get the current theme.

    Returns:
        Current theme name.
    """
    return self._current_theme
instance() classmethod

Get the singleton instance.

Returns:

Type Description
StylesheetManager

The shared StylesheetManager instance.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
@classmethod
def instance(cls) -> "StylesheetManager":
    """
    Get the singleton instance.

    Returns:
        The shared StylesheetManager instance.
    """
    if cls._instance is None:
        cls._instance = cls()
    return cls._instance
load_stylesheet(stylesheet, theme=None)

Load and translate a stylesheet.

This is the main function for loading stylesheets. It: 1. Resolves PathDef or path string to absolute path 2. Reads the source stylesheet file 3. Translates colors if needed for the target theme 4. Resolves SVG paths in the stylesheet 5. Caches the result

Parameters:

Name Type Description Default
stylesheet StylesheetSpec

PathDef object or path string to the stylesheet.

required
theme Optional[str]

Target theme (default: current theme).

None

Returns:

Type Description
str

Translated stylesheet string.

Example

Using PathDef::

from mymodule.constants.paths import Stylesheets
css = manager.load_stylesheet(Stylesheets.MainView)

Using string path::

css = manager.load_stylesheet("views/main.qss")
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def load_stylesheet(
    self,
    stylesheet: StylesheetSpec,
    theme: Optional[str] = None,
) -> str:
    """
    Load and translate a stylesheet.

    This is the main function for loading stylesheets. It:
    1. Resolves PathDef or path string to absolute path
    2. Reads the source stylesheet file
    3. Translates colors if needed for the target theme
    4. Resolves SVG paths in the stylesheet
    5. Caches the result

    Args:
        stylesheet: PathDef object or path string to the stylesheet.
        theme: Target theme (default: current theme).

    Returns:
        Translated stylesheet string.

    Example:
        Using PathDef::

            from mymodule.constants.paths import Stylesheets
            css = manager.load_stylesheet(Stylesheets.MainView)

        Using string path::

            css = manager.load_stylesheet("views/main.qss")
    """
    theme = theme or self._current_theme

    # Resolve PathDef or string to path
    resolved_path = self._resolve_spec(stylesheet)

    # Check cache
    cached = StylesheetCache.get(resolved_path, theme)
    if cached is not None:
        return cached

    # Load source stylesheet
    content = self._read_stylesheet_file(resolved_path)
    if content is None:
        return ""

    # Translate if needed
    if theme != self._source_theme:
        content = ThemeTranslator.translate(
            content,
            source_theme=self._source_theme,
            target_theme=theme,
        )

    # Resolve image paths (SVG, PNG, etc.) and replace {{ICON_VARIANT}} placeholder
    content = self._resolve_image_paths(content, resolved_path)

    # Cache and return
    StylesheetCache.put(resolved_path, theme, content)
    return content
on_theme_change(callback)

Register a callback for theme changes.

Parameters:

Name Type Description Default
callback Callable[[str], None]

Function to call with new theme name.

required
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def on_theme_change(self, callback: Callable[[str], None]) -> None:
    """
    Register a callback for theme changes.

    Args:
        callback: Function to call with new theme name.
    """
    self._theme_change_callbacks.append(callback)
refresh_all()

Refresh all registered widgets.

Call this after modifying stylesheets on disk.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def refresh_all(self) -> None:
    """
    Refresh all registered widgets.

    Call this after modifying stylesheets on disk.
    """
    StylesheetCache.clear()
    self._update_all_widgets()
refresh_widget(widget)

Refresh stylesheet for a specific widget.

Parameters:

Name Type Description Default
widget QWidget

Widget to refresh.

required
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def refresh_widget(self, widget: QWidget) -> None:
    """
    Refresh stylesheet for a specific widget.

    Args:
        widget: Widget to refresh.
    """
    for entry in self._registered:
        if entry.widget_ref() is widget:
            self._apply_stylesheets(widget, entry.resolved_paths)
            break
register(widget, stylesheets, module=None)

Register a widget for stylesheet management.

The widget will have its stylesheet applied immediately and will be automatically updated when the theme changes.

Parameters:

Name Type Description Default
widget QWidget

The Qt widget to style.

required
stylesheets List[StylesheetSpec]

List of PathDef objects or path strings.

required
module Optional[str]

Optional module subdirectory (legacy support for string paths).

None
Example

Using PathDef (recommended)::

from mymodule.constants.paths import Stylesheets

manager.register(self, [Stylesheets.MainView, Stylesheets.ListItem])

Using string paths (legacy)::

manager.register(
    self,
    ["startup_view.qss", "list_item.qss"],
    module="PlantDesign/StartUp"
)
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def register(
    self,
    widget: QWidget,
    stylesheets: List[StylesheetSpec],
    module: Optional[str] = None,
) -> None:
    """
    Register a widget for stylesheet management.

    The widget will have its stylesheet applied immediately and
    will be automatically updated when the theme changes.

    Args:
        widget: The Qt widget to style.
        stylesheets: List of PathDef objects or path strings.
        module: Optional module subdirectory (legacy support for string paths).

    Example:
        Using PathDef (recommended)::

            from mymodule.constants.paths import Stylesheets

            manager.register(self, [Stylesheets.MainView, Stylesheets.ListItem])

        Using string paths (legacy)::

            manager.register(
                self,
                ["startup_view.qss", "list_item.qss"],
                module="PlantDesign/StartUp"
            )
    """
    widget_ref = weakref.ref(widget)

    # Resolve stylesheet paths
    resolved_paths = self._resolve_stylesheets(stylesheets, module)

    entry = RegisteredWidget(
        widget_ref=widget_ref,
        stylesheets=stylesheets,
        resolved_paths=resolved_paths,
    )

    self._registered.append(entry)

    # Apply stylesheet immediately
    self._apply_stylesheets(widget, resolved_paths)
reset_instance() classmethod

Reset the singleton instance.

Useful for testing or reinitializing the manager.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
@classmethod
def reset_instance(cls) -> None:
    """
    Reset the singleton instance.

    Useful for testing or reinitializing the manager.
    """
    cls._instance = None
    StylesheetCache.clear()
set_theme(theme, persist=True)

Switch theme and update all registered widgets.

This method performs a complete theme switch: 1. Updates the QPalette for native Qt widgets and Windows frame 2. Clears stylesheet cache and re-applies all stylesheets 3. Applies global context menu styles 4. Syncs the icon system 5. Refreshes special widgets (QListWidget, QTextBrowser)

The theme selection is persisted to settings by default.

Parameters:

Name Type Description Default
theme str

Theme name ("light" or "dark").

required
persist bool

If True, save the theme to settings for persistence across application restarts.

True

Example::

manager.set_theme("dark")
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def set_theme(self, theme: str, persist: bool = True) -> None:
    """
    Switch theme and update all registered widgets.

    This method performs a complete theme switch:
    1. Updates the QPalette for native Qt widgets and Windows frame
    2. Clears stylesheet cache and re-applies all stylesheets
    3. Applies global context menu styles
    4. Syncs the icon system
    5. Refreshes special widgets (QListWidget, QTextBrowser)

    The theme selection is persisted to settings by default.

    Args:
        theme: Theme name ("light" or "dark").
        persist: If True, save the theme to settings for
            persistence across application restarts.

    Example::

        manager.set_theme("dark")
    """
    if theme == self._current_theme:
        return

    self._current_theme = theme

    # Apply QPalette for native widgets and Windows frame
    palette = create_palette(theme)
    self._apply_palette(palette)

    # Clear cache and update registered widgets
    StylesheetCache.clear()
    self._update_all_widgets()

    # Apply global context menu styles
    self._apply_global_styles(theme)

    if persist:
        self._save_theme(theme)

    # Sync icon system with new theme
    self._sync_icon_theme(theme)

    # Refresh special widgets
    self._refresh_special_widgets(palette)

    # Notify callbacks
    for callback in self._theme_change_callbacks:
        try:
            callback(theme)
        except Exception:
            pass
unregister(widget)

Unregister a widget from stylesheet management.

Parameters:

Name Type Description Default
widget QWidget

The widget to unregister.

required

Returns:

Type Description
bool

True if the widget was found and unregistered.

Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def unregister(self, widget: QWidget) -> bool:
    """
    Unregister a widget from stylesheet management.

    Args:
        widget: The widget to unregister.

    Returns:
        True if the widget was found and unregistered.
    """
    initial_count = len(self._registered)
    self._registered = [
        entry
        for entry in self._registered
        if entry.widget_ref() is not widget
    ]
    return len(self._registered) < initial_count

ThemeTranslator

Runtime theme translator for QSS stylesheets.

Translates colors in stylesheet text from one theme to another using the centralized ColorSystem. Supports hex colors, rgb(), and rgba() formats.

The translator uses intelligent color mapping to convert semantic colors (backgrounds, text, surfaces) appropriately between themes while preserving the stylesheet structure.

Example

Basic usage::

# Light to dark translation
dark_css = ThemeTranslator.translate(
    light_css,
    source_theme="light",
    target_theme="dark"
)

# Check if translation needed
if not ThemeTranslator.is_source_theme(css, "light"):
    # Already translated or unknown source
    pass
Note

For best results, source stylesheets should use colors from the ColorSystem light theme palette.

Source code in src\shared_services\rendering\stylesheets\theme_translator.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
class ThemeTranslator:
    """
    Runtime theme translator for QSS stylesheets.

    Translates colors in stylesheet text from one theme to another
    using the centralized ColorSystem. Supports hex colors, rgb(),
    and rgba() formats.

    The translator uses intelligent color mapping to convert
    semantic colors (backgrounds, text, surfaces) appropriately
    between themes while preserving the stylesheet structure.

    Example:
        Basic usage::

            # Light to dark translation
            dark_css = ThemeTranslator.translate(
                light_css,
                source_theme="light",
                target_theme="dark"
            )

            # Check if translation needed
            if not ThemeTranslator.is_source_theme(css, "light"):
                # Already translated or unknown source
                pass

    Note:
        For best results, source stylesheets should use colors
        from the ColorSystem light theme palette.
    """

    # Cache for color mappings per theme pair
    _mapping_cache: Dict[Tuple[str, str], Dict[str, str]] = {}

    @classmethod
    def get_available_themes(cls) -> List[str]:
        """
        Get list of available themes.

        Returns:
            List of theme names that can be used for translation.
        """
        return ["light", "dark", "soft", "gray"]

    @classmethod
    def translate(
        cls,
        stylesheet: str,
        source_theme: str = "light",
        target_theme: str = "dark",
    ) -> str:
        """
        Translate a stylesheet from source theme to target theme.

        Args:
            stylesheet: QSS stylesheet text to translate.
            source_theme: Source theme name (default: "light").
            target_theme: Target theme name (default: "dark").

        Returns:
            Translated stylesheet text.

        Raises:
            ValueError: If source and target themes are the same.

        Example::

            light_css = "background: #FFFFFF; color: #0F172A;"
            dark_css = ThemeTranslator.translate(light_css)
            # dark_css now has dark theme colors
        """
        if source_theme == target_theme:
            return stylesheet

        # Get color mappings for this theme pair
        mappings = cls._get_color_mappings(source_theme, target_theme)

        # Perform translation
        translated = cls._translate_colors(stylesheet, mappings)

        return translated

    @classmethod
    def is_source_theme(cls, stylesheet: str, expected_source: str = "light") -> bool:
        """
        Check if a stylesheet appears to be in the expected source theme.

        This is a heuristic check based on common colors. Use it to
        determine if translation is needed.

        Args:
            stylesheet: Stylesheet text to check.
            expected_source: Expected source theme.

        Returns:
            True if the stylesheet appears to be in the source theme.
        """
        if expected_source == "light":
            # Check for common light theme indicators
            light_indicators = ["#FFFFFF", "#FFF", "#F8FAFC", "#F1F5F9", "white"]
            dark_indicators = ["#1E1E1E", "#252526", "#2D2D2D", "#1A1A1A"]

            light_count = sum(
                1 for indicator in light_indicators
                if indicator.lower() in stylesheet.lower()
            )
            dark_count = sum(
                1 for indicator in dark_indicators
                if indicator.lower() in stylesheet.lower()
            )

            return light_count > dark_count

        return True  # Default to assuming it's the source theme

    @classmethod
    def _get_color_mappings(
        cls,
        source_theme: str,
        target_theme: str,
    ) -> Dict[str, str]:
        """
        Get or build color mappings for a theme pair.

        Args:
            source_theme: Source theme name.
            target_theme: Target theme name.

        Returns:
            Dictionary mapping source colors to target colors.
        """
        cache_key = (source_theme, target_theme)

        if cache_key not in cls._mapping_cache:
            cls._mapping_cache[cache_key] = cls._build_color_mappings(
                source_theme, target_theme
            )

        return cls._mapping_cache[cache_key]

    @classmethod
    def _build_color_mappings(
        cls,
        source_theme: str,
        target_theme: str,
    ) -> Dict[str, str]:
        """
        Build color mappings from ColorSystem.

        Args:
            source_theme: Source theme name.
            target_theme: Target theme name.

        Returns:
            Dictionary mapping source colors to target colors.
        """
        source_colors = ColorSystem.get_colors(source_theme)
        target_colors = ColorSystem.get_colors(target_theme)

        mappings = {}

        # Add common CSS color name mappings
        if source_theme == "light" and target_theme == "dark":
            css_mappings = {
                "white": target_colors["background"],
                "#FFFFFF": target_colors["background"],
                "#FFF": target_colors["background"],
                "black": target_colors["text_primary"],
                "#000000": target_colors["text_primary"],
                "#000": target_colors["text_primary"],
            }
            mappings.update(css_mappings)

            # Map semantic colors
            for key in source_colors:
                if not key.startswith("#") and key in target_colors:
                    source_value = source_colors[key]
                    if source_value not in css_mappings:
                        mappings[source_value] = target_colors[key]

            # Add grayscale mappings for common Material Design colors
            grayscale_mappings = {
                "#FAFAFA": target_colors["surface_1"],
                "#F5F5F5": target_colors["surface_1"],
                "#F0F0F0": target_colors["surface_2"],
                "#EEEEEE": target_colors["surface_2"],
                "#E0E0E0": target_colors["surface_3"],
                "#DDDDDD": target_colors["surface_3"],
                "#CCCCCC": target_colors["surface_4"],
                "#BDBDBD": target_colors["text_disabled"],
                "#9E9E9E": target_colors["text_tertiary"],
                "#757575": target_colors["text_secondary"],
                "#616161": target_colors["text_secondary"],
                "#424242": target_colors["text_primary"],
                "#303030": target_colors["text_primary"],
                "#212121": target_colors["text_primary"],
            }
            mappings.update(grayscale_mappings)

        elif source_theme == "light" and target_theme == "soft":
            # Light to soft mapping - shift pure whites to warm off-whites
            css_mappings = {
                "white": target_colors["background"],
                "#FFFFFF": target_colors["background"],
                "#FFF": target_colors["background"],
                "black": target_colors["text_primary"],
                "#000000": target_colors["text_primary"],
                "#000": target_colors["text_primary"],
            }
            mappings.update(css_mappings)

            # Map semantic colors
            for key in source_colors:
                if not key.startswith("#") and key in target_colors:
                    source_value = source_colors[key]
                    if source_value not in css_mappings:
                        mappings[source_value] = target_colors[key]

            # Add grayscale mappings - shift cool grays to warm grays
            grayscale_mappings = {
                "#FAFAFA": target_colors["surface_1"],
                "#F5F5F5": target_colors["surface_1"],
                "#F0F0F0": target_colors["surface_2"],
                "#EEEEEE": target_colors["surface_2"],
                "#E0E0E0": target_colors["surface_3"],
                "#DDDDDD": target_colors["surface_3"],
                "#CCCCCC": target_colors["surface_4"],
                "#BDBDBD": target_colors["text_disabled"],
                "#9E9E9E": target_colors["text_tertiary"],
                "#757575": target_colors["text_secondary"],
                "#616161": target_colors["text_secondary"],
                "#424242": target_colors["text_primary"],
                "#303030": target_colors["text_primary"],
                "#212121": target_colors["text_primary"],
            }
            mappings.update(grayscale_mappings)

        elif source_theme == "light" and target_theme == "gray":
            # Light to gray mapping - similar to dark but with lifted grays
            css_mappings = {
                "white": target_colors["background"],
                "#FFFFFF": target_colors["background"],
                "#FFF": target_colors["background"],
                "black": target_colors["text_primary"],
                "#000000": target_colors["text_primary"],
                "#000": target_colors["text_primary"],
            }
            mappings.update(css_mappings)

            # Map semantic colors
            for key in source_colors:
                if not key.startswith("#") and key in target_colors:
                    source_value = source_colors[key]
                    if source_value not in css_mappings:
                        mappings[source_value] = target_colors[key]

            # Add grayscale mappings
            grayscale_mappings = {
                "#FAFAFA": target_colors["surface_1"],
                "#F5F5F5": target_colors["surface_1"],
                "#F0F0F0": target_colors["surface_2"],
                "#EEEEEE": target_colors["surface_2"],
                "#E0E0E0": target_colors["surface_3"],
                "#DDDDDD": target_colors["surface_3"],
                "#CCCCCC": target_colors["surface_4"],
                "#BDBDBD": target_colors["text_disabled"],
                "#9E9E9E": target_colors["text_tertiary"],
                "#757575": target_colors["text_secondary"],
                "#616161": target_colors["text_secondary"],
                "#424242": target_colors["text_primary"],
                "#303030": target_colors["text_primary"],
                "#212121": target_colors["text_primary"],
            }
            mappings.update(grayscale_mappings)

        elif source_theme == "dark" and target_theme == "light":
            # Reverse mapping for dark to light
            css_mappings = {
                "#1E1E1E": target_colors["background"],
                "#252526": target_colors["surface_1"],
                "#2D2D2D": target_colors["surface_2"],
            }
            mappings.update(css_mappings)

            for key in source_colors:
                if not key.startswith("#") and key in target_colors:
                    source_value = source_colors[key]
                    if source_value not in css_mappings:
                        mappings[source_value] = target_colors[key]

        else:
            # Generic mapping: map all matching semantic keys
            for key in source_colors:
                if not key.startswith("#") and key in target_colors:
                    mappings[source_colors[key]] = target_colors[key]

        return mappings

    @classmethod
    def _translate_colors(
        cls,
        stylesheet: str,
        mappings: Dict[str, str],
    ) -> str:
        """
        Perform color translation on stylesheet text.

        Args:
            stylesheet: Stylesheet text to translate.
            mappings: Color mappings to apply.

        Returns:
            Translated stylesheet text.
        """
        translated = stylesheet

        # Sort by length (longest first) to avoid partial matches
        sorted_mappings = sorted(
            mappings.items(),
            key=lambda x: len(x[0]),
            reverse=True,
        )

        # Replace hex and named colors
        for source_color, target_color in sorted_mappings:
            if source_color.startswith("#"):
                # Hex color - use negative lookbehind/lookahead
                pattern = re.compile(
                    r"(?<![a-fA-F0-9])" + re.escape(source_color) + r"(?![a-fA-F0-9])",
                    re.IGNORECASE,
                )
            elif source_color.startswith("rgb"):
                pattern = re.compile(re.escape(source_color), re.IGNORECASE)
            else:
                # Named color - word boundary
                pattern = re.compile(
                    r"\b" + re.escape(source_color) + r"\b",
                    re.IGNORECASE,
                )

            translated = pattern.sub(target_color, translated)

        # Handle RGB variants
        translated = cls._translate_rgb_colors(translated, mappings)

        return translated

    @classmethod
    def _translate_rgb_colors(
        cls,
        stylesheet: str,
        mappings: Dict[str, str],
    ) -> str:
        """
        Translate rgb() and rgba() color formats.

        Args:
            stylesheet: Stylesheet text.
            mappings: Color mappings (hex to hex).

        Returns:
            Stylesheet with RGB colors translated.
        """
        # Build RGB mappings from hex mappings
        rgb_mappings: Dict[Tuple[int, int, int], Tuple[int, int, int]] = {}

        for source_hex, target_hex in mappings.items():
            if source_hex.startswith("#") and target_hex.startswith("#"):
                try:
                    source_rgb = cls._hex_to_rgb(source_hex)
                    target_rgb = cls._hex_to_rgb(target_hex)
                    rgb_mappings[source_rgb] = target_rgb
                except ValueError:
                    continue

        def replace_rgb(match: re.Match) -> str:
            r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
            if (r, g, b) in rgb_mappings:
                new_r, new_g, new_b = rgb_mappings[(r, g, b)]
                return f"rgb({new_r}, {new_g}, {new_b})"
            return match.group(0)

        def replace_rgba(match: re.Match) -> str:
            r, g, b = int(match.group(1)), int(match.group(2)), int(match.group(3))
            a = match.group(4)
            if (r, g, b) in rgb_mappings:
                new_r, new_g, new_b = rgb_mappings[(r, g, b)]
                return f"rgba({new_r}, {new_g}, {new_b}, {a})"
            return match.group(0)

        # Apply RGB replacements
        stylesheet = re.sub(
            r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)",
            replace_rgb,
            stylesheet,
        )
        stylesheet = re.sub(
            r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([01](?:\.\d+)?|\.\d+)\s*\)",
            replace_rgba,
            stylesheet,
        )

        return stylesheet

    @staticmethod
    def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
        """
        Convert hex color to RGB tuple.

        Args:
            hex_color: Hex color string (e.g., "#FFFFFF" or "#FFF").

        Returns:
            Tuple of (red, green, blue) values (0-255).

        Raises:
            ValueError: If hex color format is invalid.
        """
        hex_color = hex_color.lstrip("#")
        if len(hex_color) == 3:
            hex_color = "".join([c * 2 for c in hex_color])
        if len(hex_color) != 6:
            raise ValueError(f"Invalid hex color: {hex_color}")
        return (
            int(hex_color[0:2], 16),
            int(hex_color[2:4], 16),
            int(hex_color[4:6], 16),
        )

    @classmethod
    def clear_mapping_cache(cls) -> None:
        """
        Clear the color mapping cache.

        Call this if ColorSystem colors are modified at runtime.
        """
        cls._mapping_cache.clear()
clear_mapping_cache() classmethod

Clear the color mapping cache.

Call this if ColorSystem colors are modified at runtime.

Source code in src\shared_services\rendering\stylesheets\theme_translator.py
@classmethod
def clear_mapping_cache(cls) -> None:
    """
    Clear the color mapping cache.

    Call this if ColorSystem colors are modified at runtime.
    """
    cls._mapping_cache.clear()
get_available_themes() classmethod

Get list of available themes.

Returns:

Type Description
List[str]

List of theme names that can be used for translation.

Source code in src\shared_services\rendering\stylesheets\theme_translator.py
@classmethod
def get_available_themes(cls) -> List[str]:
    """
    Get list of available themes.

    Returns:
        List of theme names that can be used for translation.
    """
    return ["light", "dark", "soft", "gray"]
is_source_theme(stylesheet, expected_source='light') classmethod

Check if a stylesheet appears to be in the expected source theme.

This is a heuristic check based on common colors. Use it to determine if translation is needed.

Parameters:

Name Type Description Default
stylesheet str

Stylesheet text to check.

required
expected_source str

Expected source theme.

'light'

Returns:

Type Description
bool

True if the stylesheet appears to be in the source theme.

Source code in src\shared_services\rendering\stylesheets\theme_translator.py
@classmethod
def is_source_theme(cls, stylesheet: str, expected_source: str = "light") -> bool:
    """
    Check if a stylesheet appears to be in the expected source theme.

    This is a heuristic check based on common colors. Use it to
    determine if translation is needed.

    Args:
        stylesheet: Stylesheet text to check.
        expected_source: Expected source theme.

    Returns:
        True if the stylesheet appears to be in the source theme.
    """
    if expected_source == "light":
        # Check for common light theme indicators
        light_indicators = ["#FFFFFF", "#FFF", "#F8FAFC", "#F1F5F9", "white"]
        dark_indicators = ["#1E1E1E", "#252526", "#2D2D2D", "#1A1A1A"]

        light_count = sum(
            1 for indicator in light_indicators
            if indicator.lower() in stylesheet.lower()
        )
        dark_count = sum(
            1 for indicator in dark_indicators
            if indicator.lower() in stylesheet.lower()
        )

        return light_count > dark_count

    return True  # Default to assuming it's the source theme
translate(stylesheet, source_theme='light', target_theme='dark') classmethod

Translate a stylesheet from source theme to target theme.

Parameters:

Name Type Description Default
stylesheet str

QSS stylesheet text to translate.

required
source_theme str

Source theme name (default: "light").

'light'
target_theme str

Target theme name (default: "dark").

'dark'

Returns:

Type Description
str

Translated stylesheet text.

Raises:

Type Description
ValueError

If source and target themes are the same.

Example::

light_css = "background: #FFFFFF; color: #0F172A;"
dark_css = ThemeTranslator.translate(light_css)
# dark_css now has dark theme colors
Source code in src\shared_services\rendering\stylesheets\theme_translator.py
@classmethod
def translate(
    cls,
    stylesheet: str,
    source_theme: str = "light",
    target_theme: str = "dark",
) -> str:
    """
    Translate a stylesheet from source theme to target theme.

    Args:
        stylesheet: QSS stylesheet text to translate.
        source_theme: Source theme name (default: "light").
        target_theme: Target theme name (default: "dark").

    Returns:
        Translated stylesheet text.

    Raises:
        ValueError: If source and target themes are the same.

    Example::

        light_css = "background: #FFFFFF; color: #0F172A;"
        dark_css = ThemeTranslator.translate(light_css)
        # dark_css now has dark theme colors
    """
    if source_theme == target_theme:
        return stylesheet

    # Get color mappings for this theme pair
    mappings = cls._get_color_mappings(source_theme, target_theme)

    # Perform translation
    translated = cls._translate_colors(stylesheet, mappings)

    return translated

apply_initial_theme()

Apply the saved theme on application startup.

Call this after the QApplication is created to apply the palette and global styles for the saved theme.

Example::

app = QApplication(sys.argv)
apply_initial_theme()  # Apply palette and context menu styles
Source code in src\shared_services\rendering\stylesheets\api.py
def apply_initial_theme() -> None:
    """
    Apply the saved theme on application startup.

    Call this after the QApplication is created to apply
    the palette and global styles for the saved theme.

    Example::

        app = QApplication(sys.argv)
        apply_initial_theme()  # Apply palette and context menu styles
    """
    manager = StylesheetManager.instance()
    manager.apply_initial_theme()

clear_stylesheet_cache()

Clear the stylesheet cache.

Call this if stylesheets have been modified on disk if you want to reload them.

Source code in src\shared_services\rendering\stylesheets\api.py
def clear_stylesheet_cache() -> None:
    """
    Clear the stylesheet cache.

    Call this if stylesheets have been modified on disk
    if you want to reload them.
    """
    StylesheetCache.clear()

get_color(key, theme=None)

Get a QColor from the ColorSystem for the current (or specified) theme.

This gives custom paint code access to the same semantic color tokens that QSS stylesheets use, so delegates and other owner-draw code stay consistent with the application theme.

Parameters:

Name Type Description Default
key str

Color token name (e.g. "primary", "surface_2", "text_secondary"). Must match a key in ColorSystem.COLORS[theme].

required
theme Optional[str]

Optional theme override. Defaults to the current theme.

None

Returns:

Type Description
QColor

QColor for the requested token.

Raises:

Type Description
KeyError

If key is not found in the color palette.

Example::

from src.shared_services.rendering.stylesheets.api import get_color

hover_bg = get_color("surface_2")
painter.fillRect(rect, hover_bg)
Source code in src\shared_services\rendering\stylesheets\api.py
def get_color(key: str, theme: Optional[str] = None) -> QColor:
    """
    Get a QColor from the ColorSystem for the current (or specified) theme.

    This gives custom paint code access to the same semantic color tokens
    that QSS stylesheets use, so delegates and other owner-draw code
    stay consistent with the application theme.

    Args:
        key: Color token name (e.g. ``"primary"``, ``"surface_2"``,
             ``"text_secondary"``).  Must match a key in
             ``ColorSystem.COLORS[theme]``.
        theme: Optional theme override. Defaults to the current theme.

    Returns:
        QColor for the requested token.

    Raises:
        KeyError: If *key* is not found in the color palette.

    Example::

        from src.shared_services.rendering.stylesheets.api import get_color

        hover_bg = get_color("surface_2")
        painter.fillRect(rect, hover_bg)
    """
    theme = theme or StylesheetManager.instance().get_theme()
    colors = ColorSystem.get_colors(theme)
    value = colors[key]
    return _parse_color_value(value)

get_stylesheet_manager()

Convenience function to get the StylesheetManager singleton.

Returns:

Type Description
StylesheetManager

The shared StylesheetManager instance.

Example::

manager = get_stylesheet_manager()
manager.register(widget, ["style.qss"])
Source code in src\shared_services\rendering\stylesheets\stylesheet_manager.py
def get_stylesheet_manager() -> StylesheetManager:
    """
    Convenience function to get the StylesheetManager singleton.

    Returns:
        The shared StylesheetManager instance.

    Example::

        manager = get_stylesheet_manager()
        manager.register(widget, ["style.qss"])
    """
    return StylesheetManager.instance()

get_theme()

Get the current theme.

Returns:

Type Description
str

Current theme name.

Source code in src\shared_services\rendering\stylesheets\api.py
def get_theme() -> str:
    """
    Get the current theme.

    Returns:
        Current theme name.
    """
    manager = StylesheetManager.instance()
    return manager.get_theme()

load_stylesheet(stylesheet, theme=None)

Load and translate a stylesheet file.

This is a convenience function that loads a stylesheet and translates it to the specified (or current) theme.

Parameters:

Name Type Description Default
stylesheet StylesheetSpec

PathDef object or path string to the stylesheet.

required
theme Optional[str]

Target theme (default: current theme from manager).

None

Returns:

Type Description
str

Translated stylesheet string ready to apply.

Example

Using PathDef (recommended)::

from mymodule.constants.paths import Stylesheets
css = load_stylesheet(Stylesheets.MainView)
widget.setStyleSheet(css)

Using string path::

css = load_stylesheet("views/main_view.qss")

Force specific theme::

dark_css = load_stylesheet(Stylesheets.MainView, theme="dark")
Source code in src\shared_services\rendering\stylesheets\api.py
def load_stylesheet(
    stylesheet: StylesheetSpec,
    theme: Optional[str] = None,
) -> str:
    """
    Load and translate a stylesheet file.

    This is a convenience function that loads a stylesheet and
    translates it to the specified (or current) theme.

    Args:
        stylesheet: PathDef object or path string to the stylesheet.
        theme: Target theme (default: current theme from manager).

    Returns:
        Translated stylesheet string ready to apply.

    Example:
        Using PathDef (recommended)::

            from mymodule.constants.paths import Stylesheets
            css = load_stylesheet(Stylesheets.MainView)
            widget.setStyleSheet(css)

        Using string path::

            css = load_stylesheet("views/main_view.qss")

        Force specific theme::

            dark_css = load_stylesheet(Stylesheets.MainView, theme="dark")
    """
    manager = StylesheetManager.instance()
    return manager.load_stylesheet(stylesheet, theme)

load_stylesheet_intelligent(path)

Load a stylesheet with intelligent path handling.

This function is a drop-in replacement for get_dynamic_sheet_intelligent. It handles paths that may contain /DM/ or /WM/ markers and loads from the appropriate source.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to the stylesheet (may contain /DM/ or /WM/).

required

Returns:

Type Description
str

Translated stylesheet string for current theme.

Example::

# These all work the same:
css = load_stylesheet_intelligent("QssHandler/Stylesheets/WM/view.qss")
css = load_stylesheet_intelligent("QssHandler/Stylesheets/DM/view.qss")
css = load_stylesheet_intelligent("view.qss")
Source code in src\shared_services\rendering\stylesheets\api.py
def load_stylesheet_intelligent(path: Union[str, Path]) -> str:
    """
    Load a stylesheet with intelligent path handling.

    This function is a drop-in replacement for get_dynamic_sheet_intelligent.
    It handles paths that may contain /DM/ or /WM/ markers and loads from
    the appropriate source.

    Args:
        path: Path to the stylesheet (may contain /DM/ or /WM/).

    Returns:
        Translated stylesheet string for current theme.

    Example::

        # These all work the same:
        css = load_stylesheet_intelligent("QssHandler/Stylesheets/WM/view.qss")
        css = load_stylesheet_intelligent("QssHandler/Stylesheets/DM/view.qss")
        css = load_stylesheet_intelligent("view.qss")
    """
    path_str = str(path)

    # Normalize path - convert DM paths to WM (source)
    normalized = path_str.replace("/DM/", "/WM/").replace("\\DM\\", "\\WM\\")

    return load_stylesheet(normalized)

set_theme(theme)

Set the application theme.

This updates the StylesheetManager theme and refreshes all registered widgets.

Parameters:

Name Type Description Default
theme str

Theme name ("light" or "dark").

required

Example::

set_theme("dark")  # All registered widgets update
Source code in src\shared_services\rendering\stylesheets\api.py
def set_theme(theme: str) -> None:
    """
    Set the application theme.

    This updates the StylesheetManager theme and refreshes all
    registered widgets.

    Args:
        theme: Theme name ("light" or "dark").

    Example::

        set_theme("dark")  # All registered widgets update
    """
    manager = StylesheetManager.instance()
    manager.set_theme(theme)