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.
- bubble/QUI.py +4 -0
- bubble/__init__.py +8 -0
- bubble/cards.py +1503 -0
- bubble/dev_navview.py +4334 -0
- bubble/dev_titlebar.py +2900 -0
- bubble/glasskit/__init__.py +94 -0
- bubble/glasskit/config.py +77 -0
- bubble/glasskit/energy.py +202 -0
- bubble/glasskit/glass.py +1034 -0
- bubble/glasskit/gpu.py +43 -0
- bubble/glasskit/imaging.py +823 -0
- bubble/glasskit/settings.py +82 -0
- bubble/i18n.py +27 -0
- bubble/icon.py +261 -0
- bubble/levels/__init__.py +32 -0
- bubble/levels/component.py +106 -0
- bubble/levels/manager.py +334 -0
- bubble/styles.py +282 -0
- bubble/uiWindow.py +262 -0
- bubble/widgets.py +962 -0
- bubbleui-0.4.4a1.dist-info/METADATA +18 -0
- bubbleui-0.4.4a1.dist-info/RECORD +25 -0
- bubbleui-0.4.4a1.dist-info/WHEEL +5 -0
- bubbleui-0.4.4a1.dist-info/licenses/LICENSE +21 -0
- bubbleui-0.4.4a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Core frosted-glass toolkit for PySide6.
|
|
2
|
+
|
|
3
|
+
The package contains the reusable pieces only:
|
|
4
|
+
|
|
5
|
+
* :class:`FrostStyle` / :func:`paint_frost_overlay` -- material model and paint
|
|
6
|
+
helper. All material controls, including the vignette, live here. Every
|
|
7
|
+
field is also exposed as a QSS ``qproperty-frost*`` on the painting widgets
|
|
8
|
+
(:class:`GlassBackdrop`, :class:`GlassMenuBar`, :class:`GlassPanel`), so the
|
|
9
|
+
four presets in :mod:`.config` can be tuned from the theme style sheet; see
|
|
10
|
+
:func:`apply_glass_style` for the ``"thin"`` variants.
|
|
11
|
+
* :class:`BlurredGlassPanel` -- blur the content underneath one widget.
|
|
12
|
+
* :class:`DesktopSource` -- capture the screen and supply blurred regions for
|
|
13
|
+
live window-level glass.
|
|
14
|
+
* :class:`WallpaperSource` -- static system-wallpaper backdrop that reloads
|
|
15
|
+
only when the wallpaper changes (Mica-like idle behaviour).
|
|
16
|
+
* :class:`GlassRuntimeSettings` -- background source, FPS, GPU, energy saving
|
|
17
|
+
and focus-loss behaviour.
|
|
18
|
+
* :class:`GpuSurface` -- optional QOpenGLWidget painting surface.
|
|
19
|
+
* :class:`EnergyModeController` -- standard/energy switching with an animated
|
|
20
|
+
cross-fade, including automatic switching on focus loss.
|
|
21
|
+
|
|
22
|
+
Window chrome, parameter panels and demo styling belong to the application,
|
|
23
|
+
not to this library.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .energy import EnergyModeController
|
|
27
|
+
from .glass import (
|
|
28
|
+
BlurSource,
|
|
29
|
+
BlurredGlassPanel,
|
|
30
|
+
FrostStyle,
|
|
31
|
+
apply_glass_style,
|
|
32
|
+
mix,
|
|
33
|
+
paint_frost_overlay,
|
|
34
|
+
qcolor,
|
|
35
|
+
rounded_path,
|
|
36
|
+
GlassMainWindow,
|
|
37
|
+
GlassDialog,
|
|
38
|
+
GlassMenuBar,
|
|
39
|
+
GlassPanel,
|
|
40
|
+
GlassBackdrop
|
|
41
|
+
)
|
|
42
|
+
from .gpu import GpuBackdrop, GpuSurface
|
|
43
|
+
from .imaging import (
|
|
44
|
+
DesktopSource,
|
|
45
|
+
WallpaperSource,
|
|
46
|
+
blur_qimage,
|
|
47
|
+
clear_texture_cache,
|
|
48
|
+
exclude_window_from_capture,
|
|
49
|
+
noise_texture,
|
|
50
|
+
pil_to_qimage,
|
|
51
|
+
qimage_to_pil,
|
|
52
|
+
system_wallpaper_path,
|
|
53
|
+
)
|
|
54
|
+
from .settings import (
|
|
55
|
+
BACKGROUND_SCREEN,
|
|
56
|
+
BACKGROUND_WALLPAPER,
|
|
57
|
+
GlassRuntimeSettings,
|
|
58
|
+
)
|
|
59
|
+
from .config import * # noqa: F403
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"BACKGROUND_SCREEN",
|
|
63
|
+
"BACKGROUND_WALLPAPER",
|
|
64
|
+
"BlurSource",
|
|
65
|
+
"BlurredGlassPanel",
|
|
66
|
+
"DesktopSource",
|
|
67
|
+
"EnergyModeController",
|
|
68
|
+
"FrostStyle",
|
|
69
|
+
"GlassRuntimeSettings",
|
|
70
|
+
"GpuBackdrop",
|
|
71
|
+
"GpuSurface",
|
|
72
|
+
"WallpaperSource",
|
|
73
|
+
"blur_qimage",
|
|
74
|
+
"clear_texture_cache",
|
|
75
|
+
"exclude_window_from_capture",
|
|
76
|
+
"mix",
|
|
77
|
+
"noise_texture",
|
|
78
|
+
"paint_frost_overlay",
|
|
79
|
+
"pil_to_qimage",
|
|
80
|
+
"qcolor",
|
|
81
|
+
"qimage_to_pil",
|
|
82
|
+
"rounded_path",
|
|
83
|
+
"system_wallpaper_path",
|
|
84
|
+
"BUBBLE_BACKGROUND_NORMAL",
|
|
85
|
+
"BUBBLE_BACKGROUND_THIN",
|
|
86
|
+
"BUBBLE_SIDEBAR_NORMAL",
|
|
87
|
+
"BUBBLE_SIDEBAR_THIN",
|
|
88
|
+
"GlassMainWindow",
|
|
89
|
+
"GlassDialog",
|
|
90
|
+
"GlassMenuBar",
|
|
91
|
+
"GlassPanel",
|
|
92
|
+
"FROST_PRESETS",
|
|
93
|
+
"apply_glass_style"
|
|
94
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from .settings import (
|
|
2
|
+
BACKGROUND_WALLPAPER,
|
|
3
|
+
GlassRuntimeSettings,
|
|
4
|
+
FrostStyle
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
RUNTIME = GlassRuntimeSettings(
|
|
8
|
+
background_mode=BACKGROUND_WALLPAPER,
|
|
9
|
+
fps=24,
|
|
10
|
+
gpu=False,
|
|
11
|
+
auto_energy_on_focus_loss=True,
|
|
12
|
+
transition_ms=250,
|
|
13
|
+
)
|
|
14
|
+
CAPTURE_SCALE = 0.5 # region capture scale; 1.0 = sharper, more CPU
|
|
15
|
+
|
|
16
|
+
# Whole-window desktop glass material.
|
|
17
|
+
BUBBLE_BACKGROUND_THIN = FrostStyle(
|
|
18
|
+
tint="#4c4c4c",
|
|
19
|
+
opacity=60,
|
|
20
|
+
blur=24,
|
|
21
|
+
frost=20,
|
|
22
|
+
noise=5,
|
|
23
|
+
sheen=15,
|
|
24
|
+
saturation=100,
|
|
25
|
+
brightness=100,
|
|
26
|
+
) # 透明模式
|
|
27
|
+
|
|
28
|
+
# Whole-window desktop glass material.
|
|
29
|
+
BUBBLE_BACKGROUND_NORMAL = FrostStyle(
|
|
30
|
+
tint="#272727",
|
|
31
|
+
opacity=90,
|
|
32
|
+
blur=48,
|
|
33
|
+
frost=3,
|
|
34
|
+
noise=3,
|
|
35
|
+
sheen=0,
|
|
36
|
+
saturation=75,
|
|
37
|
+
brightness=60,
|
|
38
|
+
) # 透明模式
|
|
39
|
+
|
|
40
|
+
# Whole-window desktop glass material.
|
|
41
|
+
BUBBLE_SIDEBAR_NORMAL = FrostStyle(
|
|
42
|
+
tint="#202020",
|
|
43
|
+
opacity=70,
|
|
44
|
+
blur=48,
|
|
45
|
+
frost=5,
|
|
46
|
+
noise=0,
|
|
47
|
+
sheen=40,
|
|
48
|
+
saturation=80,
|
|
49
|
+
brightness=55,
|
|
50
|
+
) # 透明模式
|
|
51
|
+
|
|
52
|
+
# Whole-window desktop glass material.
|
|
53
|
+
BUBBLE_SIDEBAR_THIN = FrostStyle(
|
|
54
|
+
tint="#545454",
|
|
55
|
+
opacity=30,
|
|
56
|
+
blur=24,
|
|
57
|
+
frost=5,
|
|
58
|
+
noise=0,
|
|
59
|
+
sheen=10,
|
|
60
|
+
saturation=130,
|
|
61
|
+
brightness=120,
|
|
62
|
+
) # 透明模式
|
|
63
|
+
#: The four built-in materials. The same values are written to the theme
|
|
64
|
+
#: style sheets (`res/styles/dark.qss` / `light.qss`) as
|
|
65
|
+
#: `qproperty-frost*` rules; those QSS rules are applied at runtime and are
|
|
66
|
+
#: the recommended place to tune the material. The presets below remain the
|
|
67
|
+
#: code-level defaults (what a standalone glasskit app gets without a style
|
|
68
|
+
#: sheet). Widget mapping used by the theme style sheets:
|
|
69
|
+
#:
|
|
70
|
+
#: * `background_normal` / `background_thin` -> :class:GlassBackdrop
|
|
71
|
+
#: * `sidebar_normal` / `sidebar_thin` -> :class:GlassMenuBar
|
|
72
|
+
FROST_PRESETS = {
|
|
73
|
+
"background_normal": BUBBLE_BACKGROUND_NORMAL,
|
|
74
|
+
"background_thin": BUBBLE_BACKGROUND_THIN,
|
|
75
|
+
"sidebar_normal": BUBBLE_SIDEBAR_NORMAL,
|
|
76
|
+
"sidebar_thin": BUBBLE_SIDEBAR_THIN,
|
|
77
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Standard <-> energy-saving mode switching for glass windows.
|
|
2
|
+
|
|
3
|
+
The controller owns:
|
|
4
|
+
|
|
5
|
+
* explicit energy-saving state and optional automatic switching on focus loss
|
|
6
|
+
* a 0.25s cross-fade between the standard and energy backgrounds
|
|
7
|
+
* the paint hook for that transition
|
|
8
|
+
|
|
9
|
+
The host supplies a ``render_background(energy: bool) -> QPixmap`` callback and
|
|
10
|
+
reacts to ``energyChanged`` / ``transitionFinished`` to pause its desktop
|
|
11
|
+
source, hide/show a GPU surface, or update its controls.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
from PySide6.QtCore import (
|
|
19
|
+
QEasingCurve,
|
|
20
|
+
QEvent,
|
|
21
|
+
QObject,
|
|
22
|
+
QTimer,
|
|
23
|
+
Qt,
|
|
24
|
+
QVariantAnimation,
|
|
25
|
+
Signal,
|
|
26
|
+
)
|
|
27
|
+
from PySide6.QtGui import QPainter, QPixmap
|
|
28
|
+
from PySide6.QtWidgets import QWidget
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EnergyModeController(QObject):
|
|
32
|
+
"""Drive standard/energy mode with an animated cross-fade."""
|
|
33
|
+
|
|
34
|
+
energyChanged = Signal(bool)
|
|
35
|
+
transitionFinished = Signal(bool)
|
|
36
|
+
|
|
37
|
+
def __init__(self, window: QWidget,
|
|
38
|
+
render_background: Callable[[bool], QPixmap],
|
|
39
|
+
*, duration_ms: int = 250,
|
|
40
|
+
auto_on_focus_loss: bool = True,
|
|
41
|
+
parent: QObject | None = None):
|
|
42
|
+
super().__init__(parent if parent is not None else window)
|
|
43
|
+
self._window = window
|
|
44
|
+
self._render_background = render_background
|
|
45
|
+
self._duration_ms = max(0, int(duration_ms))
|
|
46
|
+
self._auto = bool(auto_on_focus_loss)
|
|
47
|
+
self._explicit = False
|
|
48
|
+
self._energy_active = False
|
|
49
|
+
self._transition_active = False
|
|
50
|
+
self._transition_t = 0.0
|
|
51
|
+
self._transition_from: QPixmap | None = None
|
|
52
|
+
self._transition_to: QPixmap | None = None
|
|
53
|
+
|
|
54
|
+
self._anim = QVariantAnimation(self)
|
|
55
|
+
self._anim.setDuration(self._duration_ms)
|
|
56
|
+
self._anim.setEasingCurve(QEasingCurve.Type.InOutCubic)
|
|
57
|
+
self._anim.valueChanged.connect(self._on_anim_value)
|
|
58
|
+
self._anim.finished.connect(self._on_anim_finished)
|
|
59
|
+
|
|
60
|
+
self._window.installEventFilter(self)
|
|
61
|
+
|
|
62
|
+
# -- public state ------------------------------------------------------
|
|
63
|
+
@property
|
|
64
|
+
def active(self) -> bool:
|
|
65
|
+
"""Whether the energy configuration is the current target."""
|
|
66
|
+
return self._energy_active
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def transitioning(self) -> bool:
|
|
70
|
+
return self._transition_active
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def progress(self) -> float:
|
|
74
|
+
return self._transition_t
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def explicit(self) -> bool:
|
|
78
|
+
return self._explicit
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def auto_on_focus_loss(self) -> bool:
|
|
82
|
+
return self._auto
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def duration_ms(self) -> int:
|
|
86
|
+
return self._duration_ms
|
|
87
|
+
|
|
88
|
+
def set_explicit(self, enabled: bool) -> None:
|
|
89
|
+
self._explicit = bool(enabled)
|
|
90
|
+
self._refresh()
|
|
91
|
+
|
|
92
|
+
def set_auto_on_focus_loss(self, enabled: bool) -> None:
|
|
93
|
+
self._auto = bool(enabled)
|
|
94
|
+
self._refresh()
|
|
95
|
+
|
|
96
|
+
def set_duration(self, duration_ms: int) -> None:
|
|
97
|
+
self._duration_ms = max(0, int(duration_ms))
|
|
98
|
+
self._anim.setDuration(self._duration_ms)
|
|
99
|
+
|
|
100
|
+
# -- painting ----------------------------------------------------------
|
|
101
|
+
def paint(self, painter: QPainter) -> bool:
|
|
102
|
+
"""Draw the cross-fade. Returns True while a transition is active."""
|
|
103
|
+
if not self._transition_active:
|
|
104
|
+
return False
|
|
105
|
+
if self._transition_from is not None:
|
|
106
|
+
painter.drawPixmap(0, 0, self._transition_from)
|
|
107
|
+
if self._transition_to is not None:
|
|
108
|
+
painter.setOpacity(self._transition_t)
|
|
109
|
+
painter.drawPixmap(0, 0, self._transition_to)
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
# -- internal state ----------------------------------------------------
|
|
113
|
+
def _refresh(self) -> None:
|
|
114
|
+
if not self._window.isVisible():
|
|
115
|
+
return
|
|
116
|
+
target = bool(self._explicit or (self._auto and not self._window.isActiveWindow()))
|
|
117
|
+
if target == self._energy_active:
|
|
118
|
+
return
|
|
119
|
+
self._start_transition(target)
|
|
120
|
+
|
|
121
|
+
def _start_transition(self, target: bool) -> None:
|
|
122
|
+
if self._transition_active:
|
|
123
|
+
from_pixmap = self._render_transition_frame()
|
|
124
|
+
self._transition_active = False
|
|
125
|
+
self._anim.stop()
|
|
126
|
+
else:
|
|
127
|
+
from_pixmap = self._render_background(self._energy_active)
|
|
128
|
+
|
|
129
|
+
self._energy_active = bool(target)
|
|
130
|
+
self.energyChanged.emit(self._energy_active)
|
|
131
|
+
|
|
132
|
+
if self._duration_ms <= 0:
|
|
133
|
+
self._transition_active = False
|
|
134
|
+
self.transitionFinished.emit(self._energy_active)
|
|
135
|
+
self._window.update()
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
to_pixmap = self._render_background(self._energy_active)
|
|
139
|
+
self._transition_from = from_pixmap
|
|
140
|
+
self._transition_to = to_pixmap
|
|
141
|
+
self._transition_t = 0.0
|
|
142
|
+
self._transition_active = True
|
|
143
|
+
self._anim.stop()
|
|
144
|
+
self._anim.setStartValue(0.0)
|
|
145
|
+
self._anim.setEndValue(1.0)
|
|
146
|
+
self._anim.start()
|
|
147
|
+
self._window.update()
|
|
148
|
+
|
|
149
|
+
def _on_anim_value(self, value) -> None:
|
|
150
|
+
if not self._transition_active:
|
|
151
|
+
return
|
|
152
|
+
self._transition_t = float(value)
|
|
153
|
+
self._window.update()
|
|
154
|
+
|
|
155
|
+
def _on_anim_finished(self) -> None:
|
|
156
|
+
if not self._transition_active:
|
|
157
|
+
return
|
|
158
|
+
self._transition_active = False
|
|
159
|
+
self._transition_from = None
|
|
160
|
+
self._transition_to = None
|
|
161
|
+
self._transition_t = 1.0
|
|
162
|
+
self.transitionFinished.emit(self._energy_active)
|
|
163
|
+
self._window.update()
|
|
164
|
+
|
|
165
|
+
def _render_transition_frame(self) -> QPixmap:
|
|
166
|
+
pixmap = QPixmap(self._window.size())
|
|
167
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
168
|
+
painter = QPainter(pixmap)
|
|
169
|
+
if self._transition_from is not None:
|
|
170
|
+
painter.drawPixmap(0, 0, self._transition_from)
|
|
171
|
+
if self._transition_to is not None:
|
|
172
|
+
painter.setOpacity(self._transition_t)
|
|
173
|
+
painter.drawPixmap(0, 0, self._transition_to)
|
|
174
|
+
painter.end()
|
|
175
|
+
return pixmap
|
|
176
|
+
|
|
177
|
+
def _scale_snapshots(self) -> None:
|
|
178
|
+
size = self._window.size()
|
|
179
|
+
if self._transition_from is not None:
|
|
180
|
+
self._transition_from = self._transition_from.scaled(
|
|
181
|
+
size, Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
182
|
+
Qt.TransformationMode.SmoothTransformation,
|
|
183
|
+
)
|
|
184
|
+
if self._transition_to is not None:
|
|
185
|
+
self._transition_to = self._transition_to.scaled(
|
|
186
|
+
size, Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
187
|
+
Qt.TransformationMode.SmoothTransformation,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# -- focus / resize ----------------------------------------------------
|
|
191
|
+
def eventFilter(self, obj, event) -> bool: # noqa: N802
|
|
192
|
+
if obj is self._window:
|
|
193
|
+
event_type = event.type()
|
|
194
|
+
if event_type == QEvent.Type.Show:
|
|
195
|
+
QTimer.singleShot(0, self._refresh)
|
|
196
|
+
elif event_type in (QEvent.Type.WindowActivate,
|
|
197
|
+
QEvent.Type.WindowDeactivate,
|
|
198
|
+
QEvent.Type.ActivationChange):
|
|
199
|
+
self._refresh()
|
|
200
|
+
elif event_type == QEvent.Type.Resize and self._transition_active:
|
|
201
|
+
self._scale_snapshots()
|
|
202
|
+
return super().eventFilter(obj, event)
|