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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyloid/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
+ )