bubbleUI 0.4.4a1__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.
@@ -0,0 +1,82 @@
1
+ """Reusable runtime settings for desktop-glass surfaces.
2
+
3
+ Material values live in :class:`glasskit.glass.FrostStyle` (including the
4
+ vignette). This module only holds the non-visual/runtime switches shared by
5
+ desktop-glass windows:
6
+
7
+ * background source: static system wallpaper or live screen tracking
8
+ * screen-tracking frame rate
9
+ * optional GPU composition
10
+ * explicit energy saving
11
+ * automatic energy saving when the window loses focus
12
+ * transition duration between standard and energy configurations
13
+ * capture compatibility for screen tracking
14
+
15
+ ``background_mode`` defaults to ``"wallpaper"``. That is the Mica-like path:
16
+ the wallpaper is sampled once, blurred by glasskit, and only reloaded when
17
+ Windows reports a wallpaper change. ``"screen"`` keeps the original live
18
+ desktop tracking, which needs Windows' capture exclusion while it is active.
19
+
20
+ ``allow_capture`` only applies to ``"screen"`` mode. It pauses live sampling
21
+ and reuses the last frame so the window itself becomes visible to OBS and
22
+ screenshot tools.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, replace
28
+
29
+ BACKGROUND_WALLPAPER = "wallpaper"
30
+ BACKGROUND_SCREEN = "screen"
31
+
32
+
33
+ @dataclass
34
+ class GlassRuntimeSettings:
35
+ background_mode: str = BACKGROUND_WALLPAPER
36
+ fps: int = 15
37
+ gpu: bool = False
38
+ energy_saving: bool = False
39
+ auto_energy_on_focus_loss: bool = True
40
+ transition_ms: int = 250
41
+ allow_capture: bool = False
42
+
43
+ def __post_init__(self) -> None:
44
+ if self.background_mode not in (BACKGROUND_WALLPAPER, BACKGROUND_SCREEN):
45
+ self.background_mode = BACKGROUND_WALLPAPER
46
+ self.fps = max(1, min(30, int(self.fps)))
47
+ self.transition_ms = max(0, int(self.transition_ms))
48
+
49
+ @property
50
+ def wallpaper(self) -> bool:
51
+ return self.background_mode == BACKGROUND_WALLPAPER
52
+
53
+ @property
54
+ def screen_tracking(self) -> bool:
55
+ return self.background_mode == BACKGROUND_SCREEN
56
+
57
+ def copy(self) -> "GlassRuntimeSettings":
58
+ return replace(self)
59
+
60
+ @dataclass
61
+ class FrostStyle:
62
+ """All user-tunable properties of a frosted surface."""
63
+
64
+ tint: str = "#272727"
65
+ opacity: int = 74
66
+ blur: int = 32
67
+ frost: int = 16
68
+ noise: int = 9
69
+ sheen: int = 34
70
+ saturation: int = 102
71
+ brightness: int = 94
72
+ border: int = 24
73
+ radius: int = 18
74
+ vignette: int = 0
75
+
76
+ def copy(self) -> "FrostStyle":
77
+ return replace(self)
78
+
79
+ def snapshot(self) -> tuple:
80
+ return (self.tint, self.opacity, self.blur, self.frost, self.noise,
81
+ self.sheen, self.saturation, self.brightness, self.border,
82
+ self.radius, self.vignette)
bubble/i18n.py ADDED
@@ -0,0 +1,27 @@
1
+ DEFAULT_SOURCE = None
2
+ DEFAULT_LANG = None
3
+
4
+ def get_lang(id, source=None, lang=None, default=''):
5
+ '''
6
+ Get the language text from the source list.
7
+ '''
8
+ source = source or DEFAULT_SOURCE # 获取默认语言包
9
+ lang = lang or DEFAULT_LANG # 获取默认语言
10
+
11
+ if not (source and lang):
12
+ print(f'Warning: Source or lang not set')
13
+ return default
14
+
15
+ for i in source:
16
+ if i['lang_id'] == 0: # 设置默认语言包
17
+ default_lang_text = i['lang_package']
18
+ if i['lang_id'] == lang: # 设置目前语言包
19
+ lang_text = i['lang_package']
20
+ try:
21
+ return lang_text[id]
22
+ except KeyError:
23
+ print(f'Warning: Language {id} not found')
24
+ return default
25
+ except UnboundLocalError:
26
+ lang_text = {}
27
+ return lang_text.get(id, default_lang_text[id])
bubble/icon.py ADDED
@@ -0,0 +1,261 @@
1
+ from .QUI import *
2
+
3
+ from typing import Optional
4
+ from uiml import set_style
5
+
6
+ _FONT_FAMILIES = ["Segoe Fluent Icons", "Segoe MDL2 Assets"]
7
+
8
+ class FontIcon(QLabel):
9
+ """图标字体标签(Segoe Fluent Icons / Segoe MDL2 Assets),支持原地旋转。
10
+
11
+ angle 属性可直接交给 QPropertyAnimation 做旋转动画。
12
+ icon_color / icon_size 也可由 qss 用 qproperty-* 覆盖。
13
+
14
+ 配色由 qss 决定:默认取 `.FontIcon`(或全局文字色)的 color;
15
+ 需要单独配色的位置在 qss 里用自己的 class 声明 color(如 `.ExpanderArrow`),
16
+ 代码只在显式传入 color 时才把颜色写进控件自身样式表。
17
+
18
+ 字号通过「控件自身样式表」设置:全局样式表里的 `* { font-size: ... }`
19
+ 会覆盖 setFont(),但覆盖不了控件自身样式表,因此图标字号不会被带跑。
20
+
21
+ 示例:
22
+ icon = FontIcon("\uE799", size=24) # 颜色交给 qss 的 .FontIcon
23
+ icon = FontIcon("\uE70D", size=12, color="#F7F7F7") # 单独配色
24
+ icon.set_angle(90) # 原地旋转 90°
25
+ # angle 是 Qt Property,可直接交给 QPropertyAnimation 做动画
26
+ """
27
+
28
+ STYLE_CLASS = "FontIcon"
29
+
30
+ def __init__(self, icon="", size: int = 20, color: Optional[str] = None, parent=None):
31
+ super().__init__(icon, parent)
32
+
33
+ self._color = QColor(color) if color is not None else None
34
+ self._icon_size = int(size)
35
+ self._angle = 0.0
36
+ self._pixmap = None
37
+ self._pixmap_key = None
38
+
39
+ self.setAlignment(Qt.AlignCenter)
40
+ self.setMinimumWidth(0)
41
+ self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
42
+ self._apply_style()
43
+ set_style(self, self.STYLE_CLASS)
44
+
45
+ def _apply_style(self):
46
+ """把字号写进控件自身样式表(优先级高于全局 `*` 规则)。
47
+
48
+ 只有显式指定颜色时才写入 color,否则交给 qss 的 `.FontIcon` 决定。
49
+ """
50
+ f = self.font()
51
+ f.setPixelSize(self._icon_size)
52
+ f.setFamilies(_FONT_FAMILIES)
53
+ self.setFont(f)
54
+ # 不在这里写 background:背景交给样式表按层级(level)决定
55
+ css = f"font-size: {self._icon_size}px;"
56
+ if self._color is not None:
57
+ css += f" color: {self._color.name()};"
58
+ self.setStyleSheet(css)
59
+ self.updateGeometry()
60
+
61
+ def _effective_color(self) -> QColor:
62
+ """当前生效的图标颜色:显式颜色优先,否则取 qss 设置的前景色。"""
63
+ if self._color is not None:
64
+ return self._color
65
+ return self.palette().color(self.foregroundRole())
66
+
67
+ # --------------------------- 原地旋转 ---------------------------
68
+ def get_angle(self) -> float:
69
+ return self._angle
70
+
71
+ def set_angle(self, angle: float):
72
+ angle = float(angle)
73
+ if angle != self._angle:
74
+ self._angle = angle
75
+ self.update()
76
+
77
+ angle = Property(float, get_angle, set_angle)
78
+
79
+ # --------------------------- 配色 / 字号 ---------------------------
80
+ def get_icon_color(self) -> QColor:
81
+ return self._effective_color()
82
+
83
+ def set_icon_color(self, color):
84
+ new_color = QColor(color) if color is not None else QColor()
85
+ if not new_color.isValid():
86
+ new_color = None
87
+ if new_color != self._color:
88
+ self._color = new_color
89
+ # 旋转时由 _effective_color() 自绘;未旋转时 QLabel 取样式表的颜色
90
+ self._apply_style()
91
+
92
+ def get_icon_size(self) -> int:
93
+ return self._icon_size
94
+
95
+ def set_icon_size(self, size: int):
96
+ size = int(size)
97
+ if size != self._icon_size:
98
+ self._icon_size = size
99
+ self._apply_style()
100
+
101
+ icon_color = Property(QColor, get_icon_color, set_icon_color)
102
+ icon_size = Property(int, get_icon_size, set_icon_size)
103
+
104
+ def _icon_pixmap(self) -> QPixmap:
105
+ """先把图标按 0° 渲染成 pixmap,再整体旋转绘制。
106
+
107
+ 直接把文字交给旋转后的 painter 画,在部分平台上不会输出任何像素,
108
+ 因此这里始终先渲染成位图再旋转。
109
+ """
110
+ color = self._effective_color()
111
+ dpr = self.devicePixelRatioF()
112
+ key = (self.size(), self.text(), self.font().key(), color.rgba(), dpr)
113
+ if self._pixmap is None or self._pixmap_key != key:
114
+ pm = QPixmap(int(self.width() * dpr), int(self.height() * dpr))
115
+ pm.setDevicePixelRatio(dpr)
116
+ pm.fill(Qt.transparent)
117
+
118
+ painter = QPainter(pm)
119
+ painter.setRenderHint(QPainter.Antialiasing, True)
120
+ painter.setRenderHint(QPainter.TextAntialiasing, True)
121
+ painter.setPen(color)
122
+ painter.setFont(self.font())
123
+ painter.drawText(QRect(QPoint(0, 0), self.size()), Qt.AlignCenter, self.text())
124
+ painter.end()
125
+
126
+ self._pixmap = pm
127
+ self._pixmap_key = key
128
+ return self._pixmap
129
+
130
+ def changeEvent(self, event):
131
+ # 主题 / 样式变化会改变 qss 决定的颜色,缓存的旋转位图必须失效
132
+ if event.type() in (QEvent.PaletteChange, QEvent.StyleChange, QEvent.FontChange):
133
+ self._pixmap = None
134
+ self._pixmap_key = None
135
+ super().changeEvent(event)
136
+
137
+ def paintEvent(self, event):
138
+ # 未旋转时走 QLabel 默认绘制,保证与普通图标标签完全一致
139
+ if self._angle % 360.0 == 0.0:
140
+ super().paintEvent(event)
141
+ return
142
+
143
+ painter = QPainter(self)
144
+ painter.setRenderHint(QPainter.Antialiasing, True)
145
+ painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
146
+
147
+ rect = QRectF(self.rect())
148
+ painter.translate(rect.center())
149
+ painter.rotate(self._angle)
150
+ painter.translate(-rect.center())
151
+ painter.drawPixmap(0, 0, self._icon_pixmap())
152
+
153
+ class IconImage(QLabel):
154
+ """内嵌图片图标(QLabel + QPixmap)。
155
+
156
+ 与 FontIcon(图标字体)不同,它不依赖图标字体,图片直接内嵌在控件里,
157
+ 因此完全不受全局 `* { font-size / font-family }` 之类规则影响。
158
+
159
+ 参数:
160
+ image —— QPixmap / QIcon / 图片文件路径;None 表示暂无图片
161
+ size —— 目标尺寸:int(正方形)或 QSize;None 表示按原图尺寸
162
+
163
+ 示例:
164
+ logo = IconImage("logo.png", size=48) # 文件路径 + 48×48
165
+ logo = IconImage(QPixmap(48, 48), size=QSize(48, 48)) # QPixmap + 指定尺寸
166
+ logo.set_angle(90) # 原地旋转(可做动画)
167
+ """
168
+
169
+ def __init__(self, image=None, size=None, parent=None):
170
+ super().__init__(parent)
171
+
172
+ self._source = QPixmap() # 原图
173
+ self._icon_size = QSize() # 空 QSize 表示按原图
174
+ self._angle = 0.0
175
+
176
+ self.setAlignment(Qt.AlignCenter)
177
+ self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
178
+ self.setMinimumWidth(0)
179
+
180
+ if size is not None:
181
+ self.set_icon_size(size)
182
+ if image is not None:
183
+ self.set_image(image)
184
+
185
+ # --------------------------- 图片 ---------------------------
186
+ def set_image(self, image):
187
+ """设置图片:QPixmap / QIcon / 文件路径。"""
188
+ if isinstance(image, QPixmap):
189
+ pixmap = image
190
+ elif isinstance(image, QIcon):
191
+ size = self._icon_size if not self._icon_size.isEmpty() else QSize(64, 64)
192
+ pixmap = image.pixmap(size)
193
+ elif isinstance(image, str):
194
+ pixmap = QPixmap(image)
195
+ elif image is None:
196
+ pixmap = QPixmap()
197
+ else:
198
+ raise TypeError("image 必须是 QPixmap / QIcon / 文件路径")
199
+
200
+ self._source = pixmap
201
+ self._refresh()
202
+
203
+ def image(self) -> QPixmap:
204
+ return self._source
205
+
206
+ def has_image(self) -> bool:
207
+ return not self._source.isNull()
208
+
209
+ def _refresh(self):
210
+ """按目标尺寸等比缩放后显示(保持宽高比)。"""
211
+ if self._source.isNull():
212
+ self.clear()
213
+ else:
214
+ if self._icon_size.isEmpty():
215
+ shown = self._source
216
+ else:
217
+ shown = self._source.scaled(self._icon_size, Qt.KeepAspectRatio,
218
+ Qt.SmoothTransformation)
219
+ super().setPixmap(shown)
220
+ self.setFixedSize(shown.size())
221
+ self.updateGeometry()
222
+
223
+ # --------------------------- 尺寸 ---------------------------
224
+ def get_icon_size(self) -> QSize:
225
+ return self._icon_size
226
+
227
+ def set_icon_size(self, size):
228
+ size = QSize(size, size) if isinstance(size, int) else QSize(size)
229
+ if size != self._icon_size:
230
+ self._icon_size = size
231
+ self._refresh()
232
+
233
+ icon_size = Property(QSize, get_icon_size, set_icon_size)
234
+
235
+ # --------------------------- 原地旋转(与 FontIcon 一致,可交给动画) ---------------------------
236
+ def get_angle(self) -> float:
237
+ return self._angle
238
+
239
+ def set_angle(self, angle: float):
240
+ angle = float(angle)
241
+ if angle != self._angle:
242
+ self._angle = angle
243
+ self.update()
244
+
245
+ angle = Property(float, get_angle, set_angle)
246
+
247
+ def paintEvent(self, event):
248
+ # 未旋转时走 QLabel 默认绘制
249
+ if self._angle % 360.0 == 0.0 or self.pixmap() is None:
250
+ super().paintEvent(event)
251
+ return
252
+
253
+ painter = QPainter(self)
254
+ painter.setRenderHint(QPainter.Antialiasing, True)
255
+ painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
256
+
257
+ rect = QRectF(self.rect())
258
+ painter.translate(rect.center())
259
+ painter.rotate(self._angle)
260
+ painter.translate(-rect.center())
261
+ painter.drawPixmap(rect, self.pixmap(), QRectF(self.pixmap().rect()))
@@ -0,0 +1,32 @@
1
+ """pyside6_levels —— 为 PySide6 提供“组件层级(level)”概念。
2
+
3
+ 一个控件(组件)可以声明自己为**子控件**增加多少层级(contribution),
4
+ 所有祖先贡献之和就是控件的基础层级;QSS 通过 ``level`` 动态属性按层级配色,
5
+ 未定义的中间层级自动就近向下填充。
6
+ """
7
+
8
+ from .component import (
9
+ level_component,
10
+ level_of,
11
+ raw_level_of,
12
+ set_contribution,
13
+ set_level,
14
+ )
15
+ from .manager import (
16
+ LevelManager,
17
+ normalize_level_selectors,
18
+ parse_defined_levels,
19
+ )
20
+
21
+ __version__ = "0.1.0"
22
+
23
+ __all__ = [
24
+ "LevelManager",
25
+ "level_component",
26
+ "set_contribution",
27
+ "set_level",
28
+ "raw_level_of",
29
+ "level_of",
30
+ "normalize_level_selectors",
31
+ "parse_defined_levels",
32
+ ]
@@ -0,0 +1,106 @@
1
+ """pyside6_levels.component —— 声明式组件 API。
2
+
3
+ 用法一:类装饰器(推荐,自制组件“自己设置 +1”)
4
+
5
+ @level_component(contribution=1)
6
+ class Card(QFrame):
7
+ ...
8
+
9
+ 用法二:给任意内置控件/实例单独设置
10
+
11
+ btn = QPushButton("普通按钮") # 默认贡献 0
12
+ manager.set_contribution(btn, 0)
13
+ manager.set_override(inner_btn, 0) # 只改这一个子控件的层级
14
+
15
+ 用法三:不带参数时读取类属性 ``level_contribution``
16
+
17
+ @level_component
18
+ class Card(QFrame):
19
+ level_contribution = 1
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import functools
25
+ from typing import Callable, Optional, TypeVar
26
+
27
+ from PySide6.QtWidgets import QWidget
28
+
29
+ from .manager import LevelManager
30
+
31
+ __all__ = [
32
+ "level_component",
33
+ "set_contribution",
34
+ "set_level",
35
+ "raw_level_of",
36
+ "level_of",
37
+ ]
38
+
39
+ _T = TypeVar("_T", bound=type)
40
+
41
+
42
+ def level_component(
43
+ cls: Optional[_T] = None,
44
+ *,
45
+ contribution: Optional[int] = None,
46
+ level: Optional[int] = None,
47
+ ):
48
+ """类装饰器:把这个组件类标记为“为子控件增加 ``contribution`` 层”。
49
+
50
+ :param contribution: 层级贡献(≥0)。省略时读取类属性 ``level_contribution``。
51
+ :param level: 需要强制指定该组件自身的基础层级时传入(等价于
52
+ :meth:`LevelManager.set_override`)。
53
+
54
+ 装饰器在实例 ``__init__`` 之后生效,因此即使子控件在构造函数里创建、
55
+ 或实例尚无 parent,后续事件循环也会自动补齐层级。
56
+ """
57
+
58
+ def decorate(component_cls: _T) -> _T:
59
+ # 类自己定义了 __init__ 就包装它;否则走父类(QWidget/QFrame)的初始化。
60
+ own_init = component_cls.__dict__.get("__init__")
61
+
62
+ def __init__(self, *args, **kwargs):
63
+ if own_init is not None:
64
+ own_init(self, *args, **kwargs)
65
+ else:
66
+ super(component_cls, self).__init__(*args, **kwargs)
67
+ manager = LevelManager.instance()
68
+ value = contribution
69
+ if value is None:
70
+ value = int(getattr(type(self), "level_contribution", 0))
71
+ manager.set_contribution(self, value)
72
+ if level is not None:
73
+ manager.set_override(self, level)
74
+
75
+ if own_init is not None:
76
+ __init__ = functools.wraps(own_init)(__init__)
77
+
78
+ component_cls.__init__ = __init__ # type: ignore[method-assign]
79
+ if contribution is not None:
80
+ component_cls.level_contribution = contribution # type: ignore[attr-defined]
81
+ return component_cls
82
+
83
+ if cls is None:
84
+ return decorate
85
+ return decorate(cls)
86
+
87
+
88
+ # --------------------------------------------------------------------- 便捷函数
89
+ def set_contribution(widget: QWidget, contribution: int) -> None:
90
+ """设置控件的层级贡献(≥0)。"""
91
+ LevelManager.instance().set_contribution(widget, contribution)
92
+
93
+
94
+ def set_level(widget: QWidget, level: Optional[int]) -> None:
95
+ """强制指定控件的基础层级(绝对覆盖父链),传 None 取消。"""
96
+ LevelManager.instance().set_override(widget, level)
97
+
98
+
99
+ def raw_level_of(widget: QWidget) -> int:
100
+ """控件自身的基础层级(祖先贡献之和,不含自身贡献)。"""
101
+ return LevelManager.instance().raw_level(widget)
102
+
103
+
104
+ def level_of(widget: QWidget) -> int:
105
+ """控件当前生效的样式层级(已按断点填充)。"""
106
+ return LevelManager.instance().level(widget)