pyloid 0.9.5__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.
- pyloid-0.9.5.dist-info/LICENSE +201 -0
- pyloid-0.9.5.dist-info/METADATA +185 -0
- pyloid-0.9.5.dist-info/RECORD +11 -0
- pyloid-0.9.5.dist-info/WHEEL +4 -0
- pylonic/__init__.py +6 -0
- pylonic/api.py +10 -0
- pylonic/autostart.py +101 -0
- pylonic/monitor.py +372 -0
- pylonic/pylonic.py +915 -0
- pylonic/tray.py +18 -0
- pylonic/utils.py +29 -0
pylonic/pylonic.py
ADDED
@@ -0,0 +1,915 @@
|
|
1
|
+
import sys
|
2
|
+
import os
|
3
|
+
from PySide6.QtWidgets import (
|
4
|
+
QApplication,
|
5
|
+
QMainWindow,
|
6
|
+
QSystemTrayIcon,
|
7
|
+
QMenu,
|
8
|
+
)
|
9
|
+
from PySide6.QtWebEngineWidgets import QWebEngineView
|
10
|
+
from PySide6.QtWebChannel import QWebChannel
|
11
|
+
from PySide6.QtGui import QIcon, QKeySequence, QShortcut, QClipboard, QImage
|
12
|
+
from PySide6.QtCore import Qt, Signal, QUrl, QObject
|
13
|
+
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
14
|
+
from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings
|
15
|
+
from .api import PylonicAPI, Bridge
|
16
|
+
import uuid
|
17
|
+
from typing import List, Optional, Dict, Callable, Union, Any
|
18
|
+
from PySide6.QtCore import qInstallMessageHandler
|
19
|
+
import signal
|
20
|
+
from .utils import is_production
|
21
|
+
from .monitor import Monitor
|
22
|
+
import json
|
23
|
+
from .autostart import AutoStart
|
24
|
+
|
25
|
+
# for linux debug
|
26
|
+
os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = '/'
|
27
|
+
|
28
|
+
# for macos debug
|
29
|
+
os.environ['QT_MAC_WANTS_LAYER'] = '1'
|
30
|
+
|
31
|
+
def custom_message_handler(mode, context, message):
|
32
|
+
if not hasattr(custom_message_handler, 'vulkan_warning_shown') and (('Failed to load vulkan' in message) or ('No Vulkan library available' in message) or ('Failed to create platform Vulkan instance' in message)):
|
33
|
+
print('\033[93mPylon Warning: Vulkan GPU API issue detected. Switching to software backend.\033[0m')
|
34
|
+
os.environ['QT_QUICK_BACKEND'] = 'software'
|
35
|
+
custom_message_handler.vulkan_warning_shown = True
|
36
|
+
if 'vulkan' not in message.lower():
|
37
|
+
print(message)
|
38
|
+
|
39
|
+
qInstallMessageHandler(custom_message_handler)
|
40
|
+
|
41
|
+
class WindowAPI(PylonAPI):
|
42
|
+
def __init__(self, window_id: str, app):
|
43
|
+
super().__init__()
|
44
|
+
self.window_id: str = window_id
|
45
|
+
self.app: PylonApp = app
|
46
|
+
|
47
|
+
@Bridge(result=str)
|
48
|
+
def getWindowId(self):
|
49
|
+
"""Returns the current window ID."""
|
50
|
+
return self.window_id
|
51
|
+
|
52
|
+
@Bridge()
|
53
|
+
def close(self):
|
54
|
+
"""Closes the window."""
|
55
|
+
window = self.app.get_window_by_id(self.window_id)
|
56
|
+
if window:
|
57
|
+
window.close()
|
58
|
+
|
59
|
+
@Bridge()
|
60
|
+
def hide(self):
|
61
|
+
"""Hides the window."""
|
62
|
+
window = self.app.get_window_by_id(self.window_id)
|
63
|
+
if window:
|
64
|
+
window.hide()
|
65
|
+
|
66
|
+
@Bridge()
|
67
|
+
def show(self):
|
68
|
+
"""Shows and focuses the window."""
|
69
|
+
window = self.app.get_window_by_id(self.window_id)
|
70
|
+
if window:
|
71
|
+
window.show()
|
72
|
+
|
73
|
+
@Bridge()
|
74
|
+
def toggleFullscreen(self):
|
75
|
+
"""Toggles fullscreen mode for the window."""
|
76
|
+
window = self.app.get_window_by_id(self.window_id)
|
77
|
+
if window:
|
78
|
+
window.toggle_fullscreen()
|
79
|
+
|
80
|
+
@Bridge()
|
81
|
+
def minimize(self):
|
82
|
+
"""Minimizes the window."""
|
83
|
+
window = self.app.get_window_by_id(self.window_id)
|
84
|
+
if window:
|
85
|
+
window.minimize()
|
86
|
+
|
87
|
+
@Bridge()
|
88
|
+
def maximize(self):
|
89
|
+
"""Maximizes the window."""
|
90
|
+
window = self.app.get_window_by_id(self.window_id)
|
91
|
+
if window:
|
92
|
+
window.maximize()
|
93
|
+
|
94
|
+
@Bridge()
|
95
|
+
def unmaximize(self):
|
96
|
+
"""Restores the window to its normal state."""
|
97
|
+
window = self.app.get_window_by_id(self.window_id)
|
98
|
+
if window:
|
99
|
+
window.unmaximize()
|
100
|
+
|
101
|
+
@Bridge(str)
|
102
|
+
def setTitle(self, title: str):
|
103
|
+
"""Sets the title of the window."""
|
104
|
+
window = self.app.get_window_by_id(self.window_id)
|
105
|
+
if window:
|
106
|
+
window.set_title(title)
|
107
|
+
|
108
|
+
@Bridge(int, int)
|
109
|
+
def setSize(self, width: int, height: int):
|
110
|
+
"""Sets the size of the window."""
|
111
|
+
window = self.app.get_window_by_id(self.window_id)
|
112
|
+
if window:
|
113
|
+
window.set_size(width, height)
|
114
|
+
|
115
|
+
@Bridge(int, int)
|
116
|
+
def setPosition(self, x: int, y: int):
|
117
|
+
"""Sets the position of the window."""
|
118
|
+
window = self.app.get_window_by_id(self.window_id)
|
119
|
+
if window:
|
120
|
+
window.set_position(x, y)
|
121
|
+
|
122
|
+
@Bridge(bool)
|
123
|
+
def setFrame(self, frame: bool):
|
124
|
+
"""Sets the frame of the window."""
|
125
|
+
window = self.app.get_window_by_id(self.window_id)
|
126
|
+
if window:
|
127
|
+
window.set_frame(frame)
|
128
|
+
|
129
|
+
@Bridge(bool)
|
130
|
+
def setContextMenu(self, context_menu: bool):
|
131
|
+
"""Sets the context menu of the window."""
|
132
|
+
window = self.app.get_window_by_id(self.window_id)
|
133
|
+
if window:
|
134
|
+
window.set_context_menu(context_menu)
|
135
|
+
|
136
|
+
@Bridge(bool)
|
137
|
+
def setDevTools(self, enable: bool):
|
138
|
+
"""Sets the developer tools of the window."""
|
139
|
+
window = self.app.get_window_by_id(self.window_id)
|
140
|
+
if window:
|
141
|
+
window.set_dev_tools(enable)
|
142
|
+
|
143
|
+
@Bridge(str, result=Optional[str])
|
144
|
+
def capture(self, save_path: str) -> Optional[str]:
|
145
|
+
"""Captures the current window."""
|
146
|
+
window = self.app.get_window_by_id(self.window_id)
|
147
|
+
if window:
|
148
|
+
return window.capture(save_path)
|
149
|
+
return None
|
150
|
+
|
151
|
+
# class EventAPI(PylonAPI):
|
152
|
+
# def __init__(self, window_id: str, app):
|
153
|
+
# super().__init__()
|
154
|
+
# self.window_id: str = window_id
|
155
|
+
# self.app: PylonApp = app
|
156
|
+
# self.subscribers = {}
|
157
|
+
|
158
|
+
# @Bridge(str, Callable)
|
159
|
+
# def on(self, event_name: str, callback: Callable):
|
160
|
+
# """특정 이벤트를 구독합니다."""
|
161
|
+
# if event_name not in self.subscribers:
|
162
|
+
# self.subscribers[event_name] = []
|
163
|
+
# self.subscribers[event_name].append(callback)
|
164
|
+
|
165
|
+
# @Bridge(str, result=Optional[str])
|
166
|
+
# def emit(self, event_name: str, *args, **kwargs):
|
167
|
+
# """다른 윈도우로 특정 이벤트를 보냅니다."""
|
168
|
+
# if event_name in self.subscribers:
|
169
|
+
# for callback in self.subscribers[event_name]:
|
170
|
+
# callback(*args, **kwargs)
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
class BrowserWindow:
|
175
|
+
def __init__(
|
176
|
+
self,
|
177
|
+
app,
|
178
|
+
title: str="pylon app",
|
179
|
+
width: int=800,
|
180
|
+
height: int=600,
|
181
|
+
x: int=200,
|
182
|
+
y: int=200,
|
183
|
+
frame: bool=True,
|
184
|
+
context_menu: bool=False,
|
185
|
+
dev_tools: bool=False,
|
186
|
+
js_apis: List[PylonAPI]=[],
|
187
|
+
):
|
188
|
+
###########################################################################################
|
189
|
+
self.id = str(uuid.uuid4()) # Generate unique ID
|
190
|
+
|
191
|
+
self._window = QMainWindow()
|
192
|
+
self.web_view = QWebEngineView()
|
193
|
+
|
194
|
+
self._window.closeEvent = self.closeEvent # Override closeEvent method
|
195
|
+
###########################################################################################
|
196
|
+
self.app = app
|
197
|
+
self.title = title
|
198
|
+
self.width = width
|
199
|
+
self.height = height
|
200
|
+
self.x = x
|
201
|
+
self.y = y
|
202
|
+
self.frame = frame
|
203
|
+
self.context_menu = context_menu
|
204
|
+
self.dev_tools = dev_tools
|
205
|
+
self.js_apis = [WindowAPI(self.id, self.app)]
|
206
|
+
for js_api in js_apis:
|
207
|
+
self.js_apis.append(js_api)
|
208
|
+
self.shortcuts = {}
|
209
|
+
###########################################################################################
|
210
|
+
|
211
|
+
def _load(self):
|
212
|
+
self._window.setWindowTitle(self.title)
|
213
|
+
|
214
|
+
self._window.setGeometry(self.x, self.y, self.width, self.height)
|
215
|
+
|
216
|
+
# allow local file access to remote urls
|
217
|
+
self.web_view.settings().setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
|
218
|
+
|
219
|
+
# Set icon
|
220
|
+
if self.app.icon:
|
221
|
+
self._window.setWindowIcon(self.app.icon)
|
222
|
+
else:
|
223
|
+
print("Icon is not set.")
|
224
|
+
|
225
|
+
# Set Windows taskbar icon
|
226
|
+
if sys.platform == "win32":
|
227
|
+
import ctypes
|
228
|
+
|
229
|
+
myappid = "mycompany.myproduct.subproduct.version"
|
230
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
231
|
+
|
232
|
+
|
233
|
+
# Remove title bar and borders (if needed)
|
234
|
+
if not self.frame:
|
235
|
+
self._window.setWindowFlags(Qt.FramelessWindowHint)
|
236
|
+
|
237
|
+
# Disable default context menu
|
238
|
+
if not self.context_menu:
|
239
|
+
self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
|
240
|
+
|
241
|
+
# Set up QWebChannel
|
242
|
+
self.channel = QWebChannel()
|
243
|
+
|
244
|
+
# Register additional JS APIs
|
245
|
+
if self.js_apis:
|
246
|
+
for js_api in self.js_apis:
|
247
|
+
self.channel.registerObject(js_api.__class__.__name__, js_api)
|
248
|
+
|
249
|
+
|
250
|
+
self.web_view.page().setWebChannel(self.channel)
|
251
|
+
|
252
|
+
# Connect pylonjs bridge
|
253
|
+
self.web_view.loadFinished.connect(self._on_load_finished)
|
254
|
+
|
255
|
+
# Add QWebEngineView to main window
|
256
|
+
self._window.setCentralWidget(self.web_view)
|
257
|
+
|
258
|
+
|
259
|
+
# Set F12 shortcut
|
260
|
+
self.set_dev_tools(self.dev_tools)
|
261
|
+
|
262
|
+
def _on_load_finished(self, ok):
|
263
|
+
"""Handles the event when the web page finishes loading."""
|
264
|
+
if ok and self.js_apis:
|
265
|
+
js_code = """
|
266
|
+
if (typeof QWebChannel !== 'undefined') {
|
267
|
+
new QWebChannel(qt.webChannelTransport, function (channel) {
|
268
|
+
window.pylon = {
|
269
|
+
EventAPI: {
|
270
|
+
listen: function(eventName, callback) {
|
271
|
+
document.addEventListener(eventName, function(event) {
|
272
|
+
let eventData;
|
273
|
+
try {
|
274
|
+
eventData = JSON.parse(event.detail);
|
275
|
+
} catch (e) {
|
276
|
+
eventData = event.detail;
|
277
|
+
}
|
278
|
+
callback(eventData);
|
279
|
+
});
|
280
|
+
},
|
281
|
+
unlisten: function(eventName) {
|
282
|
+
document.removeEventListener(eventName);
|
283
|
+
}
|
284
|
+
}
|
285
|
+
};
|
286
|
+
console.log('pylon.EventAPI object initialized:', window.pylon.EventAPI);
|
287
|
+
|
288
|
+
%s
|
289
|
+
|
290
|
+
// Dispatch a custom event to signal that the initialization is ready
|
291
|
+
const event = new CustomEvent('pylonReady');
|
292
|
+
document.dispatchEvent(event);
|
293
|
+
});
|
294
|
+
} else {
|
295
|
+
console.error('QWebChannel is not defined.');
|
296
|
+
}
|
297
|
+
"""
|
298
|
+
js_api_init = "\n".join(
|
299
|
+
[
|
300
|
+
f"window.pylon['{js_api.__class__.__name__}'] = channel.objects['{js_api.__class__.__name__}'];\n"
|
301
|
+
f"console.log('pylon.{js_api.__class__.__name__} object initialized:', window.pylon['{js_api.__class__.__name__}']);"
|
302
|
+
for js_api in self.js_apis
|
303
|
+
]
|
304
|
+
)
|
305
|
+
self.web_view.page().runJavaScript(js_code % js_api_init)
|
306
|
+
else:
|
307
|
+
pass
|
308
|
+
|
309
|
+
###########################################################################################
|
310
|
+
# Load
|
311
|
+
###########################################################################################
|
312
|
+
def load_file(self, file_path):
|
313
|
+
"""Loads a local HTML file into the web view."""
|
314
|
+
self._load()
|
315
|
+
file_path = os.path.abspath(file_path) # absolute path
|
316
|
+
self.web_view.setUrl(QUrl.fromLocalFile(file_path))
|
317
|
+
|
318
|
+
|
319
|
+
def load_url(self, url):
|
320
|
+
"""Sets the URL of the window."""
|
321
|
+
self._load()
|
322
|
+
self.web_view.setUrl(QUrl(url))
|
323
|
+
###########################################################################################
|
324
|
+
# Set Parameters
|
325
|
+
###########################################################################################
|
326
|
+
def set_title(self, title: str):
|
327
|
+
"""Sets the title of the window."""
|
328
|
+
self.title = title
|
329
|
+
self._window.setWindowTitle(self.title)
|
330
|
+
|
331
|
+
def set_size(self, width: int, height: int):
|
332
|
+
"""Sets the size of the window."""
|
333
|
+
self.width = width
|
334
|
+
self.height = height
|
335
|
+
self._window.setGeometry(self.x, self.y, self.width, self.height)
|
336
|
+
|
337
|
+
def set_position(self, x: int, y: int):
|
338
|
+
"""Sets the position of the window."""
|
339
|
+
self.x = x
|
340
|
+
self.y = y
|
341
|
+
self._window.setGeometry(self.x, self.y, self.width, self.height)
|
342
|
+
|
343
|
+
def set_frame(self, frame: bool):
|
344
|
+
"""Sets the frame of the window."""
|
345
|
+
self.frame = frame
|
346
|
+
if self.frame:
|
347
|
+
self._window.setWindowFlags(Qt.Window)
|
348
|
+
else:
|
349
|
+
self._window.setWindowFlags(Qt.FramelessWindowHint)
|
350
|
+
|
351
|
+
def set_context_menu(self, context_menu: bool):
|
352
|
+
"""Sets the context menu of the window."""
|
353
|
+
self.context_menu = context_menu
|
354
|
+
if self.context_menu:
|
355
|
+
self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
|
356
|
+
else:
|
357
|
+
self.web_view.setContextMenuPolicy(Qt.DefaultContextMenu)
|
358
|
+
|
359
|
+
def set_dev_tools(self, enable: bool):
|
360
|
+
"""Sets the developer tools of the window.
|
361
|
+
|
362
|
+
If enabled, the developer tools can be opened using the F12 key.
|
363
|
+
"""
|
364
|
+
self.dev_tools = enable
|
365
|
+
if self.dev_tools:
|
366
|
+
self.add_shortcut("F12", self.open_dev_tools)
|
367
|
+
else:
|
368
|
+
self.remove_shortcut("F12")
|
369
|
+
|
370
|
+
def open_dev_tools(self):
|
371
|
+
"""Opens the developer tools window."""
|
372
|
+
self.web_view.page().setDevToolsPage(QWebEnginePage(self.web_view.page()))
|
373
|
+
self.dev_tools_window = QMainWindow(self._window)
|
374
|
+
dev_tools_view = QWebEngineView(self.dev_tools_window)
|
375
|
+
dev_tools_view.setPage(self.web_view.page().devToolsPage())
|
376
|
+
self.dev_tools_window.setCentralWidget(dev_tools_view)
|
377
|
+
self.dev_tools_window.resize(800, 600)
|
378
|
+
self.dev_tools_window.show()
|
379
|
+
|
380
|
+
def get_window_properties(self):
|
381
|
+
"""Returns the properties of the window."""
|
382
|
+
return {
|
383
|
+
"id": self.id,
|
384
|
+
"title": self.title,
|
385
|
+
"width": self.width,
|
386
|
+
"height": self.height,
|
387
|
+
"x": self.x,
|
388
|
+
"y": self.y,
|
389
|
+
"frame": self.frame,
|
390
|
+
"context_menu": self.context_menu,
|
391
|
+
"dev_tools": self.dev_tools,
|
392
|
+
"js_apis": self.js_apis,
|
393
|
+
}
|
394
|
+
|
395
|
+
def get_id(self):
|
396
|
+
"""Returns the ID of the window."""
|
397
|
+
return self.id
|
398
|
+
|
399
|
+
def closeEvent(self, event):
|
400
|
+
"""Handles the event when the window is closed."""
|
401
|
+
self._remove_from_app_windows()
|
402
|
+
event.accept() # Accept the event (allow the window to close)
|
403
|
+
|
404
|
+
def _remove_from_app_windows(self):
|
405
|
+
"""Removes the window from the app's window list."""
|
406
|
+
if self in self.app.windows:
|
407
|
+
self.app.windows.remove(self)
|
408
|
+
if not self.app.windows:
|
409
|
+
self.app.quit() # Quit the app if all windows are closed
|
410
|
+
|
411
|
+
###########################################################################################
|
412
|
+
# Window management (no ID required)
|
413
|
+
###########################################################################################
|
414
|
+
def hide(self):
|
415
|
+
"""Hides the window."""
|
416
|
+
self._window.hide()
|
417
|
+
|
418
|
+
def show(self):
|
419
|
+
"""Shows the window."""
|
420
|
+
self._window.show()
|
421
|
+
|
422
|
+
def focus(self):
|
423
|
+
"""Focuses the window."""
|
424
|
+
self._window.activateWindow()
|
425
|
+
self._window.raise_()
|
426
|
+
self._window.setWindowState(self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
427
|
+
|
428
|
+
def show_and_focus(self):
|
429
|
+
"""Shows and focuses the window."""
|
430
|
+
self._window.show()
|
431
|
+
self._window.activateWindow()
|
432
|
+
self._window.raise_()
|
433
|
+
self._window.setWindowState(self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
434
|
+
|
435
|
+
def close(self):
|
436
|
+
"""Closes the window."""
|
437
|
+
self._window.close()
|
438
|
+
|
439
|
+
def toggle_fullscreen(self):
|
440
|
+
"""Toggles fullscreen mode for the window."""
|
441
|
+
if self._window.isFullScreen():
|
442
|
+
self._window.showNormal()
|
443
|
+
else:
|
444
|
+
self._window.showFullScreen()
|
445
|
+
|
446
|
+
def minimize(self):
|
447
|
+
"""Minimizes the window."""
|
448
|
+
self._window.showMinimized()
|
449
|
+
|
450
|
+
def maximize(self):
|
451
|
+
"""Maximizes the window."""
|
452
|
+
self._window.showMaximized()
|
453
|
+
|
454
|
+
def unmaximize(self):
|
455
|
+
"""Unmaximizes the window."""
|
456
|
+
self._window.showNormal()
|
457
|
+
|
458
|
+
def capture(self, save_path: str) -> Optional[str]:
|
459
|
+
"""
|
460
|
+
Captures the current window.
|
461
|
+
|
462
|
+
:param save_path: Path to save the captured image. If not specified, it will be saved in the current directory.
|
463
|
+
:return: Path of the saved image
|
464
|
+
"""
|
465
|
+
try:
|
466
|
+
# Capture window
|
467
|
+
screenshot = self._window.grab()
|
468
|
+
|
469
|
+
# Save image
|
470
|
+
screenshot.save(save_path)
|
471
|
+
return save_path
|
472
|
+
except Exception as e:
|
473
|
+
print(f"Error occurred while capturing the window: {e}")
|
474
|
+
return None
|
475
|
+
|
476
|
+
###########################################################################################
|
477
|
+
# Shortcut
|
478
|
+
###########################################################################################
|
479
|
+
def add_shortcut(self, key_sequence: str, callback: Callable):
|
480
|
+
"""
|
481
|
+
Adds a keyboard shortcut to the window if it does not already exist.
|
482
|
+
|
483
|
+
:param key_sequence: Shortcut sequence (e.g., "Ctrl+C")
|
484
|
+
:param callback: Function to be executed when the shortcut is pressed
|
485
|
+
:return: Created QShortcut object or None if the shortcut already exists
|
486
|
+
"""
|
487
|
+
if key_sequence in self.shortcuts:
|
488
|
+
# print(f"Shortcut {key_sequence} already exists.")
|
489
|
+
return None
|
490
|
+
|
491
|
+
shortcut = QShortcut(QKeySequence(key_sequence), self._window)
|
492
|
+
shortcut.activated.connect(callback)
|
493
|
+
self.shortcuts[key_sequence] = shortcut
|
494
|
+
return shortcut
|
495
|
+
|
496
|
+
def remove_shortcut(self, key_sequence: str):
|
497
|
+
"""
|
498
|
+
Removes a keyboard shortcut from the window.
|
499
|
+
|
500
|
+
:param key_sequence: Shortcut sequence to be removed
|
501
|
+
"""
|
502
|
+
if key_sequence in self.shortcuts:
|
503
|
+
shortcut = self.shortcuts.pop(key_sequence)
|
504
|
+
shortcut.setEnabled(False)
|
505
|
+
shortcut.deleteLater()
|
506
|
+
|
507
|
+
def get_all_shortcuts(self):
|
508
|
+
"""
|
509
|
+
Returns all registered shortcuts in the window.
|
510
|
+
|
511
|
+
:return: Dictionary of shortcut sequences and QShortcut objects
|
512
|
+
"""
|
513
|
+
return self.shortcuts
|
514
|
+
|
515
|
+
###########################################################################################
|
516
|
+
# Event (Calling the JS from Python)
|
517
|
+
###########################################################################################
|
518
|
+
def emit(self, event_name, data: Optional[Dict]=None):
|
519
|
+
"""
|
520
|
+
Emits an event to the JavaScript side.
|
521
|
+
|
522
|
+
:param event_name: Name of the event
|
523
|
+
:param data: Data to be sent with the event (optional)
|
524
|
+
"""
|
525
|
+
script = f"""
|
526
|
+
(function() {{
|
527
|
+
const eventData = {json.dumps(data)};
|
528
|
+
const customEvent = new CustomEvent('{event_name}', {{ detail: eventData }});
|
529
|
+
document.dispatchEvent(customEvent);
|
530
|
+
}})();
|
531
|
+
"""
|
532
|
+
self.web_view.page().runJavaScript(script)
|
533
|
+
|
534
|
+
|
535
|
+
class _WindowController(QObject):
|
536
|
+
create_window_signal = Signal(
|
537
|
+
QApplication, str, int, int, int, int, bool, bool, bool, list
|
538
|
+
)
|
539
|
+
|
540
|
+
class Pylonic(QApplication):
|
541
|
+
def __init__(self, app_name, single_instance=True, icon_path: str=None, tray_icon_path: str=None):
|
542
|
+
super().__init__(sys.argv)
|
543
|
+
|
544
|
+
self.windows = []
|
545
|
+
self.server = None
|
546
|
+
|
547
|
+
self.clipboard_class = self.clipboard()
|
548
|
+
self.shortcuts = {}
|
549
|
+
|
550
|
+
self.single_instance = single_instance
|
551
|
+
if self.single_instance:
|
552
|
+
self._init_single_instance()
|
553
|
+
|
554
|
+
self.controller = _WindowController()
|
555
|
+
self.controller.create_window_signal.connect(self._create_window_signal_function)
|
556
|
+
|
557
|
+
self.icon = QIcon(icon_path) if icon_path else None
|
558
|
+
self.tray_icon = QIcon(tray_icon_path) if tray_icon_path else None
|
559
|
+
self.tray_menu_items = []
|
560
|
+
self.tray_actions = {}
|
561
|
+
|
562
|
+
self.app_name = app_name
|
563
|
+
self.app_path = sys.executable
|
564
|
+
|
565
|
+
self.auto_start = AutoStart(self.app_name, self.app_path)
|
566
|
+
|
567
|
+
def set_icon(self, icon_path: str):
|
568
|
+
"""Sets the icon for the application."""
|
569
|
+
self.icon = QIcon(icon_path)
|
570
|
+
|
571
|
+
def set_tray_icon(self, tray_icon_path: str):
|
572
|
+
"""Sets the path for the tray icon."""
|
573
|
+
self.tray_icon = QIcon(tray_icon_path)
|
574
|
+
|
575
|
+
def set_tray_menu_items(self, tray_menu_items: Dict[str, Callable]):
|
576
|
+
"""Sets the menu items for the tray icon."""
|
577
|
+
self.tray_menu_items = tray_menu_items
|
578
|
+
|
579
|
+
def create_window(
|
580
|
+
self,
|
581
|
+
title: str="pylon app",
|
582
|
+
width: int=800,
|
583
|
+
height: int=600,
|
584
|
+
x: int=200,
|
585
|
+
y: int=200,
|
586
|
+
frame: bool=True,
|
587
|
+
context_menu: bool=False,
|
588
|
+
dev_tools: bool=False,
|
589
|
+
js_apis: List[PylonicAPI]=[],
|
590
|
+
) -> BrowserWindow:
|
591
|
+
"""Creates a new browser window."""
|
592
|
+
self.controller.create_window_signal.emit(
|
593
|
+
self,
|
594
|
+
title,
|
595
|
+
width,
|
596
|
+
height,
|
597
|
+
x,
|
598
|
+
y,
|
599
|
+
frame,
|
600
|
+
context_menu,
|
601
|
+
dev_tools,
|
602
|
+
js_apis,
|
603
|
+
)
|
604
|
+
return self.windows[-1]
|
605
|
+
|
606
|
+
def _create_window_signal_function(
|
607
|
+
self,
|
608
|
+
app,
|
609
|
+
title: str,
|
610
|
+
width: int,
|
611
|
+
height: int,
|
612
|
+
x: int,
|
613
|
+
y: int,
|
614
|
+
frame: bool,
|
615
|
+
context_menu: bool,
|
616
|
+
dev_tools: bool,
|
617
|
+
js_apis: List[PylonAPI]=[],
|
618
|
+
) -> BrowserWindow:
|
619
|
+
"""Function to create a new browser window."""
|
620
|
+
window = BrowserWindow(
|
621
|
+
app,
|
622
|
+
title,
|
623
|
+
width,
|
624
|
+
height,
|
625
|
+
x,
|
626
|
+
y,
|
627
|
+
frame,
|
628
|
+
context_menu,
|
629
|
+
dev_tools,
|
630
|
+
js_apis,
|
631
|
+
)
|
632
|
+
self.windows.append(window)
|
633
|
+
return window
|
634
|
+
|
635
|
+
def run(self):
|
636
|
+
"""Runs the application event loop."""
|
637
|
+
if is_production():
|
638
|
+
sys.exit(self.exec())
|
639
|
+
else:
|
640
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
641
|
+
sys.exit(self.exec())
|
642
|
+
|
643
|
+
def _init_single_instance(self):
|
644
|
+
"""Initializes the application as a single instance."""
|
645
|
+
socket = QLocalSocket()
|
646
|
+
socket.connectToServer("PylonBrowserApp")
|
647
|
+
if socket.waitForConnected(500):
|
648
|
+
# Another instance is already running
|
649
|
+
sys.exit(1)
|
650
|
+
|
651
|
+
# Create a new server
|
652
|
+
self.server = QLocalServer()
|
653
|
+
self.server.listen("PylonBrowserApp")
|
654
|
+
self.server.newConnection.connect(self._handle_new_connection)
|
655
|
+
|
656
|
+
def _handle_new_connection(self):
|
657
|
+
"""Handles new connections for the single instance server."""
|
658
|
+
pass
|
659
|
+
|
660
|
+
|
661
|
+
###########################################################################################
|
662
|
+
# App window
|
663
|
+
###########################################################################################
|
664
|
+
def get_windows(self) -> List[BrowserWindow]:
|
665
|
+
"""Returns a list of all browser windows."""
|
666
|
+
return self.windows
|
667
|
+
|
668
|
+
def show_main_window(self):
|
669
|
+
"""Shows and focuses the first window."""
|
670
|
+
if self.windows:
|
671
|
+
main_window = self.windows[0]
|
672
|
+
main_window._window.show()
|
673
|
+
|
674
|
+
def focus_main_window(self):
|
675
|
+
"""Focuses the first window."""
|
676
|
+
if self.windows:
|
677
|
+
main_window = self.windows[0]
|
678
|
+
main_window._window.activateWindow()
|
679
|
+
main_window._window.raise_()
|
680
|
+
main_window._window.setWindowState(main_window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
681
|
+
|
682
|
+
def show_and_focus_main_window(self):
|
683
|
+
"""Shows and focuses the first window."""
|
684
|
+
if self.windows:
|
685
|
+
main_window = self.windows[0]
|
686
|
+
main_window._window.show()
|
687
|
+
main_window._window.activateWindow()
|
688
|
+
main_window._window.raise_()
|
689
|
+
main_window._window.setWindowState(main_window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
690
|
+
|
691
|
+
def close_all_windows(self):
|
692
|
+
"""Closes all windows."""
|
693
|
+
for window in self.windows:
|
694
|
+
window._window.close()
|
695
|
+
|
696
|
+
def quit(self):
|
697
|
+
"""Quits the application."""
|
698
|
+
self.close_all_windows()
|
699
|
+
QApplication.quit()
|
700
|
+
###########################################################################################
|
701
|
+
# Window management in the app (ID required)
|
702
|
+
###########################################################################################
|
703
|
+
def get_window_by_id(self, window_id: str) -> Optional[BrowserWindow]:
|
704
|
+
"""Returns the window with the given ID."""
|
705
|
+
for window in self.windows:
|
706
|
+
if window.id == window_id:
|
707
|
+
return window
|
708
|
+
return None
|
709
|
+
|
710
|
+
def hide_window_by_id(self, window_id: str):
|
711
|
+
"""Hides the window with the given ID."""
|
712
|
+
window = self.get_window_by_id(window_id)
|
713
|
+
if window:
|
714
|
+
window.hide()
|
715
|
+
|
716
|
+
def show_window_by_id(self, window_id: str):
|
717
|
+
"""Shows and focuses the window with the given ID."""
|
718
|
+
window = self.get_window_by_id(window_id)
|
719
|
+
if window:
|
720
|
+
window._window.show()
|
721
|
+
window._window.activateWindow()
|
722
|
+
window._window.raise_()
|
723
|
+
window._window.setWindowState(window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
724
|
+
|
725
|
+
def close_window_by_id(self, window_id: str):
|
726
|
+
"""Closes the window with the given ID."""
|
727
|
+
window = self.get_window_by_id(window_id)
|
728
|
+
if window:
|
729
|
+
window._window.close()
|
730
|
+
|
731
|
+
def toggle_fullscreen_by_id(self, window_id: str):
|
732
|
+
"""Toggles fullscreen mode for the window with the given ID."""
|
733
|
+
window = self.get_window_by_id(window_id)
|
734
|
+
window.toggle_fullscreen()
|
735
|
+
|
736
|
+
def minimize_window_by_id(self, window_id: str):
|
737
|
+
"""Minimizes the window with the given ID."""
|
738
|
+
window = self.get_window_by_id(window_id)
|
739
|
+
if window:
|
740
|
+
window.minimize()
|
741
|
+
|
742
|
+
def maximize_window_by_id(self, window_id: str):
|
743
|
+
"""Maximizes the window with the given ID."""
|
744
|
+
window = self.get_window_by_id(window_id)
|
745
|
+
if window:
|
746
|
+
window.maximize()
|
747
|
+
|
748
|
+
def unmaximize_window_by_id(self, window_id: str):
|
749
|
+
"""Unmaximizes the window with the given ID."""
|
750
|
+
window = self.get_window_by_id(window_id)
|
751
|
+
if window:
|
752
|
+
window.unmaximize()
|
753
|
+
|
754
|
+
def capture_window_by_id(self, window_id: str, save_path: str) -> Optional[str]:
|
755
|
+
"""
|
756
|
+
Captures a specific window.
|
757
|
+
|
758
|
+
:param window_id: ID of the window to capture
|
759
|
+
:param save_path: Path to save the captured image. If not specified, it will be saved in the current directory.
|
760
|
+
:return: Path of the saved image
|
761
|
+
"""
|
762
|
+
try:
|
763
|
+
window = self.get_window_by_id(window_id)
|
764
|
+
if not window:
|
765
|
+
print(f"Cannot find window with the specified ID: {window_id}")
|
766
|
+
return None
|
767
|
+
|
768
|
+
# Capture window
|
769
|
+
screenshot = window._window.grab()
|
770
|
+
|
771
|
+
# Save image
|
772
|
+
screenshot.save(save_path)
|
773
|
+
return save_path
|
774
|
+
except Exception as e:
|
775
|
+
print(f"Error occurred while capturing the window: {e}")
|
776
|
+
return None
|
777
|
+
###########################################################################################
|
778
|
+
# Tray
|
779
|
+
###########################################################################################
|
780
|
+
def run_tray(self):
|
781
|
+
"""Sets up the system tray icon and menu."""
|
782
|
+
if not hasattr(self, 'tray'):
|
783
|
+
self.tray = QSystemTrayIcon(self)
|
784
|
+
if self.tray_icon:
|
785
|
+
self.tray.setIcon(self.tray_icon)
|
786
|
+
else:
|
787
|
+
if self.icon:
|
788
|
+
self.tray.setIcon(self.icon)
|
789
|
+
else:
|
790
|
+
print("Icon and Tray icon are not set.")
|
791
|
+
|
792
|
+
tray_menu = QMenu()
|
793
|
+
|
794
|
+
# Add menu items from external source
|
795
|
+
if self.tray_menu_items:
|
796
|
+
for item in self.tray_menu_items:
|
797
|
+
action = tray_menu.addAction(item["label"])
|
798
|
+
action.triggered.connect(item["callback"])
|
799
|
+
|
800
|
+
self.tray.setContextMenu(tray_menu)
|
801
|
+
self.tray.activated.connect(self._tray_activated)
|
802
|
+
self.tray.show()
|
803
|
+
|
804
|
+
def _tray_activated(self, reason):
|
805
|
+
"""Handles the event when the tray icon is activated."""
|
806
|
+
reason_enum = QSystemTrayIcon.ActivationReason(reason)
|
807
|
+
|
808
|
+
if reason_enum in self.tray_actions:
|
809
|
+
self.tray_actions[reason_enum]()
|
810
|
+
|
811
|
+
def set_tray_actions(self, actions):
|
812
|
+
"""
|
813
|
+
Sets the actions for tray icon activation.
|
814
|
+
|
815
|
+
actions: Dictionary where keys are TrayEvent enum values,
|
816
|
+
and values are callback functions for the respective activation reasons.
|
817
|
+
"""
|
818
|
+
self.tray_actions = actions
|
819
|
+
|
820
|
+
def show_notification(self, title: str, message: str):
|
821
|
+
"""Displays a notification in the system tray."""
|
822
|
+
if not hasattr(self, 'tray'):
|
823
|
+
self.run_tray() # Ensure the tray is initialized
|
824
|
+
|
825
|
+
self.tray.showMessage(title, message, QIcon(self.icon), 5000)
|
826
|
+
|
827
|
+
###########################################################################################
|
828
|
+
# Monitor
|
829
|
+
###########################################################################################
|
830
|
+
def get_all_monitors(self) -> List[Monitor]:
|
831
|
+
"""
|
832
|
+
Returns a list of information for all connected monitors.
|
833
|
+
|
834
|
+
:return: List containing monitor information
|
835
|
+
"""
|
836
|
+
monitors = [Monitor(index, screen) for index, screen in enumerate(self.screens())]
|
837
|
+
return monitors
|
838
|
+
|
839
|
+
def get_primary_monitor(self) -> Monitor:
|
840
|
+
"""
|
841
|
+
Returns information for the primary monitor.
|
842
|
+
|
843
|
+
:return: Primary monitor information
|
844
|
+
"""
|
845
|
+
primary_monitor = self.screens()[0]
|
846
|
+
return Monitor(0, primary_monitor)
|
847
|
+
|
848
|
+
###########################################################################################
|
849
|
+
# Clipboard
|
850
|
+
###########################################################################################
|
851
|
+
def copy_to_clipboard(self, text):
|
852
|
+
"""
|
853
|
+
Copies text to the clipboard.
|
854
|
+
|
855
|
+
:param text: Text to be copied
|
856
|
+
"""
|
857
|
+
self.clipboard_class.setText(text, QClipboard.Clipboard)
|
858
|
+
|
859
|
+
def get_clipboard_text(self):
|
860
|
+
"""
|
861
|
+
Retrieves text from the clipboard.
|
862
|
+
|
863
|
+
:return: Text from the clipboard
|
864
|
+
"""
|
865
|
+
return self.clipboard_class.text()
|
866
|
+
|
867
|
+
def set_clipboard_image(self, image: Union[str, bytes, os.PathLike]):
|
868
|
+
"""
|
869
|
+
Copies an image to the clipboard.
|
870
|
+
|
871
|
+
:param image: Path to the image to be copied
|
872
|
+
"""
|
873
|
+
self.clipboard_class.setImage(QImage(image), QClipboard.Clipboard)
|
874
|
+
|
875
|
+
def get_clipboard_image(self):
|
876
|
+
"""
|
877
|
+
Retrieves an image from the clipboard.
|
878
|
+
|
879
|
+
:return: QImage object from the clipboard (None if no image)
|
880
|
+
"""
|
881
|
+
return self.clipboard_class.image()
|
882
|
+
|
883
|
+
###########################################################################################
|
884
|
+
# Atostart
|
885
|
+
###########################################################################################
|
886
|
+
def set_auto_start(self, enable: bool):
|
887
|
+
"""
|
888
|
+
Sets the application to start automatically with the system. (set_auto_start(True) only works in production)
|
889
|
+
True only works in production.
|
890
|
+
False works in both environments.
|
891
|
+
|
892
|
+
:param enable: True to enable auto-start, False to disable
|
893
|
+
"""
|
894
|
+
if not enable:
|
895
|
+
self.auto_start.set_auto_start(False)
|
896
|
+
return False
|
897
|
+
|
898
|
+
if is_production():
|
899
|
+
if enable:
|
900
|
+
self.auto_start.set_auto_start(True)
|
901
|
+
return True
|
902
|
+
else:
|
903
|
+
print("\033[93mset_auto_start(True) is not supported in non-production environment\033[0m")
|
904
|
+
return None
|
905
|
+
|
906
|
+
def is_auto_start(self):
|
907
|
+
"""
|
908
|
+
Checks if the application is set to start automatically with the system.
|
909
|
+
|
910
|
+
:return: True if auto-start is enabled, False otherwise
|
911
|
+
"""
|
912
|
+
|
913
|
+
return self.auto_start.is_auto_start()
|
914
|
+
|
915
|
+
|