pyloid 0.20.1.dev1__py3-none-any.whl → 0.20.2__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/__init__.py +7 -7
- pyloid/api.py +104 -104
- pyloid/autostart.py +101 -101
- pyloid/browser_window.py +1915 -1968
- pyloid/builder/__init__.py +80 -80
- pyloid/builder/build_config.schema.json +72 -72
- pyloid/builder/spec.py +246 -246
- pyloid/custom/titlebar.py +116 -116
- pyloid/filewatcher.py +163 -163
- pyloid/js_api/event_api.py +24 -24
- pyloid/js_api/window_api.py +255 -255
- pyloid/monitor.py +921 -921
- pyloid/pyloid.py +1404 -1404
- pyloid/thread_pool.py +500 -500
- pyloid/timer.py +307 -307
- pyloid/tray.py +48 -48
- pyloid/utils.py +122 -122
- {pyloid-0.20.1.dev1.dist-info → pyloid-0.20.2.dist-info}/LICENSE +200 -200
- {pyloid-0.20.1.dev1.dist-info → pyloid-0.20.2.dist-info}/METADATA +4 -2
- pyloid-0.20.2.dist-info/RECORD +21 -0
- {pyloid-0.20.1.dev1.dist-info → pyloid-0.20.2.dist-info}/WHEEL +1 -1
- pyloid-0.20.1.dev1.dist-info/RECORD +0 -21
pyloid/pyloid.py
CHANGED
@@ -1,1404 +1,1404 @@
|
|
1
|
-
import sys
|
2
|
-
import os
|
3
|
-
from PySide6.QtWidgets import (
|
4
|
-
QApplication,
|
5
|
-
QSystemTrayIcon,
|
6
|
-
QMenu,
|
7
|
-
QFileDialog,
|
8
|
-
)
|
9
|
-
from PySide6.QtGui import (
|
10
|
-
QIcon,
|
11
|
-
QClipboard,
|
12
|
-
QImage,
|
13
|
-
QAction,
|
14
|
-
)
|
15
|
-
from PySide6.QtCore import Qt, Signal, QObject, QTimer, QEvent
|
16
|
-
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
17
|
-
from .api import PyloidAPI
|
18
|
-
from typing import List, Optional, Dict, Callable, Union, Literal
|
19
|
-
from PySide6.QtCore import qInstallMessageHandler
|
20
|
-
import signal
|
21
|
-
from .utils import is_production
|
22
|
-
from .monitor import Monitor
|
23
|
-
from .autostart import AutoStart
|
24
|
-
from .filewatcher import FileWatcher
|
25
|
-
import logging
|
26
|
-
from .browser_window import BrowserWindow
|
27
|
-
from .tray import TrayEvent
|
28
|
-
from PySide6.QtCore import QCoreApplication
|
29
|
-
from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject
|
30
|
-
import time
|
31
|
-
from .thread_pool import PyloidThreadPool
|
32
|
-
|
33
|
-
# for linux debug
|
34
|
-
os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = "/"
|
35
|
-
|
36
|
-
# for macos debug
|
37
|
-
logging.getLogger("Qt").setLevel(logging.ERROR)
|
38
|
-
|
39
|
-
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
|
40
|
-
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = (
|
41
|
-
"--enable-features=WebRTCPipeWireCapturer --ignore-certificate-errors --allow-insecure-localhost"
|
42
|
-
)
|
43
|
-
|
44
|
-
|
45
|
-
def custom_message_handler(mode, context, message):
|
46
|
-
if not hasattr(custom_message_handler, "vulkan_warning_shown") and (
|
47
|
-
("Failed to load vulkan" in message)
|
48
|
-
or ("No Vulkan library available" in message)
|
49
|
-
or ("Failed to create platform Vulkan instance" in message)
|
50
|
-
):
|
51
|
-
print(
|
52
|
-
"\033[93mPyloid Warning: Vulkan GPU API issue detected. Switching to software backend.\033[0m"
|
53
|
-
)
|
54
|
-
if "linux" in sys.platform:
|
55
|
-
os.environ["QT_QUICK_BACKEND"] = "software"
|
56
|
-
custom_message_handler.vulkan_warning_shown = True
|
57
|
-
|
58
|
-
if "Autofill.enable failed" in message:
|
59
|
-
print(
|
60
|
-
"\033[93mPyloid Warning: Autofill is not enabled in developer tools.\033[0m"
|
61
|
-
)
|
62
|
-
|
63
|
-
if "vulkan" not in message.lower() and "Autofill.enable failed" not in message:
|
64
|
-
print(message)
|
65
|
-
|
66
|
-
|
67
|
-
qInstallMessageHandler(custom_message_handler)
|
68
|
-
|
69
|
-
|
70
|
-
class _WindowController(QObject):
|
71
|
-
create_window_signal = Signal(
|
72
|
-
QApplication, str, int, int, int, int, bool, bool, bool, list
|
73
|
-
)
|
74
|
-
|
75
|
-
|
76
|
-
class Pyloid(QApplication):
|
77
|
-
def __init__(
|
78
|
-
self,
|
79
|
-
app_name,
|
80
|
-
single_instance=True,
|
81
|
-
):
|
82
|
-
"""
|
83
|
-
Initializes the Pyloid application.
|
84
|
-
|
85
|
-
Parameters
|
86
|
-
----------
|
87
|
-
app_name : str, required
|
88
|
-
The name of the application
|
89
|
-
single_instance : bool, optional
|
90
|
-
Whether to run the application as a single instance (default is True)
|
91
|
-
|
92
|
-
Examples
|
93
|
-
--------
|
94
|
-
```python
|
95
|
-
app = Pyloid(app_name="Pyloid-App")
|
96
|
-
|
97
|
-
window = app.create_window(title="New Window", width=1024, height=768)
|
98
|
-
window.show()
|
99
|
-
|
100
|
-
app.run()
|
101
|
-
```
|
102
|
-
"""
|
103
|
-
super().__init__(sys.argv)
|
104
|
-
|
105
|
-
self.windows = []
|
106
|
-
self.server = None
|
107
|
-
|
108
|
-
self.app_name = app_name
|
109
|
-
self.icon = None
|
110
|
-
|
111
|
-
self.clipboard_class = self.clipboard()
|
112
|
-
self.shortcuts = {}
|
113
|
-
|
114
|
-
self.single_instance = single_instance
|
115
|
-
if self.single_instance:
|
116
|
-
self._init_single_instance()
|
117
|
-
|
118
|
-
self.controller = _WindowController()
|
119
|
-
self.controller.create_window_signal.connect(
|
120
|
-
self._create_window_signal_function
|
121
|
-
)
|
122
|
-
|
123
|
-
self.file_watcher = FileWatcher()
|
124
|
-
|
125
|
-
self.tray_menu_items = []
|
126
|
-
self.tray_actions = {}
|
127
|
-
|
128
|
-
self.app_name = app_name
|
129
|
-
self.app_path = sys.executable
|
130
|
-
|
131
|
-
self.auto_start = AutoStart(self.app_name, self.app_path)
|
132
|
-
|
133
|
-
self.animation_timer = None
|
134
|
-
self.icon_frames = []
|
135
|
-
self.current_frame = 0
|
136
|
-
|
137
|
-
self.theme = (
|
138
|
-
"dark"
|
139
|
-
if self.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
140
|
-
else "light"
|
141
|
-
)
|
142
|
-
|
143
|
-
# Add color scheme tracking
|
144
|
-
self.styleHints().colorSchemeChanged.connect(self._handle_color_scheme_change)
|
145
|
-
|
146
|
-
# def set_theme(self, theme: Literal["system", "dark", "light"]):
|
147
|
-
# """
|
148
|
-
# 시스템의 테마를 설정합니다.
|
149
|
-
|
150
|
-
# Parameters
|
151
|
-
# ----------
|
152
|
-
# theme : Literal["system", "dark", "light"]
|
153
|
-
# 설정할 테마 ("system", "dark", "light" 중 하나)
|
154
|
-
|
155
|
-
# Examples
|
156
|
-
# --------
|
157
|
-
# >>> app = Pyloid(app_name="Pyloid-App")
|
158
|
-
# >>> app.set_theme("dark") # 다크 테마로 설정
|
159
|
-
# >>> app.set_theme("light") # 라이트 테마로 설정
|
160
|
-
# >>> app.set_theme("system") # 시스템 테마를 따름
|
161
|
-
# """
|
162
|
-
# self.theme = theme
|
163
|
-
|
164
|
-
# if theme == "system":
|
165
|
-
# # 시스템 테마를 light/dark 문자열로 변환
|
166
|
-
# system_theme = (
|
167
|
-
# "dark"
|
168
|
-
# if self.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
169
|
-
# else "light"
|
170
|
-
# )
|
171
|
-
# self._handle_color_scheme_change(system_theme)
|
172
|
-
# self.styleHints().colorSchemeChanged.connect(
|
173
|
-
# lambda: self._handle_color_scheme_change(system_theme)
|
174
|
-
# )
|
175
|
-
# else:
|
176
|
-
# # 기존 이벤트 연결 해제
|
177
|
-
# self.styleHints().colorSchemeChanged.disconnect(
|
178
|
-
# lambda: self._handle_color_scheme_change(self.theme)
|
179
|
-
# )
|
180
|
-
# self._handle_color_scheme_change(self.theme)
|
181
|
-
|
182
|
-
def set_icon(self, icon_path: str):
|
183
|
-
"""
|
184
|
-
Dynamically sets the application's icon.
|
185
|
-
|
186
|
-
This method can be called while the application is running.
|
187
|
-
The icon can be changed at any time and will be applied immediately.
|
188
|
-
|
189
|
-
Parameters
|
190
|
-
----------
|
191
|
-
icon_path : str
|
192
|
-
Path to the new icon file
|
193
|
-
|
194
|
-
Examples
|
195
|
-
--------
|
196
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
197
|
-
>>> app.set_icon("icons/icon.png")
|
198
|
-
"""
|
199
|
-
self.icon = QIcon(icon_path)
|
200
|
-
|
201
|
-
# Immediately update the icon for all open windows.
|
202
|
-
for window in self.windows:
|
203
|
-
window._window.setWindowIcon(self.icon)
|
204
|
-
|
205
|
-
def create_window(
|
206
|
-
self,
|
207
|
-
title: str,
|
208
|
-
width: int = 800,
|
209
|
-
height: int = 600,
|
210
|
-
x: int = 200,
|
211
|
-
y: int = 200,
|
212
|
-
frame: bool = True,
|
213
|
-
context_menu: bool = False,
|
214
|
-
dev_tools: bool = False,
|
215
|
-
js_apis: List[PyloidAPI] = [],
|
216
|
-
) -> BrowserWindow:
|
217
|
-
"""
|
218
|
-
Creates a new browser window.
|
219
|
-
|
220
|
-
Parameters
|
221
|
-
----------
|
222
|
-
title : str, required
|
223
|
-
Title of the window
|
224
|
-
width : int, optional
|
225
|
-
Width of the window (default is 800)
|
226
|
-
height : int, optional
|
227
|
-
Height of the window (default is 600)
|
228
|
-
x : int, optional
|
229
|
-
X coordinate of the window (default is 200)
|
230
|
-
y : int, optional
|
231
|
-
Y coordinate of the window (default is 200)
|
232
|
-
frame : bool, optional
|
233
|
-
Whether the window has a frame (default is True)
|
234
|
-
context_menu : bool, optional
|
235
|
-
Whether to use the context menu (default is False)
|
236
|
-
dev_tools : bool, optional
|
237
|
-
Whether to use developer tools (default is False)
|
238
|
-
js_apis : list of PyloidAPI, optional
|
239
|
-
List of JavaScript APIs to add to the window (default is an empty list)
|
240
|
-
|
241
|
-
Returns
|
242
|
-
-------
|
243
|
-
BrowserWindow
|
244
|
-
The created browser window object
|
245
|
-
|
246
|
-
Examples
|
247
|
-
--------
|
248
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
249
|
-
>>> window = app.create_window(title="New Window", width=1024, height=768)
|
250
|
-
>>> window.show()
|
251
|
-
"""
|
252
|
-
self.controller.create_window_signal.emit(
|
253
|
-
self,
|
254
|
-
title,
|
255
|
-
width,
|
256
|
-
height,
|
257
|
-
x,
|
258
|
-
y,
|
259
|
-
frame,
|
260
|
-
context_menu,
|
261
|
-
dev_tools,
|
262
|
-
js_apis,
|
263
|
-
)
|
264
|
-
return self.windows[-1]
|
265
|
-
|
266
|
-
def _create_window_signal_function(
|
267
|
-
self,
|
268
|
-
app,
|
269
|
-
title: str,
|
270
|
-
width: int,
|
271
|
-
height: int,
|
272
|
-
x: int,
|
273
|
-
y: int,
|
274
|
-
frame: bool,
|
275
|
-
context_menu: bool,
|
276
|
-
dev_tools: bool,
|
277
|
-
js_apis: List[PyloidAPI] = [],
|
278
|
-
) -> BrowserWindow:
|
279
|
-
"""Function to create a new browser window."""
|
280
|
-
window = BrowserWindow(
|
281
|
-
app,
|
282
|
-
title,
|
283
|
-
width,
|
284
|
-
height,
|
285
|
-
x,
|
286
|
-
y,
|
287
|
-
frame,
|
288
|
-
context_menu,
|
289
|
-
dev_tools,
|
290
|
-
js_apis,
|
291
|
-
)
|
292
|
-
self.windows.append(window)
|
293
|
-
return window
|
294
|
-
|
295
|
-
def run(self):
|
296
|
-
"""
|
297
|
-
Runs the application event loop.
|
298
|
-
|
299
|
-
This method starts the application's event loop, allowing the application to run.
|
300
|
-
|
301
|
-
This code should be written at the very end of the file.
|
302
|
-
|
303
|
-
Examples
|
304
|
-
--------
|
305
|
-
```python
|
306
|
-
app = Pyloid(app_name="Pyloid-App")
|
307
|
-
app.run()
|
308
|
-
```
|
309
|
-
"""
|
310
|
-
if is_production():
|
311
|
-
sys.exit(self.exec())
|
312
|
-
else:
|
313
|
-
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
314
|
-
sys.exit(self.exec())
|
315
|
-
|
316
|
-
def _init_single_instance(self):
|
317
|
-
"""Initializes the application as a single instance."""
|
318
|
-
socket = QLocalSocket()
|
319
|
-
socket.connectToServer(self.app_name)
|
320
|
-
if socket.waitForConnected(500):
|
321
|
-
# Another instance is already running
|
322
|
-
sys.exit(1)
|
323
|
-
|
324
|
-
# Create a new server
|
325
|
-
self.server = QLocalServer()
|
326
|
-
self.server.listen(self.app_name)
|
327
|
-
self.server.newConnection.connect(self._handle_new_connection)
|
328
|
-
|
329
|
-
def _handle_new_connection(self):
|
330
|
-
"""Handles new connections for the single instance server."""
|
331
|
-
pass
|
332
|
-
|
333
|
-
###########################################################################################
|
334
|
-
# App window
|
335
|
-
###########################################################################################
|
336
|
-
def get_windows(self) -> List[BrowserWindow]:
|
337
|
-
"""
|
338
|
-
Returns a list of all browser windows.
|
339
|
-
|
340
|
-
Returns
|
341
|
-
-------
|
342
|
-
List[BrowserWindow]
|
343
|
-
List of all browser windows
|
344
|
-
|
345
|
-
Examples
|
346
|
-
--------
|
347
|
-
```python
|
348
|
-
app = Pyloid(app_name="Pyloid-App")
|
349
|
-
windows = app.get_windows()
|
350
|
-
for window in windows:
|
351
|
-
print(window.get_id())
|
352
|
-
```
|
353
|
-
"""
|
354
|
-
return self.windows
|
355
|
-
|
356
|
-
def show_main_window(self):
|
357
|
-
"""
|
358
|
-
Shows and focuses the first window.
|
359
|
-
|
360
|
-
Examples
|
361
|
-
--------
|
362
|
-
```python
|
363
|
-
app = Pyloid(app_name="Pyloid-App")
|
364
|
-
app.show_main_window()
|
365
|
-
```
|
366
|
-
"""
|
367
|
-
if self.windows:
|
368
|
-
main_window = self.windows[0]
|
369
|
-
main_window._window.show()
|
370
|
-
|
371
|
-
def focus_main_window(self):
|
372
|
-
"""
|
373
|
-
Focuses the first window.
|
374
|
-
|
375
|
-
Examples
|
376
|
-
--------
|
377
|
-
```python
|
378
|
-
app = Pyloid(app_name="Pyloid-App")
|
379
|
-
app.focus_main_window()
|
380
|
-
```
|
381
|
-
"""
|
382
|
-
if self.windows:
|
383
|
-
main_window = self.windows[0]
|
384
|
-
main_window._window.activateWindow()
|
385
|
-
main_window._window.raise_()
|
386
|
-
main_window._window.setWindowState(
|
387
|
-
main_window._window.windowState() & ~Qt.WindowMinimized
|
388
|
-
| Qt.WindowActive
|
389
|
-
)
|
390
|
-
|
391
|
-
def show_and_focus_main_window(self):
|
392
|
-
"""
|
393
|
-
Shows and focuses the first window.
|
394
|
-
|
395
|
-
Examples
|
396
|
-
--------
|
397
|
-
```python
|
398
|
-
app = Pyloid(app_name="Pyloid-App")
|
399
|
-
app.show_and_focus_main_window()
|
400
|
-
```
|
401
|
-
"""
|
402
|
-
if self.windows:
|
403
|
-
main_window = self.windows[0]
|
404
|
-
main_window._window.show()
|
405
|
-
main_window._window.activateWindow()
|
406
|
-
main_window._window.raise_()
|
407
|
-
main_window._window.setWindowState(
|
408
|
-
main_window._window.windowState() & ~Qt.WindowMinimized
|
409
|
-
| Qt.WindowActive
|
410
|
-
)
|
411
|
-
|
412
|
-
def close_all_windows(self):
|
413
|
-
"""
|
414
|
-
Closes all windows.
|
415
|
-
|
416
|
-
Examples
|
417
|
-
--------
|
418
|
-
```python
|
419
|
-
app = Pyloid(app_name="Pyloid-App")
|
420
|
-
app.close_all_windows()
|
421
|
-
```
|
422
|
-
"""
|
423
|
-
for window in self.windows:
|
424
|
-
window._window.close()
|
425
|
-
|
426
|
-
def quit(self):
|
427
|
-
"""
|
428
|
-
Quits the application.
|
429
|
-
|
430
|
-
Examples
|
431
|
-
--------
|
432
|
-
```python
|
433
|
-
app = Pyloid(app_name="Pyloid-App")
|
434
|
-
app.quit()
|
435
|
-
```
|
436
|
-
"""
|
437
|
-
|
438
|
-
# 윈도우 정리
|
439
|
-
for window in self.windows:
|
440
|
-
window._window.close()
|
441
|
-
window.web_page.deleteLater()
|
442
|
-
window.web_view.deleteLater()
|
443
|
-
|
444
|
-
QApplication.quit()
|
445
|
-
|
446
|
-
###########################################################################################
|
447
|
-
# Window management in the app (ID required)
|
448
|
-
###########################################################################################
|
449
|
-
def get_window_by_id(self, window_id: str) -> Optional[BrowserWindow]:
|
450
|
-
"""
|
451
|
-
Returns the window with the given ID.
|
452
|
-
|
453
|
-
Parameters
|
454
|
-
----------
|
455
|
-
window_id : str
|
456
|
-
The ID of the window to find
|
457
|
-
|
458
|
-
Returns
|
459
|
-
-------
|
460
|
-
Optional[BrowserWindow]
|
461
|
-
The window object with the given ID. Returns None if the window is not found.
|
462
|
-
|
463
|
-
Examples
|
464
|
-
--------
|
465
|
-
```python
|
466
|
-
app = Pyloid(app_name="Pyloid-App")
|
467
|
-
|
468
|
-
window = app.get_window_by_id("123e4567-e89b-12d3-a456-426614174000")
|
469
|
-
|
470
|
-
if window:
|
471
|
-
print("Window found:", window)
|
472
|
-
```
|
473
|
-
"""
|
474
|
-
for window in self.windows:
|
475
|
-
if window.id == window_id:
|
476
|
-
return window
|
477
|
-
return None
|
478
|
-
|
479
|
-
def hide_window_by_id(self, window_id: str):
|
480
|
-
"""
|
481
|
-
Hides the window with the given ID.
|
482
|
-
|
483
|
-
Parameters
|
484
|
-
----------
|
485
|
-
window_id : str
|
486
|
-
The ID of the window to hide
|
487
|
-
|
488
|
-
Examples
|
489
|
-
--------
|
490
|
-
```python
|
491
|
-
app = Pyloid(app_name="Pyloid-App")
|
492
|
-
|
493
|
-
window = app.create_window(title="pyloid-window")
|
494
|
-
|
495
|
-
app.hide_window_by_id(window.id)
|
496
|
-
```
|
497
|
-
"""
|
498
|
-
window = self.get_window_by_id(window_id)
|
499
|
-
if window:
|
500
|
-
window.hide()
|
501
|
-
|
502
|
-
def show_window_by_id(self, window_id: str):
|
503
|
-
"""
|
504
|
-
Shows and focuses the window with the given ID.
|
505
|
-
|
506
|
-
Parameters
|
507
|
-
----------
|
508
|
-
window_id : str
|
509
|
-
The ID of the window to show
|
510
|
-
|
511
|
-
Examples
|
512
|
-
--------
|
513
|
-
```python
|
514
|
-
app = Pyloid(app_name="Pyloid-App")
|
515
|
-
|
516
|
-
window = app.create_window(title="pyloid-window")
|
517
|
-
|
518
|
-
app.show_window_by_id(window.id)
|
519
|
-
```
|
520
|
-
"""
|
521
|
-
window = self.get_window_by_id(window_id)
|
522
|
-
if window:
|
523
|
-
window._window.show()
|
524
|
-
window._window.activateWindow()
|
525
|
-
window._window.raise_()
|
526
|
-
window._window.setWindowState(
|
527
|
-
window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
|
528
|
-
)
|
529
|
-
|
530
|
-
def close_window_by_id(self, window_id: str):
|
531
|
-
"""
|
532
|
-
Closes the window with the given ID.
|
533
|
-
|
534
|
-
Parameters
|
535
|
-
----------
|
536
|
-
window_id : str
|
537
|
-
The ID of the window to close
|
538
|
-
|
539
|
-
Examples
|
540
|
-
--------
|
541
|
-
```python
|
542
|
-
app = Pyloid(app_name="Pyloid-App")
|
543
|
-
|
544
|
-
window = app.create_window(title="pyloid-window")
|
545
|
-
|
546
|
-
app.close_window_by_id(window.id)
|
547
|
-
```
|
548
|
-
"""
|
549
|
-
window = self.get_window_by_id(window_id)
|
550
|
-
if window:
|
551
|
-
window._window.close()
|
552
|
-
|
553
|
-
def toggle_fullscreen_by_id(self, window_id: str):
|
554
|
-
"""
|
555
|
-
Toggles fullscreen mode for the window with the given ID.
|
556
|
-
|
557
|
-
Parameters
|
558
|
-
----------
|
559
|
-
window_id : str
|
560
|
-
The ID of the window to toggle fullscreen mode
|
561
|
-
|
562
|
-
Examples
|
563
|
-
--------
|
564
|
-
```python
|
565
|
-
app = Pyloid(app_name="Pyloid-App")
|
566
|
-
|
567
|
-
window = app.create_window(title="pyloid-window")
|
568
|
-
|
569
|
-
app.toggle_fullscreen_by_id(window.id)
|
570
|
-
```
|
571
|
-
"""
|
572
|
-
window = self.get_window_by_id(window_id)
|
573
|
-
window.toggle_fullscreen()
|
574
|
-
|
575
|
-
def minimize_window_by_id(self, window_id: str):
|
576
|
-
"""
|
577
|
-
Minimizes the window with the given ID.
|
578
|
-
|
579
|
-
Parameters
|
580
|
-
----------
|
581
|
-
window_id : str
|
582
|
-
The ID of the window to minimize
|
583
|
-
|
584
|
-
Examples
|
585
|
-
--------
|
586
|
-
```python
|
587
|
-
app = Pyloid(app_name="Pyloid-App")
|
588
|
-
|
589
|
-
window = app.create_window(title="pyloid-window")
|
590
|
-
|
591
|
-
app.minimize_window_by_id(window.id)
|
592
|
-
```
|
593
|
-
"""
|
594
|
-
window = self.get_window_by_id(window_id)
|
595
|
-
if window:
|
596
|
-
window.minimize()
|
597
|
-
|
598
|
-
def maximize_window_by_id(self, window_id: str):
|
599
|
-
"""
|
600
|
-
Maximizes the window with the given ID.
|
601
|
-
|
602
|
-
Parameters
|
603
|
-
----------
|
604
|
-
window_id : str
|
605
|
-
The ID of the window to maximize
|
606
|
-
|
607
|
-
Examples
|
608
|
-
--------
|
609
|
-
```python
|
610
|
-
app = Pyloid(app_name="Pyloid-App")
|
611
|
-
|
612
|
-
window = app.create_window(title="pyloid-window")
|
613
|
-
|
614
|
-
app.maximize_window_by_id(window.id)
|
615
|
-
```
|
616
|
-
"""
|
617
|
-
window = self.get_window_by_id(window_id)
|
618
|
-
if window:
|
619
|
-
window.maximize()
|
620
|
-
|
621
|
-
def unmaximize_window_by_id(self, window_id: str):
|
622
|
-
"""
|
623
|
-
Unmaximizes the window with the given ID.
|
624
|
-
|
625
|
-
Parameters
|
626
|
-
----------
|
627
|
-
window_id : str
|
628
|
-
The ID of the window to unmaximize
|
629
|
-
|
630
|
-
Examples
|
631
|
-
--------
|
632
|
-
```python
|
633
|
-
app = Pyloid(app_name="Pyloid-App")
|
634
|
-
|
635
|
-
window = app.create_window(title="pyloid-window")
|
636
|
-
|
637
|
-
app.unmaximize_window_by_id(window.id)
|
638
|
-
```
|
639
|
-
"""
|
640
|
-
window = self.get_window_by_id(window_id)
|
641
|
-
if window:
|
642
|
-
window.unmaximize()
|
643
|
-
|
644
|
-
def capture_window_by_id(self, window_id: str, save_path: str) -> Optional[str]:
|
645
|
-
"""
|
646
|
-
Captures the specified window.
|
647
|
-
|
648
|
-
Parameters
|
649
|
-
----------
|
650
|
-
window_id : str
|
651
|
-
The ID of the window to capture
|
652
|
-
save_path : str
|
653
|
-
The path to save the captured image. If not specified, it will be saved in the current directory.
|
654
|
-
|
655
|
-
Returns
|
656
|
-
-------
|
657
|
-
Optional[str]
|
658
|
-
The path of the saved image. Returns None if the window is not found or an error occurs.
|
659
|
-
|
660
|
-
Examples
|
661
|
-
--------
|
662
|
-
```python
|
663
|
-
app = Pyloid(app_name="Pyloid-App")
|
664
|
-
|
665
|
-
window = app.create_window(title="pyloid-window")
|
666
|
-
|
667
|
-
image_path = app.capture_window_by_id(window.id, "save/image.png")
|
668
|
-
|
669
|
-
if image_path:
|
670
|
-
print("Image saved at:", image_path)
|
671
|
-
```
|
672
|
-
"""
|
673
|
-
try:
|
674
|
-
window = self.get_window_by_id(window_id)
|
675
|
-
if not window:
|
676
|
-
print(f"Cannot find window with the specified ID: {window_id}")
|
677
|
-
return None
|
678
|
-
|
679
|
-
# Capture window
|
680
|
-
screenshot = window._window.grab()
|
681
|
-
|
682
|
-
# Save image
|
683
|
-
screenshot.save(save_path)
|
684
|
-
return save_path
|
685
|
-
except Exception as e:
|
686
|
-
print(f"Error occurred while capturing the window: {e}")
|
687
|
-
return None
|
688
|
-
|
689
|
-
###########################################################################################
|
690
|
-
# Tray
|
691
|
-
###########################################################################################
|
692
|
-
def set_tray_icon(self, tray_icon_path: str):
|
693
|
-
"""
|
694
|
-
Dynamically sets the tray icon.
|
695
|
-
Can be called while the application is running, and changes are applied immediately.
|
696
|
-
|
697
|
-
Parameters
|
698
|
-
----------
|
699
|
-
tray_icon_path : str
|
700
|
-
The path of the new tray icon file
|
701
|
-
|
702
|
-
Examples
|
703
|
-
--------
|
704
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
705
|
-
>>> app.set_tray_icon("icons/icon.png")
|
706
|
-
"""
|
707
|
-
# Stop and remove existing animation timer if present
|
708
|
-
if hasattr(self, "animation_timer") and self.animation_timer is not None:
|
709
|
-
self.animation_timer.stop()
|
710
|
-
self.animation_timer.deleteLater()
|
711
|
-
self.animation_timer = None
|
712
|
-
|
713
|
-
# Remove existing icon frames
|
714
|
-
if hasattr(self, "icon_frames"):
|
715
|
-
self.icon_frames = []
|
716
|
-
|
717
|
-
# Set new icon
|
718
|
-
self.tray_icon = QIcon(tray_icon_path)
|
719
|
-
|
720
|
-
if not hasattr(self, "tray"):
|
721
|
-
self._init_tray()
|
722
|
-
else:
|
723
|
-
self.tray.setIcon(self.tray_icon)
|
724
|
-
|
725
|
-
def set_tray_menu_items(
|
726
|
-
self, tray_menu_items: List[Dict[str, Union[str, Callable]]]
|
727
|
-
):
|
728
|
-
"""
|
729
|
-
Dynamically sets the tray menu items.
|
730
|
-
Can be called while the application is running, and changes are applied immediately.
|
731
|
-
|
732
|
-
Parameters
|
733
|
-
----------
|
734
|
-
tray_menu_items : List[Dict[str, Union[str, Callable]]]
|
735
|
-
The list of new tray menu items
|
736
|
-
|
737
|
-
Examples
|
738
|
-
--------
|
739
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
740
|
-
>>> menu_items = [
|
741
|
-
>>> {"label": "Open", "callback": lambda: print("Open clicked")},
|
742
|
-
>>> {"label": "Exit", "callback": app.quit}
|
743
|
-
>>> ]
|
744
|
-
>>> app.set_tray_menu_items(menu_items)
|
745
|
-
"""
|
746
|
-
self.tray_menu_items = tray_menu_items
|
747
|
-
if not hasattr(self, "tray"):
|
748
|
-
self._init_tray()
|
749
|
-
self._update_tray_menu()
|
750
|
-
|
751
|
-
def _init_tray(self):
|
752
|
-
"""Initializes the tray icon."""
|
753
|
-
self.tray = QSystemTrayIcon(self)
|
754
|
-
if self.tray_icon:
|
755
|
-
self.tray.setIcon(self.tray_icon)
|
756
|
-
else:
|
757
|
-
print("Icon and tray icon have not been set.")
|
758
|
-
if self.tray_menu_items:
|
759
|
-
pass
|
760
|
-
else:
|
761
|
-
self.tray.setContextMenu(QMenu())
|
762
|
-
self.tray.show()
|
763
|
-
|
764
|
-
def _update_tray_menu(self):
|
765
|
-
"""Updates the tray menu."""
|
766
|
-
tray_menu = self.tray.contextMenu()
|
767
|
-
tray_menu.clear()
|
768
|
-
for item in self.tray_menu_items:
|
769
|
-
action = QAction(item["label"], self)
|
770
|
-
action.triggered.connect(item["callback"])
|
771
|
-
tray_menu.addAction(action)
|
772
|
-
|
773
|
-
def _tray_activated(self, reason):
|
774
|
-
"""Handles events when the tray icon is activated."""
|
775
|
-
reason_enum = QSystemTrayIcon.ActivationReason(reason)
|
776
|
-
|
777
|
-
if reason_enum in self.tray_actions:
|
778
|
-
self.tray_actions[reason_enum]()
|
779
|
-
|
780
|
-
def set_tray_actions(self, actions: Dict[TrayEvent, Callable]):
|
781
|
-
"""
|
782
|
-
Dynamically sets the actions for tray icon activation.
|
783
|
-
Can be called while the application is running, and changes are applied immediately.
|
784
|
-
|
785
|
-
Parameters
|
786
|
-
----------
|
787
|
-
actions: Dict[TrayEvent, Callable]
|
788
|
-
Dictionary with TrayEvent enum values as keys and corresponding callback functions as values
|
789
|
-
|
790
|
-
Examples
|
791
|
-
--------
|
792
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
793
|
-
>>> app.set_tray_actions(
|
794
|
-
>>> {
|
795
|
-
>>> TrayEvent.DoubleClick: lambda: print("Tray icon was double-clicked."),
|
796
|
-
>>> TrayEvent.MiddleClick: lambda: print("Tray icon was middle-clicked."),
|
797
|
-
>>> TrayEvent.RightClick: lambda: print("Tray icon was right-clicked."),
|
798
|
-
>>> TrayEvent.LeftClick: lambda: print("Tray icon was left-clicked."),
|
799
|
-
>>> }
|
800
|
-
>>> )
|
801
|
-
"""
|
802
|
-
if self.tray_actions:
|
803
|
-
self.tray.activated.disconnect() # Disconnect existing connections
|
804
|
-
|
805
|
-
self.tray_actions = actions
|
806
|
-
if not hasattr(self, "tray"):
|
807
|
-
self._init_tray()
|
808
|
-
|
809
|
-
self.tray.activated.connect(lambda reason: self._tray_activated(reason))
|
810
|
-
|
811
|
-
def show_notification(self, title: str, message: str):
|
812
|
-
"""
|
813
|
-
Displays a notification in the system tray.
|
814
|
-
Can be called while the application is running, and the notification is displayed immediately.
|
815
|
-
|
816
|
-
Parameters
|
817
|
-
----------
|
818
|
-
title : str
|
819
|
-
Notification title
|
820
|
-
message : str
|
821
|
-
Notification message
|
822
|
-
|
823
|
-
Examples
|
824
|
-
--------
|
825
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
826
|
-
>>> app.show_notification("Update Available", "A new update is available for download.")
|
827
|
-
"""
|
828
|
-
if not hasattr(self, "tray"):
|
829
|
-
self._init_tray() # Ensure the tray is initialized
|
830
|
-
|
831
|
-
self.tray.showMessage(title, message, QIcon(self.icon), 5000)
|
832
|
-
|
833
|
-
def _update_tray_icon(self):
|
834
|
-
"""
|
835
|
-
Updates the animation frame.
|
836
|
-
"""
|
837
|
-
if hasattr(self, "tray") and self.icon_frames:
|
838
|
-
self.tray.setIcon(self.icon_frames[self.current_frame])
|
839
|
-
self.current_frame = (self.current_frame + 1) % len(self.icon_frames)
|
840
|
-
|
841
|
-
def set_tray_icon_animation(self, icon_frames: List[str], interval: int = 200):
|
842
|
-
"""
|
843
|
-
Dynamically sets and starts the animation for the tray icon.
|
844
|
-
Can be called while the application is running, and changes are applied immediately.
|
845
|
-
|
846
|
-
Parameters
|
847
|
-
----------
|
848
|
-
icon_frames : list of str
|
849
|
-
List of animation frame image paths
|
850
|
-
interval : int, optional
|
851
|
-
Frame interval in milliseconds, default is 200
|
852
|
-
|
853
|
-
Examples
|
854
|
-
--------
|
855
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
856
|
-
>>> icon_frames = ["frame1.png", "frame2.png", "frame3.png"]
|
857
|
-
>>> app.set_tray_icon_animation(icon_frames, 100)
|
858
|
-
"""
|
859
|
-
if not hasattr(self, "tray"):
|
860
|
-
self._init_tray()
|
861
|
-
|
862
|
-
# Remove existing icon
|
863
|
-
if hasattr(self, "tray_icon"):
|
864
|
-
del self.tray_icon
|
865
|
-
|
866
|
-
# Stop and remove existing animation timer
|
867
|
-
if hasattr(self, "animation_timer") and self.animation_timer is not None:
|
868
|
-
self.animation_timer.stop()
|
869
|
-
self.animation_timer.deleteLater()
|
870
|
-
self.animation_timer = None
|
871
|
-
|
872
|
-
self.icon_frames = [QIcon(frame) for frame in icon_frames]
|
873
|
-
self.animation_interval = interval
|
874
|
-
self._start_tray_icon_animation()
|
875
|
-
|
876
|
-
def _start_tray_icon_animation(self):
|
877
|
-
"""
|
878
|
-
Starts the tray icon animation.
|
879
|
-
"""
|
880
|
-
if self.icon_frames:
|
881
|
-
if self.animation_timer is None:
|
882
|
-
self.animation_timer = QTimer(self)
|
883
|
-
self.animation_timer.timeout.connect(lambda: self._update_tray_icon())
|
884
|
-
self.animation_timer.start(self.animation_interval)
|
885
|
-
self.current_frame = 0
|
886
|
-
|
887
|
-
def set_tray_tooltip(self, message: str):
|
888
|
-
"""
|
889
|
-
Dynamically sets the tooltip for the tray icon.
|
890
|
-
Can be called while the application is running, and changes are applied immediately.
|
891
|
-
|
892
|
-
Parameters
|
893
|
-
----------
|
894
|
-
message : str
|
895
|
-
New tooltip message
|
896
|
-
|
897
|
-
Examples
|
898
|
-
--------
|
899
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
900
|
-
>>> app.set_tray_tooltip("Pyloid is running")
|
901
|
-
"""
|
902
|
-
if not hasattr(self, "tray"):
|
903
|
-
self._init_tray()
|
904
|
-
self.tray.setToolTip(message)
|
905
|
-
|
906
|
-
def set_notification_callback(self, callback: Callable[[str], None]):
|
907
|
-
"""
|
908
|
-
Sets the callback function to be called when a notification is clicked.
|
909
|
-
|
910
|
-
Parameters
|
911
|
-
----------
|
912
|
-
callback : function
|
913
|
-
Callback function to be called when a notification is clicked
|
914
|
-
|
915
|
-
Examples
|
916
|
-
--------
|
917
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
918
|
-
>>> def on_notification_click():
|
919
|
-
>>> print("Notification clicked")
|
920
|
-
>>> app.set_notification_callback(on_notification_click)
|
921
|
-
"""
|
922
|
-
if not hasattr(self, "tray"):
|
923
|
-
self._init_tray()
|
924
|
-
self.tray.messageClicked.connect(callback)
|
925
|
-
|
926
|
-
###########################################################################################
|
927
|
-
# Monitor
|
928
|
-
###########################################################################################
|
929
|
-
def get_all_monitors(self) -> List[Monitor]:
|
930
|
-
"""
|
931
|
-
Returns information about all connected monitors.
|
932
|
-
|
933
|
-
Returns
|
934
|
-
-------
|
935
|
-
list of Monitor
|
936
|
-
List containing monitor information
|
937
|
-
|
938
|
-
Examples
|
939
|
-
--------
|
940
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
941
|
-
>>> monitors = app.get_all_monitors()
|
942
|
-
>>> for monitor in monitors:
|
943
|
-
>>> print(monitor.info())
|
944
|
-
"""
|
945
|
-
monitors = [
|
946
|
-
Monitor(index, screen) for index, screen in enumerate(self.screens())
|
947
|
-
]
|
948
|
-
return monitors
|
949
|
-
|
950
|
-
def get_primary_monitor(self) -> Monitor:
|
951
|
-
"""
|
952
|
-
Returns information about the primary monitor.
|
953
|
-
|
954
|
-
Returns
|
955
|
-
-------
|
956
|
-
Monitor
|
957
|
-
Primary monitor information
|
958
|
-
|
959
|
-
Examples
|
960
|
-
--------
|
961
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
962
|
-
>>> primary_monitor = app.get_primary_monitor()
|
963
|
-
>>> print(primary_monitor.info())
|
964
|
-
"""
|
965
|
-
primary_monitor = self.screens()[0]
|
966
|
-
return Monitor(0, primary_monitor)
|
967
|
-
|
968
|
-
###########################################################################################
|
969
|
-
# Clipboard
|
970
|
-
###########################################################################################
|
971
|
-
def set_clipboard_text(self, text):
|
972
|
-
"""
|
973
|
-
Copies text to the clipboard.
|
974
|
-
|
975
|
-
This function copies the given text to the clipboard. The text copied to the clipboard can be pasted into other applications.
|
976
|
-
|
977
|
-
Parameters
|
978
|
-
----------
|
979
|
-
text : str
|
980
|
-
Text to copy to the clipboard
|
981
|
-
|
982
|
-
Examples
|
983
|
-
--------
|
984
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
985
|
-
>>> app.set_clipboard_text("Hello, World!")
|
986
|
-
"""
|
987
|
-
self.clipboard_class.setText(text, QClipboard.Clipboard)
|
988
|
-
|
989
|
-
def get_clipboard_text(self):
|
990
|
-
"""
|
991
|
-
Retrieves text from the clipboard.
|
992
|
-
|
993
|
-
This function returns the text stored in the clipboard. If there is no text in the clipboard, it may return an empty string.
|
994
|
-
|
995
|
-
Returns
|
996
|
-
-------
|
997
|
-
str
|
998
|
-
Text stored in the clipboard
|
999
|
-
|
1000
|
-
Examples
|
1001
|
-
--------
|
1002
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1003
|
-
>>> text = app.get_clipboard_text()
|
1004
|
-
>>> print(text)
|
1005
|
-
Hello, World!
|
1006
|
-
"""
|
1007
|
-
return self.clipboard_class.text()
|
1008
|
-
|
1009
|
-
def set_clipboard_image(self, image: Union[str, bytes, os.PathLike]):
|
1010
|
-
"""
|
1011
|
-
Copies an image to the clipboard.
|
1012
|
-
|
1013
|
-
This function copies the given image file to the clipboard. The image copied to the clipboard can be pasted into other applications.
|
1014
|
-
|
1015
|
-
Parameters
|
1016
|
-
----------
|
1017
|
-
image : Union[str, bytes, os.PathLike]
|
1018
|
-
Path to the image file to copy to the clipboard
|
1019
|
-
|
1020
|
-
Examples
|
1021
|
-
--------
|
1022
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1023
|
-
>>> app.set_clipboard_image("/path/to/image.png")
|
1024
|
-
"""
|
1025
|
-
self.clipboard_class.setImage(QImage(image), QClipboard.Clipboard)
|
1026
|
-
|
1027
|
-
def get_clipboard_image(self):
|
1028
|
-
"""
|
1029
|
-
Retrieves an image from the clipboard.
|
1030
|
-
|
1031
|
-
This function returns the image stored in the clipboard. If there is no image in the clipboard, it may return None.
|
1032
|
-
|
1033
|
-
Returns
|
1034
|
-
-------
|
1035
|
-
QImage
|
1036
|
-
QImage object stored in the clipboard (None if no image)
|
1037
|
-
|
1038
|
-
Examples
|
1039
|
-
--------
|
1040
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1041
|
-
>>> image = app.get_clipboard_image()
|
1042
|
-
>>> if image is not None:
|
1043
|
-
>>> image.save("/path/to/save/image.png")
|
1044
|
-
"""
|
1045
|
-
return self.clipboard_class.image()
|
1046
|
-
|
1047
|
-
###########################################################################################
|
1048
|
-
# Autostart
|
1049
|
-
###########################################################################################
|
1050
|
-
def set_auto_start(self, enable: bool):
|
1051
|
-
"""
|
1052
|
-
Sets the application to start automatically at system startup. (set_auto_start(True) only works in production environment)
|
1053
|
-
True only works in production environment.
|
1054
|
-
False works in all environments.
|
1055
|
-
|
1056
|
-
Parameters
|
1057
|
-
----------
|
1058
|
-
enable : bool
|
1059
|
-
True to enable auto start, False to disable
|
1060
|
-
|
1061
|
-
Returns
|
1062
|
-
-------
|
1063
|
-
bool or None
|
1064
|
-
True if auto start is successfully set, False if disabled, None if trying to enable in non-production environment
|
1065
|
-
|
1066
|
-
Examples
|
1067
|
-
--------
|
1068
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1069
|
-
>>> app.set_auto_start(True)
|
1070
|
-
True
|
1071
|
-
"""
|
1072
|
-
if not enable:
|
1073
|
-
self.auto_start.set_auto_start(False)
|
1074
|
-
return False
|
1075
|
-
|
1076
|
-
if is_production():
|
1077
|
-
if enable:
|
1078
|
-
self.auto_start.set_auto_start(True)
|
1079
|
-
return True
|
1080
|
-
else:
|
1081
|
-
print(
|
1082
|
-
"\033[93mset_auto_start(True) is not supported in non-production environment\033[0m"
|
1083
|
-
)
|
1084
|
-
return None
|
1085
|
-
|
1086
|
-
def is_auto_start(self):
|
1087
|
-
"""
|
1088
|
-
Checks if the application is set to start automatically at system startup.
|
1089
|
-
|
1090
|
-
Returns
|
1091
|
-
-------
|
1092
|
-
bool
|
1093
|
-
True if auto start is enabled, False otherwise
|
1094
|
-
|
1095
|
-
Examples
|
1096
|
-
--------
|
1097
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1098
|
-
>>> auto_start_enabled = app.is_auto_start()
|
1099
|
-
>>> print(auto_start_enabled)
|
1100
|
-
True
|
1101
|
-
"""
|
1102
|
-
return self.auto_start.is_auto_start()
|
1103
|
-
|
1104
|
-
###########################################################################################
|
1105
|
-
# File watcher
|
1106
|
-
###########################################################################################
|
1107
|
-
def watch_file(self, file_path: str) -> bool:
|
1108
|
-
"""
|
1109
|
-
Adds a file to the watch list.
|
1110
|
-
|
1111
|
-
This function adds the specified file to the watch list. When the file is changed, the set callback function is called.
|
1112
|
-
|
1113
|
-
Parameters
|
1114
|
-
----------
|
1115
|
-
file_path : str
|
1116
|
-
Path to the file to watch
|
1117
|
-
|
1118
|
-
Returns
|
1119
|
-
-------
|
1120
|
-
bool
|
1121
|
-
True if the file is successfully added to the watch list, False otherwise
|
1122
|
-
|
1123
|
-
Examples
|
1124
|
-
--------
|
1125
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1126
|
-
>>> app.watch_file("/path/to/file.txt")
|
1127
|
-
True
|
1128
|
-
"""
|
1129
|
-
return self.file_watcher.add_path(file_path)
|
1130
|
-
|
1131
|
-
def watch_directory(self, dir_path: str) -> bool:
|
1132
|
-
"""
|
1133
|
-
Adds a directory to the watch list.
|
1134
|
-
|
1135
|
-
This function adds the specified directory to the watch list. When a file in the directory is changed, the set callback function is called.
|
1136
|
-
|
1137
|
-
Parameters
|
1138
|
-
----------
|
1139
|
-
dir_path : str
|
1140
|
-
Path to the directory to watch
|
1141
|
-
|
1142
|
-
Returns
|
1143
|
-
-------
|
1144
|
-
bool
|
1145
|
-
True if the directory is successfully added to the watch list, False otherwise
|
1146
|
-
|
1147
|
-
Examples
|
1148
|
-
--------
|
1149
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1150
|
-
>>> app.watch_directory("/path/to/directory")
|
1151
|
-
True
|
1152
|
-
"""
|
1153
|
-
return self.file_watcher.add_path(dir_path)
|
1154
|
-
|
1155
|
-
def stop_watching(self, path: str) -> bool:
|
1156
|
-
"""
|
1157
|
-
Removes a file or directory from the watch list.
|
1158
|
-
|
1159
|
-
This function removes the specified file or directory from the watch list.
|
1160
|
-
|
1161
|
-
Parameters
|
1162
|
-
----------
|
1163
|
-
path : str
|
1164
|
-
Path to the file or directory to stop watching
|
1165
|
-
|
1166
|
-
Returns
|
1167
|
-
-------
|
1168
|
-
bool
|
1169
|
-
True if the path is successfully removed from the watch list, False otherwise
|
1170
|
-
|
1171
|
-
Examples
|
1172
|
-
--------
|
1173
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1174
|
-
>>> app.stop_watching("/path/to/file_or_directory")
|
1175
|
-
True
|
1176
|
-
"""
|
1177
|
-
return self.file_watcher.remove_path(path)
|
1178
|
-
|
1179
|
-
def get_watched_paths(self) -> List[str]:
|
1180
|
-
"""
|
1181
|
-
Returns all currently watched paths.
|
1182
|
-
|
1183
|
-
This function returns the paths of all files and directories currently being watched.
|
1184
|
-
|
1185
|
-
Returns
|
1186
|
-
-------
|
1187
|
-
List[str]
|
1188
|
-
List of all watched paths
|
1189
|
-
|
1190
|
-
Examples
|
1191
|
-
--------
|
1192
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1193
|
-
>>> app.get_watched_paths()
|
1194
|
-
['/path/to/file1.txt', '/path/to/directory']
|
1195
|
-
"""
|
1196
|
-
return self.file_watcher.get_watched_paths()
|
1197
|
-
|
1198
|
-
def get_watched_files(self) -> List[str]:
|
1199
|
-
"""
|
1200
|
-
Returns all currently watched files.
|
1201
|
-
|
1202
|
-
This function returns the paths of all files currently being watched.
|
1203
|
-
|
1204
|
-
Returns
|
1205
|
-
-------
|
1206
|
-
List[str]
|
1207
|
-
List of all watched files
|
1208
|
-
|
1209
|
-
Examples
|
1210
|
-
--------
|
1211
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1212
|
-
>>> app.get_watched_files()
|
1213
|
-
['/path/to/file1.txt', '/path/to/file2.txt']
|
1214
|
-
"""
|
1215
|
-
return self.file_watcher.get_watched_files()
|
1216
|
-
|
1217
|
-
def get_watched_directories(self) -> List[str]:
|
1218
|
-
"""
|
1219
|
-
Returns all currently watched directories.
|
1220
|
-
|
1221
|
-
This function returns the paths of all directories currently being watched.
|
1222
|
-
|
1223
|
-
Returns
|
1224
|
-
-------
|
1225
|
-
List[str]
|
1226
|
-
List of all watched directories
|
1227
|
-
|
1228
|
-
Examples
|
1229
|
-
--------
|
1230
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1231
|
-
>>> app.get_watched_directories()
|
1232
|
-
['/path/to/directory1', '/path/to/directory2']
|
1233
|
-
"""
|
1234
|
-
return self.file_watcher.get_watched_directories()
|
1235
|
-
|
1236
|
-
def remove_all_watched_paths(self) -> None:
|
1237
|
-
"""
|
1238
|
-
Removes all paths from the watch list.
|
1239
|
-
|
1240
|
-
This function removes the paths of all files and directories from the watch list.
|
1241
|
-
|
1242
|
-
Returns
|
1243
|
-
-------
|
1244
|
-
None
|
1245
|
-
|
1246
|
-
Examples
|
1247
|
-
--------
|
1248
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1249
|
-
>>> app.remove_all_watched_paths()
|
1250
|
-
"""
|
1251
|
-
self.file_watcher.remove_all_paths()
|
1252
|
-
|
1253
|
-
def set_file_change_callback(self, callback: Callable[[str], None]) -> None:
|
1254
|
-
"""
|
1255
|
-
Sets the callback function to be called when a file is changed.
|
1256
|
-
|
1257
|
-
This function sets the callback function to be called when a file is changed.
|
1258
|
-
|
1259
|
-
Parameters
|
1260
|
-
----------
|
1261
|
-
callback : Callable[[str], None]
|
1262
|
-
Function to be called when a file is changed
|
1263
|
-
|
1264
|
-
Returns
|
1265
|
-
-------
|
1266
|
-
None
|
1267
|
-
|
1268
|
-
Examples
|
1269
|
-
--------
|
1270
|
-
>>> def on_file_change(file_path):
|
1271
|
-
>>> print(f"File changed: {file_path}")
|
1272
|
-
>>>
|
1273
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1274
|
-
>>> app.set_file_change_callback(on_file_change)
|
1275
|
-
"""
|
1276
|
-
self.file_watcher.file_changed.connect(callback)
|
1277
|
-
|
1278
|
-
def set_directory_change_callback(self, callback: Callable[[str], None]) -> None:
|
1279
|
-
"""
|
1280
|
-
Sets the callback function to be called when a directory is changed.
|
1281
|
-
|
1282
|
-
This function sets the callback function to be called when a directory is changed.
|
1283
|
-
|
1284
|
-
Parameters
|
1285
|
-
----------
|
1286
|
-
callback : Callable[[str], None]
|
1287
|
-
Function to be called when a directory is changed
|
1288
|
-
|
1289
|
-
Returns
|
1290
|
-
-------
|
1291
|
-
None
|
1292
|
-
|
1293
|
-
Examples
|
1294
|
-
--------
|
1295
|
-
>>> def on_directory_change(dir_path):
|
1296
|
-
>>> print(f"Directory changed: {dir_path}")
|
1297
|
-
>>>
|
1298
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1299
|
-
>>> app.set_directory_change_callback(on_directory_change)
|
1300
|
-
"""
|
1301
|
-
self.file_watcher.directory_changed.connect(callback)
|
1302
|
-
|
1303
|
-
###########################################################################################
|
1304
|
-
# File dialog
|
1305
|
-
###########################################################################################
|
1306
|
-
def open_file_dialog(
|
1307
|
-
self, dir: Optional[str] = None, filter: Optional[str] = None
|
1308
|
-
) -> Optional[str]:
|
1309
|
-
"""
|
1310
|
-
Opens a file dialog to select a file to open.
|
1311
|
-
|
1312
|
-
Parameters
|
1313
|
-
----------
|
1314
|
-
dir : str, optional
|
1315
|
-
The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
|
1316
|
-
filter : str, optional
|
1317
|
-
A string that specifies the file types that can be selected. For example, "Text Files (*.txt);;All Files (*)".
|
1318
|
-
|
1319
|
-
Returns
|
1320
|
-
-------
|
1321
|
-
Optional[str]
|
1322
|
-
The path of the selected file. Returns None if no file is selected.
|
1323
|
-
|
1324
|
-
Examples
|
1325
|
-
--------
|
1326
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1327
|
-
>>> file_path = app.open_file_dialog(dir="/home/user", filter="Text Files (*.txt)")
|
1328
|
-
>>> if file_path:
|
1329
|
-
>>> print("Selected file:", file_path)
|
1330
|
-
"""
|
1331
|
-
file_path, _ = QFileDialog.getOpenFileName(None, dir=dir, filter=filter)
|
1332
|
-
return file_path if file_path else None
|
1333
|
-
|
1334
|
-
def save_file_dialog(
|
1335
|
-
self, dir: Optional[str] = None, filter: Optional[str] = None
|
1336
|
-
) -> Optional[str]:
|
1337
|
-
"""
|
1338
|
-
Opens a file dialog to select a file to save.
|
1339
|
-
|
1340
|
-
Parameters
|
1341
|
-
----------
|
1342
|
-
dir : str, optional
|
1343
|
-
The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
|
1344
|
-
filter : str, optional
|
1345
|
-
A string that specifies the file types that can be saved. For example, "Text Files (*.txt);;All Files (*)".
|
1346
|
-
|
1347
|
-
Returns
|
1348
|
-
-------
|
1349
|
-
Optional[str]
|
1350
|
-
The path of the selected file. Returns None if no file is selected.
|
1351
|
-
|
1352
|
-
Examples
|
1353
|
-
--------
|
1354
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1355
|
-
>>> file_path = app.save_file_dialog(dir="/home/user", filter="Text Files (*.txt)")
|
1356
|
-
>>> if file_path:
|
1357
|
-
>>> print("File will be saved to:", file_path)
|
1358
|
-
"""
|
1359
|
-
file_path, _ = QFileDialog.getSaveFileName(None, dir=dir, filter=filter)
|
1360
|
-
return file_path if file_path else None
|
1361
|
-
|
1362
|
-
def select_directory_dialog(self, dir: Optional[str] = None) -> Optional[str]:
|
1363
|
-
"""
|
1364
|
-
Opens a dialog to select a directory.
|
1365
|
-
|
1366
|
-
Parameters
|
1367
|
-
----------
|
1368
|
-
dir : str, optional
|
1369
|
-
The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
|
1370
|
-
|
1371
|
-
Returns
|
1372
|
-
-------
|
1373
|
-
Optional[str]
|
1374
|
-
The path of the selected directory. Returns None if no directory is selected.
|
1375
|
-
|
1376
|
-
Examples
|
1377
|
-
--------
|
1378
|
-
>>> app = Pyloid(app_name="Pyloid-App")
|
1379
|
-
>>> directory_path = app.select_directory_dialog(dir="/home/user")
|
1380
|
-
>>> if directory_path:
|
1381
|
-
>>> print("Selected directory:", directory_path)
|
1382
|
-
"""
|
1383
|
-
directory_path = QFileDialog.getExistingDirectory(None, dir=dir)
|
1384
|
-
return directory_path if directory_path else None
|
1385
|
-
|
1386
|
-
def _handle_color_scheme_change(self):
|
1387
|
-
self.theme = (
|
1388
|
-
"dark"
|
1389
|
-
if self.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
1390
|
-
else "light"
|
1391
|
-
)
|
1392
|
-
|
1393
|
-
js_code = f"""
|
1394
|
-
document.dispatchEvent(new CustomEvent('themeChange', {{
|
1395
|
-
detail: {{ theme: "{self.theme}" }}
|
1396
|
-
}}));
|
1397
|
-
"""
|
1398
|
-
|
1399
|
-
# 모든 윈도우에 변경사항 적용
|
1400
|
-
for window in self.windows:
|
1401
|
-
window.web_view.page().runJavaScript(js_code)
|
1402
|
-
window.web_view.page().setBackgroundColor(
|
1403
|
-
Qt.GlobalColor.black if self.theme == "dark" else Qt.GlobalColor.white
|
1404
|
-
)
|
1
|
+
import sys
|
2
|
+
import os
|
3
|
+
from PySide6.QtWidgets import (
|
4
|
+
QApplication,
|
5
|
+
QSystemTrayIcon,
|
6
|
+
QMenu,
|
7
|
+
QFileDialog,
|
8
|
+
)
|
9
|
+
from PySide6.QtGui import (
|
10
|
+
QIcon,
|
11
|
+
QClipboard,
|
12
|
+
QImage,
|
13
|
+
QAction,
|
14
|
+
)
|
15
|
+
from PySide6.QtCore import Qt, Signal, QObject, QTimer, QEvent
|
16
|
+
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
17
|
+
from .api import PyloidAPI
|
18
|
+
from typing import List, Optional, Dict, Callable, Union, Literal
|
19
|
+
from PySide6.QtCore import qInstallMessageHandler
|
20
|
+
import signal
|
21
|
+
from .utils import is_production
|
22
|
+
from .monitor import Monitor
|
23
|
+
from .autostart import AutoStart
|
24
|
+
from .filewatcher import FileWatcher
|
25
|
+
import logging
|
26
|
+
from .browser_window import BrowserWindow
|
27
|
+
from .tray import TrayEvent
|
28
|
+
from PySide6.QtCore import QCoreApplication
|
29
|
+
from PySide6.QtCore import QRunnable, QThreadPool, Signal, QObject
|
30
|
+
import time
|
31
|
+
from .thread_pool import PyloidThreadPool
|
32
|
+
|
33
|
+
# for linux debug
|
34
|
+
os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = "/"
|
35
|
+
|
36
|
+
# for macos debug
|
37
|
+
logging.getLogger("Qt").setLevel(logging.ERROR)
|
38
|
+
|
39
|
+
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
|
40
|
+
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = (
|
41
|
+
"--enable-features=WebRTCPipeWireCapturer --ignore-certificate-errors --allow-insecure-localhost"
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
def custom_message_handler(mode, context, message):
|
46
|
+
if not hasattr(custom_message_handler, "vulkan_warning_shown") and (
|
47
|
+
("Failed to load vulkan" in message)
|
48
|
+
or ("No Vulkan library available" in message)
|
49
|
+
or ("Failed to create platform Vulkan instance" in message)
|
50
|
+
):
|
51
|
+
print(
|
52
|
+
"\033[93mPyloid Warning: Vulkan GPU API issue detected. Switching to software backend.\033[0m"
|
53
|
+
)
|
54
|
+
if "linux" in sys.platform:
|
55
|
+
os.environ["QT_QUICK_BACKEND"] = "software"
|
56
|
+
custom_message_handler.vulkan_warning_shown = True
|
57
|
+
|
58
|
+
if "Autofill.enable failed" in message:
|
59
|
+
print(
|
60
|
+
"\033[93mPyloid Warning: Autofill is not enabled in developer tools.\033[0m"
|
61
|
+
)
|
62
|
+
|
63
|
+
if "vulkan" not in message.lower() and "Autofill.enable failed" not in message:
|
64
|
+
print(message)
|
65
|
+
|
66
|
+
|
67
|
+
qInstallMessageHandler(custom_message_handler)
|
68
|
+
|
69
|
+
|
70
|
+
class _WindowController(QObject):
|
71
|
+
create_window_signal = Signal(
|
72
|
+
QApplication, str, int, int, int, int, bool, bool, bool, list
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
class Pyloid(QApplication):
|
77
|
+
def __init__(
|
78
|
+
self,
|
79
|
+
app_name,
|
80
|
+
single_instance=True,
|
81
|
+
):
|
82
|
+
"""
|
83
|
+
Initializes the Pyloid application.
|
84
|
+
|
85
|
+
Parameters
|
86
|
+
----------
|
87
|
+
app_name : str, required
|
88
|
+
The name of the application
|
89
|
+
single_instance : bool, optional
|
90
|
+
Whether to run the application as a single instance (default is True)
|
91
|
+
|
92
|
+
Examples
|
93
|
+
--------
|
94
|
+
```python
|
95
|
+
app = Pyloid(app_name="Pyloid-App")
|
96
|
+
|
97
|
+
window = app.create_window(title="New Window", width=1024, height=768)
|
98
|
+
window.show()
|
99
|
+
|
100
|
+
app.run()
|
101
|
+
```
|
102
|
+
"""
|
103
|
+
super().__init__(sys.argv)
|
104
|
+
|
105
|
+
self.windows = []
|
106
|
+
self.server = None
|
107
|
+
|
108
|
+
self.app_name = app_name
|
109
|
+
self.icon = None
|
110
|
+
|
111
|
+
self.clipboard_class = self.clipboard()
|
112
|
+
self.shortcuts = {}
|
113
|
+
|
114
|
+
self.single_instance = single_instance
|
115
|
+
if self.single_instance:
|
116
|
+
self._init_single_instance()
|
117
|
+
|
118
|
+
self.controller = _WindowController()
|
119
|
+
self.controller.create_window_signal.connect(
|
120
|
+
self._create_window_signal_function
|
121
|
+
)
|
122
|
+
|
123
|
+
self.file_watcher = FileWatcher()
|
124
|
+
|
125
|
+
self.tray_menu_items = []
|
126
|
+
self.tray_actions = {}
|
127
|
+
|
128
|
+
self.app_name = app_name
|
129
|
+
self.app_path = sys.executable
|
130
|
+
|
131
|
+
self.auto_start = AutoStart(self.app_name, self.app_path)
|
132
|
+
|
133
|
+
self.animation_timer = None
|
134
|
+
self.icon_frames = []
|
135
|
+
self.current_frame = 0
|
136
|
+
|
137
|
+
self.theme = (
|
138
|
+
"dark"
|
139
|
+
if self.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
140
|
+
else "light"
|
141
|
+
)
|
142
|
+
|
143
|
+
# Add color scheme tracking
|
144
|
+
self.styleHints().colorSchemeChanged.connect(self._handle_color_scheme_change)
|
145
|
+
|
146
|
+
# def set_theme(self, theme: Literal["system", "dark", "light"]):
|
147
|
+
# """
|
148
|
+
# 시스템의 테마를 설정합니다.
|
149
|
+
|
150
|
+
# Parameters
|
151
|
+
# ----------
|
152
|
+
# theme : Literal["system", "dark", "light"]
|
153
|
+
# 설정할 테마 ("system", "dark", "light" 중 하나)
|
154
|
+
|
155
|
+
# Examples
|
156
|
+
# --------
|
157
|
+
# >>> app = Pyloid(app_name="Pyloid-App")
|
158
|
+
# >>> app.set_theme("dark") # 다크 테마로 설정
|
159
|
+
# >>> app.set_theme("light") # 라이트 테마로 설정
|
160
|
+
# >>> app.set_theme("system") # 시스템 테마를 따름
|
161
|
+
# """
|
162
|
+
# self.theme = theme
|
163
|
+
|
164
|
+
# if theme == "system":
|
165
|
+
# # 시스템 테마를 light/dark 문자열로 변환
|
166
|
+
# system_theme = (
|
167
|
+
# "dark"
|
168
|
+
# if self.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
169
|
+
# else "light"
|
170
|
+
# )
|
171
|
+
# self._handle_color_scheme_change(system_theme)
|
172
|
+
# self.styleHints().colorSchemeChanged.connect(
|
173
|
+
# lambda: self._handle_color_scheme_change(system_theme)
|
174
|
+
# )
|
175
|
+
# else:
|
176
|
+
# # 기존 이벤트 연결 해제
|
177
|
+
# self.styleHints().colorSchemeChanged.disconnect(
|
178
|
+
# lambda: self._handle_color_scheme_change(self.theme)
|
179
|
+
# )
|
180
|
+
# self._handle_color_scheme_change(self.theme)
|
181
|
+
|
182
|
+
def set_icon(self, icon_path: str):
|
183
|
+
"""
|
184
|
+
Dynamically sets the application's icon.
|
185
|
+
|
186
|
+
This method can be called while the application is running.
|
187
|
+
The icon can be changed at any time and will be applied immediately.
|
188
|
+
|
189
|
+
Parameters
|
190
|
+
----------
|
191
|
+
icon_path : str
|
192
|
+
Path to the new icon file
|
193
|
+
|
194
|
+
Examples
|
195
|
+
--------
|
196
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
197
|
+
>>> app.set_icon("icons/icon.png")
|
198
|
+
"""
|
199
|
+
self.icon = QIcon(icon_path)
|
200
|
+
|
201
|
+
# Immediately update the icon for all open windows.
|
202
|
+
for window in self.windows:
|
203
|
+
window._window.setWindowIcon(self.icon)
|
204
|
+
|
205
|
+
def create_window(
|
206
|
+
self,
|
207
|
+
title: str,
|
208
|
+
width: int = 800,
|
209
|
+
height: int = 600,
|
210
|
+
x: int = 200,
|
211
|
+
y: int = 200,
|
212
|
+
frame: bool = True,
|
213
|
+
context_menu: bool = False,
|
214
|
+
dev_tools: bool = False,
|
215
|
+
js_apis: List[PyloidAPI] = [],
|
216
|
+
) -> BrowserWindow:
|
217
|
+
"""
|
218
|
+
Creates a new browser window.
|
219
|
+
|
220
|
+
Parameters
|
221
|
+
----------
|
222
|
+
title : str, required
|
223
|
+
Title of the window
|
224
|
+
width : int, optional
|
225
|
+
Width of the window (default is 800)
|
226
|
+
height : int, optional
|
227
|
+
Height of the window (default is 600)
|
228
|
+
x : int, optional
|
229
|
+
X coordinate of the window (default is 200)
|
230
|
+
y : int, optional
|
231
|
+
Y coordinate of the window (default is 200)
|
232
|
+
frame : bool, optional
|
233
|
+
Whether the window has a frame (default is True)
|
234
|
+
context_menu : bool, optional
|
235
|
+
Whether to use the context menu (default is False)
|
236
|
+
dev_tools : bool, optional
|
237
|
+
Whether to use developer tools (default is False)
|
238
|
+
js_apis : list of PyloidAPI, optional
|
239
|
+
List of JavaScript APIs to add to the window (default is an empty list)
|
240
|
+
|
241
|
+
Returns
|
242
|
+
-------
|
243
|
+
BrowserWindow
|
244
|
+
The created browser window object
|
245
|
+
|
246
|
+
Examples
|
247
|
+
--------
|
248
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
249
|
+
>>> window = app.create_window(title="New Window", width=1024, height=768)
|
250
|
+
>>> window.show()
|
251
|
+
"""
|
252
|
+
self.controller.create_window_signal.emit(
|
253
|
+
self,
|
254
|
+
title,
|
255
|
+
width,
|
256
|
+
height,
|
257
|
+
x,
|
258
|
+
y,
|
259
|
+
frame,
|
260
|
+
context_menu,
|
261
|
+
dev_tools,
|
262
|
+
js_apis,
|
263
|
+
)
|
264
|
+
return self.windows[-1]
|
265
|
+
|
266
|
+
def _create_window_signal_function(
|
267
|
+
self,
|
268
|
+
app,
|
269
|
+
title: str,
|
270
|
+
width: int,
|
271
|
+
height: int,
|
272
|
+
x: int,
|
273
|
+
y: int,
|
274
|
+
frame: bool,
|
275
|
+
context_menu: bool,
|
276
|
+
dev_tools: bool,
|
277
|
+
js_apis: List[PyloidAPI] = [],
|
278
|
+
) -> BrowserWindow:
|
279
|
+
"""Function to create a new browser window."""
|
280
|
+
window = BrowserWindow(
|
281
|
+
app,
|
282
|
+
title,
|
283
|
+
width,
|
284
|
+
height,
|
285
|
+
x,
|
286
|
+
y,
|
287
|
+
frame,
|
288
|
+
context_menu,
|
289
|
+
dev_tools,
|
290
|
+
js_apis,
|
291
|
+
)
|
292
|
+
self.windows.append(window)
|
293
|
+
return window
|
294
|
+
|
295
|
+
def run(self):
|
296
|
+
"""
|
297
|
+
Runs the application event loop.
|
298
|
+
|
299
|
+
This method starts the application's event loop, allowing the application to run.
|
300
|
+
|
301
|
+
This code should be written at the very end of the file.
|
302
|
+
|
303
|
+
Examples
|
304
|
+
--------
|
305
|
+
```python
|
306
|
+
app = Pyloid(app_name="Pyloid-App")
|
307
|
+
app.run()
|
308
|
+
```
|
309
|
+
"""
|
310
|
+
if is_production():
|
311
|
+
sys.exit(self.exec())
|
312
|
+
else:
|
313
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
314
|
+
sys.exit(self.exec())
|
315
|
+
|
316
|
+
def _init_single_instance(self):
|
317
|
+
"""Initializes the application as a single instance."""
|
318
|
+
socket = QLocalSocket()
|
319
|
+
socket.connectToServer(self.app_name)
|
320
|
+
if socket.waitForConnected(500):
|
321
|
+
# Another instance is already running
|
322
|
+
sys.exit(1)
|
323
|
+
|
324
|
+
# Create a new server
|
325
|
+
self.server = QLocalServer()
|
326
|
+
self.server.listen(self.app_name)
|
327
|
+
self.server.newConnection.connect(self._handle_new_connection)
|
328
|
+
|
329
|
+
def _handle_new_connection(self):
|
330
|
+
"""Handles new connections for the single instance server."""
|
331
|
+
pass
|
332
|
+
|
333
|
+
###########################################################################################
|
334
|
+
# App window
|
335
|
+
###########################################################################################
|
336
|
+
def get_windows(self) -> List[BrowserWindow]:
|
337
|
+
"""
|
338
|
+
Returns a list of all browser windows.
|
339
|
+
|
340
|
+
Returns
|
341
|
+
-------
|
342
|
+
List[BrowserWindow]
|
343
|
+
List of all browser windows
|
344
|
+
|
345
|
+
Examples
|
346
|
+
--------
|
347
|
+
```python
|
348
|
+
app = Pyloid(app_name="Pyloid-App")
|
349
|
+
windows = app.get_windows()
|
350
|
+
for window in windows:
|
351
|
+
print(window.get_id())
|
352
|
+
```
|
353
|
+
"""
|
354
|
+
return self.windows
|
355
|
+
|
356
|
+
def show_main_window(self):
|
357
|
+
"""
|
358
|
+
Shows and focuses the first window.
|
359
|
+
|
360
|
+
Examples
|
361
|
+
--------
|
362
|
+
```python
|
363
|
+
app = Pyloid(app_name="Pyloid-App")
|
364
|
+
app.show_main_window()
|
365
|
+
```
|
366
|
+
"""
|
367
|
+
if self.windows:
|
368
|
+
main_window = self.windows[0]
|
369
|
+
main_window._window.show()
|
370
|
+
|
371
|
+
def focus_main_window(self):
|
372
|
+
"""
|
373
|
+
Focuses the first window.
|
374
|
+
|
375
|
+
Examples
|
376
|
+
--------
|
377
|
+
```python
|
378
|
+
app = Pyloid(app_name="Pyloid-App")
|
379
|
+
app.focus_main_window()
|
380
|
+
```
|
381
|
+
"""
|
382
|
+
if self.windows:
|
383
|
+
main_window = self.windows[0]
|
384
|
+
main_window._window.activateWindow()
|
385
|
+
main_window._window.raise_()
|
386
|
+
main_window._window.setWindowState(
|
387
|
+
main_window._window.windowState() & ~Qt.WindowMinimized
|
388
|
+
| Qt.WindowActive
|
389
|
+
)
|
390
|
+
|
391
|
+
def show_and_focus_main_window(self):
|
392
|
+
"""
|
393
|
+
Shows and focuses the first window.
|
394
|
+
|
395
|
+
Examples
|
396
|
+
--------
|
397
|
+
```python
|
398
|
+
app = Pyloid(app_name="Pyloid-App")
|
399
|
+
app.show_and_focus_main_window()
|
400
|
+
```
|
401
|
+
"""
|
402
|
+
if self.windows:
|
403
|
+
main_window = self.windows[0]
|
404
|
+
main_window._window.show()
|
405
|
+
main_window._window.activateWindow()
|
406
|
+
main_window._window.raise_()
|
407
|
+
main_window._window.setWindowState(
|
408
|
+
main_window._window.windowState() & ~Qt.WindowMinimized
|
409
|
+
| Qt.WindowActive
|
410
|
+
)
|
411
|
+
|
412
|
+
def close_all_windows(self):
|
413
|
+
"""
|
414
|
+
Closes all windows.
|
415
|
+
|
416
|
+
Examples
|
417
|
+
--------
|
418
|
+
```python
|
419
|
+
app = Pyloid(app_name="Pyloid-App")
|
420
|
+
app.close_all_windows()
|
421
|
+
```
|
422
|
+
"""
|
423
|
+
for window in self.windows:
|
424
|
+
window._window.close()
|
425
|
+
|
426
|
+
def quit(self):
|
427
|
+
"""
|
428
|
+
Quits the application.
|
429
|
+
|
430
|
+
Examples
|
431
|
+
--------
|
432
|
+
```python
|
433
|
+
app = Pyloid(app_name="Pyloid-App")
|
434
|
+
app.quit()
|
435
|
+
```
|
436
|
+
"""
|
437
|
+
|
438
|
+
# 윈도우 정리
|
439
|
+
for window in self.windows:
|
440
|
+
window._window.close()
|
441
|
+
window.web_page.deleteLater()
|
442
|
+
window.web_view.deleteLater()
|
443
|
+
|
444
|
+
QApplication.quit()
|
445
|
+
|
446
|
+
###########################################################################################
|
447
|
+
# Window management in the app (ID required)
|
448
|
+
###########################################################################################
|
449
|
+
def get_window_by_id(self, window_id: str) -> Optional[BrowserWindow]:
|
450
|
+
"""
|
451
|
+
Returns the window with the given ID.
|
452
|
+
|
453
|
+
Parameters
|
454
|
+
----------
|
455
|
+
window_id : str
|
456
|
+
The ID of the window to find
|
457
|
+
|
458
|
+
Returns
|
459
|
+
-------
|
460
|
+
Optional[BrowserWindow]
|
461
|
+
The window object with the given ID. Returns None if the window is not found.
|
462
|
+
|
463
|
+
Examples
|
464
|
+
--------
|
465
|
+
```python
|
466
|
+
app = Pyloid(app_name="Pyloid-App")
|
467
|
+
|
468
|
+
window = app.get_window_by_id("123e4567-e89b-12d3-a456-426614174000")
|
469
|
+
|
470
|
+
if window:
|
471
|
+
print("Window found:", window)
|
472
|
+
```
|
473
|
+
"""
|
474
|
+
for window in self.windows:
|
475
|
+
if window.id == window_id:
|
476
|
+
return window
|
477
|
+
return None
|
478
|
+
|
479
|
+
def hide_window_by_id(self, window_id: str):
|
480
|
+
"""
|
481
|
+
Hides the window with the given ID.
|
482
|
+
|
483
|
+
Parameters
|
484
|
+
----------
|
485
|
+
window_id : str
|
486
|
+
The ID of the window to hide
|
487
|
+
|
488
|
+
Examples
|
489
|
+
--------
|
490
|
+
```python
|
491
|
+
app = Pyloid(app_name="Pyloid-App")
|
492
|
+
|
493
|
+
window = app.create_window(title="pyloid-window")
|
494
|
+
|
495
|
+
app.hide_window_by_id(window.id)
|
496
|
+
```
|
497
|
+
"""
|
498
|
+
window = self.get_window_by_id(window_id)
|
499
|
+
if window:
|
500
|
+
window.hide()
|
501
|
+
|
502
|
+
def show_window_by_id(self, window_id: str):
|
503
|
+
"""
|
504
|
+
Shows and focuses the window with the given ID.
|
505
|
+
|
506
|
+
Parameters
|
507
|
+
----------
|
508
|
+
window_id : str
|
509
|
+
The ID of the window to show
|
510
|
+
|
511
|
+
Examples
|
512
|
+
--------
|
513
|
+
```python
|
514
|
+
app = Pyloid(app_name="Pyloid-App")
|
515
|
+
|
516
|
+
window = app.create_window(title="pyloid-window")
|
517
|
+
|
518
|
+
app.show_window_by_id(window.id)
|
519
|
+
```
|
520
|
+
"""
|
521
|
+
window = self.get_window_by_id(window_id)
|
522
|
+
if window:
|
523
|
+
window._window.show()
|
524
|
+
window._window.activateWindow()
|
525
|
+
window._window.raise_()
|
526
|
+
window._window.setWindowState(
|
527
|
+
window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
|
528
|
+
)
|
529
|
+
|
530
|
+
def close_window_by_id(self, window_id: str):
|
531
|
+
"""
|
532
|
+
Closes the window with the given ID.
|
533
|
+
|
534
|
+
Parameters
|
535
|
+
----------
|
536
|
+
window_id : str
|
537
|
+
The ID of the window to close
|
538
|
+
|
539
|
+
Examples
|
540
|
+
--------
|
541
|
+
```python
|
542
|
+
app = Pyloid(app_name="Pyloid-App")
|
543
|
+
|
544
|
+
window = app.create_window(title="pyloid-window")
|
545
|
+
|
546
|
+
app.close_window_by_id(window.id)
|
547
|
+
```
|
548
|
+
"""
|
549
|
+
window = self.get_window_by_id(window_id)
|
550
|
+
if window:
|
551
|
+
window._window.close()
|
552
|
+
|
553
|
+
def toggle_fullscreen_by_id(self, window_id: str):
|
554
|
+
"""
|
555
|
+
Toggles fullscreen mode for the window with the given ID.
|
556
|
+
|
557
|
+
Parameters
|
558
|
+
----------
|
559
|
+
window_id : str
|
560
|
+
The ID of the window to toggle fullscreen mode
|
561
|
+
|
562
|
+
Examples
|
563
|
+
--------
|
564
|
+
```python
|
565
|
+
app = Pyloid(app_name="Pyloid-App")
|
566
|
+
|
567
|
+
window = app.create_window(title="pyloid-window")
|
568
|
+
|
569
|
+
app.toggle_fullscreen_by_id(window.id)
|
570
|
+
```
|
571
|
+
"""
|
572
|
+
window = self.get_window_by_id(window_id)
|
573
|
+
window.toggle_fullscreen()
|
574
|
+
|
575
|
+
def minimize_window_by_id(self, window_id: str):
|
576
|
+
"""
|
577
|
+
Minimizes the window with the given ID.
|
578
|
+
|
579
|
+
Parameters
|
580
|
+
----------
|
581
|
+
window_id : str
|
582
|
+
The ID of the window to minimize
|
583
|
+
|
584
|
+
Examples
|
585
|
+
--------
|
586
|
+
```python
|
587
|
+
app = Pyloid(app_name="Pyloid-App")
|
588
|
+
|
589
|
+
window = app.create_window(title="pyloid-window")
|
590
|
+
|
591
|
+
app.minimize_window_by_id(window.id)
|
592
|
+
```
|
593
|
+
"""
|
594
|
+
window = self.get_window_by_id(window_id)
|
595
|
+
if window:
|
596
|
+
window.minimize()
|
597
|
+
|
598
|
+
def maximize_window_by_id(self, window_id: str):
|
599
|
+
"""
|
600
|
+
Maximizes the window with the given ID.
|
601
|
+
|
602
|
+
Parameters
|
603
|
+
----------
|
604
|
+
window_id : str
|
605
|
+
The ID of the window to maximize
|
606
|
+
|
607
|
+
Examples
|
608
|
+
--------
|
609
|
+
```python
|
610
|
+
app = Pyloid(app_name="Pyloid-App")
|
611
|
+
|
612
|
+
window = app.create_window(title="pyloid-window")
|
613
|
+
|
614
|
+
app.maximize_window_by_id(window.id)
|
615
|
+
```
|
616
|
+
"""
|
617
|
+
window = self.get_window_by_id(window_id)
|
618
|
+
if window:
|
619
|
+
window.maximize()
|
620
|
+
|
621
|
+
def unmaximize_window_by_id(self, window_id: str):
|
622
|
+
"""
|
623
|
+
Unmaximizes the window with the given ID.
|
624
|
+
|
625
|
+
Parameters
|
626
|
+
----------
|
627
|
+
window_id : str
|
628
|
+
The ID of the window to unmaximize
|
629
|
+
|
630
|
+
Examples
|
631
|
+
--------
|
632
|
+
```python
|
633
|
+
app = Pyloid(app_name="Pyloid-App")
|
634
|
+
|
635
|
+
window = app.create_window(title="pyloid-window")
|
636
|
+
|
637
|
+
app.unmaximize_window_by_id(window.id)
|
638
|
+
```
|
639
|
+
"""
|
640
|
+
window = self.get_window_by_id(window_id)
|
641
|
+
if window:
|
642
|
+
window.unmaximize()
|
643
|
+
|
644
|
+
def capture_window_by_id(self, window_id: str, save_path: str) -> Optional[str]:
|
645
|
+
"""
|
646
|
+
Captures the specified window.
|
647
|
+
|
648
|
+
Parameters
|
649
|
+
----------
|
650
|
+
window_id : str
|
651
|
+
The ID of the window to capture
|
652
|
+
save_path : str
|
653
|
+
The path to save the captured image. If not specified, it will be saved in the current directory.
|
654
|
+
|
655
|
+
Returns
|
656
|
+
-------
|
657
|
+
Optional[str]
|
658
|
+
The path of the saved image. Returns None if the window is not found or an error occurs.
|
659
|
+
|
660
|
+
Examples
|
661
|
+
--------
|
662
|
+
```python
|
663
|
+
app = Pyloid(app_name="Pyloid-App")
|
664
|
+
|
665
|
+
window = app.create_window(title="pyloid-window")
|
666
|
+
|
667
|
+
image_path = app.capture_window_by_id(window.id, "save/image.png")
|
668
|
+
|
669
|
+
if image_path:
|
670
|
+
print("Image saved at:", image_path)
|
671
|
+
```
|
672
|
+
"""
|
673
|
+
try:
|
674
|
+
window = self.get_window_by_id(window_id)
|
675
|
+
if not window:
|
676
|
+
print(f"Cannot find window with the specified ID: {window_id}")
|
677
|
+
return None
|
678
|
+
|
679
|
+
# Capture window
|
680
|
+
screenshot = window._window.grab()
|
681
|
+
|
682
|
+
# Save image
|
683
|
+
screenshot.save(save_path)
|
684
|
+
return save_path
|
685
|
+
except Exception as e:
|
686
|
+
print(f"Error occurred while capturing the window: {e}")
|
687
|
+
return None
|
688
|
+
|
689
|
+
###########################################################################################
|
690
|
+
# Tray
|
691
|
+
###########################################################################################
|
692
|
+
def set_tray_icon(self, tray_icon_path: str):
|
693
|
+
"""
|
694
|
+
Dynamically sets the tray icon.
|
695
|
+
Can be called while the application is running, and changes are applied immediately.
|
696
|
+
|
697
|
+
Parameters
|
698
|
+
----------
|
699
|
+
tray_icon_path : str
|
700
|
+
The path of the new tray icon file
|
701
|
+
|
702
|
+
Examples
|
703
|
+
--------
|
704
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
705
|
+
>>> app.set_tray_icon("icons/icon.png")
|
706
|
+
"""
|
707
|
+
# Stop and remove existing animation timer if present
|
708
|
+
if hasattr(self, "animation_timer") and self.animation_timer is not None:
|
709
|
+
self.animation_timer.stop()
|
710
|
+
self.animation_timer.deleteLater()
|
711
|
+
self.animation_timer = None
|
712
|
+
|
713
|
+
# Remove existing icon frames
|
714
|
+
if hasattr(self, "icon_frames"):
|
715
|
+
self.icon_frames = []
|
716
|
+
|
717
|
+
# Set new icon
|
718
|
+
self.tray_icon = QIcon(tray_icon_path)
|
719
|
+
|
720
|
+
if not hasattr(self, "tray"):
|
721
|
+
self._init_tray()
|
722
|
+
else:
|
723
|
+
self.tray.setIcon(self.tray_icon)
|
724
|
+
|
725
|
+
def set_tray_menu_items(
|
726
|
+
self, tray_menu_items: List[Dict[str, Union[str, Callable]]]
|
727
|
+
):
|
728
|
+
"""
|
729
|
+
Dynamically sets the tray menu items.
|
730
|
+
Can be called while the application is running, and changes are applied immediately.
|
731
|
+
|
732
|
+
Parameters
|
733
|
+
----------
|
734
|
+
tray_menu_items : List[Dict[str, Union[str, Callable]]]
|
735
|
+
The list of new tray menu items
|
736
|
+
|
737
|
+
Examples
|
738
|
+
--------
|
739
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
740
|
+
>>> menu_items = [
|
741
|
+
>>> {"label": "Open", "callback": lambda: print("Open clicked")},
|
742
|
+
>>> {"label": "Exit", "callback": app.quit}
|
743
|
+
>>> ]
|
744
|
+
>>> app.set_tray_menu_items(menu_items)
|
745
|
+
"""
|
746
|
+
self.tray_menu_items = tray_menu_items
|
747
|
+
if not hasattr(self, "tray"):
|
748
|
+
self._init_tray()
|
749
|
+
self._update_tray_menu()
|
750
|
+
|
751
|
+
def _init_tray(self):
|
752
|
+
"""Initializes the tray icon."""
|
753
|
+
self.tray = QSystemTrayIcon(self)
|
754
|
+
if self.tray_icon:
|
755
|
+
self.tray.setIcon(self.tray_icon)
|
756
|
+
else:
|
757
|
+
print("Icon and tray icon have not been set.")
|
758
|
+
if self.tray_menu_items:
|
759
|
+
pass
|
760
|
+
else:
|
761
|
+
self.tray.setContextMenu(QMenu())
|
762
|
+
self.tray.show()
|
763
|
+
|
764
|
+
def _update_tray_menu(self):
|
765
|
+
"""Updates the tray menu."""
|
766
|
+
tray_menu = self.tray.contextMenu()
|
767
|
+
tray_menu.clear()
|
768
|
+
for item in self.tray_menu_items:
|
769
|
+
action = QAction(item["label"], self)
|
770
|
+
action.triggered.connect(item["callback"])
|
771
|
+
tray_menu.addAction(action)
|
772
|
+
|
773
|
+
def _tray_activated(self, reason):
|
774
|
+
"""Handles events when the tray icon is activated."""
|
775
|
+
reason_enum = QSystemTrayIcon.ActivationReason(reason)
|
776
|
+
|
777
|
+
if reason_enum in self.tray_actions:
|
778
|
+
self.tray_actions[reason_enum]()
|
779
|
+
|
780
|
+
def set_tray_actions(self, actions: Dict[TrayEvent, Callable]):
|
781
|
+
"""
|
782
|
+
Dynamically sets the actions for tray icon activation.
|
783
|
+
Can be called while the application is running, and changes are applied immediately.
|
784
|
+
|
785
|
+
Parameters
|
786
|
+
----------
|
787
|
+
actions: Dict[TrayEvent, Callable]
|
788
|
+
Dictionary with TrayEvent enum values as keys and corresponding callback functions as values
|
789
|
+
|
790
|
+
Examples
|
791
|
+
--------
|
792
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
793
|
+
>>> app.set_tray_actions(
|
794
|
+
>>> {
|
795
|
+
>>> TrayEvent.DoubleClick: lambda: print("Tray icon was double-clicked."),
|
796
|
+
>>> TrayEvent.MiddleClick: lambda: print("Tray icon was middle-clicked."),
|
797
|
+
>>> TrayEvent.RightClick: lambda: print("Tray icon was right-clicked."),
|
798
|
+
>>> TrayEvent.LeftClick: lambda: print("Tray icon was left-clicked."),
|
799
|
+
>>> }
|
800
|
+
>>> )
|
801
|
+
"""
|
802
|
+
if self.tray_actions:
|
803
|
+
self.tray.activated.disconnect() # Disconnect existing connections
|
804
|
+
|
805
|
+
self.tray_actions = actions
|
806
|
+
if not hasattr(self, "tray"):
|
807
|
+
self._init_tray()
|
808
|
+
|
809
|
+
self.tray.activated.connect(lambda reason: self._tray_activated(reason))
|
810
|
+
|
811
|
+
def show_notification(self, title: str, message: str):
|
812
|
+
"""
|
813
|
+
Displays a notification in the system tray.
|
814
|
+
Can be called while the application is running, and the notification is displayed immediately.
|
815
|
+
|
816
|
+
Parameters
|
817
|
+
----------
|
818
|
+
title : str
|
819
|
+
Notification title
|
820
|
+
message : str
|
821
|
+
Notification message
|
822
|
+
|
823
|
+
Examples
|
824
|
+
--------
|
825
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
826
|
+
>>> app.show_notification("Update Available", "A new update is available for download.")
|
827
|
+
"""
|
828
|
+
if not hasattr(self, "tray"):
|
829
|
+
self._init_tray() # Ensure the tray is initialized
|
830
|
+
|
831
|
+
self.tray.showMessage(title, message, QIcon(self.icon), 5000)
|
832
|
+
|
833
|
+
def _update_tray_icon(self):
|
834
|
+
"""
|
835
|
+
Updates the animation frame.
|
836
|
+
"""
|
837
|
+
if hasattr(self, "tray") and self.icon_frames:
|
838
|
+
self.tray.setIcon(self.icon_frames[self.current_frame])
|
839
|
+
self.current_frame = (self.current_frame + 1) % len(self.icon_frames)
|
840
|
+
|
841
|
+
def set_tray_icon_animation(self, icon_frames: List[str], interval: int = 200):
|
842
|
+
"""
|
843
|
+
Dynamically sets and starts the animation for the tray icon.
|
844
|
+
Can be called while the application is running, and changes are applied immediately.
|
845
|
+
|
846
|
+
Parameters
|
847
|
+
----------
|
848
|
+
icon_frames : list of str
|
849
|
+
List of animation frame image paths
|
850
|
+
interval : int, optional
|
851
|
+
Frame interval in milliseconds, default is 200
|
852
|
+
|
853
|
+
Examples
|
854
|
+
--------
|
855
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
856
|
+
>>> icon_frames = ["frame1.png", "frame2.png", "frame3.png"]
|
857
|
+
>>> app.set_tray_icon_animation(icon_frames, 100)
|
858
|
+
"""
|
859
|
+
if not hasattr(self, "tray"):
|
860
|
+
self._init_tray()
|
861
|
+
|
862
|
+
# Remove existing icon
|
863
|
+
if hasattr(self, "tray_icon"):
|
864
|
+
del self.tray_icon
|
865
|
+
|
866
|
+
# Stop and remove existing animation timer
|
867
|
+
if hasattr(self, "animation_timer") and self.animation_timer is not None:
|
868
|
+
self.animation_timer.stop()
|
869
|
+
self.animation_timer.deleteLater()
|
870
|
+
self.animation_timer = None
|
871
|
+
|
872
|
+
self.icon_frames = [QIcon(frame) for frame in icon_frames]
|
873
|
+
self.animation_interval = interval
|
874
|
+
self._start_tray_icon_animation()
|
875
|
+
|
876
|
+
def _start_tray_icon_animation(self):
|
877
|
+
"""
|
878
|
+
Starts the tray icon animation.
|
879
|
+
"""
|
880
|
+
if self.icon_frames:
|
881
|
+
if self.animation_timer is None:
|
882
|
+
self.animation_timer = QTimer(self)
|
883
|
+
self.animation_timer.timeout.connect(lambda: self._update_tray_icon())
|
884
|
+
self.animation_timer.start(self.animation_interval)
|
885
|
+
self.current_frame = 0
|
886
|
+
|
887
|
+
def set_tray_tooltip(self, message: str):
|
888
|
+
"""
|
889
|
+
Dynamically sets the tooltip for the tray icon.
|
890
|
+
Can be called while the application is running, and changes are applied immediately.
|
891
|
+
|
892
|
+
Parameters
|
893
|
+
----------
|
894
|
+
message : str
|
895
|
+
New tooltip message
|
896
|
+
|
897
|
+
Examples
|
898
|
+
--------
|
899
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
900
|
+
>>> app.set_tray_tooltip("Pyloid is running")
|
901
|
+
"""
|
902
|
+
if not hasattr(self, "tray"):
|
903
|
+
self._init_tray()
|
904
|
+
self.tray.setToolTip(message)
|
905
|
+
|
906
|
+
def set_notification_callback(self, callback: Callable[[str], None]):
|
907
|
+
"""
|
908
|
+
Sets the callback function to be called when a notification is clicked.
|
909
|
+
|
910
|
+
Parameters
|
911
|
+
----------
|
912
|
+
callback : function
|
913
|
+
Callback function to be called when a notification is clicked
|
914
|
+
|
915
|
+
Examples
|
916
|
+
--------
|
917
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
918
|
+
>>> def on_notification_click():
|
919
|
+
>>> print("Notification clicked")
|
920
|
+
>>> app.set_notification_callback(on_notification_click)
|
921
|
+
"""
|
922
|
+
if not hasattr(self, "tray"):
|
923
|
+
self._init_tray()
|
924
|
+
self.tray.messageClicked.connect(callback)
|
925
|
+
|
926
|
+
###########################################################################################
|
927
|
+
# Monitor
|
928
|
+
###########################################################################################
|
929
|
+
def get_all_monitors(self) -> List[Monitor]:
|
930
|
+
"""
|
931
|
+
Returns information about all connected monitors.
|
932
|
+
|
933
|
+
Returns
|
934
|
+
-------
|
935
|
+
list of Monitor
|
936
|
+
List containing monitor information
|
937
|
+
|
938
|
+
Examples
|
939
|
+
--------
|
940
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
941
|
+
>>> monitors = app.get_all_monitors()
|
942
|
+
>>> for monitor in monitors:
|
943
|
+
>>> print(monitor.info())
|
944
|
+
"""
|
945
|
+
monitors = [
|
946
|
+
Monitor(index, screen) for index, screen in enumerate(self.screens())
|
947
|
+
]
|
948
|
+
return monitors
|
949
|
+
|
950
|
+
def get_primary_monitor(self) -> Monitor:
|
951
|
+
"""
|
952
|
+
Returns information about the primary monitor.
|
953
|
+
|
954
|
+
Returns
|
955
|
+
-------
|
956
|
+
Monitor
|
957
|
+
Primary monitor information
|
958
|
+
|
959
|
+
Examples
|
960
|
+
--------
|
961
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
962
|
+
>>> primary_monitor = app.get_primary_monitor()
|
963
|
+
>>> print(primary_monitor.info())
|
964
|
+
"""
|
965
|
+
primary_monitor = self.screens()[0]
|
966
|
+
return Monitor(0, primary_monitor)
|
967
|
+
|
968
|
+
###########################################################################################
|
969
|
+
# Clipboard
|
970
|
+
###########################################################################################
|
971
|
+
def set_clipboard_text(self, text):
|
972
|
+
"""
|
973
|
+
Copies text to the clipboard.
|
974
|
+
|
975
|
+
This function copies the given text to the clipboard. The text copied to the clipboard can be pasted into other applications.
|
976
|
+
|
977
|
+
Parameters
|
978
|
+
----------
|
979
|
+
text : str
|
980
|
+
Text to copy to the clipboard
|
981
|
+
|
982
|
+
Examples
|
983
|
+
--------
|
984
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
985
|
+
>>> app.set_clipboard_text("Hello, World!")
|
986
|
+
"""
|
987
|
+
self.clipboard_class.setText(text, QClipboard.Clipboard)
|
988
|
+
|
989
|
+
def get_clipboard_text(self):
|
990
|
+
"""
|
991
|
+
Retrieves text from the clipboard.
|
992
|
+
|
993
|
+
This function returns the text stored in the clipboard. If there is no text in the clipboard, it may return an empty string.
|
994
|
+
|
995
|
+
Returns
|
996
|
+
-------
|
997
|
+
str
|
998
|
+
Text stored in the clipboard
|
999
|
+
|
1000
|
+
Examples
|
1001
|
+
--------
|
1002
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1003
|
+
>>> text = app.get_clipboard_text()
|
1004
|
+
>>> print(text)
|
1005
|
+
Hello, World!
|
1006
|
+
"""
|
1007
|
+
return self.clipboard_class.text()
|
1008
|
+
|
1009
|
+
def set_clipboard_image(self, image: Union[str, bytes, os.PathLike]):
|
1010
|
+
"""
|
1011
|
+
Copies an image to the clipboard.
|
1012
|
+
|
1013
|
+
This function copies the given image file to the clipboard. The image copied to the clipboard can be pasted into other applications.
|
1014
|
+
|
1015
|
+
Parameters
|
1016
|
+
----------
|
1017
|
+
image : Union[str, bytes, os.PathLike]
|
1018
|
+
Path to the image file to copy to the clipboard
|
1019
|
+
|
1020
|
+
Examples
|
1021
|
+
--------
|
1022
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1023
|
+
>>> app.set_clipboard_image("/path/to/image.png")
|
1024
|
+
"""
|
1025
|
+
self.clipboard_class.setImage(QImage(image), QClipboard.Clipboard)
|
1026
|
+
|
1027
|
+
def get_clipboard_image(self):
|
1028
|
+
"""
|
1029
|
+
Retrieves an image from the clipboard.
|
1030
|
+
|
1031
|
+
This function returns the image stored in the clipboard. If there is no image in the clipboard, it may return None.
|
1032
|
+
|
1033
|
+
Returns
|
1034
|
+
-------
|
1035
|
+
QImage
|
1036
|
+
QImage object stored in the clipboard (None if no image)
|
1037
|
+
|
1038
|
+
Examples
|
1039
|
+
--------
|
1040
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1041
|
+
>>> image = app.get_clipboard_image()
|
1042
|
+
>>> if image is not None:
|
1043
|
+
>>> image.save("/path/to/save/image.png")
|
1044
|
+
"""
|
1045
|
+
return self.clipboard_class.image()
|
1046
|
+
|
1047
|
+
###########################################################################################
|
1048
|
+
# Autostart
|
1049
|
+
###########################################################################################
|
1050
|
+
def set_auto_start(self, enable: bool):
|
1051
|
+
"""
|
1052
|
+
Sets the application to start automatically at system startup. (set_auto_start(True) only works in production environment)
|
1053
|
+
True only works in production environment.
|
1054
|
+
False works in all environments.
|
1055
|
+
|
1056
|
+
Parameters
|
1057
|
+
----------
|
1058
|
+
enable : bool
|
1059
|
+
True to enable auto start, False to disable
|
1060
|
+
|
1061
|
+
Returns
|
1062
|
+
-------
|
1063
|
+
bool or None
|
1064
|
+
True if auto start is successfully set, False if disabled, None if trying to enable in non-production environment
|
1065
|
+
|
1066
|
+
Examples
|
1067
|
+
--------
|
1068
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1069
|
+
>>> app.set_auto_start(True)
|
1070
|
+
True
|
1071
|
+
"""
|
1072
|
+
if not enable:
|
1073
|
+
self.auto_start.set_auto_start(False)
|
1074
|
+
return False
|
1075
|
+
|
1076
|
+
if is_production():
|
1077
|
+
if enable:
|
1078
|
+
self.auto_start.set_auto_start(True)
|
1079
|
+
return True
|
1080
|
+
else:
|
1081
|
+
print(
|
1082
|
+
"\033[93mset_auto_start(True) is not supported in non-production environment\033[0m"
|
1083
|
+
)
|
1084
|
+
return None
|
1085
|
+
|
1086
|
+
def is_auto_start(self):
|
1087
|
+
"""
|
1088
|
+
Checks if the application is set to start automatically at system startup.
|
1089
|
+
|
1090
|
+
Returns
|
1091
|
+
-------
|
1092
|
+
bool
|
1093
|
+
True if auto start is enabled, False otherwise
|
1094
|
+
|
1095
|
+
Examples
|
1096
|
+
--------
|
1097
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1098
|
+
>>> auto_start_enabled = app.is_auto_start()
|
1099
|
+
>>> print(auto_start_enabled)
|
1100
|
+
True
|
1101
|
+
"""
|
1102
|
+
return self.auto_start.is_auto_start()
|
1103
|
+
|
1104
|
+
###########################################################################################
|
1105
|
+
# File watcher
|
1106
|
+
###########################################################################################
|
1107
|
+
def watch_file(self, file_path: str) -> bool:
|
1108
|
+
"""
|
1109
|
+
Adds a file to the watch list.
|
1110
|
+
|
1111
|
+
This function adds the specified file to the watch list. When the file is changed, the set callback function is called.
|
1112
|
+
|
1113
|
+
Parameters
|
1114
|
+
----------
|
1115
|
+
file_path : str
|
1116
|
+
Path to the file to watch
|
1117
|
+
|
1118
|
+
Returns
|
1119
|
+
-------
|
1120
|
+
bool
|
1121
|
+
True if the file is successfully added to the watch list, False otherwise
|
1122
|
+
|
1123
|
+
Examples
|
1124
|
+
--------
|
1125
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1126
|
+
>>> app.watch_file("/path/to/file.txt")
|
1127
|
+
True
|
1128
|
+
"""
|
1129
|
+
return self.file_watcher.add_path(file_path)
|
1130
|
+
|
1131
|
+
def watch_directory(self, dir_path: str) -> bool:
|
1132
|
+
"""
|
1133
|
+
Adds a directory to the watch list.
|
1134
|
+
|
1135
|
+
This function adds the specified directory to the watch list. When a file in the directory is changed, the set callback function is called.
|
1136
|
+
|
1137
|
+
Parameters
|
1138
|
+
----------
|
1139
|
+
dir_path : str
|
1140
|
+
Path to the directory to watch
|
1141
|
+
|
1142
|
+
Returns
|
1143
|
+
-------
|
1144
|
+
bool
|
1145
|
+
True if the directory is successfully added to the watch list, False otherwise
|
1146
|
+
|
1147
|
+
Examples
|
1148
|
+
--------
|
1149
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1150
|
+
>>> app.watch_directory("/path/to/directory")
|
1151
|
+
True
|
1152
|
+
"""
|
1153
|
+
return self.file_watcher.add_path(dir_path)
|
1154
|
+
|
1155
|
+
def stop_watching(self, path: str) -> bool:
|
1156
|
+
"""
|
1157
|
+
Removes a file or directory from the watch list.
|
1158
|
+
|
1159
|
+
This function removes the specified file or directory from the watch list.
|
1160
|
+
|
1161
|
+
Parameters
|
1162
|
+
----------
|
1163
|
+
path : str
|
1164
|
+
Path to the file or directory to stop watching
|
1165
|
+
|
1166
|
+
Returns
|
1167
|
+
-------
|
1168
|
+
bool
|
1169
|
+
True if the path is successfully removed from the watch list, False otherwise
|
1170
|
+
|
1171
|
+
Examples
|
1172
|
+
--------
|
1173
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1174
|
+
>>> app.stop_watching("/path/to/file_or_directory")
|
1175
|
+
True
|
1176
|
+
"""
|
1177
|
+
return self.file_watcher.remove_path(path)
|
1178
|
+
|
1179
|
+
def get_watched_paths(self) -> List[str]:
|
1180
|
+
"""
|
1181
|
+
Returns all currently watched paths.
|
1182
|
+
|
1183
|
+
This function returns the paths of all files and directories currently being watched.
|
1184
|
+
|
1185
|
+
Returns
|
1186
|
+
-------
|
1187
|
+
List[str]
|
1188
|
+
List of all watched paths
|
1189
|
+
|
1190
|
+
Examples
|
1191
|
+
--------
|
1192
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1193
|
+
>>> app.get_watched_paths()
|
1194
|
+
['/path/to/file1.txt', '/path/to/directory']
|
1195
|
+
"""
|
1196
|
+
return self.file_watcher.get_watched_paths()
|
1197
|
+
|
1198
|
+
def get_watched_files(self) -> List[str]:
|
1199
|
+
"""
|
1200
|
+
Returns all currently watched files.
|
1201
|
+
|
1202
|
+
This function returns the paths of all files currently being watched.
|
1203
|
+
|
1204
|
+
Returns
|
1205
|
+
-------
|
1206
|
+
List[str]
|
1207
|
+
List of all watched files
|
1208
|
+
|
1209
|
+
Examples
|
1210
|
+
--------
|
1211
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1212
|
+
>>> app.get_watched_files()
|
1213
|
+
['/path/to/file1.txt', '/path/to/file2.txt']
|
1214
|
+
"""
|
1215
|
+
return self.file_watcher.get_watched_files()
|
1216
|
+
|
1217
|
+
def get_watched_directories(self) -> List[str]:
|
1218
|
+
"""
|
1219
|
+
Returns all currently watched directories.
|
1220
|
+
|
1221
|
+
This function returns the paths of all directories currently being watched.
|
1222
|
+
|
1223
|
+
Returns
|
1224
|
+
-------
|
1225
|
+
List[str]
|
1226
|
+
List of all watched directories
|
1227
|
+
|
1228
|
+
Examples
|
1229
|
+
--------
|
1230
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1231
|
+
>>> app.get_watched_directories()
|
1232
|
+
['/path/to/directory1', '/path/to/directory2']
|
1233
|
+
"""
|
1234
|
+
return self.file_watcher.get_watched_directories()
|
1235
|
+
|
1236
|
+
def remove_all_watched_paths(self) -> None:
|
1237
|
+
"""
|
1238
|
+
Removes all paths from the watch list.
|
1239
|
+
|
1240
|
+
This function removes the paths of all files and directories from the watch list.
|
1241
|
+
|
1242
|
+
Returns
|
1243
|
+
-------
|
1244
|
+
None
|
1245
|
+
|
1246
|
+
Examples
|
1247
|
+
--------
|
1248
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1249
|
+
>>> app.remove_all_watched_paths()
|
1250
|
+
"""
|
1251
|
+
self.file_watcher.remove_all_paths()
|
1252
|
+
|
1253
|
+
def set_file_change_callback(self, callback: Callable[[str], None]) -> None:
|
1254
|
+
"""
|
1255
|
+
Sets the callback function to be called when a file is changed.
|
1256
|
+
|
1257
|
+
This function sets the callback function to be called when a file is changed.
|
1258
|
+
|
1259
|
+
Parameters
|
1260
|
+
----------
|
1261
|
+
callback : Callable[[str], None]
|
1262
|
+
Function to be called when a file is changed
|
1263
|
+
|
1264
|
+
Returns
|
1265
|
+
-------
|
1266
|
+
None
|
1267
|
+
|
1268
|
+
Examples
|
1269
|
+
--------
|
1270
|
+
>>> def on_file_change(file_path):
|
1271
|
+
>>> print(f"File changed: {file_path}")
|
1272
|
+
>>>
|
1273
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1274
|
+
>>> app.set_file_change_callback(on_file_change)
|
1275
|
+
"""
|
1276
|
+
self.file_watcher.file_changed.connect(callback)
|
1277
|
+
|
1278
|
+
def set_directory_change_callback(self, callback: Callable[[str], None]) -> None:
|
1279
|
+
"""
|
1280
|
+
Sets the callback function to be called when a directory is changed.
|
1281
|
+
|
1282
|
+
This function sets the callback function to be called when a directory is changed.
|
1283
|
+
|
1284
|
+
Parameters
|
1285
|
+
----------
|
1286
|
+
callback : Callable[[str], None]
|
1287
|
+
Function to be called when a directory is changed
|
1288
|
+
|
1289
|
+
Returns
|
1290
|
+
-------
|
1291
|
+
None
|
1292
|
+
|
1293
|
+
Examples
|
1294
|
+
--------
|
1295
|
+
>>> def on_directory_change(dir_path):
|
1296
|
+
>>> print(f"Directory changed: {dir_path}")
|
1297
|
+
>>>
|
1298
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1299
|
+
>>> app.set_directory_change_callback(on_directory_change)
|
1300
|
+
"""
|
1301
|
+
self.file_watcher.directory_changed.connect(callback)
|
1302
|
+
|
1303
|
+
###########################################################################################
|
1304
|
+
# File dialog
|
1305
|
+
###########################################################################################
|
1306
|
+
def open_file_dialog(
|
1307
|
+
self, dir: Optional[str] = None, filter: Optional[str] = None
|
1308
|
+
) -> Optional[str]:
|
1309
|
+
"""
|
1310
|
+
Opens a file dialog to select a file to open.
|
1311
|
+
|
1312
|
+
Parameters
|
1313
|
+
----------
|
1314
|
+
dir : str, optional
|
1315
|
+
The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
|
1316
|
+
filter : str, optional
|
1317
|
+
A string that specifies the file types that can be selected. For example, "Text Files (*.txt);;All Files (*)".
|
1318
|
+
|
1319
|
+
Returns
|
1320
|
+
-------
|
1321
|
+
Optional[str]
|
1322
|
+
The path of the selected file. Returns None if no file is selected.
|
1323
|
+
|
1324
|
+
Examples
|
1325
|
+
--------
|
1326
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1327
|
+
>>> file_path = app.open_file_dialog(dir="/home/user", filter="Text Files (*.txt)")
|
1328
|
+
>>> if file_path:
|
1329
|
+
>>> print("Selected file:", file_path)
|
1330
|
+
"""
|
1331
|
+
file_path, _ = QFileDialog.getOpenFileName(None, dir=dir, filter=filter)
|
1332
|
+
return file_path if file_path else None
|
1333
|
+
|
1334
|
+
def save_file_dialog(
|
1335
|
+
self, dir: Optional[str] = None, filter: Optional[str] = None
|
1336
|
+
) -> Optional[str]:
|
1337
|
+
"""
|
1338
|
+
Opens a file dialog to select a file to save.
|
1339
|
+
|
1340
|
+
Parameters
|
1341
|
+
----------
|
1342
|
+
dir : str, optional
|
1343
|
+
The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
|
1344
|
+
filter : str, optional
|
1345
|
+
A string that specifies the file types that can be saved. For example, "Text Files (*.txt);;All Files (*)".
|
1346
|
+
|
1347
|
+
Returns
|
1348
|
+
-------
|
1349
|
+
Optional[str]
|
1350
|
+
The path of the selected file. Returns None if no file is selected.
|
1351
|
+
|
1352
|
+
Examples
|
1353
|
+
--------
|
1354
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1355
|
+
>>> file_path = app.save_file_dialog(dir="/home/user", filter="Text Files (*.txt)")
|
1356
|
+
>>> if file_path:
|
1357
|
+
>>> print("File will be saved to:", file_path)
|
1358
|
+
"""
|
1359
|
+
file_path, _ = QFileDialog.getSaveFileName(None, dir=dir, filter=filter)
|
1360
|
+
return file_path if file_path else None
|
1361
|
+
|
1362
|
+
def select_directory_dialog(self, dir: Optional[str] = None) -> Optional[str]:
|
1363
|
+
"""
|
1364
|
+
Opens a dialog to select a directory.
|
1365
|
+
|
1366
|
+
Parameters
|
1367
|
+
----------
|
1368
|
+
dir : str, optional
|
1369
|
+
The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
|
1370
|
+
|
1371
|
+
Returns
|
1372
|
+
-------
|
1373
|
+
Optional[str]
|
1374
|
+
The path of the selected directory. Returns None if no directory is selected.
|
1375
|
+
|
1376
|
+
Examples
|
1377
|
+
--------
|
1378
|
+
>>> app = Pyloid(app_name="Pyloid-App")
|
1379
|
+
>>> directory_path = app.select_directory_dialog(dir="/home/user")
|
1380
|
+
>>> if directory_path:
|
1381
|
+
>>> print("Selected directory:", directory_path)
|
1382
|
+
"""
|
1383
|
+
directory_path = QFileDialog.getExistingDirectory(None, dir=dir)
|
1384
|
+
return directory_path if directory_path else None
|
1385
|
+
|
1386
|
+
def _handle_color_scheme_change(self):
|
1387
|
+
self.theme = (
|
1388
|
+
"dark"
|
1389
|
+
if self.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
1390
|
+
else "light"
|
1391
|
+
)
|
1392
|
+
|
1393
|
+
js_code = f"""
|
1394
|
+
document.dispatchEvent(new CustomEvent('themeChange', {{
|
1395
|
+
detail: {{ theme: "{self.theme}" }}
|
1396
|
+
}}));
|
1397
|
+
"""
|
1398
|
+
|
1399
|
+
# 모든 윈도우에 변경사항 적용
|
1400
|
+
for window in self.windows:
|
1401
|
+
window.web_view.page().runJavaScript(js_code)
|
1402
|
+
window.web_view.page().setBackgroundColor(
|
1403
|
+
Qt.GlobalColor.black if self.theme == "dark" else Qt.GlobalColor.white
|
1404
|
+
)
|