pyloid 0.20.2__py3-none-any.whl → 0.20.2.dev2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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
+ )