pyloid 0.20.2__py3-none-any.whl → 0.20.2.dev0__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/browser_window.py CHANGED
@@ -1,1915 +1,1956 @@
1
- import sys
2
- import os
3
- from PySide6.QtWidgets import (
4
- QMainWindow,
5
- )
6
- from PySide6.QtWebEngineWidgets import QWebEngineView
7
- from PySide6.QtWebChannel import QWebChannel
8
- from PySide6.QtGui import (
9
- QKeySequence,
10
- QShortcut,
11
- QCursor,
12
- )
13
- from PySide6.QtCore import Qt, QPoint, QUrl, QEvent, QFile
14
- from PySide6.QtWebEngineCore import QWebEnginePage, QWebEngineSettings
15
- from .api import PyloidAPI
16
- import uuid
17
- from typing import List, Optional, Dict, Callable
18
- import json
19
- from PySide6.QtWidgets import (
20
- QWidget,
21
- QVBoxLayout,
22
- )
23
- from .custom.titlebar import CustomTitleBar
24
- from .js_api.window_api import WindowAPI
25
- from PySide6.QtGui import QPixmap, QMovie
26
- from PySide6.QtWidgets import QSplashScreen, QLabel
27
- from PySide6.QtCore import QSize
28
- from typing import TYPE_CHECKING
29
- from PySide6.QtWebEngineCore import QWebEngineSettings
30
-
31
- if TYPE_CHECKING:
32
- from ..pyloid import Pyloid
33
-
34
-
35
- # 어차피 load 부분에만 쓰이니까 나중에 분리해서 load 위에서 선언하자.
36
- class CustomWebPage(QWebEnginePage):
37
- def __init__(self, profile=None):
38
- super().__init__(profile)
39
- self.featurePermissionRequested.connect(self._handlePermissionRequest)
40
- self.desktopMediaRequested.connect(self._handleDesktopMediaRequest)
41
- self._permission_handlers = {}
42
- self._desktop_media_handler = None
43
-
44
- def _handlePermissionRequest(self, origin: QUrl, feature: QWebEnginePage.Feature):
45
- print(origin, feature)
46
-
47
- """Default permission request handler"""
48
- if feature in self._permission_handlers:
49
- # Execute if a handler is registered
50
- handler = self._permission_handlers[feature]
51
- handler(origin, feature)
52
- else:
53
- # Allow all permissions by default
54
- self.setFeaturePermission(
55
- origin, feature, QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
56
- )
57
-
58
- def setPermissionHandler(self, feature: QWebEnginePage.Feature, handler):
59
- """Register a handler for a specific permission"""
60
- self._permission_handlers[feature] = handler
61
-
62
- def _handleDesktopMediaRequest(self, *args, **kwargs):
63
- print("Desktop media request received:", args, kwargs)
64
-
65
- # def setDesktopMediaHandler(self, handler):
66
- # """desktop media handler"""
67
- # self._desktop_media_handler = handler
68
-
69
-
70
- class CustomWebEngineView(QWebEngineView):
71
- def __init__(self, parent: "BrowserWindow" = None):
72
- super().__init__(parent._window)
73
- self.parent: "BrowserWindow" = parent
74
-
75
- # Custom Web Page
76
- self.custom_page = CustomWebPage()
77
- self.setPage(self.custom_page)
78
-
79
- self.is_resizing = False
80
- self.resize_start_pos = None
81
- self.resize_direction = None
82
- self.screen_geometry = self.screen().virtualGeometry()
83
- self.is_resizing_enabled = True
84
-
85
- self.setAttribute(Qt.WA_SetCursor, False)
86
-
87
- self.is_in_resize_area = False
88
-
89
- def mouse_press_event(self, event):
90
- if self.parent.frame or not self.is_resizing_enabled:
91
- return
92
-
93
- if event.button() == Qt.LeftButton:
94
- self.resize_direction = self.get_resize_direction(event.pos())
95
- if self.resize_direction:
96
- self.is_resizing = True
97
- self.resize_start_pos = event.globalPos()
98
-
99
- def start_system_drag(self):
100
- """Start system window move"""
101
- if self.parent._window.windowHandle():
102
- self.parent._window.windowHandle().startSystemMove()
103
-
104
- def mouse_move_event(self, event):
105
- if self.parent.frame or not self.is_resizing_enabled:
106
- return
107
-
108
- # Check resize direction
109
- was_in_resize_area = self.is_in_resize_area
110
- resize_direction = self.get_resize_direction(event.pos())
111
- self.is_in_resize_area = bool(resize_direction)
112
-
113
- if resize_direction and not self.is_resizing:
114
- self.set_cursor_for_resize_direction(resize_direction)
115
-
116
- if self.is_resizing:
117
- self.resize_window(event.globalPos())
118
- return
119
-
120
- # Change cursor when entering/leaving resize area
121
- if self.is_in_resize_area != was_in_resize_area:
122
- if self.is_in_resize_area:
123
- # self.setAttribute(Qt.WA_SetCursor, True)
124
- pass
125
- else:
126
- self.unsetCursor()
127
- # self.setAttribute(Qt.WA_SetCursor, False)
128
-
129
- def mouse_release_event(self, event):
130
- if self.parent.frame or not self.is_resizing_enabled:
131
- return
132
-
133
- if event.button() == Qt.LeftButton:
134
- self.is_resizing = False
135
-
136
- if self.resize_direction:
137
- self.unsetCursor()
138
- self.resize_direction = None
139
-
140
- self.setAttribute(Qt.WA_SetCursor, False)
141
-
142
- def eventFilter(self, source, event):
143
- if self.focusProxy() is source:
144
- if event.type() == QEvent.MouseButtonPress:
145
- self.mouse_press_event(event)
146
- elif event.type() == QEvent.MouseMove:
147
- self.mouse_move_event(event)
148
- elif event.type() == QEvent.MouseButtonRelease:
149
- self.mouse_release_event(event)
150
- return super().eventFilter(source, event)
151
-
152
- def get_resize_direction(self, pos):
153
- if (
154
- not self.parent.frame and self.is_resizing_enabled
155
- ): # Check if frame is not present and resizing is enabled
156
- margin = 8 # Margin in pixels to detect edge
157
- rect = self.rect()
158
- direction = None
159
-
160
- if pos.x() <= margin:
161
- direction = "left"
162
- elif pos.x() >= rect.width() - margin:
163
- direction = "right"
164
-
165
- if pos.y() <= margin:
166
- direction = "top" if direction is None else direction + "-top"
167
- elif pos.y() >= rect.height() - margin:
168
- direction = "bottom" if direction is None else direction + "-bottom"
169
-
170
- return direction
171
- return None
172
-
173
- def set_cursor_for_resize_direction(self, direction):
174
- if not self.parent.frame and direction and self.is_resizing_enabled:
175
- cursor = None
176
- if direction in ["left", "right"]:
177
- cursor = Qt.SizeHorCursor
178
- elif direction in ["top", "bottom"]:
179
- cursor = Qt.SizeVerCursor
180
- elif direction in ["left-top", "right-bottom"]:
181
- cursor = Qt.SizeFDiagCursor
182
- elif direction in ["right-top", "left-bottom"]:
183
- cursor = Qt.SizeBDiagCursor
184
-
185
- if cursor:
186
- self.setCursor(cursor)
187
- self.setAttribute(Qt.WA_SetCursor, True)
188
-
189
- def resize_window(self, global_pos):
190
- if (
191
- not self.parent.frame
192
- and self.resize_start_pos
193
- and self.resize_direction
194
- and self.is_resizing_enabled
195
- ): # Check if frame is not present and resizing is enabled
196
- delta = global_pos - self.resize_start_pos
197
- new_geometry = self.parent._window.geometry()
198
-
199
- if "left" in self.resize_direction:
200
- new_geometry.setLeft(new_geometry.left() + delta.x())
201
- if "right" in self.resize_direction:
202
- new_geometry.setRight(new_geometry.right() + delta.x())
203
- if "top" in self.resize_direction:
204
- new_geometry.setTop(new_geometry.top() + delta.y())
205
- if "bottom" in self.resize_direction:
206
- new_geometry.setBottom(new_geometry.bottom() + delta.y())
207
-
208
- self.parent._window.setGeometry(new_geometry)
209
- self.resize_start_pos = global_pos
210
-
211
-
212
- class BrowserWindow:
213
- def __init__(
214
- self,
215
- app: "Pyloid",
216
- title: str = "pyloid app",
217
- width: int = 800,
218
- height: int = 600,
219
- x: int = 200,
220
- y: int = 200,
221
- frame: bool = True,
222
- context_menu: bool = False,
223
- dev_tools: bool = False,
224
- js_apis: List[PyloidAPI] = [],
225
- ):
226
- ###########################################################################################
227
- self.id = str(uuid.uuid4()) # Generate unique ID
228
- self._window = QMainWindow()
229
- self.web_view = CustomWebEngineView(self)
230
-
231
- self._window.closeEvent = self.closeEvent # Override closeEvent method
232
- ###########################################################################################
233
- self.app = app
234
- self.title = title
235
- self.width = width
236
- self.height = height
237
- self.x = x
238
- self.y = y
239
- self.frame = frame
240
- self.context_menu = context_menu
241
- self.dev_tools = dev_tools
242
- self.js_apis = [WindowAPI()]
243
- for js_api in js_apis:
244
- self.js_apis.append(js_api)
245
- self.shortcuts = {}
246
- self.close_on_load = True
247
- self.splash_screen = None
248
- ###########################################################################################
249
-
250
- def _set_custom_frame(
251
- self,
252
- use_custom: bool,
253
- title: str = "Custom Title",
254
- bg_color: str = "darkblue",
255
- text_color: str = "white",
256
- icon_path: str = None,
257
- ):
258
- """Sets or removes the custom frame."""
259
- if use_custom:
260
- self._window.setWindowFlags(Qt.FramelessWindowHint)
261
- self.custom_title_bar = CustomTitleBar(self._window)
262
- self.custom_title_bar.set_style(bg_color, text_color)
263
- self.custom_title_bar.set_title(title)
264
-
265
- if icon_path:
266
- self.custom_title_bar.set_icon(icon_path)
267
-
268
- layout = QVBoxLayout()
269
- layout.setContentsMargins(0, 0, 0, 0)
270
- layout.setSpacing(0)
271
- layout.addWidget(self.custom_title_bar)
272
- layout.addWidget(self.web_view)
273
-
274
- central_widget = QWidget()
275
- central_widget.setLayout(layout)
276
- self._window.setCentralWidget(central_widget)
277
-
278
- # Add properties for window movement
279
- self._window.moving = False
280
- self._window.offset = QPoint()
281
- else:
282
- self._window.setWindowFlags(Qt.Window)
283
- self._window.setCentralWidget(self.web_view)
284
- self.custom_title_bar = None
285
-
286
- self._window.show()
287
-
288
- def _load(self):
289
- self.set_title(self.title)
290
-
291
- self.set_size(self.width, self.height)
292
- self.set_position(self.x, self.y)
293
-
294
- # allow local file access to remote urls and screen capture
295
- self.web_view.settings().setAttribute(
296
- QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True
297
- )
298
- self.web_view.settings().setAttribute(
299
- QWebEngineSettings.WebAttribute.ScreenCaptureEnabled, True
300
- )
301
- self.web_view.settings().setAttribute(
302
- QWebEngineSettings.WebAttribute.AutoLoadImages, True
303
- )
304
- self.web_view.settings().setAttribute(
305
- QWebEngineSettings.WebAttribute.JavascriptEnabled, True
306
- )
307
- self.web_view.settings().setAttribute(
308
- QWebEngineSettings.WebAttribute.LocalStorageEnabled, True
309
- )
310
- self.web_view.settings().setAttribute(
311
- QWebEngineSettings.WebAttribute.ErrorPageEnabled, True
312
- )
313
- self.web_view.settings().setAttribute(
314
- QWebEngineSettings.WebAttribute.AutoLoadIconsForPage, True
315
- )
316
- self.web_view.settings().setAttribute(
317
- QWebEngineSettings.WebAttribute.ShowScrollBars, True
318
- )
319
- self.web_view.settings().setAttribute(
320
- QWebEngineSettings.WebAttribute.DnsPrefetchEnabled, True
321
- )
322
- self.web_view.settings().setAttribute(
323
- QWebEngineSettings.WebAttribute.PdfViewerEnabled, True
324
- )
325
- self.web_view.settings().setAttribute(
326
- QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True
327
- )
328
- self.web_view.settings().setAttribute(
329
- QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True
330
- )
331
- self.web_view.settings().setUnknownUrlSchemePolicy(
332
- QWebEngineSettings.UnknownUrlSchemePolicy.AllowAllUnknownUrlSchemes
333
- )
334
- self.web_view.settings().setAttribute(
335
- QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True
336
- )
337
- self.web_view.settings().setAttribute(
338
- QWebEngineSettings.WebAttribute.AllowGeolocationOnInsecureOrigins, True
339
- )
340
- self.web_view.settings().setAttribute(
341
- QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True
342
- )
343
- self.web_view.settings().setAttribute(
344
- QWebEngineSettings.WebAttribute.JavascriptCanPaste, True
345
- )
346
- self.web_view.settings().setAttribute(
347
- QWebEngineSettings.WebAttribute.WebRTCPublicInterfacesOnly, False
348
- )
349
-
350
- # Set icon
351
- if self.app.icon:
352
- self._window.setWindowIcon(self.app.icon)
353
- else:
354
- print("Icon is not set.")
355
-
356
- # Set Windows taskbar icon
357
- if sys.platform == "win32":
358
- import ctypes
359
-
360
- myappid = "mycompany.myproduct.subproduct.version"
361
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
362
-
363
- # Remove title bar and borders (if needed)
364
- if not self.frame:
365
- self._window.setWindowFlags(Qt.FramelessWindowHint)
366
-
367
- # Disable default context menu
368
- if not self.context_menu:
369
- self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
370
-
371
- # Set up QWebChannel
372
- self.channel = QWebChannel()
373
-
374
- # Register additional JS APIs
375
- if self.js_apis:
376
- for js_api in self.js_apis:
377
- # Define window_id, window, and app for each JS API
378
- js_api.window_id = self.id
379
- js_api.window = self
380
- js_api.app = self.app
381
-
382
- self.channel.registerObject(js_api.__class__.__name__, js_api)
383
-
384
- self.web_view.page().setWebChannel(self.channel)
385
-
386
- # Connect pylonjs bridge
387
- self.web_view.loadFinished.connect(self._on_load_finished)
388
-
389
- # Add QWebEngineView to main window
390
- self._window.setCentralWidget(self.web_view)
391
-
392
- # Set F12 shortcut
393
- self.set_dev_tools(self.dev_tools)
394
-
395
- def _on_load_finished(self, ok):
396
- """Handles the event when the web page finishes loading."""
397
- if ok and self.js_apis:
398
- # Load qwebchannel.js
399
- qwebchannel_js = QFile("://qtwebchannel/qwebchannel.js")
400
- if qwebchannel_js.open(QFile.ReadOnly):
401
- source = bytes(qwebchannel_js.readAll()).decode("utf-8")
402
- self.web_view.page().runJavaScript(source)
403
- qwebchannel_js.close()
404
-
405
- js_code = """
406
- if (typeof QWebChannel !== 'undefined') {
407
- new QWebChannel(qt.webChannelTransport, function (channel) {
408
- window.pyloid = {
409
- EventAPI: {
410
- _listeners: {}, // 콜백 함수들을 저장할 객체
411
-
412
- listen: function(eventName, callback) {
413
- // 이벤트에 대한 콜백 배열이 없다면 생성
414
- if (!this._listeners[eventName]) {
415
- this._listeners[eventName] = [];
416
- }
417
-
418
- // 콜백 함수 저장
419
- this._listeners[eventName].push(callback);
420
-
421
- document.addEventListener(eventName, function(event) {
422
- let eventData;
423
- try {
424
- eventData = JSON.parse(event.detail);
425
- } catch (e) {
426
- eventData = event.detail;
427
- }
428
- callback(eventData);
429
- });
430
- },
431
-
432
- unlisten: function(eventName) {
433
- // 해당 이벤트의 모든 리스너 제거
434
- if (this._listeners[eventName]) {
435
- this._listeners[eventName].forEach(callback => {
436
- document.removeEventListener(eventName, callback);
437
- });
438
- // 저장된 콜백 제거
439
- delete this._listeners[eventName];
440
- }
441
- }
442
- }
443
- };
444
- console.log('pyloid.EventAPI object initialized:', window.pyloid.EventAPI);
445
-
446
- %s
447
-
448
- document.addEventListener('mousedown', function (e) {
449
- if (e.target.hasAttribute('data-pyloid-drag-region')) {
450
- window.pyloid.WindowAPI.startSystemDrag();
451
- }
452
- });
453
-
454
- function updateTheme(theme) {
455
- document.documentElement.setAttribute(
456
- 'data-pyloid-theme',
457
- theme
458
- );
459
- }
460
-
461
- // 테마 변경 이벤트 리스너
462
- document.addEventListener('themeChange', (e) => {
463
- console.log('themeChange event received:', e);
464
- updateTheme(e.detail.theme);
465
- });
466
-
467
- updateTheme('%s');
468
-
469
-
470
- // Dispatch a custom event to signal that the initialization is ready
471
- const event = new CustomEvent('pyloidReady');
472
- document.dispatchEvent(event);
473
- });
474
- } else {
475
- console.error('QWebChannel is not defined.');
476
- }
477
- """
478
- js_api_init = "\n".join(
479
- [
480
- f"window.pyloid['{js_api.__class__.__name__}'] = channel.objects['{js_api.__class__.__name__}'];\n"
481
- f"console.log('pyloid.{js_api.__class__.__name__} object initialized:', window.pyloid['{js_api.__class__.__name__}']);"
482
- for js_api in self.js_apis
483
- ]
484
- )
485
- self.web_view.page().runJavaScript(js_code % (js_api_init, self.app.theme))
486
-
487
- # if splash screen is set, close it when the page is loaded
488
- if self.close_on_load and self.splash_screen:
489
- self.close_splash_screen()
490
- else:
491
- pass
492
-
493
- ###########################################################################################
494
- # Load
495
- ###########################################################################################
496
- def load_file(self, file_path: str):
497
- """
498
- Loads a local HTML file into the web view.
499
-
500
- Parameters
501
- ----------
502
- file_path : str
503
- The path to the local HTML file to be loaded.
504
-
505
- Examples
506
- --------
507
- >>> app = Pyloid(app_name="Pyloid-App")
508
- >>> window = app.create_window("pyloid-window")
509
- >>> window.load_file('/path/to/local/file.html')
510
- >>> window.show()
511
- """
512
- self._load()
513
- file_path = os.path.abspath(file_path) # absolute path
514
- self.web_view.setUrl(QUrl.fromLocalFile(file_path))
515
- self.web_view.focusProxy().installEventFilter(self.web_view)
516
-
517
- def load_url(self, url: str):
518
- """
519
- Sets the URL of the window.
520
-
521
- Parameters
522
- ----------
523
- url : str
524
- The URL to be loaded in the web view.
525
-
526
- Examples
527
- --------
528
- >>> app = Pyloid(app_name="Pyloid-App")
529
- >>> window = app.create_window("pyloid-window")
530
- >>> window.load_url('https://www.example.com')
531
- >>> window.show()
532
- """
533
- self._load()
534
- self.web_view.setUrl(QUrl(url))
535
- self.web_view.focusProxy().installEventFilter(self.web_view)
536
-
537
- def load_html(self, html_content: str, base_url: str = ""):
538
- """
539
- Loads HTML content directly into the web view.
540
-
541
- Parameters
542
- ----------
543
- html_content : str
544
- The HTML content to be loaded.
545
- base_url : str, optional
546
- The base URL to use for resolving relative URLs (default is "").
547
-
548
- Examples
549
- --------
550
- >>> app = Pyloid(app_name="Pyloid-App")
551
- >>> window = app.create_window("pyloid-window")
552
- >>> html_content = "<html><body><h1>Hello, Pyloid!</h1></body></html>"
553
- >>> window.load_html(html_content)
554
- >>> window.show()
555
- """
556
- self._load()
557
- self.web_view.setHtml(html_content, QUrl(base_url))
558
- self.web_view.focusProxy().installEventFilter(self.web_view)
559
-
560
- ###########################################################################################
561
- # Set Parameters
562
- ###########################################################################################
563
- def set_title(self, title: str):
564
- """
565
- Sets the title of the window.
566
-
567
- Parameters
568
- ----------
569
- title : str
570
- The title to be set for the window.
571
-
572
- Examples
573
- --------
574
- >>> app = Pyloid(app_name="Pyloid-App")
575
- >>> window = app.create_window("pyloid-window")
576
- >>> window.set_title('My Window Title')
577
- """
578
- self.title = title
579
- self._window.setWindowTitle(self.title)
580
-
581
- def set_size(self, width: int, height: int):
582
- """
583
- Sets the size of the window.
584
-
585
- Parameters
586
- ----------
587
- width : int
588
- The width of the window.
589
- height : int
590
- The height of the window.
591
-
592
- Examples
593
- --------
594
- >>> app = Pyloid(app_name="Pyloid-App")
595
- >>> window = app.create_window("pyloid-window")
596
- >>> window.set_size(800, 600)
597
- """
598
- self.width = width
599
- self.height = height
600
- self._window.setGeometry(self.x, self.y, self.width, self.height)
601
-
602
- def set_position(self, x: int, y: int):
603
- """
604
- Sets the position of the window.
605
-
606
- Parameters
607
- ----------
608
- x : int
609
- The x-coordinate of the window's position.
610
- y : int
611
- The y-coordinate of the window's position.
612
-
613
- Examples
614
- --------
615
- >>> app = Pyloid(app_name="Pyloid-App")
616
- >>> window = app.create_window("pyloid-window")
617
- >>> window.set_position(100, 100)
618
- """
619
- self.x = x
620
- self.y = y
621
- self._window.setGeometry(self.x, self.y, self.width, self.height)
622
-
623
- def set_position_by_anchor(self, anchor: str):
624
- """
625
- Positions the window at a specific location on the screen.
626
-
627
- Parameters
628
- ----------
629
- anchor : str
630
- The anchor point indicating where to position the window.
631
- Possible values: 'center', 'top', 'bottom', 'left', 'right',
632
- 'top-left', 'top-right', 'bottom-left', 'bottom-right'
633
-
634
- Examples
635
- --------
636
- >>> window.set_position_by_anchor('center')
637
- >>> window.set_position_by_anchor('top-right')
638
- """
639
- screen = self.app.primaryScreen().availableGeometry()
640
- window_size = self.get_size()
641
-
642
- if anchor == "center":
643
- x = (screen.width() - window_size["width"]) // 2
644
- y = (screen.height() - window_size["height"]) // 2
645
- elif anchor == "top":
646
- x = (screen.width() - window_size["width"]) // 2
647
- y = screen.top()
648
- elif anchor == "bottom":
649
- x = (screen.width() - window_size["width"]) // 2
650
- y = screen.bottom() - window_size["height"]
651
- elif anchor == "left":
652
- x = screen.left()
653
- y = (screen.height() - window_size["height"]) // 2
654
- elif anchor == "right":
655
- x = screen.right() - window_size["width"]
656
- y = (screen.height() - window_size["height"]) // 2
657
- elif anchor == "top-left":
658
- x = screen.left()
659
- y = screen.top()
660
- elif anchor == "top-right":
661
- x = screen.right() - window_size["width"]
662
- y = screen.top()
663
- elif anchor == "bottom-left":
664
- x = screen.left()
665
- y = screen.bottom() - window_size["height"]
666
- elif anchor == "bottom-right":
667
- x = screen.right() - window_size["width"]
668
- y = screen.bottom() - window_size["height"]
669
- else:
670
- raise ValueError("Invalid anchor point.")
671
-
672
- self.set_position(x, y)
673
-
674
- def set_frame(self, frame: bool):
675
- """
676
- Sets the frame of the window.
677
-
678
- Parameters
679
- ----------
680
- frame : bool
681
- If True, the window will have a frame. If False, the window will be frameless.
682
-
683
- Examples
684
- --------
685
- >>> app = Pyloid(app_name="Pyloid-App")
686
- >>> window = app.create_window("pyloid-window")
687
- >>> window.set_frame(True)
688
- >>> window.set_frame(False)
689
- """
690
- self.frame = frame
691
- was_visible = self._window.isVisible()
692
- if self.frame:
693
- self._window.setWindowFlags(Qt.Window)
694
- else:
695
- self._window.setWindowFlags(Qt.FramelessWindowHint)
696
- if was_visible:
697
- self._window.show()
698
-
699
- def set_context_menu(self, context_menu: bool):
700
- """
701
- Sets the context menu of the window.
702
-
703
- Parameters
704
- ----------
705
- context_menu : bool
706
- If True, the context menu will be disabled. If False, the default context menu will be enabled.
707
-
708
- Examples
709
- --------
710
- >>> app = Pyloid(app_name="Pyloid-App")
711
- >>> window = app.create_window("pyloid-window")
712
- >>> window.set_context_menu(True)
713
- >>> window.set_context_menu(False)
714
- """
715
- self.context_menu = context_menu
716
- if self.context_menu:
717
- self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
718
- else:
719
- self.web_view.setContextMenuPolicy(Qt.DefaultContextMenu)
720
-
721
- def set_dev_tools(self, enable: bool):
722
- """
723
- Sets the developer tools of the window.
724
-
725
- If enabled, the developer tools can be opened using the F12 key.
726
-
727
- Parameters
728
- ----------
729
- enable : bool
730
- If True, the developer tools will be enabled. If False, the developer tools will be disabled.
731
-
732
- Examples
733
- --------
734
- >>> app = Pyloid(app_name="Pyloid-App")
735
- >>> window = app.create_window("pyloid-window")
736
- >>> window.set_dev_tools(True)
737
- >>> window.set_dev_tools(False)
738
- """
739
- self.dev_tools = enable
740
- if self.dev_tools:
741
- self.add_shortcut("F12", self.open_dev_tools)
742
- else:
743
- self.remove_shortcut("F12")
744
-
745
- def open_dev_tools(self):
746
- """
747
- Opens the developer tools window.
748
-
749
- Examples
750
- --------
751
- >>> app = Pyloid(app_name="Pyloid-App")
752
- >>> window = app.create_window("pyloid-window")
753
- >>> window.open_dev_tools()
754
- """
755
- self.web_view.page().setDevToolsPage(QWebEnginePage(self.web_view.page()))
756
- self.dev_tools_window = QMainWindow(self._window)
757
- dev_tools_view = QWebEngineView(self.dev_tools_window)
758
- dev_tools_view.setPage(self.web_view.page().devToolsPage())
759
- self.dev_tools_window.setCentralWidget(dev_tools_view)
760
- self.dev_tools_window.resize(800, 600)
761
- self.dev_tools_window.show()
762
-
763
- # Add this line to handle dev tools window closure
764
- self.dev_tools_window.closeEvent = lambda event: setattr(
765
- self, "dev_tools_window", None
766
- )
767
-
768
- def closeEvent(self, event):
769
- """Handles the event when the window is closed."""
770
- # Close developer tools if open
771
- if hasattr(self, "dev_tools_window") and self.dev_tools_window:
772
- self.dev_tools_window.close()
773
- self.dev_tools_window = None
774
-
775
- # Solve memory leak issue with web view engine
776
- self.web_view.page().deleteLater()
777
- self.web_view.deleteLater()
778
- self._remove_from_app_windows()
779
- event.accept() # Accept the event (allow the window to close)
780
-
781
- def _remove_from_app_windows(self):
782
- """Removes the window from the app's window list."""
783
- if self in self.app.windows:
784
- self.app.windows.remove(self)
785
- if not self.app.windows:
786
- self.app.quit() # Quit the app if all windows are closed
787
-
788
- ###########################################################################################
789
- # Window management (no ID required)
790
- ###########################################################################################
791
- def hide(self):
792
- """
793
- Hides the window.
794
-
795
- Examples
796
- --------
797
- >>> app = Pyloid(app_name="Pyloid-App")
798
- >>> window = app.create_window("pyloid-window")
799
- >>> window.hide()
800
- """
801
- self._window.hide()
802
-
803
- def show(self):
804
- """
805
- Shows the window.
806
-
807
- Examples
808
- --------
809
- >>> app = Pyloid(app_name="Pyloid-App")
810
- >>> window = app.create_window("pyloid-window")
811
- >>> window.show()
812
- """
813
- self._window.show()
814
-
815
- def focus(self):
816
- """
817
- Focuses the window.
818
-
819
- Examples
820
- --------
821
- >>> app = Pyloid(app_name="Pyloid-App")
822
- >>> window = app.create_window("pyloid-window")
823
- >>> window.focus()
824
- """
825
- self._window.activateWindow()
826
- self._window.raise_()
827
- self._window.setWindowState(
828
- self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
829
- )
830
-
831
- def show_and_focus(self):
832
- """
833
- Shows and focuses the window.
834
-
835
- Examples
836
- --------
837
- >>> app = Pyloid(app_name="Pyloid-App")
838
- >>> window = app.create_window("pyloid-window")
839
- >>> window.show_and_focus()
840
- """
841
- self._window.show()
842
- self._window.activateWindow()
843
- self._window.raise_()
844
- self._window.setWindowState(
845
- self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
846
- )
847
-
848
- def close(self):
849
- """
850
- Closes the window.
851
-
852
- Examples
853
- --------
854
- >>> app = Pyloid(app_name="Pyloid-App")
855
- >>> window = app.create_window("pyloid-window")
856
- >>> window.close()
857
- """
858
- self._window.close()
859
-
860
- def fullscreen(self):
861
- """
862
- Enters fullscreen mode.
863
-
864
- Examples
865
- --------
866
- >>> app = Pyloid(app_name="Pyloid-App")
867
- >>> window = app.create_window("pyloid-window")
868
- >>> window.fullscreen()
869
- """
870
- self._window.showFullScreen()
871
-
872
- def toggle_fullscreen(self):
873
- """
874
- Toggles the fullscreen mode of the window.
875
-
876
- Examples
877
- --------
878
- >>> app = Pyloid(app_name="Pyloid-App")
879
- >>> window = app.create_window("pyloid-window")
880
- >>> window.toggle_fullscreen()
881
- """
882
- if self._window.isFullScreen():
883
- self._window.showNormal()
884
- else:
885
- self._window.showFullScreen()
886
-
887
- def minimize(self):
888
- """
889
- Minimizes the window.
890
-
891
- Examples
892
- --------
893
- >>> app = Pyloid(app_name="Pyloid-App")
894
- >>> window = app.create_window("pyloid-window")
895
- >>> window.minimize()
896
- """
897
- self._window.showMinimized()
898
-
899
- def maximize(self):
900
- """
901
- Maximizes the window.
902
-
903
- Examples
904
- --------
905
- >>> app = Pyloid(app_name="Pyloid-App")
906
- >>> window = app.create_window("pyloid-window")
907
- >>> window.maximize()
908
- """
909
- self._window.showMaximized()
910
-
911
- def unmaximize(self):
912
- """
913
- Restores the window from maximized state.
914
-
915
- Examples
916
- --------
917
- >>> app = Pyloid(app_name="Pyloid-App")
918
- >>> window = app.create_window("pyloid-window")
919
- >>> window.unmaximize()
920
- """
921
- self._window.showNormal()
922
-
923
- def toggle_maximize(self):
924
- """
925
- Toggles the maximized state of the window.
926
-
927
- Examples
928
- --------
929
- >>> app = Pyloid(app_name="Pyloid-App")
930
- >>> window = app.create_window("pyloid-window")
931
- >>> window.toggle_maximize()
932
- """
933
- if self._window.isMaximized():
934
- self._window.showNormal()
935
- else:
936
- self._window.showMaximized()
937
-
938
- def is_fullscreen(self) -> bool:
939
- """
940
- Returns True if the window is fullscreen.
941
-
942
- Examples
943
- --------
944
- >>> app = Pyloid(app_name="Pyloid-App")
945
- >>> window = app.create_window("pyloid-window")
946
- >>> window.is_fullscreen()
947
- """
948
- return self._window.isFullScreen()
949
-
950
- def is_maximized(self) -> bool:
951
- """
952
- Returns True if the window is maximized.
953
-
954
- Examples
955
- --------
956
- >>> app = Pyloid(app_name="Pyloid-App")
957
- >>> window = app.create_window("pyloid-window")
958
- >>> window.is_maximized()
959
- """
960
- return self._window.isMaximized()
961
-
962
- def capture(self, save_path: str) -> Optional[str]:
963
- """
964
- Captures the current window.
965
-
966
- Parameters
967
- ----------
968
- save_path : str
969
- Path to save the captured image. If not specified, it will be saved in the current directory.
970
-
971
- Returns
972
- -------
973
- Optional[str]
974
- Returns the path of the saved image.
975
-
976
- Examples
977
- --------
978
- >>> app = Pyloid(app_name="Pyloid-App")
979
- >>> window = app.create_window("pyloid-window")
980
- >>> save_path = window.capture("screenshot.png")
981
- >>> print(f"Image saved at: {save_path}")
982
- Image saved at: screenshot.png
983
- """
984
- try:
985
- # Capture the window
986
- screenshot = self._window.grab()
987
-
988
- # Save the image
989
- screenshot.save(save_path)
990
- return save_path
991
- except Exception as e:
992
- print(f"An error occurred while capturing the window: {e}")
993
- return None
994
-
995
- ###########################################################################################
996
- # Shortcut
997
- ###########################################################################################
998
- def add_shortcut(self, key_sequence: str, callback: Callable):
999
- """
1000
- Adds a keyboard shortcut to the window if it does not already exist.
1001
-
1002
- Parameters
1003
- ----------
1004
- key_sequence : str
1005
- Shortcut sequence (e.g., "Ctrl+C")
1006
- callback : Callable
1007
- Function to be executed when the shortcut is pressed
1008
-
1009
- Returns
1010
- -------
1011
- QShortcut or None
1012
- Created QShortcut object or None if the shortcut already exists
1013
-
1014
- Examples
1015
- --------
1016
- ```python
1017
- app = Pyloid(app_name="Pyloid-App")
1018
-
1019
- window = app.create_window("pyloid-window")
1020
-
1021
- def on_shortcut():
1022
- print("Shortcut activated!")
1023
- window.add_shortcut("Ctrl+C", on_shortcut)
1024
-
1025
- app.run()
1026
- ```
1027
- """
1028
- if key_sequence in self.shortcuts:
1029
- # print(f"Shortcut {key_sequence} already exists.")
1030
- return None
1031
-
1032
- shortcut = QShortcut(QKeySequence(key_sequence), self._window)
1033
- shortcut.activated.connect(callback)
1034
- self.shortcuts[key_sequence] = shortcut
1035
- return shortcut
1036
-
1037
- def remove_shortcut(self, key_sequence: str):
1038
- """
1039
- Removes a keyboard shortcut from the window.
1040
-
1041
- Parameters
1042
- ----------
1043
- key_sequence : str
1044
- Shortcut sequence to be removed
1045
-
1046
- Examples
1047
- --------
1048
- ```python
1049
- app = Pyloid(app_name="Pyloid-App")
1050
-
1051
- window = app.create_window("pyloid-window")
1052
- window.remove_shortcut("Ctrl+C")
1053
-
1054
- app.run()
1055
- ```
1056
- """
1057
- if key_sequence in self.shortcuts:
1058
- shortcut = self.shortcuts.pop(key_sequence)
1059
- shortcut.setEnabled(False)
1060
- shortcut.deleteLater()
1061
-
1062
- def get_all_shortcuts(self):
1063
- """
1064
- Returns all registered shortcuts in the window.
1065
-
1066
- Returns
1067
- -------
1068
- dict
1069
- Dictionary of shortcut sequences and QShortcut objects
1070
-
1071
- Examples
1072
- --------
1073
- ```python
1074
- app = Pyloid(app_name="Pyloid-App")
1075
-
1076
- window = app.create_window("pyloid-window")
1077
- shortcuts = window.get_all_shortcuts()
1078
- print(shortcuts)
1079
-
1080
- app.run()
1081
- ```
1082
- """
1083
- return self.shortcuts
1084
-
1085
- ###########################################################################################
1086
- # Event (Calling the JS from Python)
1087
- ###########################################################################################
1088
- def emit(self, event_name, data: Optional[Dict] = None):
1089
- """
1090
- Emits an event to the JavaScript side.
1091
-
1092
- Parameters
1093
- ----------
1094
- event_name : str
1095
- Name of the event
1096
- data : dict, optional
1097
- Data to be sent with the event (default is None)
1098
-
1099
- Examples
1100
- --------
1101
- (Python)
1102
- ```python
1103
- app = Pyloid(app_name="Pyloid-App")
1104
-
1105
- window = app.create_window("pyloid-window")
1106
- window.emit("customEvent", {"message": "Hello, Pyloid!"})
1107
-
1108
- app.run()
1109
- ```
1110
- ---
1111
-
1112
- (JavaScript)
1113
- ```javascript
1114
- document.addEventListener('customEvent', (data) => {
1115
- console.log(data.message);
1116
- });
1117
- ```
1118
- """
1119
- script = f"""
1120
- (function() {{
1121
- const eventData = {json.dumps(data)};
1122
- const customEvent = new CustomEvent('{event_name}', {{ detail: eventData }});
1123
- document.dispatchEvent(customEvent);
1124
- }})();
1125
- """
1126
- self.web_view.page().runJavaScript(script)
1127
-
1128
- ###########################################################################################
1129
- # Get Properties
1130
- ###########################################################################################
1131
- def get_window_properties(self):
1132
- """
1133
- Returns the properties of the window.
1134
-
1135
- Returns
1136
- -------
1137
- dict
1138
- Dictionary containing the properties of the window
1139
-
1140
- Examples
1141
- --------
1142
- ```python
1143
- app = Pyloid(app_name="Pyloid-App")
1144
-
1145
- window = app.create_window("pyloid-window")
1146
- properties = window.get_window_properties()
1147
- print(properties)
1148
-
1149
- app.run()
1150
- ```
1151
- """
1152
- return {
1153
- "id": self.id,
1154
- "title": self.title,
1155
- "width": self.width,
1156
- "height": self.height,
1157
- "x": self.x,
1158
- "y": self.y,
1159
- "frame": self.frame,
1160
- "context_menu": self.context_menu,
1161
- "dev_tools": self.dev_tools,
1162
- }
1163
-
1164
- def get_id(self):
1165
- """
1166
- Returns the ID of the window.
1167
-
1168
- Returns
1169
- -------
1170
- str
1171
- ID of the window
1172
-
1173
- Examples
1174
- --------
1175
- ```python
1176
- app = Pyloid(app_name="Pyloid-App")
1177
-
1178
- window = app.create_window("pyloid-window")
1179
- window_id = window.get_id()
1180
- print(window_id)
1181
-
1182
- app.run()
1183
- ```
1184
- """
1185
- return self.id
1186
-
1187
- def get_size(self) -> Dict[str, int]:
1188
- """
1189
- Returns the size of the window.
1190
-
1191
- Returns
1192
- -------
1193
- dict
1194
- Dictionary containing the width and height of the window
1195
-
1196
- Examples
1197
- --------
1198
- ```python
1199
- app = Pyloid(app_name="Pyloid-App")
1200
-
1201
- window = app.create_window("pyloid-window")
1202
- size = window.get_size()
1203
- print(size)
1204
-
1205
- app.run()
1206
- ```
1207
- """
1208
- return {"width": self.width, "height": self.height}
1209
-
1210
- def get_position(self) -> Dict[str, int]:
1211
- """
1212
- Returns the position of the window.
1213
-
1214
- Returns
1215
- -------
1216
- dict
1217
- Dictionary containing the x and y coordinates of the window
1218
-
1219
- Examples
1220
- --------
1221
- ```python
1222
- app = Pyloid(app_name="Pyloid-App")
1223
-
1224
- window = app.create_window("pyloid-window")
1225
- position = window.get_position()
1226
- print(position)
1227
-
1228
- app.run()
1229
- ```
1230
- """
1231
- return {"x": self.x, "y": self.y}
1232
-
1233
- def get_title(self) -> str:
1234
- """
1235
- Returns the title of the window.
1236
-
1237
- Returns
1238
- -------
1239
- str
1240
- Title of the window
1241
-
1242
- Examples
1243
- --------
1244
- ```python
1245
- app = Pyloid(app_name="Pyloid-App")
1246
-
1247
- window = app.create_window("pyloid-window")
1248
- title = window.get_title()
1249
- print(title)
1250
-
1251
- app.run()
1252
- ```
1253
- """
1254
- return self.title
1255
-
1256
- def get_url(self) -> str:
1257
- """
1258
- Returns the URL of the window.
1259
-
1260
- Returns
1261
- -------
1262
- str
1263
- URL of the window
1264
-
1265
- Examples
1266
- --------
1267
- ```python
1268
- app = Pyloid(app_name="Pyloid-App")
1269
-
1270
- window = app.create_window("pyloid-window")
1271
- url = window.get_url()
1272
- print(url)
1273
-
1274
- app.run()
1275
- ```
1276
- """
1277
- return self.web_view.url().toString()
1278
-
1279
- def get_visible(self) -> bool:
1280
- """
1281
- Returns the visibility of the window.
1282
-
1283
- Returns
1284
- -------
1285
- bool
1286
- True if the window is visible, False otherwise
1287
-
1288
- Examples
1289
- --------
1290
- ```python
1291
- app = Pyloid(app_name="Pyloid-App")
1292
-
1293
- window = app.create_window("pyloid-window")
1294
- visible = window.get_visible()
1295
- print(visible)
1296
-
1297
- app.run()
1298
- ```
1299
- """
1300
- return self._window.isVisible()
1301
-
1302
- def get_frame(self) -> bool:
1303
- """
1304
- Returns the frame enabled state of the window.
1305
-
1306
- Returns
1307
- -------
1308
- bool
1309
- True if the frame is enabled, False otherwise
1310
-
1311
- Examples
1312
- --------
1313
- ```python
1314
- app = Pyloid(app_name="Pyloid-App")
1315
-
1316
- window = app.create_window("pyloid-window")
1317
- frame = window.get_frame()
1318
- print(frame)
1319
-
1320
- app.run()
1321
- ```
1322
- """
1323
- return self.frame
1324
-
1325
- ###########################################################################################
1326
- # Resize
1327
- ###########################################################################################
1328
- def set_resizable(self, resizable: bool):
1329
- """
1330
- Sets the resizability of the window.
1331
-
1332
- Parameters
1333
- ----------
1334
- resizable : bool
1335
- True to make the window resizable, False to make it fixed size
1336
-
1337
- Examples
1338
- --------
1339
- ```python
1340
- app = Pyloid(app_name="Pyloid-App")
1341
-
1342
- window = app.create_window("pyloid-window")
1343
- window.set_resizable(True)
1344
-
1345
- app.run()
1346
- ```
1347
- """
1348
- self.resizable = resizable
1349
- if self.frame:
1350
- flags = self._window.windowFlags() | Qt.WindowCloseButtonHint
1351
- if resizable:
1352
- pass
1353
- else:
1354
- flags |= Qt.MSWindowsFixedSizeDialogHint
1355
- self._window.setWindowFlags(flags)
1356
- else:
1357
- # 프레임이 없는 경우 커스텀 리사이징 로직을 설정합니다.
1358
- self.web_view.is_resizing_enabled = resizable
1359
-
1360
- self._window.show() # 변경사항을 적용하기 위해 창을 다시 표시합니다.
1361
-
1362
- def set_minimum_size(self, min_width: int, min_height: int):
1363
- """
1364
- Sets the minimum size of the window.
1365
-
1366
- Parameters
1367
- ----------
1368
- min_width : int
1369
- Minimum width of the window
1370
- min_height : int
1371
- Minimum height of the window
1372
-
1373
- Examples
1374
- --------
1375
- ```python
1376
- app = Pyloid(app_name="Pyloid-App")
1377
-
1378
- window = app.create_window("pyloid-window")
1379
- window.set_minimum_size(400, 300)
1380
-
1381
- app.run()
1382
- ```
1383
- """
1384
- self._window.setMinimumSize(min_width, min_height)
1385
-
1386
- def set_maximum_size(self, max_width: int, max_height: int):
1387
- """
1388
- Sets the maximum size of the window.
1389
-
1390
- Parameters
1391
- ----------
1392
- max_width : int
1393
- Maximum width of the window
1394
- max_height : int
1395
- Maximum height of the window
1396
-
1397
- Examples
1398
- --------
1399
- ```python
1400
- app = Pyloid(app_name="Pyloid-App")
1401
-
1402
- window = app.create_window("pyloid-window")
1403
- window.set_maximum_size(1024, 768)
1404
-
1405
- app.run()
1406
- ```
1407
- """
1408
- self._window.setMaximumSize(max_width, max_height)
1409
-
1410
- def get_minimum_size(self) -> Dict[str, int]:
1411
- """
1412
- Returns the minimum size of the window.
1413
-
1414
- Returns
1415
- -------
1416
- dict
1417
- Dictionary containing the minimum width and height of the window
1418
-
1419
- Examples
1420
- --------
1421
- ```python
1422
- app = Pyloid(app_name="Pyloid-App")
1423
-
1424
- window = app.create_window("pyloid-window")
1425
- min_size = window.get_minimum_size()
1426
- print(min_size)
1427
-
1428
- app.run()
1429
- ```
1430
- """
1431
- return {
1432
- "width": self._window.minimumWidth(),
1433
- "height": self._window.minimumHeight(),
1434
- }
1435
-
1436
- def get_maximum_size(self) -> Dict[str, int]:
1437
- """
1438
- Returns the maximum size of the window.
1439
-
1440
- Returns
1441
- -------
1442
- dict
1443
- Dictionary containing the maximum width and height of the window
1444
-
1445
- Examples
1446
- --------
1447
- ```python
1448
- app = Pyloid(app_name="Pyloid-App")
1449
-
1450
- window = app.create_window("pyloid-window")
1451
- max_size = window.get_maximum_size()
1452
- print(max_size)
1453
-
1454
- app.run()
1455
- ```
1456
- """
1457
- return {
1458
- "width": self._window.maximumWidth(),
1459
- "height": self._window.maximumHeight(),
1460
- }
1461
-
1462
- def get_resizable(self) -> bool:
1463
- """
1464
- Returns the resizability of the window.
1465
-
1466
- Returns
1467
- -------
1468
- bool
1469
- True if the window is resizable, False otherwise
1470
-
1471
- Examples
1472
- --------
1473
- ```python
1474
- app = Pyloid(app_name="Pyloid-App")
1475
-
1476
- window = app.create_window("pyloid-window")
1477
- resizable = window.get_resizable()
1478
- print(resizable)
1479
-
1480
- app.run()
1481
- ```
1482
- """
1483
- return self.resizable
1484
-
1485
- ###########################################################################################
1486
- # For Custom Pyside6 Features
1487
- ###########################################################################################
1488
- def get_QMainWindow(self) -> QMainWindow:
1489
- """
1490
- Returns the QMainWindow object of the window.
1491
-
1492
- you can use all the features of QMainWindow for customizing the window.
1493
-
1494
- Returns
1495
- -------
1496
- QMainWindow
1497
- QMainWindow object of the window
1498
-
1499
- Examples
1500
- --------
1501
- ```python
1502
- from PySide6.QtCore import Qt
1503
- from pyloid import Pyloid
1504
-
1505
- app = Pyloid(app_name="Pyloid-App")
1506
-
1507
- window = app.create_window("pyloid-window")
1508
- qmain = window.get_QMainWindow()
1509
-
1510
- qmain.setWindowFlags(qmain.windowFlags() | Qt.WindowStaysOnTopHint) # window stays on top
1511
- ```
1512
- """
1513
- return self._window
1514
-
1515
- def get_QWebEngineView(self) -> CustomWebEngineView:
1516
- """
1517
- Returns the CustomWebEngineView object which inherits from QWebEngineView.
1518
-
1519
- Returns
1520
- -------
1521
- CustomWebEngineView
1522
- CustomWebEngineView object of the window
1523
-
1524
- Examples
1525
- --------
1526
- ```python
1527
- window = app.create_window("pyloid-window")
1528
- web_view = window.get_QWebEngineView()
1529
-
1530
- web_view.page().runJavaScript("console.log('Hello, Pyloid!')")
1531
- ```
1532
- """
1533
- return self.web_view
1534
-
1535
- ###########################################################################################
1536
- # QMainWindow flags
1537
- ###########################################################################################
1538
- def set_window_stay_on_top(self, on_top: bool):
1539
- """
1540
- Sets the window stay on top flag of the window.
1541
-
1542
- Parameters
1543
- ----------
1544
- on_top : bool
1545
- True to keep the window on top, False otherwise
1546
-
1547
- Examples
1548
- --------
1549
- ```python
1550
- window.set_window_stay_on_top(True)
1551
- window.set_window_stay_on_top(False)
1552
- ```
1553
- """
1554
- flags = self._window.windowFlags()
1555
- if on_top:
1556
- flags |= Qt.WindowStaysOnTopHint
1557
- else:
1558
- flags &= ~Qt.WindowStaysOnTopHint
1559
-
1560
- # Maintain existing flags while only changing WindowStaysOnTopHint
1561
- # Explicitly add the close button
1562
- flags |= Qt.WindowCloseButtonHint
1563
-
1564
- self._window.setWindowFlags(flags)
1565
-
1566
- # Show the window again to apply the changes
1567
- self._window.show()
1568
-
1569
- def set_window_stay_on_bottom(self, on_bottom: bool):
1570
- """
1571
- Sets the window stay on bottom flag of the window.
1572
-
1573
- Parameters
1574
- ----------
1575
- on_bottom : bool
1576
- True to keep the window on bottom, False otherwise
1577
-
1578
- Examples
1579
- --------
1580
- ```python
1581
- window.set_window_stay_on_bottom(True)
1582
- window.set_window_stay_on_bottom(False)
1583
- ```
1584
- """
1585
- flags = self._window.windowFlags()
1586
- if on_bottom:
1587
- flags |= Qt.WindowStaysOnBottomHint
1588
- else:
1589
- flags &= ~Qt.WindowStaysOnBottomHint
1590
-
1591
- # Maintain existing flags while only changing WindowStaysOnBottomHint
1592
- # Explicitly add the close button
1593
- flags |= Qt.WindowCloseButtonHint
1594
-
1595
- self._window.setWindowFlags(flags)
1596
-
1597
- # Show the window again to apply the changes
1598
- self._window.show()
1599
-
1600
- ###########################################################################################
1601
- # Splash Screen
1602
- ###########################################################################################
1603
- def set_static_image_splash_screen(
1604
- self,
1605
- image_path: str,
1606
- close_on_load: bool = True,
1607
- stay_on_top: bool = True,
1608
- clickable: bool = True,
1609
- position: str = "center",
1610
- ):
1611
- """
1612
- Sets the static image splash screen of the window.
1613
-
1614
- Parameters
1615
- ----------
1616
- image_path : str
1617
- Path to the image file
1618
- close_on_load : bool, optional
1619
- True to close the splash screen when the page is loaded, False otherwise (default is True)
1620
- stay_on_top : bool, optional
1621
- True to keep the splash screen on top, False otherwise (default is True)
1622
- clickable : bool, optional
1623
- True to make the splash screen clickable, False otherwise (default is True)
1624
- if clickable is True, you can click the splash screen to close it.
1625
- position : str, optional
1626
- Position of the splash screen. Options are 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right' (default is 'center')
1627
-
1628
- Examples
1629
- --------
1630
- ```python
1631
- window.set_image_splash_screen("./assets/loading.png", close_on_load=True, stay_on_top=True)
1632
- ```
1633
- """
1634
- pixmap = QPixmap(image_path)
1635
-
1636
- if not clickable:
1637
-
1638
- class NonClickableSplashScreen(QSplashScreen):
1639
- def mousePressEvent(self, event):
1640
- pass # Ignore click events
1641
-
1642
- splash = NonClickableSplashScreen(
1643
- pixmap, Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0)
1644
- )
1645
- else:
1646
- splash = QSplashScreen(
1647
- pixmap, Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0)
1648
- )
1649
-
1650
- self.close_on_load = close_on_load
1651
- self.splash_screen = splash
1652
- self._position_splash_screen(position)
1653
- self.splash_screen.show()
1654
-
1655
- def set_gif_splash_screen(
1656
- self,
1657
- gif_path: str,
1658
- close_on_load: bool = True,
1659
- stay_on_top: bool = True,
1660
- clickable: bool = True,
1661
- position: str = "center",
1662
- ):
1663
- """
1664
- Sets the gif splash screen of the window.
1665
-
1666
- Parameters
1667
- ----------
1668
- gif_path : str
1669
- Path to the gif file
1670
- close_on_load : bool, optional
1671
- True to close the splash screen when the page is loaded, False otherwise (default is True)
1672
- stay_on_top : bool, optional
1673
- True to keep the splash screen on top, False otherwise (default is True)
1674
- clickable : bool, optional
1675
- True to make the splash screen clickable, False otherwise (default is True)
1676
- if clickable is True, you can click the splash screen to close it.
1677
- position : str, optional
1678
- Position of the splash screen. Options are 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right' (default is 'center')
1679
-
1680
- Examples
1681
- --------
1682
- ```python
1683
- window.set_gif_splash_screen("./assets/loading.gif", close_on_load=True, stay_on_top=True)
1684
- ```
1685
- """
1686
-
1687
- if not clickable:
1688
-
1689
- class NonClickableSplashScreen(QSplashScreen):
1690
- def mousePressEvent(self, event):
1691
- pass # Ignore click events
1692
-
1693
- # Create splash screen (using animated GIF)
1694
- splash = NonClickableSplashScreen(
1695
- QPixmap(1, 1),
1696
- Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0),
1697
- ) # Start with 1x1 transparent pixmap
1698
- else:
1699
- splash = QSplashScreen(
1700
- QPixmap(1, 1),
1701
- Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0),
1702
- )
1703
-
1704
- splash.setAttribute(Qt.WA_TranslucentBackground)
1705
-
1706
- # Create QLabel for GIF animation
1707
- label = QLabel(splash)
1708
- movie = QMovie(gif_path)
1709
- label.setMovie(movie)
1710
-
1711
- # Adjust splash screen size to match GIF size
1712
- movie.frameChanged.connect(
1713
- lambda: splash.setFixedSize(movie.currentPixmap().size())
1714
- )
1715
-
1716
- # Start animation and show splash screen
1717
- movie.start()
1718
- self.close_on_load = close_on_load
1719
- self.splash_screen = splash
1720
- self._position_splash_screen(position)
1721
- splash.show()
1722
-
1723
- def _position_splash_screen(self, position: str):
1724
- if not self.splash_screen:
1725
- return
1726
-
1727
- screen = self.app.primaryScreen().geometry()
1728
- splash_size = self.splash_screen.size()
1729
-
1730
- if position == "center":
1731
- new_position = screen.center() - QPoint(
1732
- splash_size.width() // 2, splash_size.height() // 2
1733
- )
1734
- elif position == "top-left":
1735
- new_position = screen.topLeft()
1736
- elif position == "top-right":
1737
- new_position = screen.topRight() - QPoint(splash_size.width(), 0)
1738
- elif position == "bottom-left":
1739
- new_position = screen.bottomLeft() - QPoint(0, splash_size.height())
1740
- elif position == "bottom-right":
1741
- new_position = screen.bottomRight() - QPoint(
1742
- splash_size.width(), splash_size.height()
1743
- )
1744
- else:
1745
- new_position = screen.center() - QPoint(
1746
- splash_size.width() // 2, splash_size.height() // 2
1747
- )
1748
-
1749
- self.splash_screen.move(new_position)
1750
-
1751
- def close_splash_screen(self):
1752
- """
1753
- Closes the splash screen if it exists.
1754
-
1755
- Examples
1756
- --------
1757
- ```python
1758
- window.close_splash_screen()
1759
- ```
1760
- """
1761
- if hasattr(self, "splash_screen") and self.splash_screen:
1762
- self.splash_screen.close()
1763
- self.close_on_load = None
1764
- self.splash_screen = None
1765
-
1766
- ###########################################################################################
1767
- # WebEngineView Attribute setting
1768
- ###########################################################################################
1769
- def set_web_engine_view_attribute(self, attribute: QWebEngineSettings, on: bool):
1770
- """
1771
- Sets the attribute of the WebEngineView.
1772
-
1773
- Parameters
1774
- ----------
1775
- attribute : QWebEngineSettings
1776
- Attribute to set
1777
- on : bool
1778
- True to enable the attribute, False to disable it
1779
-
1780
- Examples
1781
- --------
1782
- ```python
1783
- window.set_web_engine_view_attribute(QWebEngineSettings.WebAttribute.ScreenCaptureEnabled, False)
1784
- ```
1785
- """
1786
- settings = self.web_view.settings()
1787
- settings.setAttribute(attribute, on)
1788
-
1789
- def is_web_engine_view_attribute(self, attribute: QWebEngineSettings) -> bool:
1790
- """
1791
- Returns the attribute of the WebEngineView.
1792
-
1793
- Parameters
1794
- ----------
1795
- attribute : QWebEngineSettings
1796
- Attribute to get
1797
-
1798
- Returns
1799
- -------
1800
- bool
1801
- True if the attribute is enabled, False otherwise
1802
-
1803
- Examples
1804
- --------
1805
- ```python
1806
- window.is_web_engine_view_attribute(QWebEngineSettings.WebAttribute.ScreenCaptureEnabled)
1807
- ```
1808
- """
1809
- settings = self.web_view.settings()
1810
- return settings.testAttribute(attribute)
1811
-
1812
- def set_permission_handler(self, feature: QWebEnginePage.Feature, handler):
1813
- """
1814
- Sets a handler for a specific permission.
1815
-
1816
- Parameters
1817
- ----------
1818
- feature : QWebEnginePage.Feature
1819
- The type of permission to set
1820
- handler : callable
1821
- The handler function to process the permission request
1822
-
1823
- Examples
1824
- --------
1825
- ```python
1826
- def handle_camera(origin, feature):
1827
- window.web_view.page().setFeaturePermission(
1828
- origin,
1829
- feature,
1830
- QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
1831
- )
1832
-
1833
- window.set_permission_handler(
1834
- QWebEnginePage.Feature.MediaVideoCapture,
1835
- handle_camera
1836
- )
1837
- ```
1838
- """
1839
- self.web_view.custom_page.setPermissionHandler(feature, handler)
1840
-
1841
- def grant_permission(self, feature: QWebEnginePage.Feature):
1842
- """
1843
- Automatically grants a specific permission when a request is made.
1844
-
1845
- Parameters
1846
- ----------
1847
- feature : QWebEnginePage.Feature
1848
- The type of permission to automatically grant
1849
-
1850
- Examples
1851
- --------
1852
- ```python
1853
- window.grant_permission(QWebEnginePage.Feature.MediaVideoCapture)
1854
- ```
1855
- """
1856
-
1857
- def auto_grant(origin, feat):
1858
- self.web_view.page().setFeaturePermission(
1859
- origin, feat, QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
1860
- )
1861
-
1862
- self.set_permission_handler(feature, auto_grant)
1863
-
1864
- def deny_permission(self, feature: QWebEnginePage.Feature):
1865
- """
1866
- Automatically denies a specific permission when a request is made.
1867
-
1868
- Parameters
1869
- ----------
1870
- feature : QWebEnginePage.Feature
1871
- The type of permission to automatically deny
1872
-
1873
- Examples
1874
- --------
1875
- ```python
1876
- window.deny_permission(QWebEnginePage.Feature.Notifications)
1877
- ```
1878
- """
1879
-
1880
- def auto_deny(origin, feat):
1881
- self.web_view.page().setFeaturePermission(
1882
- origin, feat, QWebEnginePage.PermissionPolicy.PermissionDeniedByUser
1883
- )
1884
-
1885
- self.set_permission_handler(feature, auto_deny)
1886
-
1887
- def set_desktop_media_handler(self, handler):
1888
- """
1889
- 데스크톱 미디어(화면/윈도우) 선택 핸들러를 설정합니다.
1890
-
1891
- Parameters
1892
- ----------
1893
- handler : callable
1894
- 요청을 처리할 핸들러 함수. QWebEngineDesktopMediaRequest��� 인자로 받습니다.
1895
-
1896
- Examples
1897
- --------
1898
- ```python
1899
- def custom_media_handler(request):
1900
- # 사용 가능한 화면 목록 출력
1901
- for screen in request.screenList():
1902
- print(f"Screen: {screen.name}")
1903
-
1904
- # 사용 가능한 윈도우 목록 출력
1905
- for window in request.windowList():
1906
- print(f"Window: {window.name}")
1907
-
1908
- # 첫 번째 화면 선택
1909
- if request.screenList():
1910
- request.selectScreen(request.screenList()[0])
1911
-
1912
- window.set_desktop_media_handler(custom_media_handler)
1913
- ```
1914
- """
1915
- self.web_view.custom_page.setDesktopMediaHandler(handler)
1
+ import sys
2
+ import os
3
+ from PySide6.QtWidgets import (
4
+ QMainWindow,
5
+ )
6
+ from PySide6.QtWebEngineWidgets import QWebEngineView
7
+ from PySide6.QtWebChannel import QWebChannel
8
+ from PySide6.QtGui import (
9
+ QKeySequence,
10
+ QShortcut,
11
+ QCursor,
12
+ )
13
+ from PySide6.QtCore import Qt, QPoint, QUrl, QEvent, QFile
14
+ from PySide6.QtWebEngineCore import (
15
+ QWebEnginePage,
16
+ QWebEngineSettings,
17
+ QWebEngineUrlRequestInterceptor,
18
+ )
19
+ from .api import PyloidAPI
20
+ import uuid
21
+ from typing import List, Optional, Dict, Callable
22
+ import json
23
+ from PySide6.QtWidgets import (
24
+ QWidget,
25
+ QVBoxLayout,
26
+ )
27
+ from .custom.titlebar import CustomTitleBar
28
+ from .js_api.window_api import WindowAPI
29
+ from PySide6.QtGui import QPixmap, QMovie
30
+ from PySide6.QtWidgets import QSplashScreen, QLabel
31
+ from PySide6.QtCore import QSize
32
+ from typing import TYPE_CHECKING
33
+ from PySide6.QtWebEngineCore import QWebEngineSettings
34
+
35
+ if TYPE_CHECKING:
36
+ from ..pyloid import Pyloid
37
+
38
+
39
+ # 어차피 load 부분에만 쓰이니까 나중에 분리해서 load 위에서 선언하자.
40
+ class CustomWebPage(QWebEnginePage):
41
+ def __init__(self, profile=None):
42
+ super().__init__(profile)
43
+ self.featurePermissionRequested.connect(self._handlePermissionRequest)
44
+ self.desktopMediaRequested.connect(self._handleDesktopMediaRequest)
45
+ self._permission_handlers = {}
46
+ self._desktop_media_handler = None
47
+ self._url_handlers = {} # URL 핸들러 저장을 위한 딕셔너리 추가
48
+
49
+ # interceptor ( all url request )
50
+ self.interceptor = CustomUrlInterceptor()
51
+ self.profile().setUrlRequestInterceptor(self.interceptor)
52
+
53
+ def _handlePermissionRequest(self, origin: QUrl, feature: QWebEnginePage.Feature):
54
+ print(origin, feature)
55
+
56
+ """Default permission request handler"""
57
+ if feature in self._permission_handlers:
58
+ # Execute if a handler is registered
59
+ handler = self._permission_handlers[feature]
60
+ handler(origin, feature)
61
+ else:
62
+ # Allow all permissions by default
63
+ self.setFeaturePermission(
64
+ origin, feature, QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
65
+ )
66
+
67
+ def setPermissionHandler(self, feature: QWebEnginePage.Feature, handler):
68
+ """Register a handler for a specific permission"""
69
+ self._permission_handlers[feature] = handler
70
+
71
+ def _handleDesktopMediaRequest(self, *args, **kwargs):
72
+ print("Desktop media request received:", args, kwargs)
73
+
74
+ # interceptor ( navigation request )
75
+ def acceptNavigationRequest(self, url, navigation_type, is_main_frame):
76
+ """네비게이션 요청을 처리하는 메서드"""
77
+ print(f"Navigation Request - URL: {url.toString()}")
78
+ print(f"Navigation Type: {navigation_type}")
79
+ print(f"Is Main Frame: {is_main_frame}")
80
+
81
+ # # URL이 구글이 아닐 경우에만 구글로 리다이렉트
82
+ # if "example.com" not in url.toString():
83
+ # self.setUrl(QUrl("https://www.example.com"))
84
+ # return False
85
+
86
+ return True
87
+
88
+ def add_url_handler(self, pattern: str, handler):
89
+ """URL 패턴에 대한 핸들러 등록
90
+
91
+ Parameters:
92
+ -----------
93
+ pattern : str
94
+ 정규표현식 패턴
95
+ handler : callable
96
+ URL을 인자로 받고 bool을 반환해야 함
97
+ True를 반환하면 네비게이션을 허용, False를 반환하면 차단
98
+ """
99
+ import re
100
+
101
+ self._url_handlers[re.compile(pattern)] = handler
102
+
103
+
104
+ # interceptor ( all url request )
105
+ class CustomUrlInterceptor(QWebEngineUrlRequestInterceptor):
106
+ def interceptRequest(self, info):
107
+ url = info.requestUrl().toString()
108
+ print(url)
109
+
110
+
111
+ class CustomWebEngineView(QWebEngineView):
112
+ def __init__(self, parent: "BrowserWindow" = None):
113
+ super().__init__(parent._window)
114
+ self.parent: "BrowserWindow" = parent
115
+
116
+ # Custom Web Page
117
+ self.custom_page = CustomWebPage()
118
+ self.setPage(self.custom_page)
119
+
120
+ self.is_resizing = False
121
+ self.resize_start_pos = None
122
+ self.resize_direction = None
123
+ self.screen_geometry = self.screen().virtualGeometry()
124
+ self.is_resizing_enabled = True
125
+
126
+ self.setAttribute(Qt.WA_SetCursor, False)
127
+
128
+ self.is_in_resize_area = False
129
+
130
+ def mouse_press_event(self, event):
131
+ if self.parent.frame or not self.is_resizing_enabled:
132
+ return
133
+
134
+ if event.button() == Qt.LeftButton:
135
+ self.resize_direction = self.get_resize_direction(event.pos())
136
+ if self.resize_direction:
137
+ self.is_resizing = True
138
+ self.resize_start_pos = event.globalPos()
139
+
140
+ def start_system_drag(self):
141
+ """Start system window move"""
142
+ if self.parent._window.windowHandle():
143
+ self.parent._window.windowHandle().startSystemMove()
144
+
145
+ def mouse_move_event(self, event):
146
+ if self.parent.frame or not self.is_resizing_enabled:
147
+ return
148
+
149
+ # Check resize direction
150
+ was_in_resize_area = self.is_in_resize_area
151
+ resize_direction = self.get_resize_direction(event.pos())
152
+ self.is_in_resize_area = bool(resize_direction)
153
+
154
+ if resize_direction and not self.is_resizing:
155
+ self.set_cursor_for_resize_direction(resize_direction)
156
+
157
+ if self.is_resizing:
158
+ self.resize_window(event.globalPos())
159
+ return
160
+
161
+ # Change cursor when entering/leaving resize area
162
+ if self.is_in_resize_area != was_in_resize_area:
163
+ if self.is_in_resize_area:
164
+ # self.setAttribute(Qt.WA_SetCursor, True)
165
+ pass
166
+ else:
167
+ self.unsetCursor()
168
+ # self.setAttribute(Qt.WA_SetCursor, False)
169
+
170
+ def mouse_release_event(self, event):
171
+ if self.parent.frame or not self.is_resizing_enabled:
172
+ return
173
+
174
+ if event.button() == Qt.LeftButton:
175
+ self.is_resizing = False
176
+
177
+ if self.resize_direction:
178
+ self.unsetCursor()
179
+ self.resize_direction = None
180
+
181
+ self.setAttribute(Qt.WA_SetCursor, False)
182
+
183
+ def eventFilter(self, source, event):
184
+ if self.focusProxy() is source:
185
+ if event.type() == QEvent.MouseButtonPress:
186
+ self.mouse_press_event(event)
187
+ elif event.type() == QEvent.MouseMove:
188
+ self.mouse_move_event(event)
189
+ elif event.type() == QEvent.MouseButtonRelease:
190
+ self.mouse_release_event(event)
191
+ return super().eventFilter(source, event)
192
+
193
+ def get_resize_direction(self, pos):
194
+ if (
195
+ not self.parent.frame and self.is_resizing_enabled
196
+ ): # Check if frame is not present and resizing is enabled
197
+ margin = 8 # Margin in pixels to detect edge
198
+ rect = self.rect()
199
+ direction = None
200
+
201
+ if pos.x() <= margin:
202
+ direction = "left"
203
+ elif pos.x() >= rect.width() - margin:
204
+ direction = "right"
205
+
206
+ if pos.y() <= margin:
207
+ direction = "top" if direction is None else direction + "-top"
208
+ elif pos.y() >= rect.height() - margin:
209
+ direction = "bottom" if direction is None else direction + "-bottom"
210
+
211
+ return direction
212
+ return None
213
+
214
+ def set_cursor_for_resize_direction(self, direction):
215
+ if not self.parent.frame and direction and self.is_resizing_enabled:
216
+ cursor = None
217
+ if direction in ["left", "right"]:
218
+ cursor = Qt.SizeHorCursor
219
+ elif direction in ["top", "bottom"]:
220
+ cursor = Qt.SizeVerCursor
221
+ elif direction in ["left-top", "right-bottom"]:
222
+ cursor = Qt.SizeFDiagCursor
223
+ elif direction in ["right-top", "left-bottom"]:
224
+ cursor = Qt.SizeBDiagCursor
225
+
226
+ if cursor:
227
+ self.setCursor(cursor)
228
+ self.setAttribute(Qt.WA_SetCursor, True)
229
+
230
+ def resize_window(self, global_pos):
231
+ if (
232
+ not self.parent.frame
233
+ and self.resize_start_pos
234
+ and self.resize_direction
235
+ and self.is_resizing_enabled
236
+ ): # Check if frame is not present and resizing is enabled
237
+ delta = global_pos - self.resize_start_pos
238
+ new_geometry = self.parent._window.geometry()
239
+
240
+ if "left" in self.resize_direction:
241
+ new_geometry.setLeft(new_geometry.left() + delta.x())
242
+ if "right" in self.resize_direction:
243
+ new_geometry.setRight(new_geometry.right() + delta.x())
244
+ if "top" in self.resize_direction:
245
+ new_geometry.setTop(new_geometry.top() + delta.y())
246
+ if "bottom" in self.resize_direction:
247
+ new_geometry.setBottom(new_geometry.bottom() + delta.y())
248
+
249
+ self.parent._window.setGeometry(new_geometry)
250
+ self.resize_start_pos = global_pos
251
+
252
+
253
+ class BrowserWindow:
254
+ def __init__(
255
+ self,
256
+ app: "Pyloid",
257
+ title: str = "pyloid app",
258
+ width: int = 800,
259
+ height: int = 600,
260
+ x: int = 200,
261
+ y: int = 200,
262
+ frame: bool = True,
263
+ context_menu: bool = False,
264
+ dev_tools: bool = False,
265
+ js_apis: List[PyloidAPI] = [],
266
+ ):
267
+ ###########################################################################################
268
+ self.id = str(uuid.uuid4()) # Generate unique ID
269
+ self._window = QMainWindow()
270
+ self.web_view = CustomWebEngineView(self)
271
+
272
+ self._window.closeEvent = self.closeEvent # Override closeEvent method
273
+ ###########################################################################################
274
+ self.app = app
275
+ self.title = title
276
+ self.width = width
277
+ self.height = height
278
+ self.x = x
279
+ self.y = y
280
+ self.frame = frame
281
+ self.context_menu = context_menu
282
+ self.dev_tools = dev_tools
283
+ self.js_apis = [WindowAPI()]
284
+ for js_api in js_apis:
285
+ self.js_apis.append(js_api)
286
+ self.shortcuts = {}
287
+ self.close_on_load = True
288
+ self.splash_screen = None
289
+ ###########################################################################################
290
+
291
+ def _set_custom_frame(
292
+ self,
293
+ use_custom: bool,
294
+ title: str = "Custom Title",
295
+ bg_color: str = "darkblue",
296
+ text_color: str = "white",
297
+ icon_path: str = None,
298
+ ):
299
+ """Sets or removes the custom frame."""
300
+ if use_custom:
301
+ self._window.setWindowFlags(Qt.FramelessWindowHint)
302
+ self.custom_title_bar = CustomTitleBar(self._window)
303
+ self.custom_title_bar.set_style(bg_color, text_color)
304
+ self.custom_title_bar.set_title(title)
305
+
306
+ if icon_path:
307
+ self.custom_title_bar.set_icon(icon_path)
308
+
309
+ layout = QVBoxLayout()
310
+ layout.setContentsMargins(0, 0, 0, 0)
311
+ layout.setSpacing(0)
312
+ layout.addWidget(self.custom_title_bar)
313
+ layout.addWidget(self.web_view)
314
+
315
+ central_widget = QWidget()
316
+ central_widget.setLayout(layout)
317
+ self._window.setCentralWidget(central_widget)
318
+
319
+ # Add properties for window movement
320
+ self._window.moving = False
321
+ self._window.offset = QPoint()
322
+ else:
323
+ self._window.setWindowFlags(Qt.Window)
324
+ self._window.setCentralWidget(self.web_view)
325
+ self.custom_title_bar = None
326
+
327
+ self._window.show()
328
+
329
+ def _load(self):
330
+ self.set_title(self.title)
331
+
332
+ self.set_size(self.width, self.height)
333
+ self.set_position(self.x, self.y)
334
+
335
+ # allow local file access to remote urls and screen capture
336
+ self.web_view.settings().setAttribute(
337
+ QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True
338
+ )
339
+ self.web_view.settings().setAttribute(
340
+ QWebEngineSettings.WebAttribute.ScreenCaptureEnabled, True
341
+ )
342
+ self.web_view.settings().setAttribute(
343
+ QWebEngineSettings.WebAttribute.AutoLoadImages, True
344
+ )
345
+ self.web_view.settings().setAttribute(
346
+ QWebEngineSettings.WebAttribute.JavascriptEnabled, True
347
+ )
348
+ self.web_view.settings().setAttribute(
349
+ QWebEngineSettings.WebAttribute.LocalStorageEnabled, True
350
+ )
351
+ self.web_view.settings().setAttribute(
352
+ QWebEngineSettings.WebAttribute.ErrorPageEnabled, True
353
+ )
354
+ self.web_view.settings().setAttribute(
355
+ QWebEngineSettings.WebAttribute.AutoLoadIconsForPage, True
356
+ )
357
+ self.web_view.settings().setAttribute(
358
+ QWebEngineSettings.WebAttribute.ShowScrollBars, True
359
+ )
360
+ self.web_view.settings().setAttribute(
361
+ QWebEngineSettings.WebAttribute.DnsPrefetchEnabled, True
362
+ )
363
+ self.web_view.settings().setAttribute(
364
+ QWebEngineSettings.WebAttribute.PdfViewerEnabled, True
365
+ )
366
+ self.web_view.settings().setAttribute(
367
+ QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True
368
+ )
369
+ self.web_view.settings().setAttribute(
370
+ QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True
371
+ )
372
+ self.web_view.settings().setUnknownUrlSchemePolicy(
373
+ QWebEngineSettings.UnknownUrlSchemePolicy.AllowAllUnknownUrlSchemes
374
+ )
375
+ self.web_view.settings().setAttribute(
376
+ QWebEngineSettings.WebAttribute.AllowRunningInsecureContent, True
377
+ )
378
+ self.web_view.settings().setAttribute(
379
+ QWebEngineSettings.WebAttribute.AllowGeolocationOnInsecureOrigins, True
380
+ )
381
+ self.web_view.settings().setAttribute(
382
+ QWebEngineSettings.WebAttribute.AllowWindowActivationFromJavaScript, True
383
+ )
384
+ self.web_view.settings().setAttribute(
385
+ QWebEngineSettings.WebAttribute.JavascriptCanPaste, True
386
+ )
387
+ self.web_view.settings().setAttribute(
388
+ QWebEngineSettings.WebAttribute.WebRTCPublicInterfacesOnly, False
389
+ )
390
+
391
+ # Set icon
392
+ if self.app.icon:
393
+ self._window.setWindowIcon(self.app.icon)
394
+ else:
395
+ print("Icon is not set.")
396
+
397
+ # Set Windows taskbar icon
398
+ if sys.platform == "win32":
399
+ import ctypes
400
+
401
+ myappid = "mycompany.myproduct.subproduct.version"
402
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
403
+
404
+ # Remove title bar and borders (if needed)
405
+ if not self.frame:
406
+ self._window.setWindowFlags(Qt.FramelessWindowHint)
407
+
408
+ # Disable default context menu
409
+ if not self.context_menu:
410
+ self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
411
+
412
+ # Set up QWebChannel
413
+ self.channel = QWebChannel()
414
+
415
+ # Register additional JS APIs
416
+ if self.js_apis:
417
+ for js_api in self.js_apis:
418
+ # Define window_id, window, and app for each JS API
419
+ js_api.window_id = self.id
420
+ js_api.window = self
421
+ js_api.app = self.app
422
+
423
+ self.channel.registerObject(js_api.__class__.__name__, js_api)
424
+
425
+ self.web_view.page().setWebChannel(self.channel)
426
+
427
+ # Connect pylonjs bridge
428
+ self.web_view.loadFinished.connect(self._on_load_finished)
429
+
430
+ # Add QWebEngineView to main window
431
+ self._window.setCentralWidget(self.web_view)
432
+
433
+ # Set F12 shortcut
434
+ self.set_dev_tools(self.dev_tools)
435
+
436
+ def _on_load_finished(self, ok):
437
+ """Handles the event when the web page finishes loading."""
438
+ if ok and self.js_apis:
439
+ # Load qwebchannel.js
440
+ qwebchannel_js = QFile("://qtwebchannel/qwebchannel.js")
441
+ if qwebchannel_js.open(QFile.ReadOnly):
442
+ source = bytes(qwebchannel_js.readAll()).decode("utf-8")
443
+ self.web_view.page().runJavaScript(source)
444
+ qwebchannel_js.close()
445
+
446
+ js_code = """
447
+ if (typeof QWebChannel !== 'undefined') {
448
+ new QWebChannel(qt.webChannelTransport, function (channel) {
449
+ window.pyloid = {
450
+ EventAPI: {
451
+ _listeners: {}, // 콜백 함수들을 저장할 객체
452
+
453
+ listen: function(eventName, callback) {
454
+ // 이벤트에 대한 콜백 배열이 없다면 생성
455
+ if (!this._listeners[eventName]) {
456
+ this._listeners[eventName] = [];
457
+ }
458
+
459
+ // 콜백 함수 저장
460
+ this._listeners[eventName].push(callback);
461
+
462
+ document.addEventListener(eventName, function(event) {
463
+ let eventData;
464
+ try {
465
+ eventData = JSON.parse(event.detail);
466
+ } catch (e) {
467
+ eventData = event.detail;
468
+ }
469
+ callback(eventData);
470
+ });
471
+ },
472
+
473
+ unlisten: function(eventName) {
474
+ // 해당 이벤트의 모든 리스너 제거
475
+ if (this._listeners[eventName]) {
476
+ this._listeners[eventName].forEach(callback => {
477
+ document.removeEventListener(eventName, callback);
478
+ });
479
+ // 저장된 콜백 제거
480
+ delete this._listeners[eventName];
481
+ }
482
+ }
483
+ }
484
+ };
485
+ console.log('pyloid.EventAPI object initialized:', window.pyloid.EventAPI);
486
+
487
+ %s
488
+
489
+ document.addEventListener('mousedown', function (e) {
490
+ if (e.target.hasAttribute('data-pyloid-drag-region')) {
491
+ window.pyloid.WindowAPI.startSystemDrag();
492
+ }
493
+ });
494
+
495
+ function updateTheme(theme) {
496
+ document.documentElement.setAttribute(
497
+ 'data-pyloid-theme',
498
+ theme
499
+ );
500
+ }
501
+
502
+ // 테마 변경 이벤트 리스너
503
+ document.addEventListener('themeChange', (e) => {
504
+ console.log('themeChange event received:', e);
505
+ updateTheme(e.detail.theme);
506
+ });
507
+
508
+ updateTheme('%s');
509
+
510
+
511
+ // Dispatch a custom event to signal that the initialization is ready
512
+ const event = new CustomEvent('pyloidReady');
513
+ document.dispatchEvent(event);
514
+ });
515
+ } else {
516
+ console.error('QWebChannel is not defined.');
517
+ }
518
+ """
519
+ js_api_init = "\n".join(
520
+ [
521
+ f"window.pyloid['{js_api.__class__.__name__}'] = channel.objects['{js_api.__class__.__name__}'];\n"
522
+ f"console.log('pyloid.{js_api.__class__.__name__} object initialized:', window.pyloid['{js_api.__class__.__name__}']);"
523
+ for js_api in self.js_apis
524
+ ]
525
+ )
526
+ self.web_view.page().runJavaScript(js_code % (js_api_init, self.app.theme))
527
+
528
+ # if splash screen is set, close it when the page is loaded
529
+ if self.close_on_load and self.splash_screen:
530
+ self.close_splash_screen()
531
+ else:
532
+ pass
533
+
534
+ ###########################################################################################
535
+ # Load
536
+ ###########################################################################################
537
+ def load_file(self, file_path: str):
538
+ """
539
+ Loads a local HTML file into the web view.
540
+
541
+ Parameters
542
+ ----------
543
+ file_path : str
544
+ The path to the local HTML file to be loaded.
545
+
546
+ Examples
547
+ --------
548
+ >>> app = Pyloid(app_name="Pyloid-App")
549
+ >>> window = app.create_window("pyloid-window")
550
+ >>> window.load_file('/path/to/local/file.html')
551
+ >>> window.show()
552
+ """
553
+ self._load()
554
+ file_path = os.path.abspath(file_path) # absolute path
555
+ self.web_view.setUrl(QUrl.fromLocalFile(file_path))
556
+ self.web_view.focusProxy().installEventFilter(self.web_view)
557
+
558
+ def load_url(self, url: str):
559
+ """
560
+ Sets the URL of the window.
561
+
562
+ Parameters
563
+ ----------
564
+ url : str
565
+ The URL to be loaded in the web view.
566
+
567
+ Examples
568
+ --------
569
+ >>> app = Pyloid(app_name="Pyloid-App")
570
+ >>> window = app.create_window("pyloid-window")
571
+ >>> window.load_url('https://www.example.com')
572
+ >>> window.show()
573
+ """
574
+ self._load()
575
+ self.web_view.setUrl(QUrl(url))
576
+ self.web_view.focusProxy().installEventFilter(self.web_view)
577
+
578
+ def load_html(self, html_content: str, base_url: str = ""):
579
+ """
580
+ Loads HTML content directly into the web view.
581
+
582
+ Parameters
583
+ ----------
584
+ html_content : str
585
+ The HTML content to be loaded.
586
+ base_url : str, optional
587
+ The base URL to use for resolving relative URLs (default is "").
588
+
589
+ Examples
590
+ --------
591
+ >>> app = Pyloid(app_name="Pyloid-App")
592
+ >>> window = app.create_window("pyloid-window")
593
+ >>> html_content = "<html><body><h1>Hello, Pyloid!</h1></body></html>"
594
+ >>> window.load_html(html_content)
595
+ >>> window.show()
596
+ """
597
+ self._load()
598
+ self.web_view.setHtml(html_content, QUrl(base_url))
599
+ self.web_view.focusProxy().installEventFilter(self.web_view)
600
+
601
+ ###########################################################################################
602
+ # Set Parameters
603
+ ###########################################################################################
604
+ def set_title(self, title: str):
605
+ """
606
+ Sets the title of the window.
607
+
608
+ Parameters
609
+ ----------
610
+ title : str
611
+ The title to be set for the window.
612
+
613
+ Examples
614
+ --------
615
+ >>> app = Pyloid(app_name="Pyloid-App")
616
+ >>> window = app.create_window("pyloid-window")
617
+ >>> window.set_title('My Window Title')
618
+ """
619
+ self.title = title
620
+ self._window.setWindowTitle(self.title)
621
+
622
+ def set_size(self, width: int, height: int):
623
+ """
624
+ Sets the size of the window.
625
+
626
+ Parameters
627
+ ----------
628
+ width : int
629
+ The width of the window.
630
+ height : int
631
+ The height of the window.
632
+
633
+ Examples
634
+ --------
635
+ >>> app = Pyloid(app_name="Pyloid-App")
636
+ >>> window = app.create_window("pyloid-window")
637
+ >>> window.set_size(800, 600)
638
+ """
639
+ self.width = width
640
+ self.height = height
641
+ self._window.setGeometry(self.x, self.y, self.width, self.height)
642
+
643
+ def set_position(self, x: int, y: int):
644
+ """
645
+ Sets the position of the window.
646
+
647
+ Parameters
648
+ ----------
649
+ x : int
650
+ The x-coordinate of the window's position.
651
+ y : int
652
+ The y-coordinate of the window's position.
653
+
654
+ Examples
655
+ --------
656
+ >>> app = Pyloid(app_name="Pyloid-App")
657
+ >>> window = app.create_window("pyloid-window")
658
+ >>> window.set_position(100, 100)
659
+ """
660
+ self.x = x
661
+ self.y = y
662
+ self._window.setGeometry(self.x, self.y, self.width, self.height)
663
+
664
+ def set_position_by_anchor(self, anchor: str):
665
+ """
666
+ Positions the window at a specific location on the screen.
667
+
668
+ Parameters
669
+ ----------
670
+ anchor : str
671
+ The anchor point indicating where to position the window.
672
+ Possible values: 'center', 'top', 'bottom', 'left', 'right',
673
+ 'top-left', 'top-right', 'bottom-left', 'bottom-right'
674
+
675
+ Examples
676
+ --------
677
+ >>> window.set_position_by_anchor('center')
678
+ >>> window.set_position_by_anchor('top-right')
679
+ """
680
+ screen = self.app.primaryScreen().availableGeometry()
681
+ window_size = self.get_size()
682
+
683
+ if anchor == "center":
684
+ x = (screen.width() - window_size["width"]) // 2
685
+ y = (screen.height() - window_size["height"]) // 2
686
+ elif anchor == "top":
687
+ x = (screen.width() - window_size["width"]) // 2
688
+ y = screen.top()
689
+ elif anchor == "bottom":
690
+ x = (screen.width() - window_size["width"]) // 2
691
+ y = screen.bottom() - window_size["height"]
692
+ elif anchor == "left":
693
+ x = screen.left()
694
+ y = (screen.height() - window_size["height"]) // 2
695
+ elif anchor == "right":
696
+ x = screen.right() - window_size["width"]
697
+ y = (screen.height() - window_size["height"]) // 2
698
+ elif anchor == "top-left":
699
+ x = screen.left()
700
+ y = screen.top()
701
+ elif anchor == "top-right":
702
+ x = screen.right() - window_size["width"]
703
+ y = screen.top()
704
+ elif anchor == "bottom-left":
705
+ x = screen.left()
706
+ y = screen.bottom() - window_size["height"]
707
+ elif anchor == "bottom-right":
708
+ x = screen.right() - window_size["width"]
709
+ y = screen.bottom() - window_size["height"]
710
+ else:
711
+ raise ValueError("Invalid anchor point.")
712
+
713
+ self.set_position(x, y)
714
+
715
+ def set_frame(self, frame: bool):
716
+ """
717
+ Sets the frame of the window.
718
+
719
+ Parameters
720
+ ----------
721
+ frame : bool
722
+ If True, the window will have a frame. If False, the window will be frameless.
723
+
724
+ Examples
725
+ --------
726
+ >>> app = Pyloid(app_name="Pyloid-App")
727
+ >>> window = app.create_window("pyloid-window")
728
+ >>> window.set_frame(True)
729
+ >>> window.set_frame(False)
730
+ """
731
+ self.frame = frame
732
+ was_visible = self._window.isVisible()
733
+ if self.frame:
734
+ self._window.setWindowFlags(Qt.Window)
735
+ else:
736
+ self._window.setWindowFlags(Qt.FramelessWindowHint)
737
+ if was_visible:
738
+ self._window.show()
739
+
740
+ def set_context_menu(self, context_menu: bool):
741
+ """
742
+ Sets the context menu of the window.
743
+
744
+ Parameters
745
+ ----------
746
+ context_menu : bool
747
+ If True, the context menu will be disabled. If False, the default context menu will be enabled.
748
+
749
+ Examples
750
+ --------
751
+ >>> app = Pyloid(app_name="Pyloid-App")
752
+ >>> window = app.create_window("pyloid-window")
753
+ >>> window.set_context_menu(True)
754
+ >>> window.set_context_menu(False)
755
+ """
756
+ self.context_menu = context_menu
757
+ if self.context_menu:
758
+ self.web_view.setContextMenuPolicy(Qt.NoContextMenu)
759
+ else:
760
+ self.web_view.setContextMenuPolicy(Qt.DefaultContextMenu)
761
+
762
+ def set_dev_tools(self, enable: bool):
763
+ """
764
+ Sets the developer tools of the window.
765
+
766
+ If enabled, the developer tools can be opened using the F12 key.
767
+
768
+ Parameters
769
+ ----------
770
+ enable : bool
771
+ If True, the developer tools will be enabled. If False, the developer tools will be disabled.
772
+
773
+ Examples
774
+ --------
775
+ >>> app = Pyloid(app_name="Pyloid-App")
776
+ >>> window = app.create_window("pyloid-window")
777
+ >>> window.set_dev_tools(True)
778
+ >>> window.set_dev_tools(False)
779
+ """
780
+ self.dev_tools = enable
781
+ if self.dev_tools:
782
+ self.add_shortcut("F12", self.open_dev_tools)
783
+ else:
784
+ self.remove_shortcut("F12")
785
+
786
+ def open_dev_tools(self):
787
+ """
788
+ Opens the developer tools window.
789
+
790
+ Examples
791
+ --------
792
+ >>> app = Pyloid(app_name="Pyloid-App")
793
+ >>> window = app.create_window("pyloid-window")
794
+ >>> window.open_dev_tools()
795
+ """
796
+ self.web_view.page().setDevToolsPage(QWebEnginePage(self.web_view.page()))
797
+ self.dev_tools_window = QMainWindow(self._window)
798
+ dev_tools_view = QWebEngineView(self.dev_tools_window)
799
+ dev_tools_view.setPage(self.web_view.page().devToolsPage())
800
+ self.dev_tools_window.setCentralWidget(dev_tools_view)
801
+ self.dev_tools_window.resize(800, 600)
802
+ self.dev_tools_window.show()
803
+
804
+ # Add this line to handle dev tools window closure
805
+ self.dev_tools_window.closeEvent = lambda event: setattr(
806
+ self, "dev_tools_window", None
807
+ )
808
+
809
+ def closeEvent(self, event):
810
+ """Handles the event when the window is closed."""
811
+ # Close developer tools if open
812
+ if hasattr(self, "dev_tools_window") and self.dev_tools_window:
813
+ self.dev_tools_window.close()
814
+ self.dev_tools_window = None
815
+
816
+ # Solve memory leak issue with web view engine
817
+ self.web_view.page().deleteLater()
818
+ self.web_view.deleteLater()
819
+ self._remove_from_app_windows()
820
+ event.accept() # Accept the event (allow the window to close)
821
+
822
+ def _remove_from_app_windows(self):
823
+ """Removes the window from the app's window list."""
824
+ if self in self.app.windows:
825
+ self.app.windows.remove(self)
826
+ if not self.app.windows:
827
+ self.app.quit() # Quit the app if all windows are closed
828
+
829
+ ###########################################################################################
830
+ # Window management (no ID required)
831
+ ###########################################################################################
832
+ def hide(self):
833
+ """
834
+ Hides the window.
835
+
836
+ Examples
837
+ --------
838
+ >>> app = Pyloid(app_name="Pyloid-App")
839
+ >>> window = app.create_window("pyloid-window")
840
+ >>> window.hide()
841
+ """
842
+ self._window.hide()
843
+
844
+ def show(self):
845
+ """
846
+ Shows the window.
847
+
848
+ Examples
849
+ --------
850
+ >>> app = Pyloid(app_name="Pyloid-App")
851
+ >>> window = app.create_window("pyloid-window")
852
+ >>> window.show()
853
+ """
854
+ self._window.show()
855
+
856
+ def focus(self):
857
+ """
858
+ Focuses the window.
859
+
860
+ Examples
861
+ --------
862
+ >>> app = Pyloid(app_name="Pyloid-App")
863
+ >>> window = app.create_window("pyloid-window")
864
+ >>> window.focus()
865
+ """
866
+ self._window.activateWindow()
867
+ self._window.raise_()
868
+ self._window.setWindowState(
869
+ self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
870
+ )
871
+
872
+ def show_and_focus(self):
873
+ """
874
+ Shows and focuses the window.
875
+
876
+ Examples
877
+ --------
878
+ >>> app = Pyloid(app_name="Pyloid-App")
879
+ >>> window = app.create_window("pyloid-window")
880
+ >>> window.show_and_focus()
881
+ """
882
+ self._window.show()
883
+ self._window.activateWindow()
884
+ self._window.raise_()
885
+ self._window.setWindowState(
886
+ self._window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive
887
+ )
888
+
889
+ def close(self):
890
+ """
891
+ Closes the window.
892
+
893
+ Examples
894
+ --------
895
+ >>> app = Pyloid(app_name="Pyloid-App")
896
+ >>> window = app.create_window("pyloid-window")
897
+ >>> window.close()
898
+ """
899
+ self._window.close()
900
+
901
+ def fullscreen(self):
902
+ """
903
+ Enters fullscreen mode.
904
+
905
+ Examples
906
+ --------
907
+ >>> app = Pyloid(app_name="Pyloid-App")
908
+ >>> window = app.create_window("pyloid-window")
909
+ >>> window.fullscreen()
910
+ """
911
+ self._window.showFullScreen()
912
+
913
+ def toggle_fullscreen(self):
914
+ """
915
+ Toggles the fullscreen mode of the window.
916
+
917
+ Examples
918
+ --------
919
+ >>> app = Pyloid(app_name="Pyloid-App")
920
+ >>> window = app.create_window("pyloid-window")
921
+ >>> window.toggle_fullscreen()
922
+ """
923
+ if self._window.isFullScreen():
924
+ self._window.showNormal()
925
+ else:
926
+ self._window.showFullScreen()
927
+
928
+ def minimize(self):
929
+ """
930
+ Minimizes the window.
931
+
932
+ Examples
933
+ --------
934
+ >>> app = Pyloid(app_name="Pyloid-App")
935
+ >>> window = app.create_window("pyloid-window")
936
+ >>> window.minimize()
937
+ """
938
+ self._window.showMinimized()
939
+
940
+ def maximize(self):
941
+ """
942
+ Maximizes the window.
943
+
944
+ Examples
945
+ --------
946
+ >>> app = Pyloid(app_name="Pyloid-App")
947
+ >>> window = app.create_window("pyloid-window")
948
+ >>> window.maximize()
949
+ """
950
+ self._window.showMaximized()
951
+
952
+ def unmaximize(self):
953
+ """
954
+ Restores the window from maximized state.
955
+
956
+ Examples
957
+ --------
958
+ >>> app = Pyloid(app_name="Pyloid-App")
959
+ >>> window = app.create_window("pyloid-window")
960
+ >>> window.unmaximize()
961
+ """
962
+ self._window.showNormal()
963
+
964
+ def toggle_maximize(self):
965
+ """
966
+ Toggles the maximized state of the window.
967
+
968
+ Examples
969
+ --------
970
+ >>> app = Pyloid(app_name="Pyloid-App")
971
+ >>> window = app.create_window("pyloid-window")
972
+ >>> window.toggle_maximize()
973
+ """
974
+ if self._window.isMaximized():
975
+ self._window.showNormal()
976
+ else:
977
+ self._window.showMaximized()
978
+
979
+ def is_fullscreen(self) -> bool:
980
+ """
981
+ Returns True if the window is fullscreen.
982
+
983
+ Examples
984
+ --------
985
+ >>> app = Pyloid(app_name="Pyloid-App")
986
+ >>> window = app.create_window("pyloid-window")
987
+ >>> window.is_fullscreen()
988
+ """
989
+ return self._window.isFullScreen()
990
+
991
+ def is_maximized(self) -> bool:
992
+ """
993
+ Returns True if the window is maximized.
994
+
995
+ Examples
996
+ --------
997
+ >>> app = Pyloid(app_name="Pyloid-App")
998
+ >>> window = app.create_window("pyloid-window")
999
+ >>> window.is_maximized()
1000
+ """
1001
+ return self._window.isMaximized()
1002
+
1003
+ def capture(self, save_path: str) -> Optional[str]:
1004
+ """
1005
+ Captures the current window.
1006
+
1007
+ Parameters
1008
+ ----------
1009
+ save_path : str
1010
+ Path to save the captured image. If not specified, it will be saved in the current directory.
1011
+
1012
+ Returns
1013
+ -------
1014
+ Optional[str]
1015
+ Returns the path of the saved image.
1016
+
1017
+ Examples
1018
+ --------
1019
+ >>> app = Pyloid(app_name="Pyloid-App")
1020
+ >>> window = app.create_window("pyloid-window")
1021
+ >>> save_path = window.capture("screenshot.png")
1022
+ >>> print(f"Image saved at: {save_path}")
1023
+ Image saved at: screenshot.png
1024
+ """
1025
+ try:
1026
+ # Capture the window
1027
+ screenshot = self._window.grab()
1028
+
1029
+ # Save the image
1030
+ screenshot.save(save_path)
1031
+ return save_path
1032
+ except Exception as e:
1033
+ print(f"An error occurred while capturing the window: {e}")
1034
+ return None
1035
+
1036
+ ###########################################################################################
1037
+ # Shortcut
1038
+ ###########################################################################################
1039
+ def add_shortcut(self, key_sequence: str, callback: Callable):
1040
+ """
1041
+ Adds a keyboard shortcut to the window if it does not already exist.
1042
+
1043
+ Parameters
1044
+ ----------
1045
+ key_sequence : str
1046
+ Shortcut sequence (e.g., "Ctrl+C")
1047
+ callback : Callable
1048
+ Function to be executed when the shortcut is pressed
1049
+
1050
+ Returns
1051
+ -------
1052
+ QShortcut or None
1053
+ Created QShortcut object or None if the shortcut already exists
1054
+
1055
+ Examples
1056
+ --------
1057
+ ```python
1058
+ app = Pyloid(app_name="Pyloid-App")
1059
+
1060
+ window = app.create_window("pyloid-window")
1061
+
1062
+ def on_shortcut():
1063
+ print("Shortcut activated!")
1064
+ window.add_shortcut("Ctrl+C", on_shortcut)
1065
+
1066
+ app.run()
1067
+ ```
1068
+ """
1069
+ if key_sequence in self.shortcuts:
1070
+ # print(f"Shortcut {key_sequence} already exists.")
1071
+ return None
1072
+
1073
+ shortcut = QShortcut(QKeySequence(key_sequence), self._window)
1074
+ shortcut.activated.connect(callback)
1075
+ self.shortcuts[key_sequence] = shortcut
1076
+ return shortcut
1077
+
1078
+ def remove_shortcut(self, key_sequence: str):
1079
+ """
1080
+ Removes a keyboard shortcut from the window.
1081
+
1082
+ Parameters
1083
+ ----------
1084
+ key_sequence : str
1085
+ Shortcut sequence to be removed
1086
+
1087
+ Examples
1088
+ --------
1089
+ ```python
1090
+ app = Pyloid(app_name="Pyloid-App")
1091
+
1092
+ window = app.create_window("pyloid-window")
1093
+ window.remove_shortcut("Ctrl+C")
1094
+
1095
+ app.run()
1096
+ ```
1097
+ """
1098
+ if key_sequence in self.shortcuts:
1099
+ shortcut = self.shortcuts.pop(key_sequence)
1100
+ shortcut.setEnabled(False)
1101
+ shortcut.deleteLater()
1102
+
1103
+ def get_all_shortcuts(self):
1104
+ """
1105
+ Returns all registered shortcuts in the window.
1106
+
1107
+ Returns
1108
+ -------
1109
+ dict
1110
+ Dictionary of shortcut sequences and QShortcut objects
1111
+
1112
+ Examples
1113
+ --------
1114
+ ```python
1115
+ app = Pyloid(app_name="Pyloid-App")
1116
+
1117
+ window = app.create_window("pyloid-window")
1118
+ shortcuts = window.get_all_shortcuts()
1119
+ print(shortcuts)
1120
+
1121
+ app.run()
1122
+ ```
1123
+ """
1124
+ return self.shortcuts
1125
+
1126
+ ###########################################################################################
1127
+ # Event (Calling the JS from Python)
1128
+ ###########################################################################################
1129
+ def emit(self, event_name, data: Optional[Dict] = None):
1130
+ """
1131
+ Emits an event to the JavaScript side.
1132
+
1133
+ Parameters
1134
+ ----------
1135
+ event_name : str
1136
+ Name of the event
1137
+ data : dict, optional
1138
+ Data to be sent with the event (default is None)
1139
+
1140
+ Examples
1141
+ --------
1142
+ (Python)
1143
+ ```python
1144
+ app = Pyloid(app_name="Pyloid-App")
1145
+
1146
+ window = app.create_window("pyloid-window")
1147
+ window.emit("customEvent", {"message": "Hello, Pyloid!"})
1148
+
1149
+ app.run()
1150
+ ```
1151
+ ---
1152
+
1153
+ (JavaScript)
1154
+ ```javascript
1155
+ document.addEventListener('customEvent', (data) => {
1156
+ console.log(data.message);
1157
+ });
1158
+ ```
1159
+ """
1160
+ script = f"""
1161
+ (function() {{
1162
+ const eventData = {json.dumps(data)};
1163
+ const customEvent = new CustomEvent('{event_name}', {{ detail: eventData }});
1164
+ document.dispatchEvent(customEvent);
1165
+ }})();
1166
+ """
1167
+ self.web_view.page().runJavaScript(script)
1168
+
1169
+ ###########################################################################################
1170
+ # Get Properties
1171
+ ###########################################################################################
1172
+ def get_window_properties(self):
1173
+ """
1174
+ Returns the properties of the window.
1175
+
1176
+ Returns
1177
+ -------
1178
+ dict
1179
+ Dictionary containing the properties of the window
1180
+
1181
+ Examples
1182
+ --------
1183
+ ```python
1184
+ app = Pyloid(app_name="Pyloid-App")
1185
+
1186
+ window = app.create_window("pyloid-window")
1187
+ properties = window.get_window_properties()
1188
+ print(properties)
1189
+
1190
+ app.run()
1191
+ ```
1192
+ """
1193
+ return {
1194
+ "id": self.id,
1195
+ "title": self.title,
1196
+ "width": self.width,
1197
+ "height": self.height,
1198
+ "x": self.x,
1199
+ "y": self.y,
1200
+ "frame": self.frame,
1201
+ "context_menu": self.context_menu,
1202
+ "dev_tools": self.dev_tools,
1203
+ }
1204
+
1205
+ def get_id(self):
1206
+ """
1207
+ Returns the ID of the window.
1208
+
1209
+ Returns
1210
+ -------
1211
+ str
1212
+ ID of the window
1213
+
1214
+ Examples
1215
+ --------
1216
+ ```python
1217
+ app = Pyloid(app_name="Pyloid-App")
1218
+
1219
+ window = app.create_window("pyloid-window")
1220
+ window_id = window.get_id()
1221
+ print(window_id)
1222
+
1223
+ app.run()
1224
+ ```
1225
+ """
1226
+ return self.id
1227
+
1228
+ def get_size(self) -> Dict[str, int]:
1229
+ """
1230
+ Returns the size of the window.
1231
+
1232
+ Returns
1233
+ -------
1234
+ dict
1235
+ Dictionary containing the width and height of the window
1236
+
1237
+ Examples
1238
+ --------
1239
+ ```python
1240
+ app = Pyloid(app_name="Pyloid-App")
1241
+
1242
+ window = app.create_window("pyloid-window")
1243
+ size = window.get_size()
1244
+ print(size)
1245
+
1246
+ app.run()
1247
+ ```
1248
+ """
1249
+ return {"width": self.width, "height": self.height}
1250
+
1251
+ def get_position(self) -> Dict[str, int]:
1252
+ """
1253
+ Returns the position of the window.
1254
+
1255
+ Returns
1256
+ -------
1257
+ dict
1258
+ Dictionary containing the x and y coordinates of the window
1259
+
1260
+ Examples
1261
+ --------
1262
+ ```python
1263
+ app = Pyloid(app_name="Pyloid-App")
1264
+
1265
+ window = app.create_window("pyloid-window")
1266
+ position = window.get_position()
1267
+ print(position)
1268
+
1269
+ app.run()
1270
+ ```
1271
+ """
1272
+ return {"x": self.x, "y": self.y}
1273
+
1274
+ def get_title(self) -> str:
1275
+ """
1276
+ Returns the title of the window.
1277
+
1278
+ Returns
1279
+ -------
1280
+ str
1281
+ Title of the window
1282
+
1283
+ Examples
1284
+ --------
1285
+ ```python
1286
+ app = Pyloid(app_name="Pyloid-App")
1287
+
1288
+ window = app.create_window("pyloid-window")
1289
+ title = window.get_title()
1290
+ print(title)
1291
+
1292
+ app.run()
1293
+ ```
1294
+ """
1295
+ return self.title
1296
+
1297
+ def get_url(self) -> str:
1298
+ """
1299
+ Returns the URL of the window.
1300
+
1301
+ Returns
1302
+ -------
1303
+ str
1304
+ URL of the window
1305
+
1306
+ Examples
1307
+ --------
1308
+ ```python
1309
+ app = Pyloid(app_name="Pyloid-App")
1310
+
1311
+ window = app.create_window("pyloid-window")
1312
+ url = window.get_url()
1313
+ print(url)
1314
+
1315
+ app.run()
1316
+ ```
1317
+ """
1318
+ return self.web_view.url().toString()
1319
+
1320
+ def get_visible(self) -> bool:
1321
+ """
1322
+ Returns the visibility of the window.
1323
+
1324
+ Returns
1325
+ -------
1326
+ bool
1327
+ True if the window is visible, False otherwise
1328
+
1329
+ Examples
1330
+ --------
1331
+ ```python
1332
+ app = Pyloid(app_name="Pyloid-App")
1333
+
1334
+ window = app.create_window("pyloid-window")
1335
+ visible = window.get_visible()
1336
+ print(visible)
1337
+
1338
+ app.run()
1339
+ ```
1340
+ """
1341
+ return self._window.isVisible()
1342
+
1343
+ def get_frame(self) -> bool:
1344
+ """
1345
+ Returns the frame enabled state of the window.
1346
+
1347
+ Returns
1348
+ -------
1349
+ bool
1350
+ True if the frame is enabled, False otherwise
1351
+
1352
+ Examples
1353
+ --------
1354
+ ```python
1355
+ app = Pyloid(app_name="Pyloid-App")
1356
+
1357
+ window = app.create_window("pyloid-window")
1358
+ frame = window.get_frame()
1359
+ print(frame)
1360
+
1361
+ app.run()
1362
+ ```
1363
+ """
1364
+ return self.frame
1365
+
1366
+ ###########################################################################################
1367
+ # Resize
1368
+ ###########################################################################################
1369
+ def set_resizable(self, resizable: bool):
1370
+ """
1371
+ Sets the resizability of the window.
1372
+
1373
+ Parameters
1374
+ ----------
1375
+ resizable : bool
1376
+ True to make the window resizable, False to make it fixed size
1377
+
1378
+ Examples
1379
+ --------
1380
+ ```python
1381
+ app = Pyloid(app_name="Pyloid-App")
1382
+
1383
+ window = app.create_window("pyloid-window")
1384
+ window.set_resizable(True)
1385
+
1386
+ app.run()
1387
+ ```
1388
+ """
1389
+ self.resizable = resizable
1390
+ if self.frame:
1391
+ flags = self._window.windowFlags() | Qt.WindowCloseButtonHint
1392
+ if resizable:
1393
+ pass
1394
+ else:
1395
+ flags |= Qt.MSWindowsFixedSizeDialogHint
1396
+ self._window.setWindowFlags(flags)
1397
+ else:
1398
+ # 프레임이 없는 경우 커스텀 리사이징 로직을 설정합니다.
1399
+ self.web_view.is_resizing_enabled = resizable
1400
+
1401
+ self._window.show() # 변경사항을 적용하기 위해 창을 다시 표시합니다.
1402
+
1403
+ def set_minimum_size(self, min_width: int, min_height: int):
1404
+ """
1405
+ Sets the minimum size of the window.
1406
+
1407
+ Parameters
1408
+ ----------
1409
+ min_width : int
1410
+ Minimum width of the window
1411
+ min_height : int
1412
+ Minimum height of the window
1413
+
1414
+ Examples
1415
+ --------
1416
+ ```python
1417
+ app = Pyloid(app_name="Pyloid-App")
1418
+
1419
+ window = app.create_window("pyloid-window")
1420
+ window.set_minimum_size(400, 300)
1421
+
1422
+ app.run()
1423
+ ```
1424
+ """
1425
+ self._window.setMinimumSize(min_width, min_height)
1426
+
1427
+ def set_maximum_size(self, max_width: int, max_height: int):
1428
+ """
1429
+ Sets the maximum size of the window.
1430
+
1431
+ Parameters
1432
+ ----------
1433
+ max_width : int
1434
+ Maximum width of the window
1435
+ max_height : int
1436
+ Maximum height of the window
1437
+
1438
+ Examples
1439
+ --------
1440
+ ```python
1441
+ app = Pyloid(app_name="Pyloid-App")
1442
+
1443
+ window = app.create_window("pyloid-window")
1444
+ window.set_maximum_size(1024, 768)
1445
+
1446
+ app.run()
1447
+ ```
1448
+ """
1449
+ self._window.setMaximumSize(max_width, max_height)
1450
+
1451
+ def get_minimum_size(self) -> Dict[str, int]:
1452
+ """
1453
+ Returns the minimum size of the window.
1454
+
1455
+ Returns
1456
+ -------
1457
+ dict
1458
+ Dictionary containing the minimum width and height of the window
1459
+
1460
+ Examples
1461
+ --------
1462
+ ```python
1463
+ app = Pyloid(app_name="Pyloid-App")
1464
+
1465
+ window = app.create_window("pyloid-window")
1466
+ min_size = window.get_minimum_size()
1467
+ print(min_size)
1468
+
1469
+ app.run()
1470
+ ```
1471
+ """
1472
+ return {
1473
+ "width": self._window.minimumWidth(),
1474
+ "height": self._window.minimumHeight(),
1475
+ }
1476
+
1477
+ def get_maximum_size(self) -> Dict[str, int]:
1478
+ """
1479
+ Returns the maximum size of the window.
1480
+
1481
+ Returns
1482
+ -------
1483
+ dict
1484
+ Dictionary containing the maximum width and height of the window
1485
+
1486
+ Examples
1487
+ --------
1488
+ ```python
1489
+ app = Pyloid(app_name="Pyloid-App")
1490
+
1491
+ window = app.create_window("pyloid-window")
1492
+ max_size = window.get_maximum_size()
1493
+ print(max_size)
1494
+
1495
+ app.run()
1496
+ ```
1497
+ """
1498
+ return {
1499
+ "width": self._window.maximumWidth(),
1500
+ "height": self._window.maximumHeight(),
1501
+ }
1502
+
1503
+ def get_resizable(self) -> bool:
1504
+ """
1505
+ Returns the resizability of the window.
1506
+
1507
+ Returns
1508
+ -------
1509
+ bool
1510
+ True if the window is resizable, False otherwise
1511
+
1512
+ Examples
1513
+ --------
1514
+ ```python
1515
+ app = Pyloid(app_name="Pyloid-App")
1516
+
1517
+ window = app.create_window("pyloid-window")
1518
+ resizable = window.get_resizable()
1519
+ print(resizable)
1520
+
1521
+ app.run()
1522
+ ```
1523
+ """
1524
+ return self.resizable
1525
+
1526
+ ###########################################################################################
1527
+ # For Custom Pyside6 Features
1528
+ ###########################################################################################
1529
+ def get_QMainWindow(self) -> QMainWindow:
1530
+ """
1531
+ Returns the QMainWindow object of the window.
1532
+
1533
+ you can use all the features of QMainWindow for customizing the window.
1534
+
1535
+ Returns
1536
+ -------
1537
+ QMainWindow
1538
+ QMainWindow object of the window
1539
+
1540
+ Examples
1541
+ --------
1542
+ ```python
1543
+ from PySide6.QtCore import Qt
1544
+ from pyloid import Pyloid
1545
+
1546
+ app = Pyloid(app_name="Pyloid-App")
1547
+
1548
+ window = app.create_window("pyloid-window")
1549
+ qmain = window.get_QMainWindow()
1550
+
1551
+ qmain.setWindowFlags(qmain.windowFlags() | Qt.WindowStaysOnTopHint) # window stays on top
1552
+ ```
1553
+ """
1554
+ return self._window
1555
+
1556
+ def get_QWebEngineView(self) -> CustomWebEngineView:
1557
+ """
1558
+ Returns the CustomWebEngineView object which inherits from QWebEngineView.
1559
+
1560
+ Returns
1561
+ -------
1562
+ CustomWebEngineView
1563
+ CustomWebEngineView object of the window
1564
+
1565
+ Examples
1566
+ --------
1567
+ ```python
1568
+ window = app.create_window("pyloid-window")
1569
+ web_view = window.get_QWebEngineView()
1570
+
1571
+ web_view.page().runJavaScript("console.log('Hello, Pyloid!')")
1572
+ ```
1573
+ """
1574
+ return self.web_view
1575
+
1576
+ ###########################################################################################
1577
+ # QMainWindow flags
1578
+ ###########################################################################################
1579
+ def set_window_stay_on_top(self, on_top: bool):
1580
+ """
1581
+ Sets the window stay on top flag of the window.
1582
+
1583
+ Parameters
1584
+ ----------
1585
+ on_top : bool
1586
+ True to keep the window on top, False otherwise
1587
+
1588
+ Examples
1589
+ --------
1590
+ ```python
1591
+ window.set_window_stay_on_top(True)
1592
+ window.set_window_stay_on_top(False)
1593
+ ```
1594
+ """
1595
+ flags = self._window.windowFlags()
1596
+ if on_top:
1597
+ flags |= Qt.WindowStaysOnTopHint
1598
+ else:
1599
+ flags &= ~Qt.WindowStaysOnTopHint
1600
+
1601
+ # Maintain existing flags while only changing WindowStaysOnTopHint
1602
+ # Explicitly add the close button
1603
+ flags |= Qt.WindowCloseButtonHint
1604
+
1605
+ self._window.setWindowFlags(flags)
1606
+
1607
+ # Show the window again to apply the changes
1608
+ self._window.show()
1609
+
1610
+ def set_window_stay_on_bottom(self, on_bottom: bool):
1611
+ """
1612
+ Sets the window stay on bottom flag of the window.
1613
+
1614
+ Parameters
1615
+ ----------
1616
+ on_bottom : bool
1617
+ True to keep the window on bottom, False otherwise
1618
+
1619
+ Examples
1620
+ --------
1621
+ ```python
1622
+ window.set_window_stay_on_bottom(True)
1623
+ window.set_window_stay_on_bottom(False)
1624
+ ```
1625
+ """
1626
+ flags = self._window.windowFlags()
1627
+ if on_bottom:
1628
+ flags |= Qt.WindowStaysOnBottomHint
1629
+ else:
1630
+ flags &= ~Qt.WindowStaysOnBottomHint
1631
+
1632
+ # Maintain existing flags while only changing WindowStaysOnBottomHint
1633
+ # Explicitly add the close button
1634
+ flags |= Qt.WindowCloseButtonHint
1635
+
1636
+ self._window.setWindowFlags(flags)
1637
+
1638
+ # Show the window again to apply the changes
1639
+ self._window.show()
1640
+
1641
+ ###########################################################################################
1642
+ # Splash Screen
1643
+ ###########################################################################################
1644
+ def set_static_image_splash_screen(
1645
+ self,
1646
+ image_path: str,
1647
+ close_on_load: bool = True,
1648
+ stay_on_top: bool = True,
1649
+ clickable: bool = True,
1650
+ position: str = "center",
1651
+ ):
1652
+ """
1653
+ Sets the static image splash screen of the window.
1654
+
1655
+ Parameters
1656
+ ----------
1657
+ image_path : str
1658
+ Path to the image file
1659
+ close_on_load : bool, optional
1660
+ True to close the splash screen when the page is loaded, False otherwise (default is True)
1661
+ stay_on_top : bool, optional
1662
+ True to keep the splash screen on top, False otherwise (default is True)
1663
+ clickable : bool, optional
1664
+ True to make the splash screen clickable, False otherwise (default is True)
1665
+ if clickable is True, you can click the splash screen to close it.
1666
+ position : str, optional
1667
+ Position of the splash screen. Options are 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right' (default is 'center')
1668
+
1669
+ Examples
1670
+ --------
1671
+ ```python
1672
+ window.set_image_splash_screen("./assets/loading.png", close_on_load=True, stay_on_top=True)
1673
+ ```
1674
+ """
1675
+ pixmap = QPixmap(image_path)
1676
+
1677
+ if not clickable:
1678
+
1679
+ class NonClickableSplashScreen(QSplashScreen):
1680
+ def mousePressEvent(self, event):
1681
+ pass # Ignore click events
1682
+
1683
+ splash = NonClickableSplashScreen(
1684
+ pixmap, Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0)
1685
+ )
1686
+ else:
1687
+ splash = QSplashScreen(
1688
+ pixmap, Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0)
1689
+ )
1690
+
1691
+ self.close_on_load = close_on_load
1692
+ self.splash_screen = splash
1693
+ self._position_splash_screen(position)
1694
+ self.splash_screen.show()
1695
+
1696
+ def set_gif_splash_screen(
1697
+ self,
1698
+ gif_path: str,
1699
+ close_on_load: bool = True,
1700
+ stay_on_top: bool = True,
1701
+ clickable: bool = True,
1702
+ position: str = "center",
1703
+ ):
1704
+ """
1705
+ Sets the gif splash screen of the window.
1706
+
1707
+ Parameters
1708
+ ----------
1709
+ gif_path : str
1710
+ Path to the gif file
1711
+ close_on_load : bool, optional
1712
+ True to close the splash screen when the page is loaded, False otherwise (default is True)
1713
+ stay_on_top : bool, optional
1714
+ True to keep the splash screen on top, False otherwise (default is True)
1715
+ clickable : bool, optional
1716
+ True to make the splash screen clickable, False otherwise (default is True)
1717
+ if clickable is True, you can click the splash screen to close it.
1718
+ position : str, optional
1719
+ Position of the splash screen. Options are 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right' (default is 'center')
1720
+
1721
+ Examples
1722
+ --------
1723
+ ```python
1724
+ window.set_gif_splash_screen("./assets/loading.gif", close_on_load=True, stay_on_top=True)
1725
+ ```
1726
+ """
1727
+
1728
+ if not clickable:
1729
+
1730
+ class NonClickableSplashScreen(QSplashScreen):
1731
+ def mousePressEvent(self, event):
1732
+ pass # Ignore click events
1733
+
1734
+ # Create splash screen (using animated GIF)
1735
+ splash = NonClickableSplashScreen(
1736
+ QPixmap(1, 1),
1737
+ Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0),
1738
+ ) # Start with 1x1 transparent pixmap
1739
+ else:
1740
+ splash = QSplashScreen(
1741
+ QPixmap(1, 1),
1742
+ Qt.WindowStaysOnTopHint if stay_on_top else Qt.WindowType(0),
1743
+ )
1744
+
1745
+ splash.setAttribute(Qt.WA_TranslucentBackground)
1746
+
1747
+ # Create QLabel for GIF animation
1748
+ label = QLabel(splash)
1749
+ movie = QMovie(gif_path)
1750
+ label.setMovie(movie)
1751
+
1752
+ # Adjust splash screen size to match GIF size
1753
+ movie.frameChanged.connect(
1754
+ lambda: splash.setFixedSize(movie.currentPixmap().size())
1755
+ )
1756
+
1757
+ # Start animation and show splash screen
1758
+ movie.start()
1759
+ self.close_on_load = close_on_load
1760
+ self.splash_screen = splash
1761
+ self._position_splash_screen(position)
1762
+ splash.show()
1763
+
1764
+ def _position_splash_screen(self, position: str):
1765
+ if not self.splash_screen:
1766
+ return
1767
+
1768
+ screen = self.app.primaryScreen().geometry()
1769
+ splash_size = self.splash_screen.size()
1770
+
1771
+ if position == "center":
1772
+ new_position = screen.center() - QPoint(
1773
+ splash_size.width() // 2, splash_size.height() // 2
1774
+ )
1775
+ elif position == "top-left":
1776
+ new_position = screen.topLeft()
1777
+ elif position == "top-right":
1778
+ new_position = screen.topRight() - QPoint(splash_size.width(), 0)
1779
+ elif position == "bottom-left":
1780
+ new_position = screen.bottomLeft() - QPoint(0, splash_size.height())
1781
+ elif position == "bottom-right":
1782
+ new_position = screen.bottomRight() - QPoint(
1783
+ splash_size.width(), splash_size.height()
1784
+ )
1785
+ else:
1786
+ new_position = screen.center() - QPoint(
1787
+ splash_size.width() // 2, splash_size.height() // 2
1788
+ )
1789
+
1790
+ self.splash_screen.move(new_position)
1791
+
1792
+ def close_splash_screen(self):
1793
+ """
1794
+ Closes the splash screen if it exists.
1795
+
1796
+ Examples
1797
+ --------
1798
+ ```python
1799
+ window.close_splash_screen()
1800
+ ```
1801
+ """
1802
+ if hasattr(self, "splash_screen") and self.splash_screen:
1803
+ self.splash_screen.close()
1804
+ self.close_on_load = None
1805
+ self.splash_screen = None
1806
+
1807
+ ###########################################################################################
1808
+ # WebEngineView Attribute setting
1809
+ ###########################################################################################
1810
+ def set_web_engine_view_attribute(self, attribute: QWebEngineSettings, on: bool):
1811
+ """
1812
+ Sets the attribute of the WebEngineView.
1813
+
1814
+ Parameters
1815
+ ----------
1816
+ attribute : QWebEngineSettings
1817
+ Attribute to set
1818
+ on : bool
1819
+ True to enable the attribute, False to disable it
1820
+
1821
+ Examples
1822
+ --------
1823
+ ```python
1824
+ window.set_web_engine_view_attribute(QWebEngineSettings.WebAttribute.ScreenCaptureEnabled, False)
1825
+ ```
1826
+ """
1827
+ settings = self.web_view.settings()
1828
+ settings.setAttribute(attribute, on)
1829
+
1830
+ def is_web_engine_view_attribute(self, attribute: QWebEngineSettings) -> bool:
1831
+ """
1832
+ Returns the attribute of the WebEngineView.
1833
+
1834
+ Parameters
1835
+ ----------
1836
+ attribute : QWebEngineSettings
1837
+ Attribute to get
1838
+
1839
+ Returns
1840
+ -------
1841
+ bool
1842
+ True if the attribute is enabled, False otherwise
1843
+
1844
+ Examples
1845
+ --------
1846
+ ```python
1847
+ window.is_web_engine_view_attribute(QWebEngineSettings.WebAttribute.ScreenCaptureEnabled)
1848
+ ```
1849
+ """
1850
+ settings = self.web_view.settings()
1851
+ return settings.testAttribute(attribute)
1852
+
1853
+ def set_permission_handler(self, feature: QWebEnginePage.Feature, handler):
1854
+ """
1855
+ Sets a handler for a specific permission.
1856
+
1857
+ Parameters
1858
+ ----------
1859
+ feature : QWebEnginePage.Feature
1860
+ The type of permission to set
1861
+ handler : callable
1862
+ The handler function to process the permission request
1863
+
1864
+ Examples
1865
+ --------
1866
+ ```python
1867
+ def handle_camera(origin, feature):
1868
+ window.web_view.page().setFeaturePermission(
1869
+ origin,
1870
+ feature,
1871
+ QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
1872
+ )
1873
+
1874
+ window.set_permission_handler(
1875
+ QWebEnginePage.Feature.MediaVideoCapture,
1876
+ handle_camera
1877
+ )
1878
+ ```
1879
+ """
1880
+ self.web_view.custom_page.setPermissionHandler(feature, handler)
1881
+
1882
+ def grant_permission(self, feature: QWebEnginePage.Feature):
1883
+ """
1884
+ Automatically grants a specific permission when a request is made.
1885
+
1886
+ Parameters
1887
+ ----------
1888
+ feature : QWebEnginePage.Feature
1889
+ The type of permission to automatically grant
1890
+
1891
+ Examples
1892
+ --------
1893
+ ```python
1894
+ window.grant_permission(QWebEnginePage.Feature.MediaVideoCapture)
1895
+ ```
1896
+ """
1897
+
1898
+ def auto_grant(origin, feat):
1899
+ self.web_view.page().setFeaturePermission(
1900
+ origin, feat, QWebEnginePage.PermissionPolicy.PermissionGrantedByUser
1901
+ )
1902
+
1903
+ self.set_permission_handler(feature, auto_grant)
1904
+
1905
+ def deny_permission(self, feature: QWebEnginePage.Feature):
1906
+ """
1907
+ Automatically denies a specific permission when a request is made.
1908
+
1909
+ Parameters
1910
+ ----------
1911
+ feature : QWebEnginePage.Feature
1912
+ The type of permission to automatically deny
1913
+
1914
+ Examples
1915
+ --------
1916
+ ```python
1917
+ window.deny_permission(QWebEnginePage.Feature.Notifications)
1918
+ ```
1919
+ """
1920
+
1921
+ def auto_deny(origin, feat):
1922
+ self.web_view.page().setFeaturePermission(
1923
+ origin, feat, QWebEnginePage.PermissionPolicy.PermissionDeniedByUser
1924
+ )
1925
+
1926
+ self.set_permission_handler(feature, auto_deny)
1927
+
1928
+ def set_desktop_media_handler(self, handler):
1929
+ """
1930
+ 데스크톱 미디어(화면/윈도우) 선택 핸들러를 설정합니다.
1931
+
1932
+ Parameters
1933
+ ----------
1934
+ handler : callable
1935
+ 요청을 처리할 핸들러 함수. QWebEngineDesktopMediaRequest 인자로 받습니다.
1936
+
1937
+ Examples
1938
+ --------
1939
+ ```python
1940
+ def custom_media_handler(request):
1941
+ # 사용 가능한 화면 목록 출력
1942
+ for screen in request.screenList():
1943
+ print(f"Screen: {screen.name}")
1944
+
1945
+ # 사용 가능한 윈도우 목록 출력
1946
+ for window in request.windowList():
1947
+ print(f"Window: {window.name}")
1948
+
1949
+ # 첫 번째 화면 선택
1950
+ if request.screenList():
1951
+ request.selectScreen(request.screenList()[0])
1952
+
1953
+ window.set_desktop_media_handler(custom_media_handler)
1954
+ ```
1955
+ """
1956
+ self.web_view.custom_page.setDesktopMediaHandler(handler)