sli-ui-toolkit 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. sli_ui_toolkit/__init__.py +72 -0
  2. sli_ui_toolkit/_version.py +1 -0
  3. sli_ui_toolkit/config.py +63 -0
  4. sli_ui_toolkit/core/__init__.py +7 -0
  5. sli_ui_toolkit/core/logging.py +99 -0
  6. sli_ui_toolkit/i18n.py +253 -0
  7. sli_ui_toolkit/icons.py +56 -0
  8. sli_ui_toolkit/managers.py +13 -0
  9. sli_ui_toolkit/palettes.py +176 -0
  10. sli_ui_toolkit/resources/assets/icons/dark/add.svg +1 -0
  11. sli_ui_toolkit/resources/assets/icons/dark/add_circle.svg +5 -0
  12. sli_ui_toolkit/resources/assets/icons/dark/calendar.svg +1 -0
  13. sli_ui_toolkit/resources/assets/icons/dark/chart.svg +5 -0
  14. sli_ui_toolkit/resources/assets/icons/dark/check.svg +1 -0
  15. sli_ui_toolkit/resources/assets/icons/dark/chevron-down.svg +1 -0
  16. sli_ui_toolkit/resources/assets/icons/dark/delete.svg +1 -0
  17. sli_ui_toolkit/resources/assets/icons/dark/download.svg +1 -0
  18. sli_ui_toolkit/resources/assets/icons/dark/edit.svg +1 -0
  19. sli_ui_toolkit/resources/assets/icons/dark/folder_open.svg +1 -0
  20. sli_ui_toolkit/resources/assets/icons/dark/help.svg +1 -0
  21. sli_ui_toolkit/resources/assets/icons/dark/incognito.svg +1 -0
  22. sli_ui_toolkit/resources/assets/icons/dark/photo.svg +1 -0
  23. sli_ui_toolkit/resources/assets/icons/dark/quick_save.svg +55 -0
  24. sli_ui_toolkit/resources/assets/icons/dark/remove.svg +1 -0
  25. sli_ui_toolkit/resources/assets/icons/dark/save.svg +1 -0
  26. sli_ui_toolkit/resources/assets/icons/dark/settings.svg +1 -0
  27. sli_ui_toolkit/resources/assets/icons/dark/sync.svg +1 -0
  28. sli_ui_toolkit/resources/assets/icons/dark/text-manipulator.svg +76 -0
  29. sli_ui_toolkit/resources/assets/icons/light/add.svg +1 -0
  30. sli_ui_toolkit/resources/assets/icons/light/add_circle.svg +5 -0
  31. sli_ui_toolkit/resources/assets/icons/light/calendar.svg +1 -0
  32. sli_ui_toolkit/resources/assets/icons/light/chart.svg +5 -0
  33. sli_ui_toolkit/resources/assets/icons/light/check.svg +1 -0
  34. sli_ui_toolkit/resources/assets/icons/light/chevron-down.svg +1 -0
  35. sli_ui_toolkit/resources/assets/icons/light/delete.svg +1 -0
  36. sli_ui_toolkit/resources/assets/icons/light/download.svg +1 -0
  37. sli_ui_toolkit/resources/assets/icons/light/edit.svg +1 -0
  38. sli_ui_toolkit/resources/assets/icons/light/folder_open.svg +1 -0
  39. sli_ui_toolkit/resources/assets/icons/light/help.svg +1 -0
  40. sli_ui_toolkit/resources/assets/icons/light/incognito.svg +1 -0
  41. sli_ui_toolkit/resources/assets/icons/light/photo.svg +1 -0
  42. sli_ui_toolkit/resources/assets/icons/light/quick_save.svg +16 -0
  43. sli_ui_toolkit/resources/assets/icons/light/remove.svg +1 -0
  44. sli_ui_toolkit/resources/assets/icons/light/save.svg +1 -0
  45. sli_ui_toolkit/resources/assets/icons/light/settings.svg +1 -0
  46. sli_ui_toolkit/resources/assets/icons/light/sync.svg +1 -0
  47. sli_ui_toolkit/resources/assets/icons/light/text-manipulator.svg +76 -0
  48. sli_ui_toolkit/services.py +12 -0
  49. sli_ui_toolkit/style.py +20 -0
  50. sli_ui_toolkit/theme.py +4 -0
  51. sli_ui_toolkit/ui/__init__.py +2 -0
  52. sli_ui_toolkit/ui/dialogs/__init__.py +13 -0
  53. sli_ui_toolkit/ui/dialogs/dialog_helpers.py +115 -0
  54. sli_ui_toolkit/ui/in_window_surface.py +205 -0
  55. sli_ui_toolkit/ui/managers/__init__.py +13 -0
  56. sli_ui_toolkit/ui/managers/flyout_manager.py +259 -0
  57. sli_ui_toolkit/ui/managers/flyout_timer_service.py +102 -0
  58. sli_ui_toolkit/ui/managers/icon_manager.py +44 -0
  59. sli_ui_toolkit/ui/managers/theme_manager.py +220 -0
  60. sli_ui_toolkit/ui/services/__init__.py +19 -0
  61. sli_ui_toolkit/ui/services/dragdrop_service.py +213 -0
  62. sli_ui_toolkit/ui/services/icon_service.py +100 -0
  63. sli_ui_toolkit/ui/services/window_prewarm.py +47 -0
  64. sli_ui_toolkit/ui/widgets/__init__.py +2 -0
  65. sli_ui_toolkit/ui/widgets/atomic/__init__.py +57 -0
  66. sli_ui_toolkit/ui/widgets/atomic/checkbox.py +344 -0
  67. sli_ui_toolkit/ui/widgets/atomic/combobox.py +5 -0
  68. sli_ui_toolkit/ui/widgets/atomic/comboboxes.py +5 -0
  69. sli_ui_toolkit/ui/widgets/atomic/custom_group_widget.py +148 -0
  70. sli_ui_toolkit/ui/widgets/atomic/custom_line_edit.py +218 -0
  71. sli_ui_toolkit/ui/widgets/atomic/drop_zone_label.py +147 -0
  72. sli_ui_toolkit/ui/widgets/atomic/instances_counter_button.py +256 -0
  73. sli_ui_toolkit/ui/widgets/atomic/loading_spinner.py +59 -0
  74. sli_ui_toolkit/ui/widgets/atomic/minimalist_scrollbar.py +270 -0
  75. sli_ui_toolkit/ui/widgets/atomic/radio.py +244 -0
  76. sli_ui_toolkit/ui/widgets/atomic/slider.py +227 -0
  77. sli_ui_toolkit/ui/widgets/atomic/spinbox.py +132 -0
  78. sli_ui_toolkit/ui/widgets/atomic/switch.py +301 -0
  79. sli_ui_toolkit/ui/widgets/atomic/text_labels.py +350 -0
  80. sli_ui_toolkit/ui/widgets/atomic/time_line_edit.py +377 -0
  81. sli_ui_toolkit/ui/widgets/atomic/tooltips.py +311 -0
  82. sli_ui_toolkit/ui/widgets/buttons/__init__.py +4 -0
  83. sli_ui_toolkit/ui/widgets/buttons/_dropdown_menu.py +307 -0
  84. sli_ui_toolkit/ui/widgets/buttons/button.py +811 -0
  85. sli_ui_toolkit/ui/widgets/buttons/button_group.py +97 -0
  86. sli_ui_toolkit/ui/widgets/buttons/capabilities/__init__.py +17 -0
  87. sli_ui_toolkit/ui/widgets/buttons/capabilities/base.py +27 -0
  88. sli_ui_toolkit/ui/widgets/buttons/capabilities/long_press.py +61 -0
  89. sli_ui_toolkit/ui/widgets/buttons/capabilities/menu.py +57 -0
  90. sli_ui_toolkit/ui/widgets/buttons/capabilities/scroll.py +203 -0
  91. sli_ui_toolkit/ui/widgets/buttons/content.py +255 -0
  92. sli_ui_toolkit/ui/widgets/buttons/context.py +41 -0
  93. sli_ui_toolkit/ui/widgets/buttons/layers/__init__.py +18 -0
  94. sli_ui_toolkit/ui/widgets/buttons/layers/_base.py +17 -0
  95. sli_ui_toolkit/ui/widgets/buttons/layers/background.py +87 -0
  96. sli_ui_toolkit/ui/widgets/buttons/layers/badge.py +107 -0
  97. sli_ui_toolkit/ui/widgets/buttons/layers/content.py +16 -0
  98. sli_ui_toolkit/ui/widgets/buttons/layers/strikethrough.py +22 -0
  99. sli_ui_toolkit/ui/widgets/buttons/layers/underline.py +81 -0
  100. sli_ui_toolkit/ui/widgets/buttons/painter.py +132 -0
  101. sli_ui_toolkit/ui/widgets/buttons/state.py +17 -0
  102. sli_ui_toolkit/ui/widgets/buttons/variants.py +178 -0
  103. sli_ui_toolkit/ui/widgets/comboboxes/__init__.py +4 -0
  104. sli_ui_toolkit/ui/widgets/comboboxes/_models.py +15 -0
  105. sli_ui_toolkit/ui/widgets/comboboxes/_overlay.py +323 -0
  106. sli_ui_toolkit/ui/widgets/comboboxes/_search.py +78 -0
  107. sli_ui_toolkit/ui/widgets/comboboxes/combo_box.py +671 -0
  108. sli_ui_toolkit/ui/widgets/comboboxes/scrollable_combobox.py +261 -0
  109. sli_ui_toolkit/ui/widgets/composite/__init__.py +103 -0
  110. sli_ui_toolkit/ui/widgets/composite/base_flyout.py +407 -0
  111. sli_ui_toolkit/ui/widgets/composite/calendar_widget/__init__.py +21 -0
  112. sli_ui_toolkit/ui/widgets/composite/calendar_widget/day_button.py +128 -0
  113. sli_ui_toolkit/ui/widgets/composite/calendar_widget/models.py +94 -0
  114. sli_ui_toolkit/ui/widgets/composite/calendar_widget/widget.py +574 -0
  115. sli_ui_toolkit/ui/widgets/composite/color_swatch.py +79 -0
  116. sli_ui_toolkit/ui/widgets/composite/dialog_shell.py +64 -0
  117. sli_ui_toolkit/ui/widgets/composite/drag_ghost_widget.py +45 -0
  118. sli_ui_toolkit/ui/widgets/composite/help_sections.py +83 -0
  119. sli_ui_toolkit/ui/widgets/composite/icon_action_flyout.py +185 -0
  120. sli_ui_toolkit/ui/widgets/composite/indexed_toggle_flyout.py +120 -0
  121. sli_ui_toolkit/ui/widgets/composite/log_console_widget.py +204 -0
  122. sli_ui_toolkit/ui/widgets/composite/markdown_help_dialog.py +527 -0
  123. sli_ui_toolkit/ui/widgets/composite/preview_panel.py +115 -0
  124. sli_ui_toolkit/ui/widgets/composite/process_console_widget.py +250 -0
  125. sli_ui_toolkit/ui/widgets/composite/sidebar_nav_list.py +105 -0
  126. sli_ui_toolkit/ui/widgets/composite/simple_options_flyout.py +429 -0
  127. sli_ui_toolkit/ui/widgets/composite/sunburst_chart/__init__.py +17 -0
  128. sli_ui_toolkit/ui/widgets/composite/sunburst_chart/chart_widget.py +205 -0
  129. sli_ui_toolkit/ui/widgets/composite/sunburst_chart/models.py +23 -0
  130. sli_ui_toolkit/ui/widgets/composite/sunburst_chart/segment_item.py +115 -0
  131. sli_ui_toolkit/ui/widgets/composite/timeline_widget/__init__.py +11 -0
  132. sli_ui_toolkit/ui/widgets/composite/timeline_widget/i18n.py +28 -0
  133. sli_ui_toolkit/ui/widgets/composite/timeline_widget/interaction.py +171 -0
  134. sli_ui_toolkit/ui/widgets/composite/timeline_widget/layout.py +553 -0
  135. sli_ui_toolkit/ui/widgets/composite/timeline_widget/models.py +72 -0
  136. sli_ui_toolkit/ui/widgets/composite/timeline_widget/primitives.py +161 -0
  137. sli_ui_toolkit/ui/widgets/composite/timeline_widget/render.py +704 -0
  138. sli_ui_toolkit/ui/widgets/composite/timeline_widget/theme.py +137 -0
  139. sli_ui_toolkit/ui/widgets/composite/timeline_widget/viewport.py +397 -0
  140. sli_ui_toolkit/ui/widgets/composite/timeline_widget/widget.py +503 -0
  141. sli_ui_toolkit/ui/widgets/composite/toast.py +498 -0
  142. sli_ui_toolkit/ui/widgets/composite/unified_flyout/__init__.py +109 -0
  143. sli_ui_toolkit/ui/widgets/composite/unified_flyout/bootstrap.py +157 -0
  144. sli_ui_toolkit/ui/widgets/composite/unified_flyout/common.py +32 -0
  145. sli_ui_toolkit/ui/widgets/composite/unified_flyout/content.py +48 -0
  146. sli_ui_toolkit/ui/widgets/composite/unified_flyout/delegate.py +362 -0
  147. sli_ui_toolkit/ui/widgets/composite/unified_flyout/dragdrop.py +43 -0
  148. sli_ui_toolkit/ui/widgets/composite/unified_flyout/layout.py +462 -0
  149. sli_ui_toolkit/ui/widgets/composite/unified_flyout/model.py +120 -0
  150. sli_ui_toolkit/ui/widgets/composite/unified_flyout/overlay_list_view.py +122 -0
  151. sli_ui_toolkit/ui/widgets/composite/unified_flyout/panel.py +622 -0
  152. sli_ui_toolkit/ui/widgets/composite/unified_flyout/refresh.py +121 -0
  153. sli_ui_toolkit/ui/widgets/composite/unified_flyout/session.py +116 -0
  154. sli_ui_toolkit/ui/widgets/composite/unified_flyout/simple_adapter.py +139 -0
  155. sli_ui_toolkit/ui/widgets/composite/unified_flyout/style.py +100 -0
  156. sli_ui_toolkit/ui/widgets/helpers/__init__.py +31 -0
  157. sli_ui_toolkit/ui/widgets/helpers/editable_text.py +41 -0
  158. sli_ui_toolkit/ui/widgets/helpers/hover_coordinator.py +131 -0
  159. sli_ui_toolkit/ui/widgets/helpers/icon_pixmap.py +111 -0
  160. sli_ui_toolkit/ui/widgets/helpers/overlay_geometry.py +52 -0
  161. sli_ui_toolkit/ui/widgets/helpers/shadow_painter.py +20 -0
  162. sli_ui_toolkit/ui/widgets/helpers/underline_painter.py +151 -0
  163. sli_ui_toolkit/ui/widgets/helpers/wheel_scroll_policy.py +41 -0
  164. sli_ui_toolkit/ui/widgets/list_items/__init__.py +4 -0
  165. sli_ui_toolkit/ui/widgets/list_items/editable_list_item.py +64 -0
  166. sli_ui_toolkit/ui/widgets/list_items/rating_item.py +450 -0
  167. sli_ui_toolkit/ui/widgets/overlays/__init__.py +4 -0
  168. sli_ui_toolkit/ui/widgets/overlays/drag_drop_overlay.py +131 -0
  169. sli_ui_toolkit/ui/widgets/overlays/paste_direction_overlay.py +276 -0
  170. sli_ui_toolkit/ui/widgets/style_bridge.py +129 -0
  171. sli_ui_toolkit/utils/__init__.py +4 -0
  172. sli_ui_toolkit/utils/file_utils.py +28 -0
  173. sli_ui_toolkit/utils/paths.py +12 -0
  174. sli_ui_toolkit/widgets.py +151 -0
  175. sli_ui_toolkit/workers/__init__.py +3 -0
  176. sli_ui_toolkit/workers/generic_worker.py +48 -0
  177. sli_ui_toolkit-0.2.7.dist-info/METADATA +156 -0
  178. sli_ui_toolkit-0.2.7.dist-info/RECORD +180 -0
  179. sli_ui_toolkit-0.2.7.dist-info/WHEEL +5 -0
  180. sli_ui_toolkit-0.2.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,72 @@
1
+ """Reusable PyQt toolkit primitives."""
2
+
3
+ from sli_ui_toolkit._version import __version__
4
+ from sli_ui_toolkit.config import FlyoutTimingConfig, configure_toolkit
5
+ from sli_ui_toolkit.palettes import FLUENT_LIGHT, FLUENT_DARK
6
+ from sli_ui_toolkit.core.logging import (
7
+ get_log_directory,
8
+ setup_logging,
9
+ setup_simple_logging,
10
+ )
11
+ from sli_ui_toolkit.i18n import (
12
+ ToolkitTranslationEvents,
13
+ TranslationManager,
14
+ configure_i18n,
15
+ emit_language_changed,
16
+ get_current_language,
17
+ tr,
18
+ translation_events,
19
+ )
20
+ from sli_ui_toolkit.utils.file_utils import get_unique_filepath
21
+ from sli_ui_toolkit.utils.paths import resource_path
22
+ from sli_ui_toolkit.workers.generic_worker import GenericWorker, WorkerSignals
23
+ from sli_ui_toolkit.theme import ThemeManager
24
+ from sli_ui_toolkit.widgets import (
25
+ Label,
26
+ LabelConfig,
27
+ LabelVariantSpec,
28
+ application_tooltips_enabled,
29
+ get_label_variant,
30
+ install_application_tooltips,
31
+ register_label_variant,
32
+ set_application_tooltips_enabled,
33
+ )
34
+ from sli_ui_toolkit.style import (
35
+ WidgetStyleTokens,
36
+ read_widget_style,
37
+ update_widget_style,
38
+ )
39
+
40
+ __all__ = [
41
+ "__version__",
42
+ "GenericWorker",
43
+ "WorkerSignals",
44
+ "ThemeManager",
45
+ "Label",
46
+ "LabelConfig",
47
+ "LabelVariantSpec",
48
+ "get_label_variant",
49
+ "register_label_variant",
50
+ "application_tooltips_enabled",
51
+ "install_application_tooltips",
52
+ "set_application_tooltips_enabled",
53
+ "WidgetStyleTokens",
54
+ "read_widget_style",
55
+ "update_widget_style",
56
+ "FlyoutTimingConfig",
57
+ "ToolkitTranslationEvents",
58
+ "TranslationManager",
59
+ "configure_i18n",
60
+ "configure_toolkit",
61
+ "emit_language_changed",
62
+ "get_log_directory",
63
+ "get_current_language",
64
+ "get_unique_filepath",
65
+ "resource_path",
66
+ "setup_logging",
67
+ "setup_simple_logging",
68
+ "tr",
69
+ "translation_events",
70
+ "FLUENT_LIGHT",
71
+ "FLUENT_DARK",
72
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.7"
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+
6
+ @dataclass(slots=True)
7
+ class FlyoutTimingConfig:
8
+ transient_auto_hide_delay_ms: int = 180
9
+ flyout_animation_duration_ms: int = 160
10
+ text_settings_flyout_animation_duration_ms: int = 180
11
+
12
+ _timings = FlyoutTimingConfig()
13
+ _overlay_resolver: Callable[[object | None], object | None] | None = None
14
+ _rating_gesture_factory: Callable[..., object] | None = None
15
+ _dragdrop_service_getter: Callable[[], object | None] | None = None
16
+
17
+ def configure_toolkit(
18
+ *,
19
+ timings: FlyoutTimingConfig | None = None,
20
+ overlay_resolver: Callable[[object | None], object | None] | None = None,
21
+ rating_gesture_factory: Callable[..., object] | None = None,
22
+ dragdrop_service_getter: Callable[[], object | None] | None = None,
23
+ ) -> None:
24
+ global _timings, _overlay_resolver, _rating_gesture_factory, _dragdrop_service_getter
25
+ if timings is not None:
26
+ _timings = timings
27
+ if overlay_resolver is not None:
28
+ _overlay_resolver = overlay_resolver
29
+ if rating_gesture_factory is not None:
30
+ _rating_gesture_factory = rating_gesture_factory
31
+ if dragdrop_service_getter is not None:
32
+ _dragdrop_service_getter = dragdrop_service_getter
33
+
34
+ def get_flyout_timings() -> FlyoutTimingConfig:
35
+ return _timings
36
+
37
+ def resolve_overlay_layer(widget: object | None):
38
+ if _overlay_resolver is None:
39
+ return None
40
+ try:
41
+ return _overlay_resolver(widget)
42
+ except Exception:
43
+ return None
44
+
45
+ def create_rating_gesture(**kwargs):
46
+ if _rating_gesture_factory is None:
47
+ return None
48
+ try:
49
+ return _rating_gesture_factory(**kwargs)
50
+ except Exception:
51
+ return None
52
+
53
+ def get_dragdrop_service():
54
+ if _dragdrop_service_getter is None:
55
+ from sli_ui_toolkit.ui.services.dragdrop_service import ToolkitDragDropService
56
+
57
+ return ToolkitDragDropService.get_instance()
58
+ try:
59
+ return _dragdrop_service_getter()
60
+ except Exception:
61
+ from sli_ui_toolkit.ui.services.dragdrop_service import ToolkitDragDropService
62
+
63
+ return ToolkitDragDropService.get_instance()
@@ -0,0 +1,7 @@
1
+ from sli_ui_toolkit.core.logging import (
2
+ get_log_directory,
3
+ setup_logging,
4
+ setup_simple_logging,
5
+ )
6
+
7
+ __all__ = ["get_log_directory", "setup_logging", "setup_simple_logging"]
@@ -0,0 +1,99 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+
5
+ def get_log_directory(app_name: str) -> str:
6
+ logger = logging.getLogger(app_name)
7
+
8
+ if sys.platform == "win32":
9
+ app_data_dir = os.getenv("APPDATA")
10
+ if not app_data_dir:
11
+ app_data_dir = os.path.expanduser("~")
12
+ logger.warning(
13
+ "Could not find APPDATA env variable, falling back to home directory."
14
+ )
15
+ return os.path.join(app_data_dir, app_name)
16
+
17
+ if sys.platform == "darwin":
18
+ return os.path.join(
19
+ os.path.expanduser("~/Library/Application Support"), app_name
20
+ )
21
+
22
+ xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
23
+ return os.path.join(xdg_data_home, app_name)
24
+
25
+ def setup_logging(
26
+ app_name: str, debug_enabled: bool = False, debug_env_var: str | None = None
27
+ ) -> None:
28
+ logger = logging.getLogger(app_name)
29
+
30
+ if debug_env_var:
31
+ suppress_debug = (
32
+ os.getenv(f"{debug_env_var.replace('_DEBUG', '_SUPPRESS_DEBUG')}", "0")
33
+ == "1"
34
+ )
35
+ if suppress_debug:
36
+ debug_enabled = False
37
+ elif os.getenv(debug_env_var, "0") == "1":
38
+ debug_enabled = True
39
+
40
+ level = logging.DEBUG if debug_enabled else logging.INFO
41
+
42
+ if logger.handlers:
43
+ logger.setLevel(level)
44
+ for handler in logger.handlers:
45
+ handler.setLevel(level)
46
+ return
47
+
48
+ logger.setLevel(level)
49
+ formatter = logging.Formatter(
50
+ "%(asctime)s - [%(levelname)s] - (%(filename)s:%(lineno)d) - %(message)s"
51
+ )
52
+
53
+ stream_handler = logging.StreamHandler(sys.stdout)
54
+ stream_handler.setFormatter(formatter)
55
+ stream_handler.setLevel(level)
56
+ logger.addHandler(stream_handler)
57
+
58
+ try:
59
+ log_dir = get_log_directory(app_name)
60
+ os.makedirs(log_dir, exist_ok=True)
61
+ log_file_path = os.path.join(log_dir, "log.txt")
62
+
63
+ file_handler = logging.FileHandler(log_file_path, mode="w", encoding="utf-8")
64
+ file_handler.setFormatter(formatter)
65
+ file_handler.setLevel(level)
66
+ logger.addHandler(file_handler)
67
+
68
+ logging.getLogger("markdown").setLevel(logging.WARNING)
69
+ logging.getLogger("markdown.extensions").setLevel(logging.WARNING)
70
+ except Exception:
71
+ logger.error(
72
+ "FATAL: Failed to set up file logger. Continuing with console-only logging.",
73
+ exc_info=True,
74
+ )
75
+
76
+ def setup_simple_logging(app_name: str, level: int = logging.WARNING) -> None:
77
+ root_logger = logging.getLogger()
78
+ root_logger.setLevel(level)
79
+
80
+ for handler in root_logger.handlers[:]:
81
+ root_logger.removeHandler(handler)
82
+
83
+ formatter = logging.Formatter(
84
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
85
+ datefmt="%Y-%m-%d %H:%M:%S",
86
+ )
87
+
88
+ console_handler = logging.StreamHandler()
89
+ console_handler.setLevel(level)
90
+ console_handler.setFormatter(formatter)
91
+
92
+ root_logger.addHandler(console_handler)
93
+
94
+ app_logger = logging.getLogger(app_name)
95
+ app_logger.setLevel(level)
96
+
97
+ logging.getLogger("markdown").setLevel(logging.WARNING)
98
+ logging.getLogger("markdown.extensions").setLevel(logging.WARNING)
99
+
sli_ui_toolkit/i18n.py ADDED
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from PyQt6.QtCore import QObject, pyqtSignal
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class ToolkitTranslationEvents(QObject):
14
+ language_changed = pyqtSignal(str)
15
+
16
+ def _deep_merge(
17
+ base: dict[str, Any],
18
+ incoming: dict[str, Any],
19
+ source: str,
20
+ *,
21
+ warn_on_override: bool,
22
+ ) -> None:
23
+ for key, value in incoming.items():
24
+ if key not in base:
25
+ base[key] = value
26
+ continue
27
+
28
+ if isinstance(base[key], dict) and isinstance(value, dict):
29
+ _deep_merge(base[key], value, source, warn_on_override=warn_on_override)
30
+ continue
31
+
32
+ if warn_on_override:
33
+ logger.warning("Translation key override for '%s' from %s", key, source)
34
+ base[key] = value
35
+
36
+ def _resolve_dotted_key(data: dict[str, Any], dotted_key: str) -> str:
37
+ current: Any = data
38
+ for part in dotted_key.split("."):
39
+ if not isinstance(current, dict) or part not in current:
40
+ return dotted_key
41
+ current = current[part]
42
+
43
+ return current if isinstance(current, str) else dotted_key
44
+
45
+ class TranslationManager:
46
+ _instance: TranslationManager | None = None
47
+
48
+ def __new__(cls) -> TranslationManager:
49
+ if cls._instance is None:
50
+ cls._instance = super().__new__(cls)
51
+ cls._instance._translations: dict[str, Any] = {}
52
+ cls._instance._cache: dict[str, dict[str, Any]] = {}
53
+ cls._instance._current_lang = "en"
54
+ cls._instance._events = ToolkitTranslationEvents()
55
+ cls._instance._i18n_root: Path | None = None
56
+ cls._instance._extra_roots: list[Path] = []
57
+ return cls._instance
58
+
59
+ def set_i18n_root(self, path: str | Path) -> None:
60
+ self._i18n_root = Path(path)
61
+ self._extra_roots = []
62
+ self._cache.clear()
63
+ self._translations = {}
64
+
65
+ def add_i18n_root(self, path: str | Path) -> None:
66
+ self._extra_roots.append(Path(path))
67
+ self._cache.clear()
68
+
69
+ def _load_tree(self, lang_dir: Path) -> dict[str, Any]:
70
+ translations: dict[str, Any] = {}
71
+ json_files = sorted(lang_dir.rglob("*.json"))
72
+
73
+ if not json_files:
74
+ logger.warning("No translation files found in %s", lang_dir)
75
+ return translations
76
+
77
+ for file_path in json_files:
78
+ try:
79
+ file_data = json.loads(file_path.read_text(encoding="utf-8"))
80
+ except Exception as exc:
81
+ logger.error("Failed to load translation file %s: %s", file_path, exc)
82
+ continue
83
+
84
+ if not isinstance(file_data, dict):
85
+ logger.warning("Skipping non-object translation file: %s", file_path)
86
+ continue
87
+
88
+ _deep_merge(
89
+ translations,
90
+ file_data,
91
+ file_path.relative_to(lang_dir).as_posix(),
92
+ warn_on_override=True,
93
+ )
94
+
95
+ return translations
96
+
97
+ def _build_language_pack(self, lang_code: str) -> dict[str, Any]:
98
+ if self._i18n_root is None:
99
+ logger.error("i18n root not configured — call configure_i18n(i18n_root=...)")
100
+ return {}
101
+
102
+ base_path = self._i18n_root
103
+ fallback_dir = base_path / "en"
104
+ requested_dir = base_path / lang_code
105
+
106
+ if not fallback_dir.is_dir():
107
+ logger.error("Fallback translation directory not found: %s", fallback_dir)
108
+ return {}
109
+
110
+ translations = self._load_tree(fallback_dir)
111
+
112
+ if lang_code != "en":
113
+ if not requested_dir.is_dir():
114
+ logger.warning(
115
+ "Translation directory not found: %s. Falling back to EN.",
116
+ requested_dir,
117
+ )
118
+ else:
119
+ _deep_merge(
120
+ translations,
121
+ self._load_tree(requested_dir),
122
+ requested_dir.relative_to(base_path).as_posix(),
123
+ warn_on_override=False,
124
+ )
125
+
126
+ for extra_root in self._extra_roots:
127
+ fallback_dir = extra_root / "en"
128
+ if fallback_dir.is_dir():
129
+ _deep_merge(
130
+ translations,
131
+ self._load_tree(fallback_dir),
132
+ fallback_dir.relative_to(extra_root).as_posix(),
133
+ warn_on_override=False,
134
+ )
135
+ if lang_code != "en":
136
+ lang_dir = extra_root / lang_code
137
+ if lang_dir.is_dir():
138
+ _deep_merge(
139
+ translations,
140
+ self._load_tree(lang_dir),
141
+ lang_dir.relative_to(extra_root).as_posix(),
142
+ warn_on_override=False,
143
+ )
144
+
145
+ return translations
146
+
147
+ def load_language(self, lang_code: str) -> None:
148
+ requested_lang = lang_code or "en"
149
+
150
+ if requested_lang == self._current_lang:
151
+ return
152
+
153
+ cached = self._cache.get(requested_lang)
154
+ if cached is None:
155
+ cached = self._build_language_pack(requested_lang)
156
+ self._cache[requested_lang] = cached
157
+
158
+ self._translations = cached
159
+ self._current_lang = requested_lang
160
+ self._events.language_changed.emit(requested_lang)
161
+
162
+ def get(self, text: str, *args: Any, **kwargs: Any) -> str:
163
+ translated = _resolve_dotted_key(self._translations, text)
164
+
165
+ if args or kwargs:
166
+ try:
167
+ return translated.format(*args, **kwargs)
168
+ except Exception:
169
+ return translated
170
+ return translated
171
+
172
+ _manager = TranslationManager()
173
+
174
+ def configure_i18n(
175
+ *,
176
+ i18n_root: str | Path | None = None,
177
+ translator: Callable[[str, str | None], str] | None = None,
178
+ language_provider: Callable[[], str] | None = None,
179
+ events: ToolkitTranslationEvents | None = None,
180
+ ) -> None:
181
+ if i18n_root is not None:
182
+ _manager.set_i18n_root(i18n_root)
183
+ if events is not None:
184
+ _manager._events = events
185
+
186
+ def translation_events() -> ToolkitTranslationEvents:
187
+ return _manager._events
188
+
189
+ def emit_language_changed(lang_code: str) -> None:
190
+ translation_events().language_changed.emit(str(lang_code or "en"))
191
+
192
+ def get_current_language(default: str = "en") -> str:
193
+ return getattr(_manager, "_current_lang", default) or default
194
+
195
+ def tr(key: str, language: str | None = None, default: str | None = None, *args: Any, **kwargs: Any) -> str:
196
+ lang = language or get_current_language()
197
+ _manager.load_language(lang)
198
+ result = _manager.get(key, *args, **kwargs)
199
+ if result == key and default is not None:
200
+ return default
201
+ return result
202
+
203
+
204
+ class TranslationsBinder:
205
+ """Re-applies translated strings to widgets on language change.
206
+
207
+ The engine is widget-agnostic: it stores callbacks parameterized by
208
+ `lang_code` and invokes them on `apply(lang_code)`. Apps register
209
+ bindings declaratively via `bind_text/bind_tooltip/bind_placeholder/
210
+ bind_setter` or escape to `bind_callback` for composite or conditional
211
+ updates.
212
+
213
+ `tr_func` defaults to the toolkit's `tr`; pass a custom translator
214
+ to bind against a different translation namespace.
215
+ """
216
+
217
+ def __init__(self, tr_func: Callable[..., str] | None = None):
218
+ self._tr = tr_func or tr
219
+ self._bindings: list[Callable[[str], None]] = []
220
+
221
+ def bind_text(self, widget, key: str, *, suffix: str = "") -> "TranslationsBinder":
222
+ self._bindings.append(
223
+ lambda lang: widget.setText(self._tr(key, lang) + suffix)
224
+ )
225
+ return self
226
+
227
+ def bind_tooltip(self, widget, key: str) -> "TranslationsBinder":
228
+ self._bindings.append(
229
+ lambda lang: widget.setToolTip(self._tr(key, lang))
230
+ )
231
+ return self
232
+
233
+ def bind_placeholder(self, widget, key: str) -> "TranslationsBinder":
234
+ self._bindings.append(
235
+ lambda lang: widget.setPlaceholderText(self._tr(key, lang))
236
+ )
237
+ return self
238
+
239
+ def bind_setter(self, widget, setter: str, key: str, *, suffix: str = "") -> "TranslationsBinder":
240
+ self._bindings.append(
241
+ lambda lang: getattr(widget, setter)(self._tr(key, lang) + suffix)
242
+ )
243
+ return self
244
+
245
+ def bind_callback(self, fn: Callable[[str], None]) -> "TranslationsBinder":
246
+ """Register an arbitrary callback ``fn(lang_code)`` for composite or
247
+ conditional updates that don't fit the simple setter pattern."""
248
+ self._bindings.append(fn)
249
+ return self
250
+
251
+ def apply(self, lang_code: str) -> None:
252
+ for binding in self._bindings:
253
+ binding(lang_code)
@@ -0,0 +1,56 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ from PyQt6.QtGui import QIcon
5
+
6
+ from sli_ui_toolkit.ui.services.icon_service import (
7
+ IconService,
8
+ get_icon_by_name,
9
+ get_icon_service,
10
+ )
11
+ from sli_ui_toolkit.ui.managers.icon_manager import get_icon_by_path, get_themed_icon
12
+
13
+ _icon_resolver: Callable[[Any], QIcon] | None = None
14
+ _named_icons: dict[str, Any] = {}
15
+
16
+ def configure_icon_resolver(
17
+ resolver: Callable[[Any], QIcon] | None,
18
+ *,
19
+ named_icons: dict[str, Any] | None = None,
20
+ ) -> None:
21
+ global _icon_resolver, _named_icons
22
+ _icon_resolver = resolver
23
+ _named_icons = dict(named_icons or {})
24
+
25
+ def resolve_icon(icon: Any) -> QIcon:
26
+ if icon is None:
27
+ return QIcon()
28
+ if isinstance(icon, QIcon):
29
+ return icon
30
+ if isinstance(icon, str):
31
+ mapped = _named_icons.get(icon)
32
+ if mapped is not None:
33
+ if _icon_resolver is not None:
34
+ return _icon_resolver(mapped)
35
+ return get_icon_by_name(getattr(mapped, "value", mapped))
36
+ return get_icon_by_name(icon)
37
+ if _icon_resolver is not None:
38
+ return _icon_resolver(icon)
39
+ value = getattr(icon, "value", None)
40
+ if isinstance(value, str):
41
+ return get_icon_by_name(value)
42
+ return QIcon()
43
+
44
+ def get_named_icon(name: str) -> Any:
45
+ return _named_icons.get(name)
46
+
47
+ __all__ = [
48
+ "IconService",
49
+ "configure_icon_resolver",
50
+ "get_icon_by_name",
51
+ "get_icon_by_path",
52
+ "get_icon_service",
53
+ "get_named_icon",
54
+ "get_themed_icon",
55
+ "resolve_icon",
56
+ ]
@@ -0,0 +1,13 @@
1
+ from sli_ui_toolkit.ui.managers import (
2
+ AnchoredFlyoutAutoHide,
3
+ DelayedActionTimer,
4
+ FlyoutManager,
5
+ ThemeManager,
6
+ )
7
+
8
+ __all__ = [
9
+ "AnchoredFlyoutAutoHide",
10
+ "DelayedActionTimer",
11
+ "FlyoutManager",
12
+ "ThemeManager",
13
+ ]