RinUI 0.1.0__py3-none-any.whl → 0.1.1.post1__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.
- RinUI/__init__.py +4 -4
- RinUI/assets/fonts/FluentSystemIcons-Index.js +5255 -5255
- RinUI/components/Base.qml +78 -78
- RinUI/components/BasicInput/Button.qml +146 -146
- RinUI/components/BasicInput/CheckBox.qml +98 -98
- RinUI/components/BasicInput/ComboBox.qml +159 -159
- RinUI/components/BasicInput/DropDownButton.qml +20 -20
- RinUI/components/BasicInput/Hyperlink.qml +17 -17
- RinUI/components/BasicInput/PillButton.qml +10 -10
- RinUI/components/BasicInput/RadioButton.qml +94 -94
- RinUI/components/BasicInput/RoundButton.qml +26 -26
- RinUI/components/BasicInput/Slider.qml +212 -212
- RinUI/components/BasicInput/Switch.qml +101 -101
- RinUI/components/BasicInput/ToggleButton.qml +10 -10
- RinUI/components/BasicInput/ToolButton.qml +30 -30
- RinUI/components/ContextMenu.qml +183 -183
- RinUI/components/DateAndTime/DatePicker.qml +142 -140
- RinUI/components/DateAndTime/PickerView.qml +223 -223
- RinUI/components/DateAndTime/TimePicker.qml +114 -114
- RinUI/components/DialogsAndFlyouts/Dialog.qml +106 -106
- RinUI/components/DialogsAndFlyouts/DialogButtonBox.qml +47 -47
- RinUI/components/DialogsAndFlyouts/Flyout.qml +87 -87
- RinUI/components/DialogsAndFlyouts/Popup.qml +110 -106
- RinUI/components/FocusIndicator.qml +33 -33
- RinUI/components/IconWidget.qml +52 -52
- RinUI/components/Indicator.qml +89 -89
- RinUI/components/Layout/Expander.qml +159 -159
- RinUI/components/Layout/SettingExpander.qml +66 -66
- RinUI/components/Layout/SettingItem.qml +70 -70
- RinUI/components/ListAndCollections/Clip.qml +21 -21
- RinUI/components/ListAndCollections/Frame.qml +43 -43
- RinUI/components/ListAndCollections/ListView.qml +104 -104
- RinUI/components/ListAndCollections/ListViewDelegate.qml +82 -82
- RinUI/components/ListAndCollections/SettingCard.qml +72 -72
- RinUI/components/ListAndCollections/TableView.qml +81 -81
- RinUI/components/ListAndCollections/TableViewDelegate.qml +88 -88
- RinUI/components/Media/Avatar.qml +82 -82
- RinUI/components/MenusAndToolbars/Menu.qml +150 -150
- RinUI/components/MenusAndToolbars/MenuBar.qml +42 -42
- RinUI/components/MenusAndToolbars/MenuItem.qml +131 -131
- RinUI/components/MenusAndToolbars/MenuItemGroup.qml +43 -43
- RinUI/components/MenusAndToolbars/MenuSeparator.qml +13 -13
- RinUI/components/MenusAndToolbars/TextInputMenu.qml +37 -37
- RinUI/components/MenusAndToolbars/ToolSeparator.qml +16 -16
- RinUI/components/Navigation/ErrorPage.qml +48 -48
- RinUI/components/Navigation/NavigationBar.qml +179 -179
- RinUI/components/Navigation/NavigationItem.qml +193 -193
- RinUI/components/Navigation/NavigationSubItem.qml +103 -103
- RinUI/components/Navigation/NavigationView.qml +228 -227
- RinUI/components/Navigation/Segmented.qml +16 -16
- RinUI/components/Navigation/SegmentedItem.qml +107 -107
- RinUI/components/Navigation/SelectorBar.qml +12 -12
- RinUI/components/Navigation/SelectorBarItem.qml +88 -88
- RinUI/components/ScrollBar.qml +204 -204
- RinUI/components/ScrollView.qml +12 -12
- RinUI/components/Shadow.qml +47 -47
- RinUI/components/StatusAndInfo/InfoBadge.qml +77 -77
- RinUI/components/StatusAndInfo/InfoBar.qml +256 -251
- RinUI/components/StatusAndInfo/ProgressBar.qml +126 -126
- RinUI/components/StatusAndInfo/ProgressRing.qml +149 -0
- RinUI/components/StatusAndInfo/Toast.qml +236 -236
- RinUI/components/StatusAndInfo/ToolTip.qml +93 -93
- RinUI/components/Text/SpinBox.qml +150 -133
- RinUI/components/Text/Text.qml +44 -44
- RinUI/components/Text/TextArea.qml +117 -117
- RinUI/components/Text/TextField.qml +113 -113
- RinUI/components/Text/TextInput.qml +47 -47
- RinUI/components/qmldir +80 -80
- RinUI/core/__init__.py +4 -4
- RinUI/core/config.py +129 -129
- RinUI/core/launcher.py +129 -129
- RinUI/core/theme.py +339 -339
- RinUI/core/translator.py +26 -25
- RinUI/hooks/__init__.py +3 -3
- RinUI/hooks/hook-RinUI.py +3 -3
- RinUI/qmldir +108 -104
- RinUI/themes/Appearance.qml +36 -0
- RinUI/themes/Colors.qml +36 -0
- RinUI/themes/dark.qml +145 -145
- RinUI/themes/light.qml +145 -145
- RinUI/themes/qmldir +9 -6
- RinUI/themes/theme.qml +151 -149
- RinUI/themes/utils.qml +37 -37
- RinUI/utils/Animation.qml +12 -12
- RinUI/utils/FloatLayer.qml +132 -123
- RinUI/utils/FontIconLoader.qml +13 -13
- RinUI/utils/Position.qml +19 -19
- RinUI/utils/Severity.qml +13 -13
- RinUI/utils/Typography.qml +17 -17
- RinUI/utils/qmldir +4 -4
- RinUI/windows/CtrlBtn.qml +118 -118
- RinUI/windows/FluentPage.qml +92 -92
- RinUI/windows/FluentWindow.qml +31 -30
- RinUI/windows/FluentWindowBase.qml +158 -158
- RinUI/windows/TitleBar.qml +135 -135
- RinUI/windows/qmldir +7 -7
- RinUI/windows/window/ApplicationWindow.qml +8 -8
- RinUI/windows/window/Window.qml +118 -118
- {rinui-0.1.0.data → rinui-0.1.1.post1.data}/data/LICENSE +21 -21
- {rinui-0.1.0.data → rinui-0.1.1.post1.data}/data/README.md +100 -100
- {rinui-0.1.0.dist-info → rinui-0.1.1.post1.dist-info}/METADATA +116 -115
- rinui-0.1.1.post1.dist-info/RECORD +112 -0
- {rinui-0.1.0.dist-info → rinui-0.1.1.post1.dist-info}/WHEEL +1 -1
- {rinui-0.1.0.dist-info → rinui-0.1.1.post1.dist-info/licenses}/LICENSE +21 -21
- RinUI/__pycache__/__init__.cpython-38.pyc +0 -0
- RinUI/config/rin_ui.json +0 -8
- RinUI/core/__pycache__/__init__.cpython-38.pyc +0 -0
- RinUI/core/__pycache__/config.cpython-38.pyc +0 -0
- RinUI/core/__pycache__/launcher.cpython-38.pyc +0 -0
- RinUI/core/__pycache__/theme.cpython-38.pyc +0 -0
- RinUI/core/__pycache__/translator.cpython-38.pyc +0 -0
- rinui-0.1.0.dist-info/RECORD +0 -116
- {rinui-0.1.0.dist-info → rinui-0.1.1.post1.dist-info}/entry_points.txt +0 -0
- {rinui-0.1.0.dist-info → rinui-0.1.1.post1.dist-info}/top_level.txt +0 -0
RinUI/core/theme.py
CHANGED
@@ -1,339 +1,339 @@
|
|
1
|
-
import ctypes
|
2
|
-
import platform
|
3
|
-
import time
|
4
|
-
|
5
|
-
from PySide6.QtCore import QObject, Signal, Slot, QThread
|
6
|
-
|
7
|
-
from .config import DEFAULT_CONFIG, RinConfig, is_win10, is_windows, is_win11, BackdropEffect
|
8
|
-
import sys
|
9
|
-
import darkdetect
|
10
|
-
|
11
|
-
|
12
|
-
def check_darkdetect_support():
|
13
|
-
system = platform.system()
|
14
|
-
if system == "Darwin":
|
15
|
-
mac_ver = platform.mac_ver()[0]
|
16
|
-
major, minor, *_ = map(int, mac_ver.split("."))
|
17
|
-
return (major == 10 and minor >= 14) or major > 10
|
18
|
-
|
19
|
-
elif system == "Windows":
|
20
|
-
return platform.release() >= "10"
|
21
|
-
else:
|
22
|
-
return False
|
23
|
-
|
24
|
-
|
25
|
-
ACCENT_STATES = {
|
26
|
-
"acrylic": 3,
|
27
|
-
"mica": 2,
|
28
|
-
"tabbed": 4,
|
29
|
-
"none": 0
|
30
|
-
}
|
31
|
-
|
32
|
-
ACCENT_SUPPORT = {
|
33
|
-
"acrylic": is_win10(),
|
34
|
-
"mica": is_win11(),
|
35
|
-
"tabbed": is_win10(),
|
36
|
-
"none": True
|
37
|
-
}
|
38
|
-
|
39
|
-
|
40
|
-
class ACCENT_POLICY(ctypes.Structure):
|
41
|
-
_fields_ = [
|
42
|
-
("AccentState", ctypes.c_int),
|
43
|
-
("AccentFlags", ctypes.c_int),
|
44
|
-
("GradientColor", ctypes.c_int),
|
45
|
-
("AnimationId", ctypes.c_int),
|
46
|
-
]
|
47
|
-
|
48
|
-
|
49
|
-
class WINDOWCOMPOSITIONATTRIBDATA(ctypes.Structure):
|
50
|
-
_fields_ = [
|
51
|
-
("Attrib", ctypes.c_int),
|
52
|
-
("pvData", ctypes.c_void_p),
|
53
|
-
("cbData", ctypes.c_size_t),
|
54
|
-
]
|
55
|
-
|
56
|
-
|
57
|
-
class ThemeListener(QThread):
|
58
|
-
"""
|
59
|
-
监听系统颜色模式
|
60
|
-
"""
|
61
|
-
themeChanged = Signal(str)
|
62
|
-
|
63
|
-
def run(self):
|
64
|
-
last_theme = darkdetect.theme()
|
65
|
-
while True:
|
66
|
-
current_theme = darkdetect.theme()
|
67
|
-
if current_theme != last_theme:
|
68
|
-
last_theme = current_theme
|
69
|
-
self.themeChanged.emit(current_theme)
|
70
|
-
print(f"Theme changed: {current_theme}")
|
71
|
-
time.sleep(1)
|
72
|
-
|
73
|
-
def stop(self):
|
74
|
-
self.terminate()
|
75
|
-
|
76
|
-
|
77
|
-
class ThemeManager(QObject):
|
78
|
-
themeChanged = Signal(str)
|
79
|
-
backdropChanged = Signal(str)
|
80
|
-
windows = [] # 窗口句柄们(
|
81
|
-
_instance = None
|
82
|
-
|
83
|
-
# DWM 常量保持不变
|
84
|
-
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
85
|
-
DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
86
|
-
DWMWA_NCRENDERING_POLICY = 2
|
87
|
-
DWMNCRENDERINGPOLICY_ENABLED = 2
|
88
|
-
DWMWA_SYSTEMBACKDROP_TYPE = 38
|
89
|
-
WCA_ACCENT_POLICY = 19
|
90
|
-
|
91
|
-
# 圆角
|
92
|
-
DWMWCP_DEFAULT = 0
|
93
|
-
DWMWCP_DONOTROUND = 1
|
94
|
-
DWMWCP_ROUND = 2
|
95
|
-
DWMWCP_ROUNDSMALL = 3
|
96
|
-
|
97
|
-
def clean_up(self):
|
98
|
-
"""
|
99
|
-
清理资源并停止主题监听。
|
100
|
-
"""
|
101
|
-
if self.listener:
|
102
|
-
RinConfig.save_config()
|
103
|
-
print("Save config.")
|
104
|
-
self.listener.stop()
|
105
|
-
self.listener.wait() # 等待线程结束
|
106
|
-
print("Theme listener stopped.")
|
107
|
-
|
108
|
-
def __new__(cls, *args, **kwargs):
|
109
|
-
"""
|
110
|
-
单例管理,共享主题状态
|
111
|
-
:param args:
|
112
|
-
:param kwargs:
|
113
|
-
"""
|
114
|
-
if cls._instance is None:
|
115
|
-
cls._instance = super().__new__(cls)
|
116
|
-
return cls._instance
|
117
|
-
|
118
|
-
def __init__(self):
|
119
|
-
if hasattr(self, "_initialized") and self._initialized:
|
120
|
-
return
|
121
|
-
self._initialized = True
|
122
|
-
super().__init__()
|
123
|
-
self.theme_dict = {
|
124
|
-
"Light": 0,
|
125
|
-
"Dark": 1
|
126
|
-
}
|
127
|
-
|
128
|
-
self.listener = None # 监听线程
|
129
|
-
self.current_theme = DEFAULT_CONFIG["theme"]["current_theme"] # 当前主题
|
130
|
-
self.is_darkdetect_supported = check_darkdetect_support()
|
131
|
-
|
132
|
-
try:
|
133
|
-
self.current_theme = RinConfig["theme"]["current_theme"]
|
134
|
-
except Exception as e:
|
135
|
-
print(f"Failed to load config because of {e}, using default config")
|
136
|
-
|
137
|
-
# self.hwnd = None # 窗口句柄
|
138
|
-
|
139
|
-
self.start_listener()
|
140
|
-
|
141
|
-
def start_listener(self):
|
142
|
-
if not self.is_darkdetect_supported:
|
143
|
-
print("darkdetect not supported on this platform")
|
144
|
-
return
|
145
|
-
self.listener = ThemeListener()
|
146
|
-
self.listener.themeChanged.connect(self._handle_system_theme)
|
147
|
-
self.listener.start()
|
148
|
-
|
149
|
-
def set_window(self, window): # 绑定窗口句柄
|
150
|
-
hwnd = int(window.winId())
|
151
|
-
self.windows.append(hwnd)
|
152
|
-
print(f"Window handle set: {hwnd}")
|
153
|
-
|
154
|
-
def _handle_system_theme(self):
|
155
|
-
if self.current_theme == "Auto":
|
156
|
-
self._update_window_theme()
|
157
|
-
self.themeChanged.emit(self._actual_theme())
|
158
|
-
else:
|
159
|
-
# 保持当前背景效果不变
|
160
|
-
self._update_window_theme()
|
161
|
-
|
162
|
-
@Slot(str)
|
163
|
-
def apply_backdrop_effect(self, effect_type: str):
|
164
|
-
"""
|
165
|
-
应用背景效果
|
166
|
-
:param effect_type: str, 背景效果类型(acrylic, mica, tabbed, none)
|
167
|
-
"""
|
168
|
-
self._update_window_theme()
|
169
|
-
if not is_windows() or not self.windows:
|
170
|
-
print(f"Cannot apply effect \"{effect_type}\" on this platform")
|
171
|
-
return -2 # 非 windows或未绑定窗口
|
172
|
-
self.backdropChanged.emit(effect_type)
|
173
|
-
|
174
|
-
accent_state = ACCENT_STATES.get(effect_type, 0)
|
175
|
-
if not ACCENT_SUPPORT.get(effect_type, False):
|
176
|
-
print(f"Effect \"{effect_type}\" not supported on this platform")
|
177
|
-
return -1 # 效果不支持
|
178
|
-
|
179
|
-
for hwnd in self.windows:
|
180
|
-
if is_win11():
|
181
|
-
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
182
|
-
hwnd,
|
183
|
-
self.DWMWA_SYSTEMBACKDROP_TYPE,
|
184
|
-
ctypes.byref(ctypes.c_int(accent_state)),
|
185
|
-
ctypes.sizeof(ctypes.c_int)
|
186
|
-
)
|
187
|
-
elif is_win10() and effect_type == BackdropEffect.Acrylic.value:
|
188
|
-
self._apply_win10_effect(effect_type, hwnd)
|
189
|
-
|
190
|
-
RinConfig["backdrop_effect"] = effect_type
|
191
|
-
print(
|
192
|
-
f"Applied \"{effect_type.strip().capitalize()}\" effect with "
|
193
|
-
f"{platform.system() + '11' if is_win11() else '10'}"
|
194
|
-
)
|
195
|
-
return 0 # 成功
|
196
|
-
|
197
|
-
def _apply_win10_effect(self, effect_type, hwnd):
|
198
|
-
"""
|
199
|
-
应用 Windows 10 背景效果
|
200
|
-
:param effect_type: str, 背景效果类型(acrylic, tabbed(actually blur)
|
201
|
-
"""
|
202
|
-
backdrop_color = RinConfig["win10_feat"]["backdrop_dark" if self.is_dark_theme() else "backdrop_light"]
|
203
|
-
|
204
|
-
accent = ACCENT_POLICY()
|
205
|
-
accent.AccentState = ACCENT_STATES[effect_type]
|
206
|
-
accent.AccentFlags = 2
|
207
|
-
accent.GradientColor = backdrop_color
|
208
|
-
data = WINDOWCOMPOSITIONATTRIBDATA()
|
209
|
-
data.Attrib = self.WCA_ACCENT_POLICY
|
210
|
-
data.pvData = ctypes.cast(ctypes.pointer(accent), ctypes.c_void_p)
|
211
|
-
data.cbData = ctypes.sizeof(accent)
|
212
|
-
|
213
|
-
try:
|
214
|
-
set_window_composition = ctypes.windll.user32.SetWindowCompositionAttribute
|
215
|
-
set_window_composition(hwnd, ctypes.byref(data))
|
216
|
-
except Exception as e:
|
217
|
-
print(f"Failed to apply acrylic on Win10: {e}")
|
218
|
-
|
219
|
-
def apply_window_effects(self): # 启用圆角阴影
|
220
|
-
if sys.platform != "win32" or not self.windows:
|
221
|
-
return
|
222
|
-
|
223
|
-
dwm = ctypes.windll.dwmapi
|
224
|
-
|
225
|
-
# 启用非客户端渲染策略(让窗口边框具备阴影)
|
226
|
-
ncrp = ctypes.c_int(self.DWMNCRENDERINGPOLICY_ENABLED)
|
227
|
-
for hwnd in self.windows:
|
228
|
-
dwm.DwmSetWindowAttribute(
|
229
|
-
hwnd,
|
230
|
-
self.DWMWA_NCRENDERING_POLICY,
|
231
|
-
ctypes.byref(ncrp),
|
232
|
-
ctypes.sizeof(ncrp)
|
233
|
-
)
|
234
|
-
|
235
|
-
# 启用圆角效果
|
236
|
-
corner_preference = ctypes.c_int(self.DWMWCP_ROUND)
|
237
|
-
dwm.DwmSetWindowAttribute(
|
238
|
-
hwnd,
|
239
|
-
self.DWMWA_WINDOW_CORNER_PREFERENCE,
|
240
|
-
ctypes.byref(corner_preference),
|
241
|
-
ctypes.sizeof(corner_preference)
|
242
|
-
)
|
243
|
-
print("Enabled Rounded and Shadows")
|
244
|
-
|
245
|
-
def _update_window_theme(self): # 更新窗口的颜色模式
|
246
|
-
if sys.platform != "win32" or not self.windows:
|
247
|
-
return
|
248
|
-
actual_theme = self._actual_theme()
|
249
|
-
for hwnd in self.windows:
|
250
|
-
if is_win11():
|
251
|
-
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
252
|
-
hwnd,
|
253
|
-
self.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
254
|
-
ctypes.byref(ctypes.c_int(self.theme_dict[actual_theme])),
|
255
|
-
ctypes.sizeof(ctypes.c_int)
|
256
|
-
)
|
257
|
-
elif is_win10() and RinConfig["backdrop_effect"] == BackdropEffect.Acrylic.value:
|
258
|
-
self._apply_win10_effect(RinConfig["backdrop_effect"], hwnd)
|
259
|
-
else:
|
260
|
-
print(f"Cannot apply backdrop on {platform.system()}")
|
261
|
-
|
262
|
-
print(f"Window theme updated to {actual_theme}")
|
263
|
-
|
264
|
-
def is_dark_theme(self):
|
265
|
-
"""是否为暗黑主题"""
|
266
|
-
return self._actual_theme() == "Dark"
|
267
|
-
|
268
|
-
def _actual_theme(self):
|
269
|
-
"""实际应用的主题"""
|
270
|
-
if self.current_theme == "Auto":
|
271
|
-
return darkdetect.theme() if self.is_darkdetect_supported else "Light"
|
272
|
-
return self.current_theme
|
273
|
-
|
274
|
-
@Slot(str)
|
275
|
-
def toggle_theme(self, theme: str): # 切换主题
|
276
|
-
if theme not in ["Auto", "Light", "Dark"]: # 三状态
|
277
|
-
return
|
278
|
-
if self.current_theme != theme:
|
279
|
-
print(f"Switching to '{theme}' theme")
|
280
|
-
self.current_theme = theme
|
281
|
-
RinConfig["theme"]["current_theme"] = theme
|
282
|
-
self._update_window_theme()
|
283
|
-
self.themeChanged.emit(self._actual_theme())
|
284
|
-
|
285
|
-
@Slot(result=str)
|
286
|
-
def get_theme(self):
|
287
|
-
return self._actual_theme()
|
288
|
-
|
289
|
-
@Slot(result=str)
|
290
|
-
def get_theme_name(self):
|
291
|
-
"""获取当前主题名称"""
|
292
|
-
return self.current_theme
|
293
|
-
|
294
|
-
@Slot(str)
|
295
|
-
def receive(self, message):
|
296
|
-
print(message)
|
297
|
-
|
298
|
-
@Slot(result=str)
|
299
|
-
def get_backdrop_effect(self):
|
300
|
-
"""获取当前背景效果"""
|
301
|
-
return RinConfig["backdrop_effect"]
|
302
|
-
|
303
|
-
@Slot(str)
|
304
|
-
def set_theme_color(self, color):
|
305
|
-
"""设置当前主题颜色"""
|
306
|
-
RinConfig["theme_color"] = color
|
307
|
-
RinConfig.save_config()
|
308
|
-
|
309
|
-
@Slot(result=str)
|
310
|
-
def get_theme_color(self):
|
311
|
-
"""获取当前主题颜色"""
|
312
|
-
return RinConfig["theme_color"]
|
313
|
-
|
314
|
-
@Slot(QObject, result=int)
|
315
|
-
def getWindowId(self, window):
|
316
|
-
"""获取窗口的句柄"""
|
317
|
-
print(f"GetWindowId: {window.winId()}")
|
318
|
-
return int(window.winId())
|
319
|
-
|
320
|
-
@Slot(int)
|
321
|
-
def dragWindowEvent(self, hwnd):
|
322
|
-
""" 在Windows 用原生方法拖动"""
|
323
|
-
if not is_windows() or hwnd not in self.windows:
|
324
|
-
print(
|
325
|
-
f"Use Qt method to drag window on: {platform.system()}"
|
326
|
-
if not is_windows() else f"Invalid window handle: {hwnd}"
|
327
|
-
)
|
328
|
-
return
|
329
|
-
|
330
|
-
import win32con
|
331
|
-
from win32gui import ReleaseCapture
|
332
|
-
from win32api import SendMessage
|
333
|
-
|
334
|
-
ReleaseCapture()
|
335
|
-
SendMessage(
|
336
|
-
hwnd,
|
337
|
-
win32con.WM_SYSCOMMAND,
|
338
|
-
win32con.SC_MOVE | win32con.HTCAPTION, 0
|
339
|
-
)
|
1
|
+
import ctypes
|
2
|
+
import platform
|
3
|
+
import time
|
4
|
+
|
5
|
+
from PySide6.QtCore import QObject, Signal, Slot, QThread
|
6
|
+
|
7
|
+
from .config import DEFAULT_CONFIG, RinConfig, is_win10, is_windows, is_win11, BackdropEffect
|
8
|
+
import sys
|
9
|
+
import darkdetect
|
10
|
+
|
11
|
+
|
12
|
+
def check_darkdetect_support():
|
13
|
+
system = platform.system()
|
14
|
+
if system == "Darwin":
|
15
|
+
mac_ver = platform.mac_ver()[0]
|
16
|
+
major, minor, *_ = map(int, mac_ver.split("."))
|
17
|
+
return (major == 10 and minor >= 14) or major > 10
|
18
|
+
|
19
|
+
elif system == "Windows":
|
20
|
+
return platform.release() >= "10"
|
21
|
+
else:
|
22
|
+
return False
|
23
|
+
|
24
|
+
|
25
|
+
ACCENT_STATES = {
|
26
|
+
"acrylic": 3,
|
27
|
+
"mica": 2,
|
28
|
+
"tabbed": 4,
|
29
|
+
"none": 0
|
30
|
+
}
|
31
|
+
|
32
|
+
ACCENT_SUPPORT = {
|
33
|
+
"acrylic": is_win10(),
|
34
|
+
"mica": is_win11(),
|
35
|
+
"tabbed": is_win10(),
|
36
|
+
"none": True
|
37
|
+
}
|
38
|
+
|
39
|
+
|
40
|
+
class ACCENT_POLICY(ctypes.Structure):
|
41
|
+
_fields_ = [
|
42
|
+
("AccentState", ctypes.c_int),
|
43
|
+
("AccentFlags", ctypes.c_int),
|
44
|
+
("GradientColor", ctypes.c_int),
|
45
|
+
("AnimationId", ctypes.c_int),
|
46
|
+
]
|
47
|
+
|
48
|
+
|
49
|
+
class WINDOWCOMPOSITIONATTRIBDATA(ctypes.Structure):
|
50
|
+
_fields_ = [
|
51
|
+
("Attrib", ctypes.c_int),
|
52
|
+
("pvData", ctypes.c_void_p),
|
53
|
+
("cbData", ctypes.c_size_t),
|
54
|
+
]
|
55
|
+
|
56
|
+
|
57
|
+
class ThemeListener(QThread):
|
58
|
+
"""
|
59
|
+
监听系统颜色模式
|
60
|
+
"""
|
61
|
+
themeChanged = Signal(str)
|
62
|
+
|
63
|
+
def run(self):
|
64
|
+
last_theme = darkdetect.theme()
|
65
|
+
while True:
|
66
|
+
current_theme = darkdetect.theme()
|
67
|
+
if current_theme != last_theme:
|
68
|
+
last_theme = current_theme
|
69
|
+
self.themeChanged.emit(current_theme)
|
70
|
+
print(f"Theme changed: {current_theme}")
|
71
|
+
time.sleep(1)
|
72
|
+
|
73
|
+
def stop(self):
|
74
|
+
self.terminate()
|
75
|
+
|
76
|
+
|
77
|
+
class ThemeManager(QObject):
|
78
|
+
themeChanged = Signal(str)
|
79
|
+
backdropChanged = Signal(str)
|
80
|
+
windows = [] # 窗口句柄们(
|
81
|
+
_instance = None
|
82
|
+
|
83
|
+
# DWM 常量保持不变
|
84
|
+
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
85
|
+
DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
86
|
+
DWMWA_NCRENDERING_POLICY = 2
|
87
|
+
DWMNCRENDERINGPOLICY_ENABLED = 2
|
88
|
+
DWMWA_SYSTEMBACKDROP_TYPE = 38
|
89
|
+
WCA_ACCENT_POLICY = 19
|
90
|
+
|
91
|
+
# 圆角
|
92
|
+
DWMWCP_DEFAULT = 0
|
93
|
+
DWMWCP_DONOTROUND = 1
|
94
|
+
DWMWCP_ROUND = 2
|
95
|
+
DWMWCP_ROUNDSMALL = 3
|
96
|
+
|
97
|
+
def clean_up(self):
|
98
|
+
"""
|
99
|
+
清理资源并停止主题监听。
|
100
|
+
"""
|
101
|
+
if self.listener:
|
102
|
+
RinConfig.save_config()
|
103
|
+
print("Save config.")
|
104
|
+
self.listener.stop()
|
105
|
+
self.listener.wait() # 等待线程结束
|
106
|
+
print("Theme listener stopped.")
|
107
|
+
|
108
|
+
def __new__(cls, *args, **kwargs):
|
109
|
+
"""
|
110
|
+
单例管理,共享主题状态
|
111
|
+
:param args:
|
112
|
+
:param kwargs:
|
113
|
+
"""
|
114
|
+
if cls._instance is None:
|
115
|
+
cls._instance = super().__new__(cls)
|
116
|
+
return cls._instance
|
117
|
+
|
118
|
+
def __init__(self):
|
119
|
+
if hasattr(self, "_initialized") and self._initialized:
|
120
|
+
return
|
121
|
+
self._initialized = True
|
122
|
+
super().__init__()
|
123
|
+
self.theme_dict = {
|
124
|
+
"Light": 0,
|
125
|
+
"Dark": 1
|
126
|
+
}
|
127
|
+
|
128
|
+
self.listener = None # 监听线程
|
129
|
+
self.current_theme = DEFAULT_CONFIG["theme"]["current_theme"] # 当前主题
|
130
|
+
self.is_darkdetect_supported = check_darkdetect_support()
|
131
|
+
|
132
|
+
try:
|
133
|
+
self.current_theme = RinConfig["theme"]["current_theme"]
|
134
|
+
except Exception as e:
|
135
|
+
print(f"Failed to load config because of {e}, using default config")
|
136
|
+
|
137
|
+
# self.hwnd = None # 窗口句柄
|
138
|
+
|
139
|
+
self.start_listener()
|
140
|
+
|
141
|
+
def start_listener(self):
|
142
|
+
if not self.is_darkdetect_supported:
|
143
|
+
print("darkdetect not supported on this platform")
|
144
|
+
return
|
145
|
+
self.listener = ThemeListener()
|
146
|
+
self.listener.themeChanged.connect(self._handle_system_theme)
|
147
|
+
self.listener.start()
|
148
|
+
|
149
|
+
def set_window(self, window): # 绑定窗口句柄
|
150
|
+
hwnd = int(window.winId())
|
151
|
+
self.windows.append(hwnd)
|
152
|
+
print(f"Window handle set: {hwnd}")
|
153
|
+
|
154
|
+
def _handle_system_theme(self):
|
155
|
+
if self.current_theme == "Auto":
|
156
|
+
self._update_window_theme()
|
157
|
+
self.themeChanged.emit(self._actual_theme())
|
158
|
+
else:
|
159
|
+
# 保持当前背景效果不变
|
160
|
+
self._update_window_theme()
|
161
|
+
|
162
|
+
@Slot(str)
|
163
|
+
def apply_backdrop_effect(self, effect_type: str):
|
164
|
+
"""
|
165
|
+
应用背景效果
|
166
|
+
:param effect_type: str, 背景效果类型(acrylic, mica, tabbed, none)
|
167
|
+
"""
|
168
|
+
self._update_window_theme()
|
169
|
+
if not is_windows() or not self.windows:
|
170
|
+
print(f"Cannot apply effect \"{effect_type}\" on this platform")
|
171
|
+
return -2 # 非 windows或未绑定窗口
|
172
|
+
self.backdropChanged.emit(effect_type)
|
173
|
+
|
174
|
+
accent_state = ACCENT_STATES.get(effect_type, 0)
|
175
|
+
if not ACCENT_SUPPORT.get(effect_type, False):
|
176
|
+
print(f"Effect \"{effect_type}\" not supported on this platform")
|
177
|
+
return -1 # 效果不支持
|
178
|
+
|
179
|
+
for hwnd in self.windows:
|
180
|
+
if is_win11():
|
181
|
+
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
182
|
+
hwnd,
|
183
|
+
self.DWMWA_SYSTEMBACKDROP_TYPE,
|
184
|
+
ctypes.byref(ctypes.c_int(accent_state)),
|
185
|
+
ctypes.sizeof(ctypes.c_int)
|
186
|
+
)
|
187
|
+
elif is_win10() and effect_type == BackdropEffect.Acrylic.value:
|
188
|
+
self._apply_win10_effect(effect_type, hwnd)
|
189
|
+
|
190
|
+
RinConfig["backdrop_effect"] = effect_type
|
191
|
+
print(
|
192
|
+
f"Applied \"{effect_type.strip().capitalize()}\" effect with "
|
193
|
+
f"{platform.system() + '11' if is_win11() else '10'}"
|
194
|
+
)
|
195
|
+
return 0 # 成功
|
196
|
+
|
197
|
+
def _apply_win10_effect(self, effect_type, hwnd):
|
198
|
+
"""
|
199
|
+
应用 Windows 10 背景效果
|
200
|
+
:param effect_type: str, 背景效果类型(acrylic, tabbed(actually blur)
|
201
|
+
"""
|
202
|
+
backdrop_color = RinConfig["win10_feat"]["backdrop_dark" if self.is_dark_theme() else "backdrop_light"]
|
203
|
+
|
204
|
+
accent = ACCENT_POLICY()
|
205
|
+
accent.AccentState = ACCENT_STATES[effect_type]
|
206
|
+
accent.AccentFlags = 2
|
207
|
+
accent.GradientColor = backdrop_color
|
208
|
+
data = WINDOWCOMPOSITIONATTRIBDATA()
|
209
|
+
data.Attrib = self.WCA_ACCENT_POLICY
|
210
|
+
data.pvData = ctypes.cast(ctypes.pointer(accent), ctypes.c_void_p)
|
211
|
+
data.cbData = ctypes.sizeof(accent)
|
212
|
+
|
213
|
+
try:
|
214
|
+
set_window_composition = ctypes.windll.user32.SetWindowCompositionAttribute
|
215
|
+
set_window_composition(hwnd, ctypes.byref(data))
|
216
|
+
except Exception as e:
|
217
|
+
print(f"Failed to apply acrylic on Win10: {e}")
|
218
|
+
|
219
|
+
def apply_window_effects(self): # 启用圆角阴影
|
220
|
+
if sys.platform != "win32" or not self.windows:
|
221
|
+
return
|
222
|
+
|
223
|
+
dwm = ctypes.windll.dwmapi
|
224
|
+
|
225
|
+
# 启用非客户端渲染策略(让窗口边框具备阴影)
|
226
|
+
ncrp = ctypes.c_int(self.DWMNCRENDERINGPOLICY_ENABLED)
|
227
|
+
for hwnd in self.windows:
|
228
|
+
dwm.DwmSetWindowAttribute(
|
229
|
+
hwnd,
|
230
|
+
self.DWMWA_NCRENDERING_POLICY,
|
231
|
+
ctypes.byref(ncrp),
|
232
|
+
ctypes.sizeof(ncrp)
|
233
|
+
)
|
234
|
+
|
235
|
+
# 启用圆角效果
|
236
|
+
corner_preference = ctypes.c_int(self.DWMWCP_ROUND)
|
237
|
+
dwm.DwmSetWindowAttribute(
|
238
|
+
hwnd,
|
239
|
+
self.DWMWA_WINDOW_CORNER_PREFERENCE,
|
240
|
+
ctypes.byref(corner_preference),
|
241
|
+
ctypes.sizeof(corner_preference)
|
242
|
+
)
|
243
|
+
print("Enabled Rounded and Shadows")
|
244
|
+
|
245
|
+
def _update_window_theme(self): # 更新窗口的颜色模式
|
246
|
+
if sys.platform != "win32" or not self.windows:
|
247
|
+
return
|
248
|
+
actual_theme = self._actual_theme()
|
249
|
+
for hwnd in self.windows:
|
250
|
+
if is_win11():
|
251
|
+
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
252
|
+
hwnd,
|
253
|
+
self.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
254
|
+
ctypes.byref(ctypes.c_int(self.theme_dict[actual_theme])),
|
255
|
+
ctypes.sizeof(ctypes.c_int)
|
256
|
+
)
|
257
|
+
elif is_win10() and RinConfig["backdrop_effect"] == BackdropEffect.Acrylic.value:
|
258
|
+
self._apply_win10_effect(RinConfig["backdrop_effect"], hwnd)
|
259
|
+
else:
|
260
|
+
print(f"Cannot apply backdrop on {platform.system()}")
|
261
|
+
|
262
|
+
print(f"Window theme updated to {actual_theme}")
|
263
|
+
|
264
|
+
def is_dark_theme(self):
|
265
|
+
"""是否为暗黑主题"""
|
266
|
+
return self._actual_theme() == "Dark"
|
267
|
+
|
268
|
+
def _actual_theme(self):
|
269
|
+
"""实际应用的主题"""
|
270
|
+
if self.current_theme == "Auto":
|
271
|
+
return darkdetect.theme() if self.is_darkdetect_supported else "Light"
|
272
|
+
return self.current_theme
|
273
|
+
|
274
|
+
@Slot(str)
|
275
|
+
def toggle_theme(self, theme: str): # 切换主题
|
276
|
+
if theme not in ["Auto", "Light", "Dark"]: # 三状态
|
277
|
+
return
|
278
|
+
if self.current_theme != theme:
|
279
|
+
print(f"Switching to '{theme}' theme")
|
280
|
+
self.current_theme = theme
|
281
|
+
RinConfig["theme"]["current_theme"] = theme
|
282
|
+
self._update_window_theme()
|
283
|
+
self.themeChanged.emit(self._actual_theme())
|
284
|
+
|
285
|
+
@Slot(result=str)
|
286
|
+
def get_theme(self):
|
287
|
+
return self._actual_theme()
|
288
|
+
|
289
|
+
@Slot(result=str)
|
290
|
+
def get_theme_name(self):
|
291
|
+
"""获取当前主题名称"""
|
292
|
+
return self.current_theme
|
293
|
+
|
294
|
+
@Slot(str)
|
295
|
+
def receive(self, message):
|
296
|
+
print(message)
|
297
|
+
|
298
|
+
@Slot(result=str)
|
299
|
+
def get_backdrop_effect(self):
|
300
|
+
"""获取当前背景效果"""
|
301
|
+
return RinConfig["backdrop_effect"]
|
302
|
+
|
303
|
+
@Slot(str)
|
304
|
+
def set_theme_color(self, color):
|
305
|
+
"""设置当前主题颜色"""
|
306
|
+
RinConfig["theme_color"] = color
|
307
|
+
RinConfig.save_config()
|
308
|
+
|
309
|
+
@Slot(result=str)
|
310
|
+
def get_theme_color(self):
|
311
|
+
"""获取当前主题颜色"""
|
312
|
+
return RinConfig["theme_color"]
|
313
|
+
|
314
|
+
@Slot(QObject, result=int)
|
315
|
+
def getWindowId(self, window):
|
316
|
+
"""获取窗口的句柄"""
|
317
|
+
print(f"GetWindowId: {window.winId()}")
|
318
|
+
return int(window.winId())
|
319
|
+
|
320
|
+
@Slot(int)
|
321
|
+
def dragWindowEvent(self, hwnd):
|
322
|
+
""" 在Windows 用原生方法拖动"""
|
323
|
+
if not is_windows() or hwnd not in self.windows:
|
324
|
+
print(
|
325
|
+
f"Use Qt method to drag window on: {platform.system()}"
|
326
|
+
if not is_windows() else f"Invalid window handle: {hwnd}"
|
327
|
+
)
|
328
|
+
return
|
329
|
+
|
330
|
+
import win32con
|
331
|
+
from win32gui import ReleaseCapture
|
332
|
+
from win32api import SendMessage
|
333
|
+
|
334
|
+
ReleaseCapture()
|
335
|
+
SendMessage(
|
336
|
+
hwnd,
|
337
|
+
win32con.WM_SYSCOMMAND,
|
338
|
+
win32con.SC_MOVE | win32con.HTCAPTION, 0
|
339
|
+
)
|