PyWebWinUI3 1.0.0__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.
- pywebwinui3/__init__.py +3 -0
- pywebwinui3/core.py +176 -0
- pywebwinui3/event.py +147 -0
- pywebwinui3/qt.py +887 -0
- pywebwinui3/type.py +362 -0
- pywebwinui3/util.py +194 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.0.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.1.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.10.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.11.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.12.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.13.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.14.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.15.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.16.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.17.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.18.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.19.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.2.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.20.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.21.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.22.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.23.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.24.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.25.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.26.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.27.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.28.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.29.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.3.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.30.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.31.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.32.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.33.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.34.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.35.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.36.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.37.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.38.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.39.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.4.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.40.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.41.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.42.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.43.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.44.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.45.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.46.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.47.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.48.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.49.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.5.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.50.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.51.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.52.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.53.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.54.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.55.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.56.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.57.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.58.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.59.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.6.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.60.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.61.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.62.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.63.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.64.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.65.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.66.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.67.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.68.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.69.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.7.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.70.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.71.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.72.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.73.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.74.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.75.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.76.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.77.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.78.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.79.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.8.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.80.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.81.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.82.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.83.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.84.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.85.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.86.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.87.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.88.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.89.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.9.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.90.woff2 +0 -0
- pywebwinui3/web/Pretendard/PretendardVariable.subset.91.woff2 +0 -0
- pywebwinui3/web/Pretendard/Variable-dynamic-subset.min.css +927 -0
- pywebwinui3/web/SegoeFluentIcons.ttf +0 -0
- pywebwinui3/web/_app/env.js +1 -0
- pywebwinui3/web/_app/immutable/assets/2.wYBxhgNe.css +1 -0
- pywebwinui3/web/_app/immutable/chunks/BeVEiAlX.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/C-ZTJ6AF.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/D2X8AmeL.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/DLxx5-LD.js +2 -0
- pywebwinui3/web/_app/immutable/chunks/DPxT1AeO.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/DZb1s-qq.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/DbLPBgK4.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/d_V4Mktp.js +1 -0
- pywebwinui3/web/_app/immutable/chunks/pGvP_QvR.js +1 -0
- pywebwinui3/web/_app/immutable/entry/app.DgyHcM8N.js +2 -0
- pywebwinui3/web/_app/immutable/entry/start.D7vHaDGu.js +1 -0
- pywebwinui3/web/_app/immutable/nodes/0.DO1mLoyu.js +54 -0
- pywebwinui3/web/_app/immutable/nodes/1.DvyFB-Wy.js +1 -0
- pywebwinui3/web/_app/immutable/nodes/2.CrF9bZZd.js +94 -0
- pywebwinui3/web/_app/version.json +1 -0
- pywebwinui3/web/index.html +101 -0
- pywebwinui3-1.0.0.dist-info/METADATA +92 -0
- pywebwinui3-1.0.0.dist-info/RECORD +124 -0
- pywebwinui3-1.0.0.dist-info/WHEEL +5 -0
- pywebwinui3-1.0.0.dist-info/licenses/LICENSE +201 -0
- pywebwinui3-1.0.0.dist-info/licenses/NOTICE +13 -0
- pywebwinui3-1.0.0.dist-info/top_level.txt +1 -0
pywebwinui3/qt.py
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from ctypes import wintypes
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import QEvent, QObject, QPoint, QRect, Qt, QTimer, QUrl, Signal, Slot
|
|
13
|
+
from PySide6.QtGui import QColor, QCursor, QDesktopServices, QIcon, QKeySequence, QShortcut
|
|
14
|
+
from PySide6.QtWebChannel import QWebChannel
|
|
15
|
+
from PySide6.QtWebEngineCore import QWebEngineContextMenuRequest, QWebEnginePage, QWebEngineSettings
|
|
16
|
+
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
17
|
+
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
|
|
18
|
+
import win32api
|
|
19
|
+
import win32con
|
|
20
|
+
import win32gui
|
|
21
|
+
import win32gui_struct
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("pywebwinui3.qt")
|
|
24
|
+
|
|
25
|
+
_uxtheme = ctypes.windll.uxtheme
|
|
26
|
+
_get_proc_address = ctypes.windll.kernel32.GetProcAddress
|
|
27
|
+
_get_proc_address.restype = ctypes.c_void_p
|
|
28
|
+
_get_proc_address.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_ordinal_proc(module, ordinal: int, restype, argtypes):
|
|
32
|
+
address = _get_proc_address(module._handle, ctypes.c_void_p(ordinal))
|
|
33
|
+
if not address:
|
|
34
|
+
return None
|
|
35
|
+
return ctypes.WINFUNCTYPE(restype, *argtypes)(address)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_set_preferred_app_mode = _get_ordinal_proc(_uxtheme, 135, ctypes.c_int, [ctypes.c_int])
|
|
39
|
+
_flush_menu_themes = _get_ordinal_proc(_uxtheme, 136, None, [])
|
|
40
|
+
_allow_dark_mode_for_window = _get_ordinal_proc(_uxtheme, 133, wintypes.BOOL, [wintypes.HWND, wintypes.BOOL])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _apply_native_menu_theme(dark: bool, hwnd: int | None = None):
|
|
44
|
+
if _set_preferred_app_mode is not None:
|
|
45
|
+
_set_preferred_app_mode(2 if dark else 3)
|
|
46
|
+
if hwnd is not None and _allow_dark_mode_for_window is not None:
|
|
47
|
+
_allow_dark_mode_for_window(hwnd, bool(dark))
|
|
48
|
+
if _flush_menu_themes is not None:
|
|
49
|
+
_flush_menu_themes()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def open_url(url: str | QUrl) -> bool:
|
|
53
|
+
if (raw := url if isinstance(url, str) else url.toString()):
|
|
54
|
+
if (parsed:=QUrl(raw)).isValid() and QDesktopServices.openUrl(parsed):
|
|
55
|
+
return True
|
|
56
|
+
try:
|
|
57
|
+
os.startfile(raw)
|
|
58
|
+
return True
|
|
59
|
+
except OSError:
|
|
60
|
+
logger.debug("Fallback shell open failed for %s", raw, exc_info=True)
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
class MARGINS(ctypes.Structure):
|
|
64
|
+
_fields_ = [
|
|
65
|
+
("cxLeftWidth", ctypes.c_int),
|
|
66
|
+
("cxRightWidth", ctypes.c_int),
|
|
67
|
+
("cyTopHeight", ctypes.c_int),
|
|
68
|
+
("cyBottomHeight", ctypes.c_int),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MSG(ctypes.Structure):
|
|
73
|
+
_fields_ = [
|
|
74
|
+
("hwnd", wintypes.HWND),
|
|
75
|
+
("message", wintypes.UINT),
|
|
76
|
+
("wParam", wintypes.WPARAM),
|
|
77
|
+
("lParam", wintypes.LPARAM),
|
|
78
|
+
("time", wintypes.DWORD),
|
|
79
|
+
("pt", wintypes.POINT),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
def _read_resize_border_thickness():
|
|
83
|
+
return (
|
|
84
|
+
win32api.GetSystemMetrics(win32con.SM_CXSIZEFRAME)
|
|
85
|
+
+ win32api.GetSystemMetrics(92)
|
|
86
|
+
) / 2
|
|
87
|
+
|
|
88
|
+
class ExternalAwarePage(QWebEnginePage):
|
|
89
|
+
def _is_same_document_fragment(self, url: QUrl) -> bool:
|
|
90
|
+
current = self.url()
|
|
91
|
+
return (
|
|
92
|
+
bool(url.fragment())
|
|
93
|
+
and current.isValid()
|
|
94
|
+
and url.scheme() == current.scheme()
|
|
95
|
+
and url.host() == current.host()
|
|
96
|
+
and url.port() == current.port()
|
|
97
|
+
and url.path() == current.path()
|
|
98
|
+
and url.query() == current.query()
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def acceptNavigationRequest(self, url, navigation_type, ismain_frame):
|
|
102
|
+
if (
|
|
103
|
+
ismain_frame
|
|
104
|
+
and navigation_type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked
|
|
105
|
+
and url.scheme() not in {"about", "data", "file", "qrc"}
|
|
106
|
+
):
|
|
107
|
+
if self._is_same_document_fragment(url):
|
|
108
|
+
return super().acceptNavigationRequest(url, navigation_type, ismain_frame)
|
|
109
|
+
open_url(url)
|
|
110
|
+
return False
|
|
111
|
+
return super().acceptNavigationRequest(url, navigation_type, ismain_frame)
|
|
112
|
+
|
|
113
|
+
def createWindow(self, _):
|
|
114
|
+
page = QWebEnginePage(self.profile(), self)
|
|
115
|
+
|
|
116
|
+
def open_and_cleanup(url):
|
|
117
|
+
open_url(url)
|
|
118
|
+
page.deleteLater()
|
|
119
|
+
|
|
120
|
+
page.urlChanged.connect(open_and_cleanup)
|
|
121
|
+
return page
|
|
122
|
+
|
|
123
|
+
class FramelessWindow(QMainWindow):
|
|
124
|
+
closed = Signal()
|
|
125
|
+
|
|
126
|
+
EDGE_MAP = [
|
|
127
|
+
"left",
|
|
128
|
+
"right",
|
|
129
|
+
"top",
|
|
130
|
+
"bottom",
|
|
131
|
+
"top-left",
|
|
132
|
+
"top-right",
|
|
133
|
+
"bottom-left",
|
|
134
|
+
"bottom-right",
|
|
135
|
+
]
|
|
136
|
+
CURSOR_MAP = {
|
|
137
|
+
"left": Qt.CursorShape.SizeHorCursor,
|
|
138
|
+
"right": Qt.CursorShape.SizeHorCursor,
|
|
139
|
+
"top": Qt.CursorShape.SizeVerCursor,
|
|
140
|
+
"bottom": Qt.CursorShape.SizeVerCursor,
|
|
141
|
+
"top-left": Qt.CursorShape.SizeFDiagCursor,
|
|
142
|
+
"bottom-right": Qt.CursorShape.SizeFDiagCursor,
|
|
143
|
+
"top-right": Qt.CursorShape.SizeBDiagCursor,
|
|
144
|
+
"bottom-left": Qt.CursorShape.SizeBDiagCursor,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def __init__(self, api, page_path: Path, title: str, icon_path: Path | None, debug: bool = False):
|
|
148
|
+
super().__init__()
|
|
149
|
+
self.api = api
|
|
150
|
+
self.page_path = page_path
|
|
151
|
+
self._native_frame_ready = False
|
|
152
|
+
self._resize_edge_name: str | None = None
|
|
153
|
+
self._resize_origin: QPoint | None = None
|
|
154
|
+
self._resize_geometry: QRect | None = None
|
|
155
|
+
self._active_resize_cursor = None
|
|
156
|
+
self._resize_border = _read_resize_border_thickness()
|
|
157
|
+
self._debug_tools_view: QWebEngineView | None = None
|
|
158
|
+
self._debug_tools_page: QWebEnginePage | None = None
|
|
159
|
+
|
|
160
|
+
self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
|
|
161
|
+
self.setWindowTitle(title)
|
|
162
|
+
self.resize(self.api.main.values.get("system_window_width"), self.api.main.values.get("system_window_height"))
|
|
163
|
+
self.setMouseTracking(True)
|
|
164
|
+
|
|
165
|
+
if icon_path and icon_path.exists():
|
|
166
|
+
self.setWindowIcon(QIcon(str(icon_path)))
|
|
167
|
+
|
|
168
|
+
self.view = QWebEngineView(self)
|
|
169
|
+
self.page = ExternalAwarePage(self.view)
|
|
170
|
+
self._apply_window_background()
|
|
171
|
+
|
|
172
|
+
settings = self.page.settings()
|
|
173
|
+
settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
|
|
174
|
+
settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True)
|
|
175
|
+
settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
|
|
176
|
+
settings.setAttribute(QWebEngineSettings.WebAttribute.Accelerated2dCanvasEnabled, True)
|
|
177
|
+
settings.setAttribute(QWebEngineSettings.WebAttribute.WebGLEnabled, True)
|
|
178
|
+
|
|
179
|
+
self.channel = QWebChannel(self.page)
|
|
180
|
+
self.channel.registerObject("backend", api)
|
|
181
|
+
self.page.setWebChannel(self.channel)
|
|
182
|
+
|
|
183
|
+
self.view.setPage(self.page)
|
|
184
|
+
self.setCentralWidget(self.view)
|
|
185
|
+
self.view.setMouseTracking(True)
|
|
186
|
+
self.view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
187
|
+
self.view.customContextMenuRequested.connect(self._show_context_menu)
|
|
188
|
+
self.page.loadFinished.connect(self._handle_load_finished)
|
|
189
|
+
QApplication.instance().installEventFilter(self)
|
|
190
|
+
|
|
191
|
+
if not self.page_path.is_file():
|
|
192
|
+
raise FileNotFoundError(f"Frontend entry not found: {self.page_path}")
|
|
193
|
+
|
|
194
|
+
if debug:
|
|
195
|
+
debug_shortcut = QShortcut(QKeySequence("F12"), self)
|
|
196
|
+
debug_shortcut.activated.connect(self.toggle_devtools)
|
|
197
|
+
inspector_shortcut = QShortcut(QKeySequence("Ctrl+Shift+I"), self)
|
|
198
|
+
inspector_shortcut.activated.connect(self.toggle_devtools)
|
|
199
|
+
self._debug_shortcuts = (debug_shortcut, inspector_shortcut)
|
|
200
|
+
|
|
201
|
+
self.view.setUrl(QUrl.fromLocalFile(str(self.page_path)))
|
|
202
|
+
|
|
203
|
+
def showEvent(self, event):
|
|
204
|
+
super().showEvent(event)
|
|
205
|
+
self._refresh_resize_border()
|
|
206
|
+
self._ensure_native_frame()
|
|
207
|
+
|
|
208
|
+
def resizeEvent(self, event):
|
|
209
|
+
super().resizeEvent(event)
|
|
210
|
+
self.api.main.values.set("system_window_width", self.width(), False)
|
|
211
|
+
self.api.main.values.set("system_window_height", self.height(), False)
|
|
212
|
+
|
|
213
|
+
def nativeEvent(self, event_type, message):
|
|
214
|
+
try:
|
|
215
|
+
msg = MSG.from_address(int(message))
|
|
216
|
+
except (TypeError, ValueError, OSError):
|
|
217
|
+
return super().nativeEvent(event_type, message)
|
|
218
|
+
|
|
219
|
+
if msg.message == win32con.WM_SETTINGCHANGE:
|
|
220
|
+
self.api.main.accent.refresh()
|
|
221
|
+
|
|
222
|
+
return super().nativeEvent(event_type, message)
|
|
223
|
+
|
|
224
|
+
def closeEvent(self, event):
|
|
225
|
+
app = QApplication.instance()
|
|
226
|
+
if app is not None:
|
|
227
|
+
app.removeEventFilter(self)
|
|
228
|
+
self._set_resize_cursor(None)
|
|
229
|
+
self.hide()
|
|
230
|
+
if self._debug_tools_view is not None:
|
|
231
|
+
self._debug_tools_view.close()
|
|
232
|
+
QTimer.singleShot(0, self.closed.emit)
|
|
233
|
+
super().closeEvent(event)
|
|
234
|
+
|
|
235
|
+
def _maximize_native(self):
|
|
236
|
+
self._ensure_native_frame()
|
|
237
|
+
win32gui.ShowWindow(self._hwnd(), win32con.SW_MAXIMIZE)
|
|
238
|
+
|
|
239
|
+
def _restore_native(self):
|
|
240
|
+
self._ensure_native_frame()
|
|
241
|
+
win32gui.ShowWindow(self._hwnd(), win32con.SW_RESTORE)
|
|
242
|
+
|
|
243
|
+
def _start_native_move(self):
|
|
244
|
+
handle = self.windowHandle()
|
|
245
|
+
if handle is not None and handle.startSystemMove():
|
|
246
|
+
return
|
|
247
|
+
win32gui.ReleaseCapture()
|
|
248
|
+
win32api.SendMessage(self._hwnd(), win32con.WM_NCLBUTTONDOWN, win32con.HTCAPTION, 0)
|
|
249
|
+
|
|
250
|
+
def _hwnd(self) -> int:
|
|
251
|
+
return int(self.winId())
|
|
252
|
+
|
|
253
|
+
def _ensure_native_frame(self):
|
|
254
|
+
if self._native_frame_ready:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
hwnd = self._hwnd()
|
|
258
|
+
style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
|
|
259
|
+
style |= (
|
|
260
|
+
win32con.WS_THICKFRAME
|
|
261
|
+
| win32con.WS_MINIMIZEBOX
|
|
262
|
+
| win32con.WS_MAXIMIZEBOX
|
|
263
|
+
| win32con.WS_SYSMENU
|
|
264
|
+
)
|
|
265
|
+
win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, style)
|
|
266
|
+
|
|
267
|
+
ex_style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
|
|
268
|
+
ex_style |= win32con.WS_EX_APPWINDOW
|
|
269
|
+
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, ex_style)
|
|
270
|
+
|
|
271
|
+
if sys.getwindowsversion().build >= 22000:
|
|
272
|
+
dark_mode = wintypes.BOOL(self._resolved_theme() == "dark")
|
|
273
|
+
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
274
|
+
hwnd,
|
|
275
|
+
20,
|
|
276
|
+
ctypes.byref(dark_mode),
|
|
277
|
+
ctypes.sizeof(dark_mode),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
disable_transitions = wintypes.BOOL(True)
|
|
281
|
+
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
282
|
+
hwnd,
|
|
283
|
+
3,
|
|
284
|
+
ctypes.byref(disable_transitions),
|
|
285
|
+
ctypes.sizeof(disable_transitions),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
corners = ctypes.c_int(2)
|
|
289
|
+
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
|
290
|
+
hwnd,
|
|
291
|
+
33,
|
|
292
|
+
ctypes.byref(corners),
|
|
293
|
+
ctypes.sizeof(corners),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
margins = MARGINS(1, 1, 1, 1)
|
|
297
|
+
ctypes.windll.dwmapi.DwmExtendFrameIntoClientArea(hwnd, ctypes.byref(margins))
|
|
298
|
+
|
|
299
|
+
win32gui.SetWindowPos(
|
|
300
|
+
hwnd,
|
|
301
|
+
0,
|
|
302
|
+
0,
|
|
303
|
+
0,
|
|
304
|
+
0,
|
|
305
|
+
0,
|
|
306
|
+
win32con.SWP_NOMOVE
|
|
307
|
+
| win32con.SWP_NOSIZE
|
|
308
|
+
| win32con.SWP_NOZORDER
|
|
309
|
+
| win32con.SWP_NOACTIVATE
|
|
310
|
+
| win32con.SWP_FRAMECHANGED,
|
|
311
|
+
)
|
|
312
|
+
_apply_native_menu_theme(self._resolved_theme() == "dark", hwnd)
|
|
313
|
+
self._native_frame_ready = True
|
|
314
|
+
|
|
315
|
+
def _refresh_resize_border(self):
|
|
316
|
+
self._resize_border = _read_resize_border_thickness()
|
|
317
|
+
|
|
318
|
+
def _resolved_theme(self) -> str:
|
|
319
|
+
values = self.api.main.values
|
|
320
|
+
theme = values.get("system_theme") or "system"
|
|
321
|
+
if theme == "system":
|
|
322
|
+
theme = values.get("system_theme_resolved") or "dark"
|
|
323
|
+
return theme if theme in {"light", "dark"} else "dark"
|
|
324
|
+
|
|
325
|
+
def _apply_window_background(self):
|
|
326
|
+
self.page.setBackgroundColor(QColor("#f3f3f3"if self._resolved_theme() == "light" else "#202020"))
|
|
327
|
+
|
|
328
|
+
def _handle_load_finished(self, _ok: bool):
|
|
329
|
+
self._apply_window_background()
|
|
330
|
+
|
|
331
|
+
def _sync_view_geometry(self):
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
def _show_context_menu(self, pos: QPoint):
|
|
335
|
+
request = self.view.lastContextMenuRequest()
|
|
336
|
+
if request is not None and request.isContentEditable():
|
|
337
|
+
edit_flags = request.editFlags()
|
|
338
|
+
self._exec_native_menu(
|
|
339
|
+
self.view.mapToGlobal(pos),
|
|
340
|
+
[
|
|
341
|
+
(0x5000, "Undo", bool(edit_flags & QWebEngineContextMenuRequest.EditFlag.CanUndo), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Undo)),
|
|
342
|
+
(0x5001, "Redo", bool(edit_flags & QWebEngineContextMenuRequest.EditFlag.CanRedo), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Redo)),
|
|
343
|
+
None,
|
|
344
|
+
(0x5002, "Cut", bool(edit_flags & QWebEngineContextMenuRequest.EditFlag.CanCut), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Cut)),
|
|
345
|
+
(0x5013, "Copy", bool(edit_flags & QWebEngineContextMenuRequest.EditFlag.CanCopy), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Copy)),
|
|
346
|
+
(0x5014, "Paste", bool(edit_flags & QWebEngineContextMenuRequest.EditFlag.CanPaste), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Paste)),
|
|
347
|
+
None,
|
|
348
|
+
(0x5015, "Select All", bool(edit_flags & QWebEngineContextMenuRequest.EditFlag.CanSelectAll), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.SelectAll)),
|
|
349
|
+
],
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
self._exec_native_menu(
|
|
354
|
+
self.view.mapToGlobal(pos),
|
|
355
|
+
[
|
|
356
|
+
(0x5010, "Back", self.page.action(QWebEnginePage.WebAction.Back).isEnabled(), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Back)),
|
|
357
|
+
(0x5011, "Forward", self.page.action(QWebEnginePage.WebAction.Forward).isEnabled(), False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Forward)),
|
|
358
|
+
(0x5012, "Reload", True, False, lambda: self.page.triggerAction(QWebEnginePage.WebAction.Reload)),
|
|
359
|
+
],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def _exec_native_menu(self, global_pos: QPoint, items):
|
|
363
|
+
menu = win32gui.CreatePopupMenu()
|
|
364
|
+
callbacks = {}
|
|
365
|
+
try:
|
|
366
|
+
win32gui.SetMenuInfo(menu, win32gui_struct.PackMENUINFO(dwStyle=win32con.MNS_CHECKORBMP))
|
|
367
|
+
for item in items:
|
|
368
|
+
if item is None:
|
|
369
|
+
win32gui.AppendMenu(menu, win32con.MF_SEPARATOR, 0, "")
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
command_id, label, enabled, checked, callback, *extra = item
|
|
373
|
+
bitmap = extra[0] if extra else None
|
|
374
|
+
flags = win32con.MF_STRING
|
|
375
|
+
if not enabled:
|
|
376
|
+
flags |= win32con.MF_GRAYED
|
|
377
|
+
if checked:
|
|
378
|
+
flags |= win32con.MF_CHECKED
|
|
379
|
+
win32gui.AppendMenu(menu, flags, command_id, label)
|
|
380
|
+
if bitmap is not None:
|
|
381
|
+
packed, _extras = win32gui_struct.PackMENUITEMINFO(hbmpItem=bitmap)
|
|
382
|
+
win32gui.SetMenuItemInfo(menu, command_id, False, packed)
|
|
383
|
+
callbacks[command_id] = callback
|
|
384
|
+
|
|
385
|
+
hwnd = self._hwnd()
|
|
386
|
+
win32gui.SetForegroundWindow(hwnd)
|
|
387
|
+
command = win32gui.TrackPopupMenu(
|
|
388
|
+
menu,
|
|
389
|
+
win32con.TPM_RETURNCMD | win32con.TPM_NONOTIFY | win32con.TPM_RIGHTBUTTON,
|
|
390
|
+
global_pos.x(),
|
|
391
|
+
global_pos.y(),
|
|
392
|
+
0,
|
|
393
|
+
hwnd,
|
|
394
|
+
None,
|
|
395
|
+
)
|
|
396
|
+
win32gui.PostMessage(hwnd, win32con.WM_NULL, 0, 0)
|
|
397
|
+
|
|
398
|
+
callback = callbacks.get(command)
|
|
399
|
+
if command and callback is not None:
|
|
400
|
+
callback()
|
|
401
|
+
finally:
|
|
402
|
+
win32gui.DestroyMenu(menu)
|
|
403
|
+
|
|
404
|
+
def _open_devtools(self):
|
|
405
|
+
if self._debug_tools_view is None:
|
|
406
|
+
self._debug_tools_view = QWebEngineView()
|
|
407
|
+
self._debug_tools_page = QWebEnginePage(self.page.profile(), self._debug_tools_view)
|
|
408
|
+
self._debug_tools_view.setPage(self._debug_tools_page)
|
|
409
|
+
self.page.setDevToolsPage(self._debug_tools_page)
|
|
410
|
+
self._debug_tools_view.setWindowTitle(f"{self.windowTitle()} DevTools")
|
|
411
|
+
self._debug_tools_view.resize(600, 800)
|
|
412
|
+
|
|
413
|
+
self._debug_tools_view.show()
|
|
414
|
+
self._debug_tools_view.raise_()
|
|
415
|
+
self._debug_tools_view.activateWindow()
|
|
416
|
+
|
|
417
|
+
@Slot()
|
|
418
|
+
def toggle_devtools(self):
|
|
419
|
+
if self._debug_tools_view is None or not self._debug_tools_view.isVisible():
|
|
420
|
+
self._open_devtools()
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
self._debug_tools_view.hide()
|
|
424
|
+
|
|
425
|
+
def _belongs_to_window(self, watched) -> bool:
|
|
426
|
+
if watched in {self, self.view}:
|
|
427
|
+
return True
|
|
428
|
+
if isinstance(watched, QWidget):
|
|
429
|
+
return watched.window() is self
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
def _is_native_maximized(self) -> bool:
|
|
433
|
+
try:
|
|
434
|
+
return bool(win32gui.IsZoomed(self._hwnd()))
|
|
435
|
+
except Exception:
|
|
436
|
+
return self.isMaximized()
|
|
437
|
+
|
|
438
|
+
def _is_effectively_maximized(self) -> bool:
|
|
439
|
+
state = self.windowState()
|
|
440
|
+
return bool(state & Qt.WindowState.WindowMaximized) or self._is_native_maximized()
|
|
441
|
+
|
|
442
|
+
def _edge_from_pos(self, pos: QPoint, size) -> str | None:
|
|
443
|
+
if self._is_effectively_maximized():
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
border = self._resize_border
|
|
447
|
+
on_left = 0 <= pos.x() < border
|
|
448
|
+
on_right = size.width() - border <= pos.x() < size.width()
|
|
449
|
+
on_top = 0 <= pos.y() < border
|
|
450
|
+
on_bottom = size.height() - border <= pos.y() < size.height()
|
|
451
|
+
|
|
452
|
+
if on_top and on_left:
|
|
453
|
+
return "top-left"
|
|
454
|
+
if on_top and on_right:
|
|
455
|
+
return "top-right"
|
|
456
|
+
if on_bottom and on_left:
|
|
457
|
+
return "bottom-left"
|
|
458
|
+
if on_bottom and on_right:
|
|
459
|
+
return "bottom-right"
|
|
460
|
+
if on_left:
|
|
461
|
+
return "left"
|
|
462
|
+
if on_right:
|
|
463
|
+
return "right"
|
|
464
|
+
if on_top:
|
|
465
|
+
return "top"
|
|
466
|
+
if on_bottom:
|
|
467
|
+
return "bottom"
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
def _set_resize_cursor(self, edge_name: str | None):
|
|
471
|
+
cursor = self.CURSOR_MAP.get(edge_name)
|
|
472
|
+
if cursor == self._active_resize_cursor:
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
app = QApplication.instance()
|
|
476
|
+
if app is None:
|
|
477
|
+
self._active_resize_cursor = cursor
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
if cursor is None:
|
|
481
|
+
if self._active_resize_cursor is not None:
|
|
482
|
+
app.restoreOverrideCursor()
|
|
483
|
+
self._active_resize_cursor = None
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
qcursor = QCursor(cursor)
|
|
487
|
+
if self._active_resize_cursor is None:
|
|
488
|
+
app.setOverrideCursor(qcursor)
|
|
489
|
+
else:
|
|
490
|
+
app.changeOverrideCursor(qcursor)
|
|
491
|
+
self._active_resize_cursor = cursor
|
|
492
|
+
|
|
493
|
+
def _begin_resize(self, edge_name: str, global_pos: QPoint):
|
|
494
|
+
self._refresh_resize_border()
|
|
495
|
+
self._resize_edge_name = edge_name
|
|
496
|
+
self._resize_origin = QPoint(global_pos)
|
|
497
|
+
self._resize_geometry = QRect(self.geometry())
|
|
498
|
+
self._set_resize_cursor(edge_name)
|
|
499
|
+
self.grabMouse()
|
|
500
|
+
|
|
501
|
+
def _apply_resize(self, global_pos: QPoint):
|
|
502
|
+
if self._resize_edge_name is None or self._resize_origin is None or self._resize_geometry is None:
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
self._set_resize_cursor(self._resize_edge_name)
|
|
506
|
+
dx = global_pos.x() - self._resize_origin.x()
|
|
507
|
+
dy = global_pos.y() - self._resize_origin.y()
|
|
508
|
+
geometry = QRect(self._resize_geometry)
|
|
509
|
+
minimum_width = max(self.minimumWidth(), 1)
|
|
510
|
+
minimum_height = max(self.minimumHeight(), 1)
|
|
511
|
+
|
|
512
|
+
if "left" in self._resize_edge_name:
|
|
513
|
+
max_left = geometry.right() - minimum_width + 1
|
|
514
|
+
geometry.setLeft(min(geometry.left() + dx, max_left))
|
|
515
|
+
if "right" in self._resize_edge_name:
|
|
516
|
+
min_right = geometry.left() + minimum_width - 1
|
|
517
|
+
geometry.setRight(max(geometry.right() + dx, min_right))
|
|
518
|
+
if "top" in self._resize_edge_name:
|
|
519
|
+
max_top = geometry.bottom() - minimum_height + 1
|
|
520
|
+
geometry.setTop(min(geometry.top() + dy, max_top))
|
|
521
|
+
if "bottom" in self._resize_edge_name:
|
|
522
|
+
min_bottom = geometry.top() + minimum_height - 1
|
|
523
|
+
geometry.setBottom(max(geometry.bottom() + dy, min_bottom))
|
|
524
|
+
|
|
525
|
+
if self.geometry() != geometry:
|
|
526
|
+
self.setGeometry(geometry)
|
|
527
|
+
|
|
528
|
+
def _end_resize(self):
|
|
529
|
+
if self._resize_edge_name is None:
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
self._resize_edge_name = None
|
|
533
|
+
self._resize_origin = None
|
|
534
|
+
self._resize_geometry = None
|
|
535
|
+
self.releaseMouse()
|
|
536
|
+
self._set_resize_cursor(None)
|
|
537
|
+
|
|
538
|
+
def eventFilter(self, watched:QObject, event:QEvent):
|
|
539
|
+
if event.type() not in {
|
|
540
|
+
QEvent.Type.MouseButtonPress,
|
|
541
|
+
QEvent.Type.MouseButtonRelease,
|
|
542
|
+
QEvent.Type.MouseMove,
|
|
543
|
+
}:
|
|
544
|
+
return super().eventFilter(watched, event)
|
|
545
|
+
|
|
546
|
+
if not hasattr(event, "globalPosition"):
|
|
547
|
+
return super().eventFilter(watched, event)
|
|
548
|
+
|
|
549
|
+
if self._resize_edge_name is None and not self._belongs_to_window(watched):
|
|
550
|
+
self._set_resize_cursor(None)
|
|
551
|
+
return super().eventFilter(watched, event)
|
|
552
|
+
|
|
553
|
+
global_pos = event.globalPosition().toPoint()
|
|
554
|
+
local_pos = self.mapFromGlobal(global_pos)
|
|
555
|
+
|
|
556
|
+
if self._resize_edge_name is None and not self.rect().contains(local_pos):
|
|
557
|
+
self._set_resize_cursor(None)
|
|
558
|
+
return super().eventFilter(watched, event)
|
|
559
|
+
|
|
560
|
+
edge_name = self._edge_from_pos(local_pos, self.size())
|
|
561
|
+
|
|
562
|
+
if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton and edge_name:
|
|
563
|
+
self._begin_resize(edge_name, global_pos)
|
|
564
|
+
event.accept()
|
|
565
|
+
return True
|
|
566
|
+
|
|
567
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
568
|
+
if self._resize_edge_name is not None:
|
|
569
|
+
self._apply_resize(global_pos)
|
|
570
|
+
event.accept()
|
|
571
|
+
return True
|
|
572
|
+
|
|
573
|
+
self._set_resize_cursor(edge_name)
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
if event.type() == QEvent.Type.MouseButtonRelease and self._resize_edge_name is not None:
|
|
577
|
+
self._end_resize()
|
|
578
|
+
event.accept()
|
|
579
|
+
return True
|
|
580
|
+
|
|
581
|
+
return super().eventFilter(watched, event)
|
|
582
|
+
|
|
583
|
+
@Slot()
|
|
584
|
+
def minimize(self):
|
|
585
|
+
self.showMinimized()
|
|
586
|
+
|
|
587
|
+
@Slot()
|
|
588
|
+
def close_window(self):
|
|
589
|
+
self.close()
|
|
590
|
+
|
|
591
|
+
@Slot(bool)
|
|
592
|
+
def set_on_top(self, state: bool):
|
|
593
|
+
self._ensure_native_frame()
|
|
594
|
+
win32gui.SetWindowPos(
|
|
595
|
+
self._hwnd(),
|
|
596
|
+
win32con.HWND_TOPMOST if state else win32con.HWND_NOTOPMOST,
|
|
597
|
+
0,
|
|
598
|
+
0,
|
|
599
|
+
0,
|
|
600
|
+
0,
|
|
601
|
+
win32con.SWP_NOMOVE
|
|
602
|
+
| win32con.SWP_NOSIZE
|
|
603
|
+
| win32con.SWP_NOOWNERZORDER
|
|
604
|
+
| win32con.SWP_NOACTIVATE,
|
|
605
|
+
)
|
|
606
|
+
if state:
|
|
607
|
+
self.raise_()
|
|
608
|
+
|
|
609
|
+
@Slot()
|
|
610
|
+
def start_window_drag(self):
|
|
611
|
+
self._ensure_native_frame()
|
|
612
|
+
self._start_native_move()
|
|
613
|
+
|
|
614
|
+
@Slot(str)
|
|
615
|
+
def start_window_resize(self, edge_name: str):
|
|
616
|
+
if edge_name not in self.EDGE_MAP:
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
self._begin_resize(edge_name, QCursor.pos())
|
|
620
|
+
|
|
621
|
+
@Slot()
|
|
622
|
+
def toggle_maximize(self):
|
|
623
|
+
if self._is_effectively_maximized():
|
|
624
|
+
self._restore_native()
|
|
625
|
+
return
|
|
626
|
+
self._maximize_native()
|
|
627
|
+
|
|
628
|
+
@Slot()
|
|
629
|
+
def show_window_menu(self):
|
|
630
|
+
pinned = bool(self.api.main.values.get("system_pin"))
|
|
631
|
+
maximized = self._is_effectively_maximized()
|
|
632
|
+
self._exec_native_menu(
|
|
633
|
+
QCursor.pos(),
|
|
634
|
+
[
|
|
635
|
+
(0x5020, "Pin", True, pinned, lambda: self.api.main.pin(not pinned)),
|
|
636
|
+
(win32con.SC_MINIMIZE, "Minimize", True, False, self.minimize, win32con.HBMMENU_POPUP_MINIMIZE),
|
|
637
|
+
(
|
|
638
|
+
win32con.SC_RESTORE if maximized else win32con.SC_MAXIMIZE,
|
|
639
|
+
"Restore" if maximized else "Maximize",
|
|
640
|
+
True,
|
|
641
|
+
False,
|
|
642
|
+
self.toggle_maximize,
|
|
643
|
+
win32con.HBMMENU_POPUP_RESTORE if maximized else win32con.HBMMENU_POPUP_MAXIMIZE,
|
|
644
|
+
),
|
|
645
|
+
None,
|
|
646
|
+
(win32con.SC_CLOSE, "Close", True, False, self.close_window, win32con.HBMMENU_POPUP_CLOSE),
|
|
647
|
+
],
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
@Slot(int, int)
|
|
651
|
+
def apply_window_size(self, width: int, height: int):
|
|
652
|
+
width = max(self.minimumWidth(), int(width))
|
|
653
|
+
height = max(self.minimumHeight(), int(height))
|
|
654
|
+
|
|
655
|
+
if self._is_effectively_maximized():
|
|
656
|
+
self._restore_native()
|
|
657
|
+
|
|
658
|
+
if self.width() == width and self.height() == height:
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
self.resize(width, height)
|
|
662
|
+
|
|
663
|
+
def apply_minimum_window_size(self, width: int, height: int):
|
|
664
|
+
width = max(1, int(width))
|
|
665
|
+
height = max(1, int(height))
|
|
666
|
+
|
|
667
|
+
if self.minimumWidth() == width and self.minimumHeight() == height:
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
self.setMinimumSize(width, height)
|
|
671
|
+
if not self._is_effectively_maximized():
|
|
672
|
+
self.resize(max(self.width(), width), max(self.height(), height))
|
|
673
|
+
|
|
674
|
+
class WebviewAPI(QObject):
|
|
675
|
+
WINDOW_SIZE_KEYS = frozenset({"system_window_width", "system_window_height"})
|
|
676
|
+
|
|
677
|
+
dispatch_sync_requested = Signal(str, "QVariant")
|
|
678
|
+
close_requested = Signal()
|
|
679
|
+
minimize_requested = Signal()
|
|
680
|
+
set_on_top_requested = Signal(bool)
|
|
681
|
+
resize_window_requested = Signal(int, int)
|
|
682
|
+
start_drag_requested = Signal()
|
|
683
|
+
start_resize_requested = Signal(str)
|
|
684
|
+
toggle_maximize_requested = Signal()
|
|
685
|
+
show_window_menu_requested = Signal()
|
|
686
|
+
|
|
687
|
+
def __init__(self, main_window, title: str, icon: str | None):
|
|
688
|
+
super().__init__()
|
|
689
|
+
self.main = main_window
|
|
690
|
+
self._title = title
|
|
691
|
+
self._icon = icon
|
|
692
|
+
|
|
693
|
+
self._app = None
|
|
694
|
+
self._owns_app = False
|
|
695
|
+
self._window = None
|
|
696
|
+
self._frontend_ready = False
|
|
697
|
+
self._setup_fired = False
|
|
698
|
+
self._sync_lock = threading.Lock()
|
|
699
|
+
self._pending_sync: dict[str, object] = {}
|
|
700
|
+
self.dispatch_sync_requested.connect(self._dispatch_sync_value, Qt.ConnectionType.QueuedConnection)
|
|
701
|
+
|
|
702
|
+
def ensure_runtime(self, debug: bool = False):
|
|
703
|
+
if self._window is not None:
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
app = QApplication.instance()
|
|
707
|
+
if app is None:
|
|
708
|
+
QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL, True)
|
|
709
|
+
QApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts, True)
|
|
710
|
+
app = QApplication(sys.argv)
|
|
711
|
+
self._owns_app = True
|
|
712
|
+
|
|
713
|
+
self._app = app
|
|
714
|
+
resolved_theme = self.main.values.get("system_theme") or "system"
|
|
715
|
+
if resolved_theme == "system":
|
|
716
|
+
resolved_theme = self.main.values.get("system_theme_resolved") or "dark"
|
|
717
|
+
_apply_native_menu_theme(resolved_theme == "dark")
|
|
718
|
+
|
|
719
|
+
icon_path = self.main.resolve_path(self._icon)
|
|
720
|
+
title = self.main.values.get("system_title") or self._title
|
|
721
|
+
|
|
722
|
+
self._window = FramelessWindow(self, self.main.packagePath / "index.html", title, icon_path, debug=debug)
|
|
723
|
+
self._window.closed.connect(self.main.events.closed.set)
|
|
724
|
+
self.close_requested.connect(self._window.close_window)
|
|
725
|
+
self.minimize_requested.connect(self._window.minimize)
|
|
726
|
+
self.set_on_top_requested.connect(self._window.set_on_top)
|
|
727
|
+
self.resize_window_requested.connect(self._window.apply_window_size)
|
|
728
|
+
self.start_drag_requested.connect(self._window.start_window_drag)
|
|
729
|
+
self.start_resize_requested.connect(self._window.start_window_resize)
|
|
730
|
+
self.toggle_maximize_requested.connect(self._window.toggle_maximize)
|
|
731
|
+
self.show_window_menu_requested.connect(self._window.show_window_menu)
|
|
732
|
+
|
|
733
|
+
# self.main.sync_window_size(self._window.width(), self._window.height(), sync=False)
|
|
734
|
+
self.main.values.set("system_window_width", self._window.width(), False)
|
|
735
|
+
self.main.values.set("system_window_height", self._window.height(), False)
|
|
736
|
+
|
|
737
|
+
if self.main.values.get("system_pin"):
|
|
738
|
+
self._window.set_on_top(True)
|
|
739
|
+
|
|
740
|
+
self._window.show()
|
|
741
|
+
|
|
742
|
+
logger.debug("Window created")
|
|
743
|
+
|
|
744
|
+
def start(self, debug: bool = False):
|
|
745
|
+
self.ensure_runtime(debug=debug)
|
|
746
|
+
if self._owns_app:
|
|
747
|
+
self._app.exec()
|
|
748
|
+
|
|
749
|
+
def set_on_top(self, state: bool):
|
|
750
|
+
self.ensure_runtime()
|
|
751
|
+
self.set_on_top_requested.emit(state)
|
|
752
|
+
|
|
753
|
+
def set_window_minimum_size(self, width: int, height: int):
|
|
754
|
+
self.ensure_runtime()
|
|
755
|
+
if self._window is not None:
|
|
756
|
+
self._window.apply_minimum_window_size(width, height)
|
|
757
|
+
|
|
758
|
+
def queue_sync_value(self, key: str, value):
|
|
759
|
+
if key in self.WINDOW_SIZE_KEYS:
|
|
760
|
+
width, height = self.main.sync_window_size(
|
|
761
|
+
value if key == "system_window_width" else self.main.values.get("system_window_width"),
|
|
762
|
+
value if key == "system_window_height" else self.main.values.get("system_window_height"),
|
|
763
|
+
False,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if self._window is not None:
|
|
767
|
+
self.resize_window_requested.emit(width, height)
|
|
768
|
+
|
|
769
|
+
self._dispatch_or_defer_sync("system_window_width", width)
|
|
770
|
+
self._dispatch_or_defer_sync("system_window_height", height)
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
self._dispatch_or_defer_sync(key, value)
|
|
774
|
+
|
|
775
|
+
def _dispatch_or_defer_sync(self, key: str, value):
|
|
776
|
+
with self._sync_lock:
|
|
777
|
+
if not self._frontend_ready or self._window is None:
|
|
778
|
+
self._pending_sync[key] = value
|
|
779
|
+
return
|
|
780
|
+
|
|
781
|
+
self.dispatch_sync_requested.emit(key, value)
|
|
782
|
+
|
|
783
|
+
@Slot(result="QVariant")
|
|
784
|
+
def init(self):
|
|
785
|
+
return dict(self.main.values)
|
|
786
|
+
|
|
787
|
+
@Slot()
|
|
788
|
+
def frontendReady(self):
|
|
789
|
+
if self._frontend_ready:
|
|
790
|
+
return
|
|
791
|
+
|
|
792
|
+
self._frontend_ready = True
|
|
793
|
+
|
|
794
|
+
with self._sync_lock:
|
|
795
|
+
pending_sync = tuple(self._pending_sync.items())
|
|
796
|
+
self._pending_sync.clear()
|
|
797
|
+
|
|
798
|
+
for key, value in pending_sync:
|
|
799
|
+
self.dispatch_sync_requested.emit(key, value)
|
|
800
|
+
|
|
801
|
+
if not self._setup_fired:
|
|
802
|
+
self._setup_fired = True
|
|
803
|
+
self.main.events.windowReady.set()
|
|
804
|
+
|
|
805
|
+
@Slot(str, "QVariant")
|
|
806
|
+
def syncValue(self, key: str, value):
|
|
807
|
+
if key in self.WINDOW_SIZE_KEYS:
|
|
808
|
+
width, height = self.main.sync_window_size(
|
|
809
|
+
value if key == "system_window_width" else self.main.values.get("system_window_width"),
|
|
810
|
+
value if key == "system_window_height" else self.main.values.get("system_window_height"),
|
|
811
|
+
False,
|
|
812
|
+
)
|
|
813
|
+
self.resize_window_requested.emit(width, height)
|
|
814
|
+
self._dispatch_or_defer_sync("system_window_width", width)
|
|
815
|
+
self._dispatch_or_defer_sync("system_window_height", height)
|
|
816
|
+
return
|
|
817
|
+
|
|
818
|
+
self.main.syncValue(key, value)
|
|
819
|
+
|
|
820
|
+
@Slot("QVariantMap")
|
|
821
|
+
def syncValues(self, values):
|
|
822
|
+
width = values.get("system_window_width", self.main.values.get("system_window_width"))
|
|
823
|
+
height = values.get("system_window_height", self.main.values.get("system_window_height"))
|
|
824
|
+
has_window_size_update = any(key in self.WINDOW_SIZE_KEYS for key in values)
|
|
825
|
+
|
|
826
|
+
for key, value in values.items():
|
|
827
|
+
if key in self.WINDOW_SIZE_KEYS:
|
|
828
|
+
continue
|
|
829
|
+
self.main.syncValue(key, value)
|
|
830
|
+
|
|
831
|
+
if has_window_size_update:
|
|
832
|
+
resolved_width, resolved_height = self.main.sync_window_size(width, height, False)
|
|
833
|
+
self.resize_window_requested.emit(resolved_width, resolved_height)
|
|
834
|
+
self._dispatch_or_defer_sync("system_window_width", resolved_width)
|
|
835
|
+
self._dispatch_or_defer_sync("system_window_height", resolved_height)
|
|
836
|
+
|
|
837
|
+
@Slot(bool)
|
|
838
|
+
def pin(self, state: bool):
|
|
839
|
+
self.main.pin(state)
|
|
840
|
+
|
|
841
|
+
@Slot()
|
|
842
|
+
def minimize(self):
|
|
843
|
+
self.minimize_requested.emit()
|
|
844
|
+
|
|
845
|
+
@Slot()
|
|
846
|
+
def destroy(self):
|
|
847
|
+
self.close_requested.emit()
|
|
848
|
+
|
|
849
|
+
@Slot()
|
|
850
|
+
def startWindowDrag(self):
|
|
851
|
+
self.start_drag_requested.emit()
|
|
852
|
+
|
|
853
|
+
@Slot(str)
|
|
854
|
+
def startWindowResize(self, edge_name: str):
|
|
855
|
+
self.start_resize_requested.emit(edge_name)
|
|
856
|
+
|
|
857
|
+
@Slot()
|
|
858
|
+
def toggleMaximize(self):
|
|
859
|
+
self.toggle_maximize_requested.emit()
|
|
860
|
+
|
|
861
|
+
@Slot()
|
|
862
|
+
def showWindowMenu(self):
|
|
863
|
+
self.show_window_menu_requested.emit()
|
|
864
|
+
|
|
865
|
+
@Slot(str)
|
|
866
|
+
def openExternal(self, url: str):
|
|
867
|
+
open_url(url)
|
|
868
|
+
|
|
869
|
+
@Slot(str, result=str)
|
|
870
|
+
def resolveResource(self, value: str):
|
|
871
|
+
resolved = self.main.resolve_resource_url(value)
|
|
872
|
+
return "" if resolved is None else str(resolved)
|
|
873
|
+
|
|
874
|
+
@Slot(str, "QVariant")
|
|
875
|
+
def _dispatch_sync_value(self, key: str, value):
|
|
876
|
+
if self._window is None:
|
|
877
|
+
with self._sync_lock:
|
|
878
|
+
self._pending_sync[key] = value
|
|
879
|
+
return
|
|
880
|
+
|
|
881
|
+
if key == "system_title":
|
|
882
|
+
self._window.setWindowTitle(str(value or self._title))
|
|
883
|
+
if key in {"system_theme", "system_theme_resolved"}:
|
|
884
|
+
self._window._apply_window_background()
|
|
885
|
+
|
|
886
|
+
script = f"window.syncValue({json.dumps(key, ensure_ascii=False)}, {json.dumps(value, ensure_ascii=False)}, false)"
|
|
887
|
+
self._window.page.runJavaScript(script)
|