pyloid 0.13.1__py3-none-any.whl → 0.14.1__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
@@ -2,40 +2,29 @@ import sys
2
2
  import os
3
3
  from PySide6.QtWidgets import (
4
4
  QApplication,
5
- QMainWindow,
6
5
  QSystemTrayIcon,
7
6
  QMenu,
7
+ QFileDialog,
8
8
  )
9
- from PySide6.QtWebEngineWidgets import QWebEngineView
10
- from PySide6.QtWebChannel import QWebChannel
11
9
  from PySide6.QtGui import (
12
10
  QIcon,
13
- QKeySequence,
14
- QShortcut,
15
11
  QClipboard,
16
12
  QImage,
17
13
  QAction,
18
- QCursor,
19
14
  )
20
- from PySide6.QtCore import Qt, Signal, QPoint, QUrl, QObject, QTimer, QSize, QEvent
15
+ from PySide6.QtCore import Qt, Signal, QObject, QTimer
21
16
  from PySide6.QtNetwork import QLocalServer, QLocalSocket
22
- from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings
23
- from .api import PyloidAPI, Bridge
24
- import uuid
25
- from typing import List, Optional, Dict, Callable, Union, Any
17
+ from .api import PyloidAPI
18
+ from typing import List, Optional, Dict, Callable, Union
26
19
  from PySide6.QtCore import qInstallMessageHandler
27
20
  import signal
28
21
  from .utils import is_production
29
22
  from .monitor import Monitor
30
- import json
31
23
  from .autostart import AutoStart
32
24
  from .filewatcher import FileWatcher
33
25
  import logging
34
- from PySide6.QtWidgets import (
35
- QWidget,
36
- QVBoxLayout,
37
- )
38
- from .custom.titlebar import CustomTitleBar
26
+ from .browser_window import BrowserWindow
27
+ from .tray import TrayEvent
39
28
 
40
29
  # for linux debug
41
30
  os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = "/"
@@ -67,809 +56,6 @@ def custom_message_handler(mode, context, message):
67
56
 
68
57
  qInstallMessageHandler(custom_message_handler)
69
58
 
70
-
71
- class WindowAPI(PyloidAPI):
72
- def __init__(self, window_id: str, app):
73
- super().__init__()
74
- self.window_id: str = window_id
75
- self.app: Pyloid = app
76
-
77
- @Bridge(result=str)
78
- def getWindowId(self):
79
- """Returns the current window ID."""
80
- return self.window_id
81
-
82
- @Bridge(result=dict)
83
- def getWindowProperties(self):
84
- """Returns the properties of the window."""
85
- window = self.app.get_window_by_id(self.window_id)
86
- window_properties = window.get_window_properties()
87
- return window_properties
88
-
89
- @Bridge()
90
- def close(self):
91
- """Closes the window."""
92
- window = self.app.get_window_by_id(self.window_id)
93
- if window:
94
- window.close()
95
-
96
- @Bridge()
97
- def hide(self):
98
- """Hides the window."""
99
- window = self.app.get_window_by_id(self.window_id)
100
- if window:
101
- window.hide()
102
-
103
- @Bridge()
104
- def show(self):
105
- """Shows and focuses the window."""
106
- window = self.app.get_window_by_id(self.window_id)
107
- if window:
108
- window.show()
109
-
110
- @Bridge()
111
- def toggleFullscreen(self):
112
- """Toggles fullscreen mode for the window."""
113
- window = self.app.get_window_by_id(self.window_id)
114
- if window:
115
- window.toggle_fullscreen()
116
-
117
- @Bridge()
118
- def minimize(self):
119
- """Minimizes the window."""
120
- window = self.app.get_window_by_id(self.window_id)
121
- if window:
122
- window.minimize()
123
-
124
- @Bridge()
125
- def maximize(self):
126
- """Maximizes the window."""
127
- window = self.app.get_window_by_id(self.window_id)
128
- if window:
129
- window.maximize()
130
-
131
- @Bridge()
132
- def unmaximize(self):
133
- """Restores the window to its normal state."""
134
- window = self.app.get_window_by_id(self.window_id)
135
- if window:
136
- window.unmaximize()
137
-
138
- @Bridge(str)
139
- def setTitle(self, title: str):
140
- """Sets the title of the window."""
141
- window = self.app.get_window_by_id(self.window_id)
142
- if window:
143
- window.set_title(title)
144
-
145
- @Bridge(int, int)
146
- def setSize(self, width: int, height: int):
147
- """Sets the size of the window."""
148
- window = self.app.get_window_by_id(self.window_id)
149
- if window:
150
- window.set_size(width, height)
151
-
152
- @Bridge(int, int)
153
- def setPosition(self, x: int, y: int):
154
- """Sets the position of the window."""
155
- window = self.app.get_window_by_id(self.window_id)
156
- if window:
157
- window.set_position(x, y)
158
-
159
- @Bridge(bool)
160
- def setFrame(self, frame: bool):
161
- """Sets the frame of the window."""
162
- window = self.app.get_window_by_id(self.window_id)
163
- if window:
164
- window.set_frame(frame)
165
-
166
- @Bridge(bool)
167
- def setContextMenu(self, context_menu: bool):
168
- """Sets the context menu of the window."""
169
- window = self.app.get_window_by_id(self.window_id)
170
- if window:
171
- window.set_context_menu(context_menu)
172
-
173
- @Bridge(bool)
174
- def setDevTools(self, enable: bool):
175
- """Sets the developer tools of the window."""
176
- window = self.app.get_window_by_id(self.window_id)
177
- if window:
178
- window.set_dev_tools(enable)
179
-
180
- @Bridge(str, result=Optional[str])
181
- def capture(self, save_path: str) -> Optional[str]:
182
- """Captures the current window."""
183
- window = self.app.get_window_by_id(self.window_id)
184
- if window:
185
- return window.capture(save_path)
186
- return None
187
-
188
- @Bridge(result=bool)
189
- def getFrame(self):
190
- """Returns whether the window has a frame."""
191
- window = self.app.get_window_by_id(self.window_id)
192
- return window.frame if window else False
193
-
194
- @Bridge(result=bool)
195
- def getContextMenu(self):
196
- """Returns whether the window has a context menu."""
197
- window = self.app.get_window_by_id(self.window_id)
198
- return window.context_menu if window else False
199
-
200
- @Bridge(result=bool)
201
- def getDevTools(self):
202
- """Returns whether the window has developer tools."""
203
- window = self.app.get_window_by_id(self.window_id)
204
- return window.dev_tools if window else False
205
-
206
- @Bridge(result=str)
207
- def getTitle(self):
208
- """Returns the title of the window."""
209
- window = self.app.get_window_by_id(self.window_id)
210
- return window.title if window else ""
211
-
212
- @Bridge(result=dict)
213
- def getSize(self):
214
- """Returns the size of the window."""
215
- window = self.app.get_window_by_id(self.window_id)
216
- return (
217
- {"width": window.width, "height": window.height}
218
- if window
219
- else {"width": 0, "height": 0}
220
- )
221
-
222
- @Bridge(result=dict)
223
- def getPosition(self):
224
- """Returns the position of the window."""
225
- window = self.app.get_window_by_id(self.window_id)
226
- return {"x": window.x, "y": window.y} if window else {"x": 0, "y": 0}
227
-
228
- @Bridge()
229
- def startSystemDrag(self):
230
- """Starts the system drag."""
231
- window = self.app.get_window_by_id(self.window_id)
232
- if window:
233
- window.web_view.start_system_drag()
234
-
235
-
236
- # class EventAPI(PylonAPI):
237
- # def __init__(self, window_id: str, app):
238
- # super().__init__()
239
- # self.window_id: str = window_id
240
- # self.app: PylonApp = app
241
- # self.subscribers = {}
242
-
243
- # @Bridge(str, Callable)
244
- # def on(self, event_name: str, callback: Callable):
245
- # """특정 이벤트를 구독합니다."""
246
- # if event_name not in self.subscribers:
247
- # self.subscribers[event_name] = []
248
- # self.subscribers[event_name].append(callback)
249
-
250
- # @Bridge(str, result=Optional[str])
251
- # def emit(self, event_name: str, *args, **kwargs):
252
- # """다른 윈도우로 특정 이벤트를 보냅니다."""
253
- # if event_name in self.subscribers:
254
- # for callback in self.subscribers[event_name]:
255
- # callback(*args, **kwargs)
256
-
257
-
258
- # 어차피 load 부분에만 쓰이니까 나중에 분리해서 load 위에서 선언하자.
259
- class CustomWebEngineView(QWebEngineView):
260
- def __init__(self, parent=None):
261
- super().__init__(parent._window)
262
- self.parent = parent
263
- self.drag_relative_position = None
264
- self.is_dragging = False
265
- self.is_resizing = False
266
- self.resize_start_pos = None
267
- self.resize_direction = None
268
- self.screen_geometry = self.screen().availableGeometry()
269
- self.is_resizing_enabled = True
270
-
271
- def mouse_press_event(self, event):
272
- if event.button() == Qt.LeftButton:
273
- self.drag_relative_position = event.pos()
274
- if not self.parent.frame and self.is_resizing_enabled:
275
- self.resize_direction = self.get_resize_direction(event.pos())
276
- if self.resize_direction:
277
- self.is_resizing = True
278
- self.resize_start_pos = event.globalPos()
279
-
280
- def start_system_drag(self):
281
- self.is_dragging = True
282
-
283
- def mouse_move_event(self, event):
284
- if self.is_resizing and self.is_resizing_enabled:
285
- self.resize_window(event.globalPos())
286
- elif not self.parent.frame and self.is_dragging:
287
- # 현재 마우스 위치를 전역 좌표로 가져옵니다
288
- current_global_pos = event.globalPos()
289
-
290
- # 화면 경계를 계산합니다
291
- left_boundary = self.screen_geometry.left()
292
- right_boundary = self.screen_geometry.right()
293
- top_boundary = self.screen_geometry.top()
294
- bottom_boundary = self.screen_geometry.bottom()
295
-
296
- # 마우스 커서 위치를 제한합니다
297
- new_cursor_pos = QPoint(
298
- max(left_boundary, min(current_global_pos.x(), right_boundary)),
299
- max(top_boundary, min(current_global_pos.y(), bottom_boundary)),
300
- )
301
-
302
- # 마우스 커서를 새 위치로 이동합니다
303
- QCursor.setPos(new_cursor_pos)
304
-
305
- # 창의 새 위치를 계산합니다
306
- new_window_pos = new_cursor_pos - self.drag_relative_position
307
-
308
- # 창을 새 위치로 이동합니다
309
- self.parent._window.move(new_window_pos)
310
- else:
311
- # Change cursor based on resize direction
312
- resize_direction = self.get_resize_direction(event.pos())
313
- if resize_direction and self.is_resizing_enabled:
314
- self.set_cursor_for_resize_direction(resize_direction)
315
- else:
316
- self.unsetCursor()
317
-
318
- def mouse_release_event(self, event):
319
- if event.button() == Qt.LeftButton:
320
- self.is_dragging = False
321
- self.is_resizing = False
322
- self.resize_direction = None
323
- self.unsetCursor()
324
-
325
- def eventFilter(self, source, event):
326
- if self.focusProxy() is source:
327
- if event.type() == QEvent.MouseButtonPress:
328
- self.mouse_press_event(event)
329
- elif event.type() == QEvent.MouseMove:
330
- self.mouse_move_event(event)
331
- elif event.type() == QEvent.MouseButtonRelease:
332
- self.mouse_release_event(event)
333
- return super().eventFilter(source, event)
334
-
335
- def get_resize_direction(self, pos):
336
- if not self.parent.frame and self.is_resizing_enabled: # Check if frame is not present and resizing is enabled
337
- margin = 5 # Margin in pixels to detect edge
338
- rect = self.rect()
339
- direction = None
340
-
341
- if pos.x() <= margin:
342
- direction = 'left'
343
- elif pos.x() >= rect.width() - margin:
344
- direction = 'right'
345
-
346
- if pos.y() <= margin:
347
- direction = 'top' if direction is None else direction + '-top'
348
- elif pos.y() >= rect.height() - margin:
349
- direction = 'bottom' if direction is None else direction + '-bottom'
350
-
351
- return direction
352
- return None
353
-
354
- def set_cursor_for_resize_direction(self, direction):
355
- if not self.parent.frame and direction and self.is_resizing_enabled: # Check if frame is not present and resizing is enabled
356
- if direction in ['left', 'right']:
357
- self.setCursor(Qt.SizeHorCursor)
358
- elif direction in ['top', 'bottom']:
359
- self.setCursor(Qt.SizeVerCursor)
360
- elif direction in ['left-top', 'right-bottom']:
361
- self.setCursor(Qt.SizeFDiagCursor)
362
- elif direction in ['right-top', 'left-bottom']:
363
- self.setCursor(Qt.SizeBDiagCursor)
364
-
365
- def resize_window(self, global_pos):
366
- if not self.parent.frame and self.resize_start_pos and self.resize_direction and self.is_resizing_enabled: # Check if frame is not present and resizing is enabled
367
- delta = global_pos - self.resize_start_pos
368
- new_geometry = self.parent._window.geometry()
369
-
370
- if 'left' in self.resize_direction:
371
- new_geometry.setLeft(new_geometry.left() + delta.x())
372
- if 'right' in self.resize_direction:
373
- new_geometry.setRight(new_geometry.right() + delta.x())
374
- if 'top' in self.resize_direction:
375
- new_geometry.setTop(new_geometry.top() + delta.y())
376
- if 'bottom' in self.resize_direction:
377
- new_geometry.setBottom(new_geometry.bottom() + delta.y())
378
-
379
- self.parent._window.setGeometry(new_geometry)
380
- self.resize_start_pos = global_pos
381
-
382
-
383
- class BrowserWindow:
384
- def __init__(
385
- self,
386
- app,
387
- title: str = "pyloid app",
388
- width: int = 800,
389
- height: int = 600,
390
- x: int = 200,
391
- y: int = 200,
392
- frame: bool = True,
393
- context_menu: bool = False,
394
- dev_tools: bool = False,
395
- js_apis: List[PyloidAPI] = [],
396
- ):
397
- ###########################################################################################
398
- self.id = str(uuid.uuid4()) # Generate unique ID
399
- self._window = QMainWindow()
400
- self.web_view = CustomWebEngineView(self)
401
-
402
- self._window.closeEvent = self.closeEvent # Override closeEvent method
403
- ###########################################################################################
404
- self.app = app
405
- self.title = title
406
- self.width = width
407
- self.height = height
408
- self.x = x
409
- self.y = y
410
- self.frame = frame
411
- self.context_menu = context_menu
412
- self.dev_tools = dev_tools
413
- self.js_apis = [WindowAPI(self.id, self.app)]
414
- for js_api in js_apis:
415
- self.js_apis.append(js_api)
416
- self.shortcuts = {}
417
- ###########################################################################################
418
-
419
- def _set_custom_frame(
420
- self,
421
- use_custom: bool,
422
- title: str = "Custom Title",
423
- bg_color: str = "darkblue",
424
- text_color: str = "white",
425
- icon_path: str = None,
426
- ):
427
- """Sets or removes the custom frame."""
428
- if use_custom:
429
- self._window.setWindowFlags(Qt.FramelessWindowHint)
430
- self.custom_title_bar = CustomTitleBar(self._window)
431
- self.custom_title_bar.set_style(bg_color, text_color)
432
- self.custom_title_bar.set_title(title)
433
-
434
- if icon_path:
435
- self.custom_title_bar.set_icon(icon_path)
436
-
437
- layout = QVBoxLayout()
438
- layout.setContentsMargins(0, 0, 0, 0)
439
- layout.setSpacing(0)
440
- layout.addWidget(self.custom_title_bar)
441
- layout.addWidget(self.web_view)
442
-
443
- central_widget = QWidget()
444
- central_widget.setLayout(layout)
445
- self._window.setCentralWidget(central_widget)
446
-
447
- # Add properties for window movement
448
- self._window.moving = False
449
- self._window.offset = QPoint()
450
- else:
451
- self._window.setWindowFlags(Qt.Window)
452
- self._window.setCentralWidget(self.web_view)
453
- self.custom_title_bar = None
454
-
455
- self._window.show()
456
-
457
- def _load(self):
458
- self.set_title(self.title)
459
-
460
- self.set_size(self.width, self.height)
461
- self.set_position(self.x, self.y)
462
-
463
- # allow local file access to remote urls
464
- self.web_view.settings().setAttribute(
465
- QWebEngineSettings.LocalContentCanAccessRemoteUrls, True
466
- )
467
-
468
- # Set icon
469
- if self.app.icon:
470
- self._window.setWindowIcon(self.app.icon)
471
- else:
472
- print("Icon is not set.")
473
-
474
- # Set Windows taskbar icon
475
- if sys.platform == "win32":
476
- import ctypes
477
-
478
- myappid = "mycompany.myproduct.subproduct.version"
479
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
480
-
481
- # Remove title bar and borders (if needed)
482
- if not self.frame:
483
- self._window.setWindowFlags(Qt.FramelessWindowHint)
484
-
485
- # Disable default context menu
486
- if not self.context_menu:
487
- self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
488
-
489
- # Set up QWebChannel
490
- self.channel = QWebChannel()
491
-
492
- # Register additional JS APIs
493
- if self.js_apis:
494
- for js_api in self.js_apis:
495
- self.channel.registerObject(js_api.__class__.__name__, js_api)
496
-
497
- self.web_view.page().setWebChannel(self.channel)
498
-
499
- # Connect pylonjs bridge
500
- self.web_view.loadFinished.connect(self._on_load_finished)
501
-
502
- # Add QWebEngineView to main window
503
- self._window.setCentralWidget(self.web_view)
504
-
505
- # Set F12 shortcut
506
- self.set_dev_tools(self.dev_tools)
507
-
508
- def _on_load_finished(self, ok):
509
- """Handles the event when the web page finishes loading."""
510
- if ok and self.js_apis:
511
- js_code = """
512
- if (typeof QWebChannel !== 'undefined') {
513
- new QWebChannel(qt.webChannelTransport, function (channel) {
514
- window.pyloid = {
515
- EventAPI: {
516
- listen: function(eventName, callback) {
517
- document.addEventListener(eventName, function(event) {
518
- let eventData;
519
- try {
520
- eventData = JSON.parse(event.detail);
521
- } catch (e) {
522
- eventData = event.detail;
523
- }
524
- callback(eventData);
525
- });
526
- },
527
- unlisten: function(eventName) {
528
- document.removeEventListener(eventName);
529
- }
530
- }
531
- };
532
- console.log('pyloid.EventAPI object initialized:', window.pyloid.EventAPI);
533
-
534
- %s
535
-
536
- document.addEventListener('mousedown', function (e) {
537
- if (e.target.hasAttribute('data-pyloid-drag-region')) {
538
- window.pyloid.WindowAPI.startSystemDrag();
539
- }
540
- });
541
-
542
- // Dispatch a custom event to signal that the initialization is ready
543
- const event = new CustomEvent('pyloidReady');
544
- document.dispatchEvent(event);
545
- });
546
- } else {
547
- console.error('QWebChannel is not defined.');
548
- }
549
- """
550
- js_api_init = "\n".join(
551
- [
552
- f"window.pyloid['{js_api.__class__.__name__}'] = channel.objects['{js_api.__class__.__name__}'];\n"
553
- f"console.log('pyloid.{js_api.__class__.__name__} object initialized:', window.pyloid['{js_api.__class__.__name__}']);"
554
- for js_api in self.js_apis
555
- ]
556
- )
557
- self.web_view.page().runJavaScript(js_code % js_api_init)
558
- else:
559
- pass
560
-
561
- ###########################################################################################
562
- # Load
563
- ###########################################################################################
564
- def load_file(self, file_path):
565
- """Loads a local HTML file into the web view."""
566
- self._load()
567
- file_path = os.path.abspath(file_path) # absolute path
568
- self.web_view.setUrl(QUrl.fromLocalFile(file_path))
569
- self.web_view.focusProxy().installEventFilter(self.web_view)
570
-
571
- def load_url(self, url):
572
- """Sets the URL of the window."""
573
- self._load()
574
- self.web_view.setUrl(QUrl(url))
575
- self.web_view.focusProxy().installEventFilter(self.web_view)
576
-
577
- ###########################################################################################
578
- # Set Parameters
579
- ###########################################################################################
580
- def set_title(self, title: str):
581
- """Sets the title of the window."""
582
- self.title = title
583
- self._window.setWindowTitle(self.title)
584
-
585
- def set_size(self, width: int, height: int):
586
- """Sets the size of the window."""
587
- self.width = width
588
- self.height = height
589
- self._window.setGeometry(self.x, self.y, self.width, self.height)
590
-
591
- def set_position(self, x: int, y: int):
592
- """Sets the position of the window."""
593
- self.x = x
594
- self.y = y
595
- self._window.setGeometry(self.x, self.y, self.width, self.height)
596
-
597
- def set_frame(self, frame: bool):
598
- """Sets the frame of the window."""
599
- self.frame = frame
600
- was_visible = self._window.isVisible()
601
- if self.frame:
602
- self._window.setWindowFlags(Qt.Window)
603
- else:
604
- self._window.setWindowFlags(Qt.FramelessWindowHint)
605
- if was_visible:
606
- self._window.show()
607
-
608
- def set_context_menu(self, context_menu: bool):
609
- """Sets the context menu of the window."""
610
- self.context_menu = context_menu
611
- if self.context_menu:
612
- self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
613
- else:
614
- self.web_view.setContextMenuPolicy(Qt.DefaultContextMenu)
615
-
616
- def set_dev_tools(self, enable: bool):
617
- """Sets the developer tools of the window.
618
-
619
- If enabled, the developer tools can be opened using the F12 key.
620
- """
621
- self.dev_tools = enable
622
- if self.dev_tools:
623
- self.add_shortcut("F12", self.open_dev_tools)
624
- else:
625
- self.remove_shortcut("F12")
626
-
627
- def open_dev_tools(self):
628
- """Opens the developer tools window."""
629
- self.web_view.page().setDevToolsPage(QWebEnginePage(self.web_view.page()))
630
- self.dev_tools_window = QMainWindow(self._window)
631
- dev_tools_view = QWebEngineView(self.dev_tools_window)
632
- dev_tools_view.setPage(self.web_view.page().devToolsPage())
633
- self.dev_tools_window.setCentralWidget(dev_tools_view)
634
- self.dev_tools_window.resize(800, 600)
635
- self.dev_tools_window.show()
636
-
637
- # Add this line to handle dev tools window closure
638
- self.dev_tools_window.closeEvent = lambda event: setattr(
639
- self, "dev_tools_window", None
640
- )
641
-
642
- def closeEvent(self, event):
643
- """Handles the event when the window is closed."""
644
- # Close developer tools if open
645
- if hasattr(self, "dev_tools_window") and self.dev_tools_window:
646
- self.dev_tools_window.close()
647
- self.dev_tools_window = None
648
-
649
- # Solve memory leak issue with web view engine
650
- self.web_view.page().deleteLater()
651
- self.web_view.deleteLater()
652
- self._remove_from_app_windows()
653
- event.accept() # Accept the event (allow the window to close)
654
-
655
- def _remove_from_app_windows(self):
656
- """Removes the window from the app's window list."""
657
- if self in self.app.windows:
658
- self.app.windows.remove(self)
659
- if not self.app.windows:
660
- self.app.quit() # Quit the app if all windows are closed
661
-
662
- ###########################################################################################
663
- # Window management (no ID required)
664
- ###########################################################################################
665
- def hide(self):
666
- """Hides the window."""
667
- self._window.hide()
668
-
669
- def show(self):
670
- """Shows the window."""
671
- self._window.show()
672
-
673
- def focus(self):
674
- """Focuses the window."""
675
- self._window.activateWindow()
676
- self._window.raise_()
677
- self._window.setWindowState(
678
- self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
679
- )
680
-
681
- def show_and_focus(self):
682
- """Shows and focuses the window."""
683
- self._window.show()
684
- self._window.activateWindow()
685
- self._window.raise_()
686
- self._window.setWindowState(
687
- self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
688
- )
689
-
690
- def close(self):
691
- """Closes the window."""
692
- self._window.close()
693
-
694
- def toggle_fullscreen(self):
695
- """Toggles fullscreen mode for the window."""
696
- if self._window.isFullScreen():
697
- self._window.showNormal()
698
- else:
699
- self._window.showFullScreen()
700
-
701
- def minimize(self):
702
- """Minimizes the window."""
703
- self._window.showMinimized()
704
-
705
- def maximize(self):
706
- """Maximizes the window."""
707
- self._window.showMaximized()
708
-
709
- def unmaximize(self):
710
- """Unmaximizes the window."""
711
- self._window.showNormal()
712
-
713
- def capture(self, save_path: str) -> Optional[str]:
714
- """
715
- Captures the current window.
716
-
717
- :param save_path: Path to save the captured image. If not specified, it will be saved in the current directory.
718
- :return: Path of the saved image
719
- """
720
- try:
721
- # Capture window
722
- screenshot = self._window.grab()
723
-
724
- # Save image
725
- screenshot.save(save_path)
726
- return save_path
727
- except Exception as e:
728
- print(f"Error occurred while capturing the window: {e}")
729
- return None
730
-
731
- ###########################################################################################
732
- # Shortcut
733
- ###########################################################################################
734
- def add_shortcut(self, key_sequence: str, callback: Callable):
735
- """
736
- Adds a keyboard shortcut to the window if it does not already exist.
737
-
738
- :param key_sequence: Shortcut sequence (e.g., "Ctrl+C")
739
- :param callback: Function to be executed when the shortcut is pressed
740
- :return: Created QShortcut object or None if the shortcut already exists
741
- """
742
- if key_sequence in self.shortcuts:
743
- # print(f"Shortcut {key_sequence} already exists.")
744
- return None
745
-
746
- shortcut = QShortcut(QKeySequence(key_sequence), self._window)
747
- shortcut.activated.connect(callback)
748
- self.shortcuts[key_sequence] = shortcut
749
- return shortcut
750
-
751
- def remove_shortcut(self, key_sequence: str):
752
- """
753
- Removes a keyboard shortcut from the window.
754
-
755
- :param key_sequence: Shortcut sequence to be removed
756
- """
757
- if key_sequence in self.shortcuts:
758
- shortcut = self.shortcuts.pop(key_sequence)
759
- shortcut.setEnabled(False)
760
- shortcut.deleteLater()
761
-
762
- def get_all_shortcuts(self):
763
- """
764
- Returns all registered shortcuts in the window.
765
-
766
- :return: Dictionary of shortcut sequences and QShortcut objects
767
- """
768
- return self.shortcuts
769
-
770
- ###########################################################################################
771
- # Event (Calling the JS from Python)
772
- ###########################################################################################
773
- def emit(self, event_name, data: Optional[Dict] = None):
774
- """
775
- Emits an event to the JavaScript side.
776
-
777
- :param event_name: Name of the event
778
- :param data: Data to be sent with the event (optional)
779
- """
780
- script = f"""
781
- (function() {{
782
- const eventData = {json.dumps(data)};
783
- const customEvent = new CustomEvent('{event_name}', {{ detail: eventData }});
784
- document.dispatchEvent(customEvent);
785
- }})();
786
- """
787
- self.web_view.page().runJavaScript(script)
788
-
789
- ###########################################################################################
790
- # Get Properties
791
- ###########################################################################################
792
- def get_window_properties(self):
793
- """Returns the properties of the window."""
794
- return {
795
- "id": self.id,
796
- "title": self.title,
797
- "width": self.width,
798
- "height": self.height,
799
- "x": self.x,
800
- "y": self.y,
801
- "frame": self.frame,
802
- "context_menu": self.context_menu,
803
- "dev_tools": self.dev_tools,
804
- }
805
-
806
- def get_id(self):
807
- """Returns the ID of the window."""
808
- return self.id
809
-
810
- def get_size(self) -> Dict[str, int]:
811
- """Returns the size of the window."""
812
- return {"width": self.width, "height": self.height}
813
-
814
- def get_position(self) -> Dict[str, int]:
815
- """Returns the position of the window."""
816
- return {"x": self.x, "y": self.y}
817
-
818
- def get_title(self) -> str:
819
- """Returns the title of the window."""
820
- return self.title
821
-
822
- def get_url(self) -> str:
823
- """Returns the URL of the window."""
824
- return self.web_view.url().toString()
825
-
826
- def get_visible(self) -> bool:
827
- """Returns the visibility of the window."""
828
- return self._window.isVisible()
829
-
830
- def get_frame(self) -> bool:
831
- """Returns the frame enabled state of the window."""
832
- return self.frame
833
-
834
- ###########################################################################################
835
- # Resize
836
- ###########################################################################################
837
- def set_resizable(self, resizable: bool):
838
- """Sets the resizability of the window."""
839
- self.resizable = resizable
840
- if self.frame:
841
- flags = self._window.windowFlags() | Qt.WindowCloseButtonHint
842
- if resizable:
843
- pass
844
- else:
845
- flags |= Qt.MSWindowsFixedSizeDialogHint
846
- self._window.setWindowFlags(flags)
847
- else:
848
- # 프레임이 없는 경우 커스텀 리사이징 로직을 설정합니다.
849
- self.web_view.is_resizing_enabled = resizable
850
-
851
- self._window.show() # 변경사항을 적용하기 위해 창을 다시 표시합니다.
852
-
853
- def set_minimum_size(self, min_width: int, min_height: int):
854
- """Sets the minimum size of the window."""
855
- self._window.setMinimumSize(min_width, min_height)
856
-
857
- def set_maximum_size(self, max_width: int, max_height: int):
858
- """Sets the maximum size of the window."""
859
- self._window.setMaximumSize(max_width, max_height)
860
-
861
- def get_minimum_size(self):
862
- """Returns the minimum size of the window."""
863
- return {'width': self._window.minimumWidth(), 'height': self._window.minimumHeight()}
864
-
865
- def get_maximum_size(self):
866
- """Returns the maximum size of the window."""
867
- return {'width': self._window.maximumWidth(), 'height': self._window.maximumHeight()}
868
-
869
- def get_resizable(self):
870
- """Returns the resizability of the window."""
871
- return self.resizable
872
-
873
59
  class _WindowController(QObject):
874
60
  create_window_signal = Signal(
875
61
  QApplication, str, int, int, int, int, bool, bool, bool, list
@@ -882,6 +68,27 @@ class Pyloid(QApplication):
882
68
  app_name,
883
69
  single_instance=True,
884
70
  ):
71
+ """
72
+ Initializes the Pyloid application.
73
+
74
+ Parameters
75
+ ----------
76
+ app_name : str, required
77
+ The name of the application
78
+ single_instance : bool, optional
79
+ Whether to run the application as a single instance (default is True)
80
+
81
+ Examples
82
+ --------
83
+ ```python
84
+ app = Pyloid(app_name="Pyloid-App")
85
+
86
+ window = app.create_window(title="New Window", width=1024, height=768)
87
+ window.show()
88
+
89
+ app.run()
90
+ ```
91
+ """
885
92
  super().__init__(sys.argv)
886
93
 
887
94
  self.windows = []
@@ -920,20 +127,28 @@ class Pyloid(QApplication):
920
127
  """
921
128
  Dynamically sets the application's icon.
922
129
 
923
- :param icon_path: Path to the new icon file
924
-
925
130
  This method can be called while the application is running.
926
- The icon can be changed at any time and is applied immediately.
131
+ The icon can be changed at any time and will be applied immediately.
132
+
133
+ Parameters
134
+ ----------
135
+ icon_path : str
136
+ Path to the new icon file
137
+
138
+ Examples
139
+ --------
140
+ >>> app = Pyloid(app_name="Pyloid-App")
141
+ >>> app.set_icon("icons/icon.png")
927
142
  """
928
143
  self.icon = QIcon(icon_path)
929
144
 
930
- # Immediately update the icon for all open windows
145
+ # Immediately update the icon for all open windows.
931
146
  for window in self.windows:
932
147
  window._window.setWindowIcon(self.icon)
933
148
 
934
149
  def create_window(
935
150
  self,
936
- title: str = "pyloid app",
151
+ title: str,
937
152
  width: int = 800,
938
153
  height: int = 600,
939
154
  x: int = 200,
@@ -943,7 +158,41 @@ class Pyloid(QApplication):
943
158
  dev_tools: bool = False,
944
159
  js_apis: List[PyloidAPI] = [],
945
160
  ) -> BrowserWindow:
946
- """Creates a new browser window."""
161
+ """
162
+ Creates a new browser window.
163
+
164
+ Parameters
165
+ ----------
166
+ title : str, required
167
+ Title of the window
168
+ width : int, optional
169
+ Width of the window (default is 800)
170
+ height : int, optional
171
+ Height of the window (default is 600)
172
+ x : int, optional
173
+ X coordinate of the window (default is 200)
174
+ y : int, optional
175
+ Y coordinate of the window (default is 200)
176
+ frame : bool, optional
177
+ Whether the window has a frame (default is True)
178
+ context_menu : bool, optional
179
+ Whether to use the context menu (default is False)
180
+ dev_tools : bool, optional
181
+ Whether to use developer tools (default is False)
182
+ js_apis : list of PyloidAPI, optional
183
+ List of JavaScript APIs to add to the window (default is an empty list)
184
+
185
+ Returns
186
+ -------
187
+ BrowserWindow
188
+ The created browser window object
189
+
190
+ Examples
191
+ --------
192
+ >>> app = Pyloid(app_name="Pyloid-App")
193
+ >>> window = app.create_window(title="New Window", width=1024, height=768)
194
+ >>> window.show()
195
+ """
947
196
  self.controller.create_window_signal.emit(
948
197
  self,
949
198
  title,
@@ -988,7 +237,20 @@ class Pyloid(QApplication):
988
237
  return window
989
238
 
990
239
  def run(self):
991
- """Runs the application event loop."""
240
+ """
241
+ Runs the application event loop.
242
+
243
+ This method starts the application's event loop, allowing the application to run.
244
+
245
+ This code should be written at the very end of the file.
246
+
247
+ Examples
248
+ --------
249
+ ```python
250
+ app = Pyloid(app_name="Pyloid-App")
251
+ app.run()
252
+ ```
253
+ """
992
254
  if is_production():
993
255
  sys.exit(self.exec())
994
256
  else:
@@ -1016,17 +278,51 @@ class Pyloid(QApplication):
1016
278
  # App window
1017
279
  ###########################################################################################
1018
280
  def get_windows(self) -> List[BrowserWindow]:
1019
- """Returns a list of all browser windows."""
281
+ """
282
+ Returns a list of all browser windows.
283
+
284
+ Returns
285
+ -------
286
+ List[BrowserWindow]
287
+ List of all browser windows
288
+
289
+ Examples
290
+ --------
291
+ ```python
292
+ app = Pyloid(app_name="Pyloid-App")
293
+ windows = app.get_windows()
294
+ for window in windows:
295
+ print(window.get_id())
296
+ ```
297
+ """
1020
298
  return self.windows
1021
299
 
1022
300
  def show_main_window(self):
1023
- """Shows and focuses the first window."""
301
+ """
302
+ Shows and focuses the first window.
303
+
304
+ Examples
305
+ --------
306
+ ```python
307
+ app = Pyloid(app_name="Pyloid-App")
308
+ app.show_main_window()
309
+ ```
310
+ """
1024
311
  if self.windows:
1025
312
  main_window = self.windows[0]
1026
313
  main_window._window.show()
1027
314
 
1028
315
  def focus_main_window(self):
1029
- """Focuses the first window."""
316
+ """
317
+ Focuses the first window.
318
+
319
+ Examples
320
+ --------
321
+ ```python
322
+ app = Pyloid(app_name="Pyloid-App")
323
+ app.focus_main_window()
324
+ ```
325
+ """
1030
326
  if self.windows:
1031
327
  main_window = self.windows[0]
1032
328
  main_window._window.activateWindow()
@@ -1037,7 +333,16 @@ class Pyloid(QApplication):
1037
333
  )
1038
334
 
1039
335
  def show_and_focus_main_window(self):
1040
- """Shows and focuses the first window."""
336
+ """
337
+ Shows and focuses the first window.
338
+
339
+ Examples
340
+ --------
341
+ ```python
342
+ app = Pyloid(app_name="Pyloid-App")
343
+ app.show_and_focus_main_window()
344
+ ```
345
+ """
1041
346
  if self.windows:
1042
347
  main_window = self.windows[0]
1043
348
  main_window._window.show()
@@ -1049,12 +354,30 @@ class Pyloid(QApplication):
1049
354
  )
1050
355
 
1051
356
  def close_all_windows(self):
1052
- """Closes all windows."""
357
+ """
358
+ Closes all windows.
359
+
360
+ Examples
361
+ --------
362
+ ```python
363
+ app = Pyloid(app_name="Pyloid-App")
364
+ app.close_all_windows()
365
+ ```
366
+ """
1053
367
  for window in self.windows:
1054
368
  window._window.close()
1055
369
 
1056
370
  def quit(self):
1057
- """애플리케이션을 종료합니다."""
371
+ """
372
+ Quits the application.
373
+
374
+ Examples
375
+ --------
376
+ ```python
377
+ app = Pyloid(app_name="Pyloid-App")
378
+ app.quit()
379
+ ```
380
+ """
1058
381
  for window in self.windows:
1059
382
  window._window.close()
1060
383
  window.web_page.deleteLater()
@@ -1065,20 +388,77 @@ class Pyloid(QApplication):
1065
388
  # Window management in the app (ID required)
1066
389
  ###########################################################################################
1067
390
  def get_window_by_id(self, window_id: str) -> Optional[BrowserWindow]:
1068
- """Returns the window with the given ID."""
391
+ """
392
+ Returns the window with the given ID.
393
+
394
+ Parameters
395
+ ----------
396
+ window_id : str
397
+ The ID of the window to find
398
+
399
+ Returns
400
+ -------
401
+ Optional[BrowserWindow]
402
+ The window object with the given ID. Returns None if the window is not found.
403
+
404
+ Examples
405
+ --------
406
+ ```python
407
+ app = Pyloid(app_name="Pyloid-App")
408
+
409
+ window = app.get_window_by_id("123e4567-e89b-12d3-a456-426614174000")
410
+
411
+ if window:
412
+ print("Window found:", window)
413
+ ```
414
+ """
1069
415
  for window in self.windows:
1070
416
  if window.id == window_id:
1071
417
  return window
1072
418
  return None
1073
419
 
1074
420
  def hide_window_by_id(self, window_id: str):
1075
- """Hides the window with the given ID."""
421
+ """
422
+ Hides the window with the given ID.
423
+
424
+ Parameters
425
+ ----------
426
+ window_id : str
427
+ The ID of the window to hide
428
+
429
+ Examples
430
+ --------
431
+ ```python
432
+ app = Pyloid(app_name="Pyloid-App")
433
+
434
+ window = app.create_window(title="pyloid-window")
435
+
436
+ app.hide_window_by_id(window.id)
437
+ ```
438
+ """
1076
439
  window = self.get_window_by_id(window_id)
1077
440
  if window:
1078
441
  window.hide()
1079
442
 
1080
443
  def show_window_by_id(self, window_id: str):
1081
- """Shows and focuses the window with the given ID."""
444
+ """
445
+ Shows and focuses the window with the given ID.
446
+
447
+ Parameters
448
+ ----------
449
+ window_id : str
450
+ The ID of the window to show
451
+
452
+ Examples
453
+ --------
454
+ ```python
455
+ app = Pyloid(app_name="Pyloid-App")
456
+
457
+ window = app.create_window(title="pyloid-window")
458
+
459
+ app.show_window_by_id(window.id)
460
+ ```
461
+ """
1082
462
  window = self.get_window_by_id(window_id)
1083
463
  if window:
1084
464
  window._window.show()
@@ -1089,41 +469,147 @@ class Pyloid(QApplication):
1089
469
  )
1090
470
 
1091
471
  def close_window_by_id(self, window_id: str):
1092
- """Closes the window with the given ID."""
472
+ """
473
+ Closes the window with the given ID.
474
+
475
+ Parameters
476
+ ----------
477
+ window_id : str
478
+ The ID of the window to close
479
+
480
+ Examples
481
+ --------
482
+ ```python
483
+ app = Pyloid(app_name="Pyloid-App")
484
+
485
+ window = app.create_window(title="pyloid-window")
486
+
487
+ app.close_window_by_id(window.id)
488
+ ```
489
+ """
1093
490
  window = self.get_window_by_id(window_id)
1094
491
  if window:
1095
492
  window._window.close()
1096
493
 
1097
494
  def toggle_fullscreen_by_id(self, window_id: str):
1098
- """Toggles fullscreen mode for the window with the given ID."""
495
+ """
496
+ Toggles fullscreen mode for the window with the given ID.
497
+
498
+ Parameters
499
+ ----------
500
+ window_id : str
501
+ The ID of the window to toggle fullscreen mode
502
+
503
+ Examples
504
+ --------
505
+ ```python
506
+ app = Pyloid(app_name="Pyloid-App")
507
+
508
+ window = app.create_window(title="pyloid-window")
509
+
510
+ app.toggle_fullscreen_by_id(window.id)
511
+ ```
512
+ """
1099
513
  window = self.get_window_by_id(window_id)
1100
514
  window.toggle_fullscreen()
1101
515
 
1102
516
  def minimize_window_by_id(self, window_id: str):
1103
- """Minimizes the window with the given ID."""
517
+ """
518
+ Minimizes the window with the given ID.
519
+
520
+ Parameters
521
+ ----------
522
+ window_id : str
523
+ The ID of the window to minimize
524
+
525
+ Examples
526
+ --------
527
+ ```python
528
+ app = Pyloid(app_name="Pyloid-App")
529
+
530
+ window = app.create_window(title="pyloid-window")
531
+
532
+ app.minimize_window_by_id(window.id)
533
+ ```
534
+ """
1104
535
  window = self.get_window_by_id(window_id)
1105
536
  if window:
1106
537
  window.minimize()
1107
538
 
1108
539
  def maximize_window_by_id(self, window_id: str):
1109
- """Maximizes the window with the given ID."""
540
+ """
541
+ Maximizes the window with the given ID.
542
+
543
+ Parameters
544
+ ----------
545
+ window_id : str
546
+ The ID of the window to maximize
547
+
548
+ Examples
549
+ --------
550
+ ```python
551
+ app = Pyloid(app_name="Pyloid-App")
552
+
553
+ window = app.create_window(title="pyloid-window")
554
+
555
+ app.maximize_window_by_id(window.id)
556
+ ```
557
+ """
1110
558
  window = self.get_window_by_id(window_id)
1111
559
  if window:
1112
560
  window.maximize()
1113
561
 
1114
562
  def unmaximize_window_by_id(self, window_id: str):
1115
- """Unmaximizes the window with the given ID."""
563
+ """
564
+ Unmaximizes the window with the given ID.
565
+
566
+ Parameters
567
+ ----------
568
+ window_id : str
569
+ The ID of the window to unmaximize
570
+
571
+ Examples
572
+ --------
573
+ ```python
574
+ app = Pyloid(app_name="Pyloid-App")
575
+
576
+ window = app.create_window(title="pyloid-window")
577
+
578
+ app.unmaximize_window_by_id(window.id)
579
+ ```
580
+ """
1116
581
  window = self.get_window_by_id(window_id)
1117
582
  if window:
1118
583
  window.unmaximize()
1119
584
 
1120
585
  def capture_window_by_id(self, window_id: str, save_path: str) -> Optional[str]:
1121
586
  """
1122
- Captures a specific window.
587
+ Captures the specified window.
588
+
589
+ Parameters
590
+ ----------
591
+ window_id : str
592
+ The ID of the window to capture
593
+ save_path : str
594
+ The path to save the captured image. If not specified, it will be saved in the current directory.
595
+
596
+ Returns
597
+ -------
598
+ Optional[str]
599
+ The path of the saved image. Returns None if the window is not found or an error occurs.
600
+
601
+ Examples
602
+ --------
603
+ ```python
604
+ app = Pyloid(app_name="Pyloid-App")
1123
605
 
1124
- :param window_id: ID of the window to capture
1125
- :param save_path: Path to save the captured image. If not specified, it will be saved in the current directory.
1126
- :return: Path of the saved image
606
+ window = app.create_window(title="pyloid-window")
607
+
608
+ image_path = app.capture_window_by_id(window.id, "save/image.png")
609
+
610
+ if image_path:
611
+ print("Image saved at:", image_path)
612
+ ```
1127
613
  """
1128
614
  try:
1129
615
  window = self.get_window_by_id(window_id)
@@ -1149,7 +635,15 @@ class Pyloid(QApplication):
1149
635
  Dynamically sets the tray icon.
1150
636
  Can be called while the application is running, and changes are applied immediately.
1151
637
 
1152
- :param tray_icon_path: Path to the new tray icon file
638
+ Parameters
639
+ ----------
640
+ tray_icon_path : str
641
+ The path of the new tray icon file
642
+
643
+ Examples
644
+ --------
645
+ >>> app = Pyloid(app_name="Pyloid-App")
646
+ >>> app.set_tray_icon("icons/icon.png")
1153
647
  """
1154
648
  # Stop and remove existing animation timer if present
1155
649
  if hasattr(self, "animation_timer") and self.animation_timer is not None:
@@ -1176,7 +670,19 @@ class Pyloid(QApplication):
1176
670
  Dynamically sets the tray menu items.
1177
671
  Can be called while the application is running, and changes are applied immediately.
1178
672
 
1179
- :param tray_menu_items: List of new tray menu items
673
+ Parameters
674
+ ----------
675
+ tray_menu_items : List[Dict[str, Union[str, Callable]]]
676
+ The list of new tray menu items
677
+
678
+ Examples
679
+ --------
680
+ >>> app = Pyloid(app_name="Pyloid-App")
681
+ >>> menu_items = [
682
+ >>> {"label": "Open", "callback": lambda: print("Open clicked")},
683
+ >>> {"label": "Exit", "callback": app.quit}
684
+ >>> ]
685
+ >>> app.set_tray_menu_items(menu_items)
1180
686
  """
1181
687
  self.tray_menu_items = tray_menu_items
1182
688
  if not hasattr(self, "tray"):
@@ -1212,12 +718,27 @@ class Pyloid(QApplication):
1212
718
  if reason_enum in self.tray_actions:
1213
719
  self.tray_actions[reason_enum]()
1214
720
 
1215
- def set_tray_actions(self, actions):
721
+ def set_tray_actions(self, actions: Dict[TrayEvent, Callable]):
1216
722
  """
1217
- Dynamically sets actions for tray icon activation.
723
+ Dynamically sets the actions for tray icon activation.
1218
724
  Can be called while the application is running, and changes are applied immediately.
1219
725
 
1220
- :param actions: Dictionary with TrayEvent enum values as keys and corresponding callback functions as values
726
+ Parameters
727
+ ----------
728
+ actions: Dict[TrayEvent, Callable]
729
+ Dictionary with TrayEvent enum values as keys and corresponding callback functions as values
730
+
731
+ Examples
732
+ --------
733
+ >>> app = Pyloid(app_name="Pyloid-App")
734
+ >>> app.set_tray_actions(
735
+ >>> {
736
+ >>> TrayEvent.DoubleClick: lambda: print("Tray icon was double-clicked."),
737
+ >>> TrayEvent.MiddleClick: lambda: print("Tray icon was middle-clicked."),
738
+ >>> TrayEvent.RightClick: lambda: print("Tray icon was right-clicked."),
739
+ >>> TrayEvent.LeftClick: lambda: print("Tray icon was left-clicked."),
740
+ >>> }
741
+ >>> )
1221
742
  """
1222
743
  if self.tray_actions:
1223
744
  self.tray.activated.disconnect() # Disconnect existing connections
@@ -1233,8 +754,17 @@ class Pyloid(QApplication):
1233
754
  Displays a notification in the system tray.
1234
755
  Can be called while the application is running, and the notification is displayed immediately.
1235
756
 
1236
- :param title: Notification title
1237
- :param message: Notification content
757
+ Parameters
758
+ ----------
759
+ title : str
760
+ Notification title
761
+ message : str
762
+ Notification message
763
+
764
+ Examples
765
+ --------
766
+ >>> app = Pyloid(app_name="Pyloid-App")
767
+ >>> app.show_notification("Update Available", "A new update is available for download.")
1238
768
  """
1239
769
  if not hasattr(self, "tray"):
1240
770
  self._init_tray() # Ensure the tray is initialized
@@ -1242,18 +772,30 @@ class Pyloid(QApplication):
1242
772
  self.tray.showMessage(title, message, QIcon(self.icon), 5000)
1243
773
 
1244
774
  def _update_tray_icon(self):
1245
- """Updates the animation frames."""
775
+ """
776
+ Updates the animation frame.
777
+ """
1246
778
  if hasattr(self, "tray") and self.icon_frames:
1247
779
  self.tray.setIcon(self.icon_frames[self.current_frame])
1248
780
  self.current_frame = (self.current_frame + 1) % len(self.icon_frames)
1249
781
 
1250
782
  def set_tray_icon_animation(self, icon_frames: List[str], interval: int = 200):
1251
783
  """
1252
- Dynamically sets and starts an animation for the tray icon.
784
+ Dynamically sets and starts the animation for the tray icon.
1253
785
  Can be called while the application is running, and changes are applied immediately.
1254
786
 
1255
- :param icon_frames: List of paths to animation frame images
1256
- :param interval: Interval between frames (milliseconds)
787
+ Parameters
788
+ ----------
789
+ icon_frames : list of str
790
+ List of animation frame image paths
791
+ interval : int, optional
792
+ Frame interval in milliseconds, default is 200
793
+
794
+ Examples
795
+ --------
796
+ >>> app = Pyloid(app_name="Pyloid-App")
797
+ >>> icon_frames = ["frame1.png", "frame2.png", "frame3.png"]
798
+ >>> app.set_tray_icon_animation(icon_frames, 100)
1257
799
  """
1258
800
  if not hasattr(self, "tray"):
1259
801
  self._init_tray()
@@ -1270,10 +812,12 @@ class Pyloid(QApplication):
1270
812
 
1271
813
  self.icon_frames = [QIcon(frame) for frame in icon_frames]
1272
814
  self.animation_interval = interval
1273
- self.start_tray_icon_animation()
815
+ self._start_tray_icon_animation()
1274
816
 
1275
- def start_tray_icon_animation(self):
1276
- """Starts the tray icon animation."""
817
+ def _start_tray_icon_animation(self):
818
+ """
819
+ Starts the tray icon animation.
820
+ """
1277
821
  if self.icon_frames:
1278
822
  if self.animation_timer is None:
1279
823
  self.animation_timer = QTimer(self)
@@ -1286,7 +830,15 @@ class Pyloid(QApplication):
1286
830
  Dynamically sets the tooltip for the tray icon.
1287
831
  Can be called while the application is running, and changes are applied immediately.
1288
832
 
1289
- :param message: New tooltip message
833
+ Parameters
834
+ ----------
835
+ message : str
836
+ New tooltip message
837
+
838
+ Examples
839
+ --------
840
+ >>> app = Pyloid(app_name="Pyloid-App")
841
+ >>> app.set_tray_tooltip("Pyloid is running")
1290
842
  """
1291
843
  if not hasattr(self, "tray"):
1292
844
  self._init_tray()
@@ -1296,7 +848,17 @@ class Pyloid(QApplication):
1296
848
  """
1297
849
  Sets the callback function to be called when a notification is clicked.
1298
850
 
1299
- :param callback: Callback function to be called when a notification is clicked
851
+ Parameters
852
+ ----------
853
+ callback : function
854
+ Callback function to be called when a notification is clicked
855
+
856
+ Examples
857
+ --------
858
+ >>> app = Pyloid(app_name="Pyloid-App")
859
+ >>> def on_notification_click():
860
+ >>> print("Notification clicked")
861
+ >>> app.set_notification_callback(on_notification_click)
1300
862
  """
1301
863
  if not hasattr(self, "tray"):
1302
864
  self._init_tray()
@@ -1307,9 +869,19 @@ class Pyloid(QApplication):
1307
869
  ###########################################################################################
1308
870
  def get_all_monitors(self) -> List[Monitor]:
1309
871
  """
1310
- Returns a list of information for all connected monitors.
872
+ Returns information about all connected monitors.
1311
873
 
1312
- :return: List containing monitor information
874
+ Returns
875
+ -------
876
+ list of Monitor
877
+ List containing monitor information
878
+
879
+ Examples
880
+ --------
881
+ >>> app = Pyloid(app_name="Pyloid-App")
882
+ >>> monitors = app.get_all_monitors()
883
+ >>> for monitor in monitors:
884
+ >>> print(monitor.info())
1313
885
  """
1314
886
  monitors = [
1315
887
  Monitor(index, screen) for index, screen in enumerate(self.screens())
@@ -1318,9 +890,18 @@ class Pyloid(QApplication):
1318
890
 
1319
891
  def get_primary_monitor(self) -> Monitor:
1320
892
  """
1321
- Returns information for the primary monitor.
893
+ Returns information about the primary monitor.
894
+
895
+ Returns
896
+ -------
897
+ Monitor
898
+ Primary monitor information
1322
899
 
1323
- :return: Primary monitor information
900
+ Examples
901
+ --------
902
+ >>> app = Pyloid(app_name="Pyloid-App")
903
+ >>> primary_monitor = app.get_primary_monitor()
904
+ >>> print(primary_monitor.info())
1324
905
  """
1325
906
  primary_monitor = self.screens()[0]
1326
907
  return Monitor(0, primary_monitor)
@@ -1328,11 +909,21 @@ class Pyloid(QApplication):
1328
909
  ###########################################################################################
1329
910
  # Clipboard
1330
911
  ###########################################################################################
1331
- def copy_to_clipboard(self, text):
912
+ def set_clipboard_text(self, text):
1332
913
  """
1333
914
  Copies text to the clipboard.
1334
915
 
1335
- :param text: Text to be copied
916
+ This function copies the given text to the clipboard. The text copied to the clipboard can be pasted into other applications.
917
+
918
+ Parameters
919
+ ----------
920
+ text : str
921
+ Text to copy to the clipboard
922
+
923
+ Examples
924
+ --------
925
+ >>> app = Pyloid(app_name="Pyloid-App")
926
+ >>> app.set_clipboard_text("Hello, World!")
1336
927
  """
1337
928
  self.clipboard_class.setText(text, QClipboard.Clipboard)
1338
929
 
@@ -1340,7 +931,19 @@ class Pyloid(QApplication):
1340
931
  """
1341
932
  Retrieves text from the clipboard.
1342
933
 
1343
- :return: Text from the clipboard
934
+ This function returns the text stored in the clipboard. If there is no text in the clipboard, it may return an empty string.
935
+
936
+ Returns
937
+ -------
938
+ str
939
+ Text stored in the clipboard
940
+
941
+ Examples
942
+ --------
943
+ >>> app = Pyloid(app_name="Pyloid-App")
944
+ >>> text = app.get_clipboard_text()
945
+ >>> print(text)
946
+ Hello, World!
1344
947
  """
1345
948
  return self.clipboard_class.text()
1346
949
 
@@ -1348,7 +951,17 @@ class Pyloid(QApplication):
1348
951
  """
1349
952
  Copies an image to the clipboard.
1350
953
 
1351
- :param image: Path to the image to be copied
954
+ This function copies the given image file to the clipboard. The image copied to the clipboard can be pasted into other applications.
955
+
956
+ Parameters
957
+ ----------
958
+ image : Union[str, bytes, os.PathLike]
959
+ Path to the image file to copy to the clipboard
960
+
961
+ Examples
962
+ --------
963
+ >>> app = Pyloid(app_name="Pyloid-App")
964
+ >>> app.set_clipboard_image("/path/to/image.png")
1352
965
  """
1353
966
  self.clipboard_class.setImage(QImage(image), QClipboard.Clipboard)
1354
967
 
@@ -1356,20 +969,46 @@ class Pyloid(QApplication):
1356
969
  """
1357
970
  Retrieves an image from the clipboard.
1358
971
 
1359
- :return: QImage object from the clipboard (None if no image)
972
+ This function returns the image stored in the clipboard. If there is no image in the clipboard, it may return None.
973
+
974
+ Returns
975
+ -------
976
+ QImage
977
+ QImage object stored in the clipboard (None if no image)
978
+
979
+ Examples
980
+ --------
981
+ >>> app = Pyloid(app_name="Pyloid-App")
982
+ >>> image = app.get_clipboard_image()
983
+ >>> if image is not None:
984
+ >>> image.save("/path/to/save/image.png")
1360
985
  """
1361
986
  return self.clipboard_class.image()
1362
987
 
1363
988
  ###########################################################################################
1364
- # Atostart
989
+ # Autostart
1365
990
  ###########################################################################################
1366
991
  def set_auto_start(self, enable: bool):
1367
992
  """
1368
- Sets the application to start automatically with the system. (set_auto_start(True) only works in production)
1369
- True only works in production.
1370
- False works in both environments.
993
+ Sets the application to start automatically at system startup. (set_auto_start(True) only works in production environment)
994
+ True only works in production environment.
995
+ False works in all environments.
996
+
997
+ Parameters
998
+ ----------
999
+ enable : bool
1000
+ True to enable auto start, False to disable
1001
+
1002
+ Returns
1003
+ -------
1004
+ bool or None
1005
+ True if auto start is successfully set, False if disabled, None if trying to enable in non-production environment
1371
1006
 
1372
- :param enable: True to enable auto-start, False to disable
1007
+ Examples
1008
+ --------
1009
+ >>> app = Pyloid(app_name="Pyloid-App")
1010
+ >>> app.set_auto_start(True)
1011
+ True
1373
1012
  """
1374
1013
  if not enable:
1375
1014
  self.auto_start.set_auto_start(False)
@@ -1387,11 +1026,20 @@ class Pyloid(QApplication):
1387
1026
 
1388
1027
  def is_auto_start(self):
1389
1028
  """
1390
- Checks if the application is set to start automatically with the system.
1029
+ Checks if the application is set to start automatically at system startup.
1391
1030
 
1392
- :return: True if auto-start is enabled, False otherwise
1393
- """
1031
+ Returns
1032
+ -------
1033
+ bool
1034
+ True if auto start is enabled, False otherwise
1394
1035
 
1036
+ Examples
1037
+ --------
1038
+ >>> app = Pyloid(app_name="Pyloid-App")
1039
+ >>> auto_start_enabled = app.is_auto_start()
1040
+ >>> print(auto_start_enabled)
1041
+ True
1042
+ """
1395
1043
  return self.auto_start.is_auto_start()
1396
1044
 
1397
1045
  ###########################################################################################
@@ -1401,8 +1049,23 @@ class Pyloid(QApplication):
1401
1049
  """
1402
1050
  Adds a file to the watch list.
1403
1051
 
1404
- :param file_path: Path of the file to watch
1405
- :return: True if the file was successfully added to the watch list, False otherwise
1052
+ This function adds the specified file to the watch list. When the file is changed, the set callback function is called.
1053
+
1054
+ Parameters
1055
+ ----------
1056
+ file_path : str
1057
+ Path to the file to watch
1058
+
1059
+ Returns
1060
+ -------
1061
+ bool
1062
+ True if the file is successfully added to the watch list, False otherwise
1063
+
1064
+ Examples
1065
+ --------
1066
+ >>> app = Pyloid(app_name="Pyloid-App")
1067
+ >>> app.watch_file("/path/to/file.txt")
1068
+ True
1406
1069
  """
1407
1070
  return self.file_watcher.add_path(file_path)
1408
1071
 
@@ -1410,8 +1073,23 @@ class Pyloid(QApplication):
1410
1073
  """
1411
1074
  Adds a directory to the watch list.
1412
1075
 
1413
- :param dir_path: Path of the directory to watch
1414
- :return: True if the directory was successfully added to the watch list, False otherwise
1076
+ This function adds the specified directory to the watch list. When a file in the directory is changed, the set callback function is called.
1077
+
1078
+ Parameters
1079
+ ----------
1080
+ dir_path : str
1081
+ Path to the directory to watch
1082
+
1083
+ Returns
1084
+ -------
1085
+ bool
1086
+ True if the directory is successfully added to the watch list, False otherwise
1087
+
1088
+ Examples
1089
+ --------
1090
+ >>> app = Pyloid(app_name="Pyloid-App")
1091
+ >>> app.watch_directory("/path/to/directory")
1092
+ True
1415
1093
  """
1416
1094
  return self.file_watcher.add_path(dir_path)
1417
1095
 
@@ -1419,8 +1097,23 @@ class Pyloid(QApplication):
1419
1097
  """
1420
1098
  Removes a file or directory from the watch list.
1421
1099
 
1422
- :param path: Path of the file or directory to stop watching
1423
- :return: True if the path was successfully removed from the watch list, False otherwise
1100
+ This function removes the specified file or directory from the watch list.
1101
+
1102
+ Parameters
1103
+ ----------
1104
+ path : str
1105
+ Path to the file or directory to stop watching
1106
+
1107
+ Returns
1108
+ -------
1109
+ bool
1110
+ True if the path is successfully removed from the watch list, False otherwise
1111
+
1112
+ Examples
1113
+ --------
1114
+ >>> app = Pyloid(app_name="Pyloid-App")
1115
+ >>> app.stop_watching("/path/to/file_or_directory")
1116
+ True
1424
1117
  """
1425
1118
  return self.file_watcher.remove_path(path)
1426
1119
 
@@ -1428,7 +1121,18 @@ class Pyloid(QApplication):
1428
1121
  """
1429
1122
  Returns all currently watched paths.
1430
1123
 
1431
- :return: List of all watched paths
1124
+ This function returns the paths of all files and directories currently being watched.
1125
+
1126
+ Returns
1127
+ -------
1128
+ List[str]
1129
+ List of all watched paths
1130
+
1131
+ Examples
1132
+ --------
1133
+ >>> app = Pyloid(app_name="Pyloid-App")
1134
+ >>> app.get_watched_paths()
1135
+ ['/path/to/file1.txt', '/path/to/directory']
1432
1136
  """
1433
1137
  return self.file_watcher.get_watched_paths()
1434
1138
 
@@ -1436,7 +1140,18 @@ class Pyloid(QApplication):
1436
1140
  """
1437
1141
  Returns all currently watched files.
1438
1142
 
1439
- :return: List of all watched files
1143
+ This function returns the paths of all files currently being watched.
1144
+
1145
+ Returns
1146
+ -------
1147
+ List[str]
1148
+ List of all watched files
1149
+
1150
+ Examples
1151
+ --------
1152
+ >>> app = Pyloid(app_name="Pyloid-App")
1153
+ >>> app.get_watched_files()
1154
+ ['/path/to/file1.txt', '/path/to/file2.txt']
1440
1155
  """
1441
1156
  return self.file_watcher.get_watched_files()
1442
1157
 
@@ -1444,7 +1159,18 @@ class Pyloid(QApplication):
1444
1159
  """
1445
1160
  Returns all currently watched directories.
1446
1161
 
1447
- :return: List of all watched directories
1162
+ This function returns the paths of all directories currently being watched.
1163
+
1164
+ Returns
1165
+ -------
1166
+ List[str]
1167
+ List of all watched directories
1168
+
1169
+ Examples
1170
+ --------
1171
+ >>> app = Pyloid(app_name="Pyloid-App")
1172
+ >>> app.get_watched_directories()
1173
+ ['/path/to/directory1', '/path/to/directory2']
1448
1174
  """
1449
1175
  return self.file_watcher.get_watched_directories()
1450
1176
 
@@ -1452,22 +1178,146 @@ class Pyloid(QApplication):
1452
1178
  """
1453
1179
  Removes all paths from the watch list.
1454
1180
 
1455
- :return: None
1181
+ This function removes the paths of all files and directories from the watch list.
1182
+
1183
+ Returns
1184
+ -------
1185
+ None
1186
+
1187
+ Examples
1188
+ --------
1189
+ >>> app = Pyloid(app_name="Pyloid-App")
1190
+ >>> app.remove_all_watched_paths()
1456
1191
  """
1457
1192
  self.file_watcher.remove_all_paths()
1458
1193
 
1459
1194
  def set_file_change_callback(self, callback: Callable[[str], None]) -> None:
1460
1195
  """
1461
- Sets the callback function to be called when a file changes.
1196
+ Sets the callback function to be called when a file is changed.
1462
1197
 
1463
- :param callback: Function to be called when a file changes
1198
+ This function sets the callback function to be called when a file is changed.
1199
+
1200
+ Parameters
1201
+ ----------
1202
+ callback : Callable[[str], None]
1203
+ Function to be called when a file is changed
1204
+
1205
+ Returns
1206
+ -------
1207
+ None
1208
+
1209
+ Examples
1210
+ --------
1211
+ >>> def on_file_change(file_path):
1212
+ >>> print(f"File changed: {file_path}")
1213
+ >>>
1214
+ >>> app = Pyloid(app_name="Pyloid-App")
1215
+ >>> app.set_file_change_callback(on_file_change)
1464
1216
  """
1465
1217
  self.file_watcher.file_changed.connect(callback)
1466
1218
 
1467
1219
  def set_directory_change_callback(self, callback: Callable[[str], None]) -> None:
1468
1220
  """
1469
- Sets the callback function to be called when a directory changes.
1221
+ Sets the callback function to be called when a directory is changed.
1222
+
1223
+ This function sets the callback function to be called when a directory is changed.
1224
+
1225
+ Parameters
1226
+ ----------
1227
+ callback : Callable[[str], None]
1228
+ Function to be called when a directory is changed
1470
1229
 
1471
- :param callback: Function to be called when a directory changes
1230
+ Returns
1231
+ -------
1232
+ None
1233
+
1234
+ Examples
1235
+ --------
1236
+ >>> def on_directory_change(dir_path):
1237
+ >>> print(f"Directory changed: {dir_path}")
1238
+ >>>
1239
+ >>> app = Pyloid(app_name="Pyloid-App")
1240
+ >>> app.set_directory_change_callback(on_directory_change)
1472
1241
  """
1473
1242
  self.file_watcher.directory_changed.connect(callback)
1243
+
1244
+ ###########################################################################################
1245
+ # File dialog
1246
+ ###########################################################################################
1247
+ def open_file_dialog(self, dir: Optional[str] = None, filter: Optional[str] = None) -> Optional[str]:
1248
+ """
1249
+ Opens a file dialog to select a file to open.
1250
+
1251
+ Parameters
1252
+ ----------
1253
+ dir : str, optional
1254
+ The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
1255
+ filter : str, optional
1256
+ A string that specifies the file types that can be selected. For example, "Text Files (*.txt);;All Files (*)".
1257
+
1258
+ Returns
1259
+ -------
1260
+ Optional[str]
1261
+ The path of the selected file. Returns None if no file is selected.
1262
+
1263
+ Examples
1264
+ --------
1265
+ >>> app = Pyloid(app_name="Pyloid-App")
1266
+ >>> file_path = app.open_file_dialog(dir="/home/user", filter="Text Files (*.txt)")
1267
+ >>> if file_path:
1268
+ >>> print("Selected file:", file_path)
1269
+ """
1270
+ file_path, _ = QFileDialog.getOpenFileName(None, dir=dir, filter=filter)
1271
+ return file_path if file_path else None
1272
+
1273
+ def save_file_dialog(self, dir: Optional[str] = None, filter: Optional[str] = None) -> Optional[str]:
1274
+ """
1275
+ Opens a file dialog to select a file to save.
1276
+
1277
+ Parameters
1278
+ ----------
1279
+ dir : str, optional
1280
+ The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
1281
+ filter : str, optional
1282
+ A string that specifies the file types that can be saved. For example, "Text Files (*.txt);;All Files (*)".
1283
+
1284
+ Returns
1285
+ -------
1286
+ Optional[str]
1287
+ The path of the selected file. Returns None if no file is selected.
1288
+
1289
+ Examples
1290
+ --------
1291
+ >>> app = Pyloid(app_name="Pyloid-App")
1292
+ >>> file_path = app.save_file_dialog(dir="/home/user", filter="Text Files (*.txt)")
1293
+ >>> if file_path:
1294
+ >>> print("File will be saved to:", file_path)
1295
+ """
1296
+ file_path, _ = QFileDialog.getSaveFileName(None, dir=dir, filter=filter)
1297
+ return file_path if file_path else None
1298
+
1299
+ def select_directory_dialog(self, dir: Optional[str] = None) -> Optional[str]:
1300
+ """
1301
+ Opens a dialog to select a directory.
1302
+
1303
+ Parameters
1304
+ ----------
1305
+ dir : str, optional
1306
+ The initial directory that the dialog will open in. If None, the dialog will open in the current working directory.
1307
+
1308
+ Returns
1309
+ -------
1310
+ Optional[str]
1311
+ The path of the selected directory. Returns None if no directory is selected.
1312
+
1313
+ Examples
1314
+ --------
1315
+ >>> app = Pyloid(app_name="Pyloid-App")
1316
+ >>> directory_path = app.select_directory_dialog(dir="/home/user")
1317
+ >>> if directory_path:
1318
+ >>> print("Selected directory:", directory_path)
1319
+ """
1320
+ directory_path = QFileDialog.getExistingDirectory(None, dir=dir)
1321
+ return directory_path if directory_path else None
1322
+
1323
+