pyloid 0.9.8__tar.gz → 0.10.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyloid
3
- Version: 0.9.8
3
+ Version: 0.10.2
4
4
  Summary:
5
5
  Author: aesthetics-of-record
6
6
  Author-email: 111675679+aesthetics-of-record@users.noreply.github.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyloid"
3
- version = "0.9.8"
3
+ version = "0.10.2"
4
4
  description = ""
5
5
  authors = ["aesthetics-of-record <111675679+aesthetics-of-record@users.noreply.github.com>"]
6
6
  readme = "README.md"
@@ -0,0 +1,35 @@
1
+ from PySide6.QtCore import QFileSystemWatcher, QObject, Signal
2
+
3
+ class FileWatcher(QObject):
4
+ file_changed = Signal(str)
5
+ directory_changed = Signal(str)
6
+
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.watcher = QFileSystemWatcher()
10
+ self.watcher.fileChanged.connect(self.file_changed)
11
+ self.watcher.directoryChanged.connect(self.directory_changed)
12
+
13
+ def add_path(self, path):
14
+ """Add a file or directory to the watch list."""
15
+ return self.watcher.addPath(path)
16
+
17
+ def remove_path(self, path):
18
+ """Remove a file or directory from the watch list."""
19
+ return self.watcher.removePath(path)
20
+
21
+ def get_watched_paths(self):
22
+ """Return all currently watched paths (files + directories)."""
23
+ return self.watcher.files() + self.watcher.directories()
24
+
25
+ def get_watched_files(self):
26
+ """Return all currently watched files."""
27
+ return self.watcher.files()
28
+
29
+ def get_watched_directories(self):
30
+ """Return all currently watched directories."""
31
+ return self.watcher.directories()
32
+
33
+ def remove_all_paths(self):
34
+ """Remove all paths from the watch list."""
35
+ return self.watcher.removePaths(self.get_watched_paths())
@@ -8,8 +8,8 @@ from PySide6.QtWidgets import (
8
8
  )
9
9
  from PySide6.QtWebEngineWidgets import QWebEngineView
10
10
  from PySide6.QtWebChannel import QWebChannel
11
- from PySide6.QtGui import QIcon, QKeySequence, QShortcut, QClipboard, QImage
12
- from PySide6.QtCore import Qt, Signal, QUrl, QObject
11
+ from PySide6.QtGui import QIcon, QKeySequence, QShortcut, QClipboard, QImage, QAction, QPalette, QColor
12
+ from PySide6.QtCore import Qt, Signal, QUrl, QObject, QTimer
13
13
  from PySide6.QtNetwork import QLocalServer, QLocalSocket
14
14
  from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings
15
15
  from .api import PyloidAPI, Bridge
@@ -21,22 +21,34 @@ from .utils import is_production
21
21
  from .monitor import Monitor
22
22
  import json
23
23
  from .autostart import AutoStart
24
+ from .filewatcher import FileWatcher
25
+ import logging
26
+ from PySide6.QtCore import QCoreApplication
24
27
 
25
28
  # for linux debug
26
- os.environ['QTWEBENGINE_DICTIONARIES_PATH'] = '/'
29
+ os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = "/"
27
30
 
28
31
  # for macos debug
32
+ logging.getLogger('Qt').setLevel(logging.ERROR)
29
33
 
30
34
  def custom_message_handler(mode, context, message):
31
- if not hasattr(custom_message_handler, 'vulkan_warning_shown') and (('Failed to load vulkan' in message) or ('No Vulkan library available' in message) or ('Failed to create platform Vulkan instance' in message)):
32
- print('\033[93mPyloid Warning: Vulkan GPU API issue detected. Switching to software backend.\033[0m')
33
- os.environ['QT_QUICK_BACKEND'] = 'software'
35
+ if not hasattr(custom_message_handler, "vulkan_warning_shown") and (
36
+ ("Failed to load vulkan" in message)
37
+ or ("No Vulkan library available" in message)
38
+ or ("Failed to create platform Vulkan instance" in message)
39
+ ):
40
+ print(
41
+ "\033[93mPyloid Warning: Vulkan GPU API issue detected. Switching to software backend.\033[0m"
42
+ )
43
+ os.environ["QT_QUICK_BACKEND"] = "software"
34
44
  custom_message_handler.vulkan_warning_shown = True
35
- if 'vulkan' not in message.lower():
45
+ if "vulkan" not in message.lower():
36
46
  print(message)
37
47
 
48
+
38
49
  qInstallMessageHandler(custom_message_handler)
39
50
 
51
+
40
52
  class WindowAPI(PyloidAPI):
41
53
  def __init__(self, window_id: str, app):
42
54
  super().__init__()
@@ -147,6 +159,7 @@ class WindowAPI(PyloidAPI):
147
159
  return window.capture(save_path)
148
160
  return None
149
161
 
162
+
150
163
  # class EventAPI(PylonAPI):
151
164
  # def __init__(self, window_id: str, app):
152
165
  # super().__init__()
@@ -168,21 +181,20 @@ class WindowAPI(PyloidAPI):
168
181
  # for callback in self.subscribers[event_name]:
169
182
  # callback(*args, **kwargs)
170
183
 
171
-
172
184
 
173
185
  class BrowserWindow:
174
186
  def __init__(
175
187
  self,
176
188
  app,
177
- title: str="pylon app",
178
- width: int=800,
179
- height: int=600,
180
- x: int=200,
181
- y: int=200,
182
- frame: bool=True,
183
- context_menu: bool=False,
184
- dev_tools: bool=False,
185
- js_apis: List[PyloidAPI]=[],
189
+ title: str = "pyloid app",
190
+ width: int = 800,
191
+ height: int = 600,
192
+ x: int = 200,
193
+ y: int = 200,
194
+ frame: bool = True,
195
+ context_menu: bool = False,
196
+ dev_tools: bool = False,
197
+ js_apis: List[PyloidAPI] = [],
186
198
  ):
187
199
  ###########################################################################################
188
200
  self.id = str(uuid.uuid4()) # Generate unique ID
@@ -190,9 +202,9 @@ class BrowserWindow:
190
202
  self._window = QMainWindow()
191
203
  self.web_view = QWebEngineView()
192
204
 
193
- self._window.closeEvent = self.closeEvent # Override closeEvent method
205
+ self._window.closeEvent = self.closeEvent # Override closeEvent method
194
206
  ###########################################################################################
195
- self.app = app
207
+ self.app = app
196
208
  self.title = title
197
209
  self.width = width
198
210
  self.height = height
@@ -206,14 +218,16 @@ class BrowserWindow:
206
218
  self.js_apis.append(js_api)
207
219
  self.shortcuts = {}
208
220
  ###########################################################################################
209
-
221
+
210
222
  def _load(self):
211
223
  self._window.setWindowTitle(self.title)
212
224
 
213
225
  self._window.setGeometry(self.x, self.y, self.width, self.height)
214
226
 
215
227
  # allow local file access to remote urls
216
- self.web_view.settings().setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
228
+ self.web_view.settings().setAttribute(
229
+ QWebEngineSettings.LocalContentCanAccessRemoteUrls, True
230
+ )
217
231
 
218
232
  # Set icon
219
233
  if self.app.icon:
@@ -228,7 +242,6 @@ class BrowserWindow:
228
242
  myappid = "mycompany.myproduct.subproduct.version"
229
243
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
230
244
 
231
-
232
245
  # Remove title bar and borders (if needed)
233
246
  if not self.frame:
234
247
  self._window.setWindowFlags(Qt.FramelessWindowHint)
@@ -244,7 +257,6 @@ class BrowserWindow:
244
257
  if self.js_apis:
245
258
  for js_api in self.js_apis:
246
259
  self.channel.registerObject(js_api.__class__.__name__, js_api)
247
-
248
260
 
249
261
  self.web_view.page().setWebChannel(self.channel)
250
262
 
@@ -253,7 +265,6 @@ class BrowserWindow:
253
265
 
254
266
  # Add QWebEngineView to main window
255
267
  self._window.setCentralWidget(self.web_view)
256
-
257
268
 
258
269
  # Set F12 shortcut
259
270
  self.set_dev_tools(self.dev_tools)
@@ -311,14 +322,14 @@ class BrowserWindow:
311
322
  def load_file(self, file_path):
312
323
  """Loads a local HTML file into the web view."""
313
324
  self._load()
314
- file_path = os.path.abspath(file_path) # absolute path
325
+ file_path = os.path.abspath(file_path) # absolute path
315
326
  self.web_view.setUrl(QUrl.fromLocalFile(file_path))
316
-
317
327
 
318
328
  def load_url(self, url):
319
329
  """Sets the URL of the window."""
320
330
  self._load()
321
331
  self.web_view.setUrl(QUrl(url))
332
+
322
333
  ###########################################################################################
323
334
  # Set Parameters
324
335
  ###########################################################################################
@@ -342,10 +353,13 @@ class BrowserWindow:
342
353
  def set_frame(self, frame: bool):
343
354
  """Sets the frame of the window."""
344
355
  self.frame = frame
356
+ was_visible = self._window.isVisible()
345
357
  if self.frame:
346
358
  self._window.setWindowFlags(Qt.Window)
347
359
  else:
348
360
  self._window.setWindowFlags(Qt.FramelessWindowHint)
361
+ if was_visible:
362
+ self._window.show()
349
363
 
350
364
  def set_context_menu(self, context_menu: bool):
351
365
  """Sets the context menu of the window."""
@@ -354,10 +368,10 @@ class BrowserWindow:
354
368
  self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
355
369
  else:
356
370
  self.web_view.setContextMenuPolicy(Qt.DefaultContextMenu)
357
-
371
+
358
372
  def set_dev_tools(self, enable: bool):
359
373
  """Sets the developer tools of the window.
360
-
374
+
361
375
  If enabled, the developer tools can be opened using the F12 key.
362
376
  """
363
377
  self.dev_tools = enable
@@ -390,7 +404,7 @@ class BrowserWindow:
390
404
  "dev_tools": self.dev_tools,
391
405
  "js_apis": self.js_apis,
392
406
  }
393
-
407
+
394
408
  def get_id(self):
395
409
  """Returns the ID of the window."""
396
410
  return self.id
@@ -417,19 +431,23 @@ class BrowserWindow:
417
431
  def show(self):
418
432
  """Shows the window."""
419
433
  self._window.show()
420
-
434
+
421
435
  def focus(self):
422
436
  """Focuses the window."""
423
437
  self._window.activateWindow()
424
438
  self._window.raise_()
425
- self._window.setWindowState(self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
439
+ self._window.setWindowState(
440
+ self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
441
+ )
426
442
 
427
443
  def show_and_focus(self):
428
444
  """Shows and focuses the window."""
429
445
  self._window.show()
430
446
  self._window.activateWindow()
431
447
  self._window.raise_()
432
- self._window.setWindowState(self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
448
+ self._window.setWindowState(
449
+ self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
450
+ )
433
451
 
434
452
  def close(self):
435
453
  """Closes the window."""
@@ -457,14 +475,14 @@ class BrowserWindow:
457
475
  def capture(self, save_path: str) -> Optional[str]:
458
476
  """
459
477
  Captures the current window.
460
-
478
+
461
479
  :param save_path: Path to save the captured image. If not specified, it will be saved in the current directory.
462
480
  :return: Path of the saved image
463
481
  """
464
482
  try:
465
483
  # Capture window
466
484
  screenshot = self._window.grab()
467
-
485
+
468
486
  # Save image
469
487
  screenshot.save(save_path)
470
488
  return save_path
@@ -478,7 +496,7 @@ class BrowserWindow:
478
496
  def add_shortcut(self, key_sequence: str, callback: Callable):
479
497
  """
480
498
  Adds a keyboard shortcut to the window if it does not already exist.
481
-
499
+
482
500
  :param key_sequence: Shortcut sequence (e.g., "Ctrl+C")
483
501
  :param callback: Function to be executed when the shortcut is pressed
484
502
  :return: Created QShortcut object or None if the shortcut already exists
@@ -486,7 +504,7 @@ class BrowserWindow:
486
504
  if key_sequence in self.shortcuts:
487
505
  # print(f"Shortcut {key_sequence} already exists.")
488
506
  return None
489
-
507
+
490
508
  shortcut = QShortcut(QKeySequence(key_sequence), self._window)
491
509
  shortcut.activated.connect(callback)
492
510
  self.shortcuts[key_sequence] = shortcut
@@ -495,7 +513,7 @@ class BrowserWindow:
495
513
  def remove_shortcut(self, key_sequence: str):
496
514
  """
497
515
  Removes a keyboard shortcut from the window.
498
-
516
+
499
517
  :param key_sequence: Shortcut sequence to be removed
500
518
  """
501
519
  if key_sequence in self.shortcuts:
@@ -506,18 +524,18 @@ class BrowserWindow:
506
524
  def get_all_shortcuts(self):
507
525
  """
508
526
  Returns all registered shortcuts in the window.
509
-
527
+
510
528
  :return: Dictionary of shortcut sequences and QShortcut objects
511
529
  """
512
530
  return self.shortcuts
513
-
531
+
514
532
  ###########################################################################################
515
533
  # Event (Calling the JS from Python)
516
534
  ###########################################################################################
517
- def emit(self, event_name, data: Optional[Dict]=None):
535
+ def emit(self, event_name, data: Optional[Dict] = None):
518
536
  """
519
537
  Emits an event to the JavaScript side.
520
-
538
+
521
539
  :param event_name: Name of the event
522
540
  :param data: Data to be sent with the event (optional)
523
541
  """
@@ -536,8 +554,13 @@ class _WindowController(QObject):
536
554
  QApplication, str, int, int, int, int, bool, bool, bool, list
537
555
  )
538
556
 
557
+
539
558
  class Pyloid(QApplication):
540
- def __init__(self, app_name, single_instance=True, icon_path: str=None, tray_icon_path: str=None):
559
+ def __init__(
560
+ self,
561
+ app_name,
562
+ single_instance=True,
563
+ ):
541
564
  super().__init__(sys.argv)
542
565
 
543
566
  self.windows = []
@@ -551,10 +574,12 @@ class Pyloid(QApplication):
551
574
  self._init_single_instance()
552
575
 
553
576
  self.controller = _WindowController()
554
- self.controller.create_window_signal.connect(self._create_window_signal_function)
577
+ self.controller.create_window_signal.connect(
578
+ self._create_window_signal_function
579
+ )
580
+
581
+ self.file_watcher = FileWatcher()
555
582
 
556
- self.icon = QIcon(icon_path) if icon_path else None
557
- self.tray_icon = QIcon(tray_icon_path) if tray_icon_path else None
558
583
  self.tray_menu_items = []
559
584
  self.tray_actions = {}
560
585
 
@@ -563,29 +588,36 @@ class Pyloid(QApplication):
563
588
 
564
589
  self.auto_start = AutoStart(self.app_name, self.app_path)
565
590
 
591
+ self.animation_timer = None
592
+ self.icon_frames = []
593
+ self.current_frame = 0
594
+
566
595
  def set_icon(self, icon_path: str):
567
- """Sets the icon for the application."""
568
- self.icon = QIcon(icon_path)
596
+ """
597
+ Dynamically sets the application's icon.
569
598
 
570
- def set_tray_icon(self, tray_icon_path: str):
571
- """Sets the path for the tray icon."""
572
- self.tray_icon = QIcon(tray_icon_path)
599
+ :param icon_path: Path to the new icon file
573
600
 
574
- def set_tray_menu_items(self, tray_menu_items: Dict[str, Callable]):
575
- """Sets the menu items for the tray icon."""
576
- self.tray_menu_items = tray_menu_items
601
+ This method can be called while the application is running.
602
+ The icon can be changed at any time and is applied immediately.
603
+ """
604
+ self.icon = QIcon(icon_path)
605
+
606
+ # Immediately update the icon for all open windows
607
+ for window in self.windows:
608
+ window._window.setWindowIcon(self.icon)
577
609
 
578
610
  def create_window(
579
611
  self,
580
- title: str="pylon app",
581
- width: int=800,
582
- height: int=600,
583
- x: int=200,
584
- y: int=200,
585
- frame: bool=True,
586
- context_menu: bool=False,
587
- dev_tools: bool=False,
588
- js_apis: List[PyloidAPI]=[],
612
+ title: str = "pyloid app",
613
+ width: int = 800,
614
+ height: int = 600,
615
+ x: int = 200,
616
+ y: int = 200,
617
+ frame: bool = True,
618
+ context_menu: bool = False,
619
+ dev_tools: bool = False,
620
+ js_apis: List[PyloidAPI] = [],
589
621
  ) -> BrowserWindow:
590
622
  """Creates a new browser window."""
591
623
  self.controller.create_window_signal.emit(
@@ -613,7 +645,7 @@ class Pyloid(QApplication):
613
645
  frame: bool,
614
646
  context_menu: bool,
615
647
  dev_tools: bool,
616
- js_apis: List[PyloidAPI]=[],
648
+ js_apis: List[PyloidAPI] = [],
617
649
  ) -> BrowserWindow:
618
650
  """Function to create a new browser window."""
619
651
  window = BrowserWindow(
@@ -656,7 +688,6 @@ class Pyloid(QApplication):
656
688
  """Handles new connections for the single instance server."""
657
689
  pass
658
690
 
659
-
660
691
  ###########################################################################################
661
692
  # App window
662
693
  ###########################################################################################
@@ -676,8 +707,11 @@ class Pyloid(QApplication):
676
707
  main_window = self.windows[0]
677
708
  main_window._window.activateWindow()
678
709
  main_window._window.raise_()
679
- main_window._window.setWindowState(main_window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
680
-
710
+ main_window._window.setWindowState(
711
+ main_window._window.windowState() & ~Qt.WindowMinimized
712
+ | Qt.WindowActive
713
+ )
714
+
681
715
  def show_and_focus_main_window(self):
682
716
  """Shows and focuses the first window."""
683
717
  if self.windows:
@@ -685,8 +719,11 @@ class Pyloid(QApplication):
685
719
  main_window._window.show()
686
720
  main_window._window.activateWindow()
687
721
  main_window._window.raise_()
688
- main_window._window.setWindowState(main_window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
689
-
722
+ main_window._window.setWindowState(
723
+ main_window._window.windowState() & ~Qt.WindowMinimized
724
+ | Qt.WindowActive
725
+ )
726
+
690
727
  def close_all_windows(self):
691
728
  """Closes all windows."""
692
729
  for window in self.windows:
@@ -696,6 +733,7 @@ class Pyloid(QApplication):
696
733
  """Quits the application."""
697
734
  self.close_all_windows()
698
735
  QApplication.quit()
736
+
699
737
  ###########################################################################################
700
738
  # Window management in the app (ID required)
701
739
  ###########################################################################################
@@ -719,7 +757,9 @@ class Pyloid(QApplication):
719
757
  window._window.show()
720
758
  window._window.activateWindow()
721
759
  window._window.raise_()
722
- window._window.setWindowState(window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
760
+ window._window.setWindowState(
761
+ window._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
762
+ )
723
763
 
724
764
  def close_window_by_id(self, window_id: str):
725
765
  """Closes the window with the given ID."""
@@ -753,7 +793,7 @@ class Pyloid(QApplication):
753
793
  def capture_window_by_id(self, window_id: str, save_path: str) -> Optional[str]:
754
794
  """
755
795
  Captures a specific window.
756
-
796
+
757
797
  :param window_id: ID of the window to capture
758
798
  :param save_path: Path to save the captured image. If not specified, it will be saved in the current directory.
759
799
  :return: Path of the saved image
@@ -763,45 +803,83 @@ class Pyloid(QApplication):
763
803
  if not window:
764
804
  print(f"Cannot find window with the specified ID: {window_id}")
765
805
  return None
766
-
806
+
767
807
  # Capture window
768
808
  screenshot = window._window.grab()
769
-
809
+
770
810
  # Save image
771
811
  screenshot.save(save_path)
772
812
  return save_path
773
813
  except Exception as e:
774
814
  print(f"Error occurred while capturing the window: {e}")
775
815
  return None
816
+
776
817
  ###########################################################################################
777
818
  # Tray
778
819
  ###########################################################################################
779
- def run_tray(self):
780
- """Sets up the system tray icon and menu."""
781
- if not hasattr(self, 'tray'):
782
- self.tray = QSystemTrayIcon(self)
783
- if self.tray_icon:
784
- self.tray.setIcon(self.tray_icon)
785
- else:
786
- if self.icon:
787
- self.tray.setIcon(self.icon)
788
- else:
789
- print("Icon and Tray icon are not set.")
790
-
791
- tray_menu = QMenu()
792
-
793
- # Add menu items from external source
794
- if self.tray_menu_items:
795
- for item in self.tray_menu_items:
796
- action = tray_menu.addAction(item["label"])
797
- action.triggered.connect(item["callback"])
798
-
799
- self.tray.setContextMenu(tray_menu)
800
- self.tray.activated.connect(self._tray_activated)
801
- self.tray.show()
820
+ def set_tray_icon(self, tray_icon_path: str):
821
+ """
822
+ Dynamically sets the tray icon.
823
+ Can be called while the application is running, and changes are applied immediately.
824
+
825
+ :param tray_icon_path: Path to the new tray icon file
826
+ """
827
+ # Stop and remove existing animation timer if present
828
+ if hasattr(self, "animation_timer") and self.animation_timer is not None:
829
+ self.animation_timer.stop()
830
+ self.animation_timer.deleteLater()
831
+ self.animation_timer = None
832
+
833
+ # Remove existing icon frames
834
+ if hasattr(self, "icon_frames"):
835
+ self.icon_frames = []
836
+
837
+ # Set new icon
838
+ self.tray_icon = QIcon(tray_icon_path)
839
+
840
+ if not hasattr(self, "tray"):
841
+ self._init_tray()
842
+ else:
843
+ self.tray.setIcon(self.tray_icon)
844
+
845
+ def set_tray_menu_items(
846
+ self, tray_menu_items: List[Dict[str, Union[str, Callable]]]
847
+ ):
848
+ """
849
+ Dynamically sets the tray menu items.
850
+ Can be called while the application is running, and changes are applied immediately.
851
+
852
+ :param tray_menu_items: List of new tray menu items
853
+ """
854
+ self.tray_menu_items = tray_menu_items
855
+ if not hasattr(self, "tray"):
856
+ self._init_tray()
857
+ self._update_tray_menu()
858
+
859
+ def _init_tray(self):
860
+ """Initializes the tray icon."""
861
+ self.tray = QSystemTrayIcon(self)
862
+ if self.tray_icon:
863
+ self.tray.setIcon(self.tray_icon)
864
+ else:
865
+ print("Icon and tray icon have not been set.")
866
+ if self.tray_menu_items:
867
+ pass
868
+ else:
869
+ self.tray.setContextMenu(QMenu())
870
+ self.tray.show()
871
+
872
+ def _update_tray_menu(self):
873
+ """Updates the tray menu."""
874
+ tray_menu = self.tray.contextMenu()
875
+ tray_menu.clear()
876
+ for item in self.tray_menu_items:
877
+ action = QAction(item["label"], self)
878
+ action.triggered.connect(item["callback"])
879
+ tray_menu.addAction(action)
802
880
 
803
881
  def _tray_activated(self, reason):
804
- """Handles the event when the tray icon is activated."""
882
+ """Handles events when the tray icon is activated."""
805
883
  reason_enum = QSystemTrayIcon.ActivationReason(reason)
806
884
 
807
885
  if reason_enum in self.tray_actions:
@@ -809,48 +887,124 @@ class Pyloid(QApplication):
809
887
 
810
888
  def set_tray_actions(self, actions):
811
889
  """
812
- Sets the actions for tray icon activation.
890
+ Dynamically sets actions for tray icon activation.
891
+ Can be called while the application is running, and changes are applied immediately.
813
892
 
814
- actions: Dictionary where keys are TrayEvent enum values,
815
- and values are callback functions for the respective activation reasons.
893
+ :param actions: Dictionary with TrayEvent enum values as keys and corresponding callback functions as values
816
894
  """
895
+ if self.tray_actions:
896
+ self.tray.activated.disconnect() # Disconnect existing connections
897
+
817
898
  self.tray_actions = actions
899
+ if not hasattr(self, "tray"):
900
+ self._init_tray()
901
+
902
+ self.tray.activated.connect(lambda reason: self._tray_activated(reason))
818
903
 
819
904
  def show_notification(self, title: str, message: str):
820
- """Displays a notification in the system tray."""
821
- if not hasattr(self, 'tray'):
822
- self.run_tray() # Ensure the tray is initialized
905
+ """
906
+ Displays a notification in the system tray.
907
+ Can be called while the application is running, and the notification is displayed immediately.
908
+
909
+ :param title: Notification title
910
+ :param message: Notification content
911
+ """
912
+ if not hasattr(self, "tray"):
913
+ self._init_tray() # Ensure the tray is initialized
823
914
 
824
915
  self.tray.showMessage(title, message, QIcon(self.icon), 5000)
825
916
 
917
+ def _update_tray_icon(self):
918
+ """Updates the animation frames."""
919
+ if hasattr(self, "tray") and self.icon_frames:
920
+ self.tray.setIcon(self.icon_frames[self.current_frame])
921
+ self.current_frame = (self.current_frame + 1) % len(self.icon_frames)
922
+
923
+ def set_tray_icon_animation(self, icon_frames: List[str], interval: int = 200):
924
+ """
925
+ Dynamically sets and starts an animation for the tray icon.
926
+ Can be called while the application is running, and changes are applied immediately.
927
+
928
+ :param icon_frames: List of paths to animation frame images
929
+ :param interval: Interval between frames (milliseconds)
930
+ """
931
+ if not hasattr(self, "tray"):
932
+ self._init_tray()
933
+
934
+ # Remove existing icon
935
+ if hasattr(self, "tray_icon"):
936
+ del self.tray_icon
937
+
938
+ # Stop and remove existing animation timer
939
+ if hasattr(self, "animation_timer") and self.animation_timer is not None:
940
+ self.animation_timer.stop()
941
+ self.animation_timer.deleteLater()
942
+ self.animation_timer = None
943
+
944
+ self.icon_frames = [QIcon(frame) for frame in icon_frames]
945
+ self.animation_interval = interval
946
+ self.start_tray_icon_animation()
947
+
948
+ def start_tray_icon_animation(self):
949
+ """Starts the tray icon animation."""
950
+ if self.icon_frames:
951
+ if self.animation_timer is None:
952
+ self.animation_timer = QTimer(self)
953
+ self.animation_timer.timeout.connect(lambda: self._update_tray_icon())
954
+ self.animation_timer.start(self.animation_interval)
955
+ self.current_frame = 0
956
+
957
+ def set_tray_tooltip(self, message: str):
958
+ """
959
+ Dynamically sets the tooltip for the tray icon.
960
+ Can be called while the application is running, and changes are applied immediately.
961
+
962
+ :param message: New tooltip message
963
+ """
964
+ if not hasattr(self, "tray"):
965
+ self._init_tray()
966
+ self.tray.setToolTip(message)
967
+
968
+ def set_notification_callback(self, callback: Callable[[str], None]):
969
+ """
970
+ Sets the callback function to be called when a notification is clicked.
971
+
972
+ :param callback: Callback function to be called when a notification is clicked
973
+ """
974
+ if not hasattr(self, "tray"):
975
+ self._init_tray()
976
+ self.tray.messageClicked.connect(callback)
977
+
826
978
  ###########################################################################################
827
979
  # Monitor
828
980
  ###########################################################################################
829
981
  def get_all_monitors(self) -> List[Monitor]:
830
982
  """
831
983
  Returns a list of information for all connected monitors.
832
-
984
+
833
985
  :return: List containing monitor information
834
986
  """
835
- monitors = [Monitor(index, screen) for index, screen in enumerate(self.screens())]
987
+ monitors = [
988
+ Monitor(index, screen) for index, screen in enumerate(self.screens())
989
+ ]
836
990
  return monitors
837
-
991
+
838
992
  def get_primary_monitor(self) -> Monitor:
839
993
  """
840
994
  Returns information for the primary monitor.
841
-
995
+
842
996
  :return: Primary monitor information
843
997
  """
844
998
  primary_monitor = self.screens()[0]
845
999
  return Monitor(0, primary_monitor)
846
-
1000
+
847
1001
  ###########################################################################################
848
1002
  # Clipboard
849
1003
  ###########################################################################################
850
1004
  def copy_to_clipboard(self, text):
851
1005
  """
852
1006
  Copies text to the clipboard.
853
-
1007
+
854
1008
  :param text: Text to be copied
855
1009
  """
856
1010
  self.clipboard_class.setText(text, QClipboard.Clipboard)
@@ -858,7 +1012,7 @@ class Pyloid(QApplication):
858
1012
  def get_clipboard_text(self):
859
1013
  """
860
1014
  Retrieves text from the clipboard.
861
-
1015
+
862
1016
  :return: Text from the clipboard
863
1017
  """
864
1018
  return self.clipboard_class.text()
@@ -866,7 +1020,7 @@ class Pyloid(QApplication):
866
1020
  def set_clipboard_image(self, image: Union[str, bytes, os.PathLike]):
867
1021
  """
868
1022
  Copies an image to the clipboard.
869
-
1023
+
870
1024
  :param image: Path to the image to be copied
871
1025
  """
872
1026
  self.clipboard_class.setImage(QImage(image), QClipboard.Clipboard)
@@ -874,11 +1028,11 @@ class Pyloid(QApplication):
874
1028
  def get_clipboard_image(self):
875
1029
  """
876
1030
  Retrieves an image from the clipboard.
877
-
1031
+
878
1032
  :return: QImage object from the clipboard (None if no image)
879
1033
  """
880
1034
  return self.clipboard_class.image()
881
-
1035
+
882
1036
  ###########################################################################################
883
1037
  # Atostart
884
1038
  ###########################################################################################
@@ -887,7 +1041,7 @@ class Pyloid(QApplication):
887
1041
  Sets the application to start automatically with the system. (set_auto_start(True) only works in production)
888
1042
  True only works in production.
889
1043
  False works in both environments.
890
-
1044
+
891
1045
  :param enable: True to enable auto-start, False to disable
892
1046
  """
893
1047
  if not enable:
@@ -899,7 +1053,9 @@ class Pyloid(QApplication):
899
1053
  self.auto_start.set_auto_start(True)
900
1054
  return True
901
1055
  else:
902
- print("\033[93mset_auto_start(True) is not supported in non-production environment\033[0m")
1056
+ print(
1057
+ "\033[93mset_auto_start(True) is not supported in non-production environment\033[0m"
1058
+ )
903
1059
  return None
904
1060
 
905
1061
  def is_auto_start(self):
@@ -908,7 +1064,83 @@ class Pyloid(QApplication):
908
1064
 
909
1065
  :return: True if auto-start is enabled, False otherwise
910
1066
  """
911
-
1067
+
912
1068
  return self.auto_start.is_auto_start()
913
-
914
1069
 
1070
+ ###########################################################################################
1071
+ # File watcher
1072
+ ###########################################################################################
1073
+ def watch_file(self, file_path: str) -> bool:
1074
+ """
1075
+ Adds a file to the watch list.
1076
+
1077
+ :param file_path: Path of the file to watch
1078
+ :return: True if the file was successfully added to the watch list, False otherwise
1079
+ """
1080
+ return self.file_watcher.add_path(file_path)
1081
+
1082
+ def watch_directory(self, dir_path: str) -> bool:
1083
+ """
1084
+ Adds a directory to the watch list.
1085
+
1086
+ :param dir_path: Path of the directory to watch
1087
+ :return: True if the directory was successfully added to the watch list, False otherwise
1088
+ """
1089
+ return self.file_watcher.add_path(dir_path)
1090
+
1091
+ def stop_watching(self, path: str) -> bool:
1092
+ """
1093
+ Removes a file or directory from the watch list.
1094
+
1095
+ :param path: Path of the file or directory to stop watching
1096
+ :return: True if the path was successfully removed from the watch list, False otherwise
1097
+ """
1098
+ return self.file_watcher.remove_path(path)
1099
+
1100
+ def get_watched_paths(self) -> List[str]:
1101
+ """
1102
+ Returns all currently watched paths.
1103
+
1104
+ :return: List of all watched paths
1105
+ """
1106
+ return self.file_watcher.get_watched_paths()
1107
+
1108
+ def get_watched_files(self) -> List[str]:
1109
+ """
1110
+ Returns all currently watched files.
1111
+
1112
+ :return: List of all watched files
1113
+ """
1114
+ return self.file_watcher.get_watched_files()
1115
+
1116
+ def get_watched_directories(self) -> List[str]:
1117
+ """
1118
+ Returns all currently watched directories.
1119
+
1120
+ :return: List of all watched directories
1121
+ """
1122
+ return self.file_watcher.get_watched_directories()
1123
+
1124
+ def remove_all_watched_paths(self) -> None:
1125
+ """
1126
+ Removes all paths from the watch list.
1127
+
1128
+ :return: None
1129
+ """
1130
+ self.file_watcher.remove_all_paths()
1131
+
1132
+ def set_file_change_callback(self, callback: Callable[[str], None]) -> None:
1133
+ """
1134
+ Sets the callback function to be called when a file changes.
1135
+
1136
+ :param callback: Function to be called when a file changes
1137
+ """
1138
+ self.file_watcher.file_changed.connect(callback)
1139
+
1140
+ def set_directory_change_callback(self, callback: Callable[[str], None]) -> None:
1141
+ """
1142
+ Sets the callback function to be called when a directory changes.
1143
+
1144
+ :param callback: Function to be called when a directory changes
1145
+ """
1146
+ self.file_watcher.directory_changed.connect(callback)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes