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.
- sli_ui_toolkit/__init__.py +72 -0
- sli_ui_toolkit/_version.py +1 -0
- sli_ui_toolkit/config.py +63 -0
- sli_ui_toolkit/core/__init__.py +7 -0
- sli_ui_toolkit/core/logging.py +99 -0
- sli_ui_toolkit/i18n.py +253 -0
- sli_ui_toolkit/icons.py +56 -0
- sli_ui_toolkit/managers.py +13 -0
- sli_ui_toolkit/palettes.py +176 -0
- sli_ui_toolkit/resources/assets/icons/dark/add.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/add_circle.svg +5 -0
- sli_ui_toolkit/resources/assets/icons/dark/calendar.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/chart.svg +5 -0
- sli_ui_toolkit/resources/assets/icons/dark/check.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/chevron-down.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/delete.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/download.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/edit.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/folder_open.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/help.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/incognito.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/photo.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/quick_save.svg +55 -0
- sli_ui_toolkit/resources/assets/icons/dark/remove.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/save.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/settings.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/sync.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/dark/text-manipulator.svg +76 -0
- sli_ui_toolkit/resources/assets/icons/light/add.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/add_circle.svg +5 -0
- sli_ui_toolkit/resources/assets/icons/light/calendar.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/chart.svg +5 -0
- sli_ui_toolkit/resources/assets/icons/light/check.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/chevron-down.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/delete.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/download.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/edit.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/folder_open.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/help.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/incognito.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/photo.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/quick_save.svg +16 -0
- sli_ui_toolkit/resources/assets/icons/light/remove.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/save.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/settings.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/sync.svg +1 -0
- sli_ui_toolkit/resources/assets/icons/light/text-manipulator.svg +76 -0
- sli_ui_toolkit/services.py +12 -0
- sli_ui_toolkit/style.py +20 -0
- sli_ui_toolkit/theme.py +4 -0
- sli_ui_toolkit/ui/__init__.py +2 -0
- sli_ui_toolkit/ui/dialogs/__init__.py +13 -0
- sli_ui_toolkit/ui/dialogs/dialog_helpers.py +115 -0
- sli_ui_toolkit/ui/in_window_surface.py +205 -0
- sli_ui_toolkit/ui/managers/__init__.py +13 -0
- sli_ui_toolkit/ui/managers/flyout_manager.py +259 -0
- sli_ui_toolkit/ui/managers/flyout_timer_service.py +102 -0
- sli_ui_toolkit/ui/managers/icon_manager.py +44 -0
- sli_ui_toolkit/ui/managers/theme_manager.py +220 -0
- sli_ui_toolkit/ui/services/__init__.py +19 -0
- sli_ui_toolkit/ui/services/dragdrop_service.py +213 -0
- sli_ui_toolkit/ui/services/icon_service.py +100 -0
- sli_ui_toolkit/ui/services/window_prewarm.py +47 -0
- sli_ui_toolkit/ui/widgets/__init__.py +2 -0
- sli_ui_toolkit/ui/widgets/atomic/__init__.py +57 -0
- sli_ui_toolkit/ui/widgets/atomic/checkbox.py +344 -0
- sli_ui_toolkit/ui/widgets/atomic/combobox.py +5 -0
- sli_ui_toolkit/ui/widgets/atomic/comboboxes.py +5 -0
- sli_ui_toolkit/ui/widgets/atomic/custom_group_widget.py +148 -0
- sli_ui_toolkit/ui/widgets/atomic/custom_line_edit.py +218 -0
- sli_ui_toolkit/ui/widgets/atomic/drop_zone_label.py +147 -0
- sli_ui_toolkit/ui/widgets/atomic/instances_counter_button.py +256 -0
- sli_ui_toolkit/ui/widgets/atomic/loading_spinner.py +59 -0
- sli_ui_toolkit/ui/widgets/atomic/minimalist_scrollbar.py +270 -0
- sli_ui_toolkit/ui/widgets/atomic/radio.py +244 -0
- sli_ui_toolkit/ui/widgets/atomic/slider.py +227 -0
- sli_ui_toolkit/ui/widgets/atomic/spinbox.py +132 -0
- sli_ui_toolkit/ui/widgets/atomic/switch.py +301 -0
- sli_ui_toolkit/ui/widgets/atomic/text_labels.py +350 -0
- sli_ui_toolkit/ui/widgets/atomic/time_line_edit.py +377 -0
- sli_ui_toolkit/ui/widgets/atomic/tooltips.py +311 -0
- sli_ui_toolkit/ui/widgets/buttons/__init__.py +4 -0
- sli_ui_toolkit/ui/widgets/buttons/_dropdown_menu.py +307 -0
- sli_ui_toolkit/ui/widgets/buttons/button.py +811 -0
- sli_ui_toolkit/ui/widgets/buttons/button_group.py +97 -0
- sli_ui_toolkit/ui/widgets/buttons/capabilities/__init__.py +17 -0
- sli_ui_toolkit/ui/widgets/buttons/capabilities/base.py +27 -0
- sli_ui_toolkit/ui/widgets/buttons/capabilities/long_press.py +61 -0
- sli_ui_toolkit/ui/widgets/buttons/capabilities/menu.py +57 -0
- sli_ui_toolkit/ui/widgets/buttons/capabilities/scroll.py +203 -0
- sli_ui_toolkit/ui/widgets/buttons/content.py +255 -0
- sli_ui_toolkit/ui/widgets/buttons/context.py +41 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/__init__.py +18 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/_base.py +17 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/background.py +87 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/badge.py +107 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/content.py +16 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/strikethrough.py +22 -0
- sli_ui_toolkit/ui/widgets/buttons/layers/underline.py +81 -0
- sli_ui_toolkit/ui/widgets/buttons/painter.py +132 -0
- sli_ui_toolkit/ui/widgets/buttons/state.py +17 -0
- sli_ui_toolkit/ui/widgets/buttons/variants.py +178 -0
- sli_ui_toolkit/ui/widgets/comboboxes/__init__.py +4 -0
- sli_ui_toolkit/ui/widgets/comboboxes/_models.py +15 -0
- sli_ui_toolkit/ui/widgets/comboboxes/_overlay.py +323 -0
- sli_ui_toolkit/ui/widgets/comboboxes/_search.py +78 -0
- sli_ui_toolkit/ui/widgets/comboboxes/combo_box.py +671 -0
- sli_ui_toolkit/ui/widgets/comboboxes/scrollable_combobox.py +261 -0
- sli_ui_toolkit/ui/widgets/composite/__init__.py +103 -0
- sli_ui_toolkit/ui/widgets/composite/base_flyout.py +407 -0
- sli_ui_toolkit/ui/widgets/composite/calendar_widget/__init__.py +21 -0
- sli_ui_toolkit/ui/widgets/composite/calendar_widget/day_button.py +128 -0
- sli_ui_toolkit/ui/widgets/composite/calendar_widget/models.py +94 -0
- sli_ui_toolkit/ui/widgets/composite/calendar_widget/widget.py +574 -0
- sli_ui_toolkit/ui/widgets/composite/color_swatch.py +79 -0
- sli_ui_toolkit/ui/widgets/composite/dialog_shell.py +64 -0
- sli_ui_toolkit/ui/widgets/composite/drag_ghost_widget.py +45 -0
- sli_ui_toolkit/ui/widgets/composite/help_sections.py +83 -0
- sli_ui_toolkit/ui/widgets/composite/icon_action_flyout.py +185 -0
- sli_ui_toolkit/ui/widgets/composite/indexed_toggle_flyout.py +120 -0
- sli_ui_toolkit/ui/widgets/composite/log_console_widget.py +204 -0
- sli_ui_toolkit/ui/widgets/composite/markdown_help_dialog.py +527 -0
- sli_ui_toolkit/ui/widgets/composite/preview_panel.py +115 -0
- sli_ui_toolkit/ui/widgets/composite/process_console_widget.py +250 -0
- sli_ui_toolkit/ui/widgets/composite/sidebar_nav_list.py +105 -0
- sli_ui_toolkit/ui/widgets/composite/simple_options_flyout.py +429 -0
- sli_ui_toolkit/ui/widgets/composite/sunburst_chart/__init__.py +17 -0
- sli_ui_toolkit/ui/widgets/composite/sunburst_chart/chart_widget.py +205 -0
- sli_ui_toolkit/ui/widgets/composite/sunburst_chart/models.py +23 -0
- sli_ui_toolkit/ui/widgets/composite/sunburst_chart/segment_item.py +115 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/__init__.py +11 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/i18n.py +28 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/interaction.py +171 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/layout.py +553 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/models.py +72 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/primitives.py +161 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/render.py +704 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/theme.py +137 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/viewport.py +397 -0
- sli_ui_toolkit/ui/widgets/composite/timeline_widget/widget.py +503 -0
- sli_ui_toolkit/ui/widgets/composite/toast.py +498 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/__init__.py +109 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/bootstrap.py +157 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/common.py +32 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/content.py +48 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/delegate.py +362 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/dragdrop.py +43 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/layout.py +462 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/model.py +120 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/overlay_list_view.py +122 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/panel.py +622 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/refresh.py +121 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/session.py +116 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/simple_adapter.py +139 -0
- sli_ui_toolkit/ui/widgets/composite/unified_flyout/style.py +100 -0
- sli_ui_toolkit/ui/widgets/helpers/__init__.py +31 -0
- sli_ui_toolkit/ui/widgets/helpers/editable_text.py +41 -0
- sli_ui_toolkit/ui/widgets/helpers/hover_coordinator.py +131 -0
- sli_ui_toolkit/ui/widgets/helpers/icon_pixmap.py +111 -0
- sli_ui_toolkit/ui/widgets/helpers/overlay_geometry.py +52 -0
- sli_ui_toolkit/ui/widgets/helpers/shadow_painter.py +20 -0
- sli_ui_toolkit/ui/widgets/helpers/underline_painter.py +151 -0
- sli_ui_toolkit/ui/widgets/helpers/wheel_scroll_policy.py +41 -0
- sli_ui_toolkit/ui/widgets/list_items/__init__.py +4 -0
- sli_ui_toolkit/ui/widgets/list_items/editable_list_item.py +64 -0
- sli_ui_toolkit/ui/widgets/list_items/rating_item.py +450 -0
- sli_ui_toolkit/ui/widgets/overlays/__init__.py +4 -0
- sli_ui_toolkit/ui/widgets/overlays/drag_drop_overlay.py +131 -0
- sli_ui_toolkit/ui/widgets/overlays/paste_direction_overlay.py +276 -0
- sli_ui_toolkit/ui/widgets/style_bridge.py +129 -0
- sli_ui_toolkit/utils/__init__.py +4 -0
- sli_ui_toolkit/utils/file_utils.py +28 -0
- sli_ui_toolkit/utils/paths.py +12 -0
- sli_ui_toolkit/widgets.py +151 -0
- sli_ui_toolkit/workers/__init__.py +3 -0
- sli_ui_toolkit/workers/generic_worker.py +48 -0
- sli_ui_toolkit-0.2.7.dist-info/METADATA +156 -0
- sli_ui_toolkit-0.2.7.dist-info/RECORD +180 -0
- sli_ui_toolkit-0.2.7.dist-info/WHEEL +5 -0
- 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"
|
sli_ui_toolkit/config.py
ADDED
|
@@ -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,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)
|
sli_ui_toolkit/icons.py
ADDED
|
@@ -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
|
+
]
|