pyloid 0.9.5__py3-none-any.whl

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