pygpt-net 2.6.57__py3-none-any.whl → 2.6.59__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.
- pygpt_net/CHANGELOG.txt +10 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +30 -25
- pygpt_net/controller/debug/debug.py +3 -3
- pygpt_net/controller/dialogs/info.py +6 -2
- pygpt_net/controller/ui/tabs.py +17 -0
- pygpt_net/core/agents/runners/llama_workflow.py +0 -0
- pygpt_net/core/filesystem/parser.py +37 -24
- pygpt_net/core/filesystem/url.py +5 -2
- pygpt_net/data/config/config.json +4 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +41 -2
- pygpt_net/data/js/app/ui.js +1 -1
- pygpt_net/data/js/app.min.js +2 -2
- pygpt_net/data/locale/locale.de.ini +5 -1
- pygpt_net/data/locale/locale.en.ini +5 -1
- pygpt_net/data/locale/locale.es.ini +5 -1
- pygpt_net/data/locale/locale.fr.ini +5 -1
- pygpt_net/data/locale/locale.it.ini +5 -1
- pygpt_net/data/locale/locale.pl.ini +5 -1
- pygpt_net/data/locale/locale.uk.ini +5 -1
- pygpt_net/data/locale/locale.zh.ini +5 -1
- pygpt_net/data/locale/plugin.cmd_system.en.ini +68 -0
- pygpt_net/js_rc.py +5 -5
- pygpt_net/plugin/base/plugin.py +3 -5
- pygpt_net/plugin/cmd_system/config.py +377 -1
- pygpt_net/plugin/cmd_system/plugin.py +52 -8
- pygpt_net/plugin/cmd_system/runner.py +508 -32
- pygpt_net/plugin/cmd_system/winapi.py +481 -0
- pygpt_net/plugin/cmd_system/worker.py +88 -15
- pygpt_net/provider/agents/llama_index/workflow/supervisor.py +0 -0
- pygpt_net/provider/core/config/patch.py +8 -1
- pygpt_net/provider/llms/openai.py +6 -4
- pygpt_net/tools/code_interpreter/ui/html.py +2 -1
- pygpt_net/tools/html_canvas/ui/widgets.py +19 -18
- pygpt_net/tools/web_browser/__init__.py +12 -0
- pygpt_net/tools/web_browser/tool.py +232 -0
- pygpt_net/tools/web_browser/ui/__init__.py +0 -0
- pygpt_net/tools/web_browser/ui/dialogs.py +123 -0
- pygpt_net/tools/web_browser/ui/widgets.py +351 -0
- pygpt_net/ui/widget/textarea/html.py +172 -24
- pygpt_net/ui/widget/textarea/web.py +1 -1
- {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/METADATA +81 -61
- {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/RECORD +45 -39
- {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.57.dist-info → pygpt_net-2.6.59.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.22 19:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import Qt, Slot, QUrl, QObject, Signal, QSize
|
|
13
|
+
from PySide6.QtGui import QIcon
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QVBoxLayout,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLineEdit,
|
|
18
|
+
QPushButton,
|
|
19
|
+
QWidget,
|
|
20
|
+
QSizePolicy,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
24
|
+
from pygpt_net.ui.widget.textarea.html import HtmlOutput
|
|
25
|
+
from pygpt_net.utils import trans
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolWidget:
|
|
29
|
+
def __init__(self, window=None, tool=None):
|
|
30
|
+
"""
|
|
31
|
+
HTML/JS canvas widget
|
|
32
|
+
|
|
33
|
+
:param window: Window instance
|
|
34
|
+
:param tool: Tool instance
|
|
35
|
+
"""
|
|
36
|
+
self.window = window
|
|
37
|
+
self.tool = tool # tool instance
|
|
38
|
+
self.output = None # output
|
|
39
|
+
|
|
40
|
+
# --- Navigation bar state ---
|
|
41
|
+
self.nav_bar = None
|
|
42
|
+
self.nav_layout = None
|
|
43
|
+
self.address_bar = None
|
|
44
|
+
self.btn_back = None
|
|
45
|
+
self.btn_next = None
|
|
46
|
+
self.btn_reload = None
|
|
47
|
+
self.btn_go = None
|
|
48
|
+
|
|
49
|
+
def on_open(self):
|
|
50
|
+
"""On open"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def on_delete(self):
|
|
54
|
+
"""On delete"""
|
|
55
|
+
if self.tool:
|
|
56
|
+
self.tool.signals.url.disconnect(self.open_url) # keep connections clean
|
|
57
|
+
try:
|
|
58
|
+
if hasattr(self.output, 'titleChanged'):
|
|
59
|
+
self.output.titleChanged.disconnect(self.on_update_title)
|
|
60
|
+
elif hasattr(self.output, 'page') and self.output.page() and hasattr(self.output.page(), 'titleChanged'):
|
|
61
|
+
self.output.page().titleChanged.disconnect(self.on_update_title)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
if self.output:
|
|
65
|
+
self.output.on_delete()
|
|
66
|
+
|
|
67
|
+
def set_tab(self, tab):
|
|
68
|
+
"""
|
|
69
|
+
Set tab
|
|
70
|
+
|
|
71
|
+
:param tab: Tab
|
|
72
|
+
"""
|
|
73
|
+
self.output.set_tab(tab)
|
|
74
|
+
|
|
75
|
+
def setup(self, all: bool = True) -> QVBoxLayout:
|
|
76
|
+
"""
|
|
77
|
+
Setup widget body
|
|
78
|
+
|
|
79
|
+
:param all: If True, setup all widgets
|
|
80
|
+
:return: QVBoxLayout
|
|
81
|
+
"""
|
|
82
|
+
self.output = BrowserOutput(self.window)
|
|
83
|
+
if hasattr(self.output, 'titleChanged'):
|
|
84
|
+
try:
|
|
85
|
+
self.output.titleChanged.connect(self.on_update_title)
|
|
86
|
+
except TypeError:
|
|
87
|
+
pass
|
|
88
|
+
elif hasattr(self.output, 'page') and self.output.page() and hasattr(self.output.page(), 'titleChanged'):
|
|
89
|
+
try:
|
|
90
|
+
self.output.page().titleChanged.connect(self.on_update_title)
|
|
91
|
+
except TypeError:
|
|
92
|
+
pass
|
|
93
|
+
else:
|
|
94
|
+
def _emit_title_from_load(ok: bool):
|
|
95
|
+
if not ok:
|
|
96
|
+
return
|
|
97
|
+
title = ""
|
|
98
|
+
try:
|
|
99
|
+
if hasattr(self.output, 'title'):
|
|
100
|
+
title = self.output.title()
|
|
101
|
+
elif hasattr(self.output, 'page') and self.output.page():
|
|
102
|
+
title = self.output.page().title()
|
|
103
|
+
except Exception:
|
|
104
|
+
title = ""
|
|
105
|
+
if title and title.strip() and title != "about:blank":
|
|
106
|
+
self.on_update_title(title)
|
|
107
|
+
|
|
108
|
+
# ---- Navigation bar ----
|
|
109
|
+
self.nav_bar = QWidget()
|
|
110
|
+
self.nav_layout = QHBoxLayout(self.nav_bar)
|
|
111
|
+
self.nav_layout.setContentsMargins(6, 4, 6, 4) # small margins for compact look
|
|
112
|
+
self.nav_layout.setSpacing(6)
|
|
113
|
+
self.nav_bar.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
|
114
|
+
|
|
115
|
+
# Icon size and fixed navbar height (to prevent taking too much space)
|
|
116
|
+
icon_size_px = 20 # base icon size for QPushButtons
|
|
117
|
+
nav_height = max(32, min(44, icon_size_px + 16)) # compact, never half-screen
|
|
118
|
+
self.nav_bar.setFixedHeight(nav_height)
|
|
119
|
+
|
|
120
|
+
# Buttons
|
|
121
|
+
self.btn_back = QPushButton()
|
|
122
|
+
self.btn_back.setToolTip("Back")
|
|
123
|
+
self.btn_back.setIcon(QIcon(":/icons/back.svg"))
|
|
124
|
+
self.btn_back.setIconSize(QSize(icon_size_px, icon_size_px))
|
|
125
|
+
self.btn_back.setFixedHeight(nav_height - 8)
|
|
126
|
+
self.btn_back.setEnabled(False)
|
|
127
|
+
self.btn_back.setAutoDefault(False) # prevent Enter from triggering this
|
|
128
|
+
try:
|
|
129
|
+
self.btn_back.setDefault(False)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
self.btn_next = QPushButton()
|
|
134
|
+
self.btn_next.setToolTip("Next")
|
|
135
|
+
self.btn_next.setIcon(QIcon(":/icons/forward.svg"))
|
|
136
|
+
self.btn_next.setIconSize(QSize(icon_size_px, icon_size_px))
|
|
137
|
+
self.btn_next.setFixedHeight(nav_height - 8)
|
|
138
|
+
self.btn_next.setEnabled(False)
|
|
139
|
+
self.btn_next.setAutoDefault(False)
|
|
140
|
+
try:
|
|
141
|
+
self.btn_next.setDefault(False)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
self.btn_reload = QPushButton()
|
|
146
|
+
self.btn_reload.setToolTip("Reload")
|
|
147
|
+
self.btn_reload.setIcon(QIcon(":/icons/reload.svg"))
|
|
148
|
+
self.btn_reload.setIconSize(QSize(icon_size_px, icon_size_px))
|
|
149
|
+
self.btn_reload.setFixedHeight(nav_height - 8)
|
|
150
|
+
self.btn_reload.setAutoDefault(False)
|
|
151
|
+
try:
|
|
152
|
+
self.btn_reload.setDefault(False)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# "Go" button
|
|
157
|
+
self.btn_go = QPushButton()
|
|
158
|
+
self.btn_go.setToolTip("Open URL")
|
|
159
|
+
self.btn_go.setIcon(QIcon(":/icons/redo.svg"))
|
|
160
|
+
self.btn_go.setIconSize(QSize(icon_size_px, icon_size_px))
|
|
161
|
+
self.btn_go.setFixedHeight(nav_height - 8)
|
|
162
|
+
self.btn_go.setAutoDefault(False) # avoid stealing Enter
|
|
163
|
+
try:
|
|
164
|
+
self.btn_go.setDefault(False)
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
# Address bar (custom line edit to capture Enter and avoid default button reload)
|
|
169
|
+
self.address_bar = AddressLineEdit(on_return_callback=self._on_address_enter)
|
|
170
|
+
self.address_bar.setPlaceholderText("Enter URL and press Enter")
|
|
171
|
+
self.address_bar.setFixedHeight(nav_height - 8)
|
|
172
|
+
# Still keep the Qt signal for completeness (won't fire because we intercept the key)
|
|
173
|
+
self.address_bar.returnPressed.connect(self._on_address_enter)
|
|
174
|
+
|
|
175
|
+
# Hook up button actions
|
|
176
|
+
self.btn_back.clicked.connect(lambda: self._navigate('back'))
|
|
177
|
+
self.btn_next.clicked.connect(lambda: self._navigate('forward'))
|
|
178
|
+
self.btn_reload.clicked.connect(lambda: self._navigate('reload'))
|
|
179
|
+
self.btn_go.clicked.connect(self._on_address_enter) # "Go" triggers address open
|
|
180
|
+
|
|
181
|
+
# Build nav layout
|
|
182
|
+
self.nav_layout.addWidget(self.btn_back)
|
|
183
|
+
self.nav_layout.addWidget(self.btn_next)
|
|
184
|
+
self.nav_layout.addWidget(self.btn_reload)
|
|
185
|
+
self.nav_layout.addWidget(self.address_bar, 1)
|
|
186
|
+
self.nav_layout.addWidget(self.btn_go)
|
|
187
|
+
|
|
188
|
+
sec_label = HelpLabel(trans("tool.web_browser.security.footer"))
|
|
189
|
+
sec_label.setAlignment(Qt.AlignCenter)
|
|
190
|
+
|
|
191
|
+
bottom_layout = QHBoxLayout()
|
|
192
|
+
bottom_layout.addWidget(sec_label)
|
|
193
|
+
|
|
194
|
+
output_layout = QVBoxLayout()
|
|
195
|
+
# put navigation bar above the web output
|
|
196
|
+
output_layout.addWidget(self.nav_bar)
|
|
197
|
+
output_layout.addWidget(self.output, 1)
|
|
198
|
+
output_layout.setContentsMargins(0, 0, 0, 0)
|
|
199
|
+
|
|
200
|
+
# connect signals
|
|
201
|
+
self.output.signals.save_as.connect(
|
|
202
|
+
self.tool.handle_save_as)
|
|
203
|
+
self.output.signals.audio_read.connect(
|
|
204
|
+
self.window.controller.chat.render.handle_audio_read)
|
|
205
|
+
|
|
206
|
+
# Keep nav bar in sync with the underlying view if signals are available
|
|
207
|
+
# (urlChanged/loadStarted/loadFinished are part of QWebEngineView)
|
|
208
|
+
if hasattr(self.output, 'urlChanged'):
|
|
209
|
+
self.output.urlChanged.connect(self._on_url_changed)
|
|
210
|
+
if hasattr(self.output, 'loadStarted'):
|
|
211
|
+
try:
|
|
212
|
+
self.output.loadStarted.connect(lambda: self._update_nav_controls())
|
|
213
|
+
except TypeError:
|
|
214
|
+
pass
|
|
215
|
+
if hasattr(self.output, 'loadFinished'):
|
|
216
|
+
try:
|
|
217
|
+
self.output.loadFinished.connect(lambda ok: self._update_nav_controls())
|
|
218
|
+
except TypeError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
self.tool.signals.url.connect(self.open_url)
|
|
222
|
+
self.tool.signals.closed.connect(self.on_delete)
|
|
223
|
+
|
|
224
|
+
layout = QVBoxLayout()
|
|
225
|
+
layout.addLayout(output_layout, 1)
|
|
226
|
+
layout.addLayout(bottom_layout, 0)
|
|
227
|
+
return layout
|
|
228
|
+
|
|
229
|
+
@Slot(str)
|
|
230
|
+
def open_url(self, url: str):
|
|
231
|
+
"""
|
|
232
|
+
Open URL in output
|
|
233
|
+
|
|
234
|
+
:param url: URL
|
|
235
|
+
"""
|
|
236
|
+
print("Opening URL:", url)
|
|
237
|
+
if self.address_bar:
|
|
238
|
+
self.address_bar.setText(url)
|
|
239
|
+
self.output.setUrl(QUrl(url))
|
|
240
|
+
self._update_nav_controls()
|
|
241
|
+
|
|
242
|
+
# ------------------
|
|
243
|
+
# Navigation helpers
|
|
244
|
+
# ------------------
|
|
245
|
+
def _show_navbar(self, show: bool):
|
|
246
|
+
"""Show/hide the navigation bar."""
|
|
247
|
+
if self.nav_bar:
|
|
248
|
+
self.nav_bar.setVisible(show)
|
|
249
|
+
# Keep controls state up-to-date
|
|
250
|
+
self._update_nav_controls()
|
|
251
|
+
|
|
252
|
+
def _navigate(self, action: str):
|
|
253
|
+
"""Perform navigation action on the output view."""
|
|
254
|
+
if not self.output:
|
|
255
|
+
return
|
|
256
|
+
try:
|
|
257
|
+
if action == 'back' and hasattr(self.output, 'back'):
|
|
258
|
+
self.output.back()
|
|
259
|
+
elif action == 'forward' and hasattr(self.output, 'forward'):
|
|
260
|
+
self.output.forward()
|
|
261
|
+
elif action == 'reload' and hasattr(self.output, 'reload'):
|
|
262
|
+
self.output.reload()
|
|
263
|
+
finally:
|
|
264
|
+
self._update_nav_controls()
|
|
265
|
+
|
|
266
|
+
def _on_address_enter(self):
|
|
267
|
+
"""Handle Enter pressed in the address bar or clicking GO."""
|
|
268
|
+
if not self.output or not self.address_bar:
|
|
269
|
+
return
|
|
270
|
+
text = self.address_bar.text().strip()
|
|
271
|
+
if not text:
|
|
272
|
+
return
|
|
273
|
+
url = QUrl.fromUserInput(text)
|
|
274
|
+
if url.isValid():
|
|
275
|
+
self._show_navbar(True)
|
|
276
|
+
self.output.setUrl(url)
|
|
277
|
+
self._update_nav_controls()
|
|
278
|
+
|
|
279
|
+
def _on_url_changed(self, url: QUrl):
|
|
280
|
+
"""Keep address bar in sync with the view URL."""
|
|
281
|
+
if self.address_bar:
|
|
282
|
+
self.address_bar.setText(url.toString())
|
|
283
|
+
self._update_nav_controls()
|
|
284
|
+
|
|
285
|
+
def _update_nav_controls(self):
|
|
286
|
+
"""Enable/disable back/forward buttons based on history."""
|
|
287
|
+
if not self.output:
|
|
288
|
+
return
|
|
289
|
+
# Reload is generally available when navbar is visible
|
|
290
|
+
if self.btn_reload:
|
|
291
|
+
self.btn_reload.setEnabled(True)
|
|
292
|
+
# Back/forward depend on history if available
|
|
293
|
+
if self.btn_back and self.btn_next:
|
|
294
|
+
try:
|
|
295
|
+
hist = self.output.history()
|
|
296
|
+
self.btn_back.setEnabled(bool(hist and hist.canGoBack()))
|
|
297
|
+
self.btn_next.setEnabled(bool(hist and hist.canGoForward()))
|
|
298
|
+
except Exception:
|
|
299
|
+
# If history is not available, keep safe defaults
|
|
300
|
+
self.btn_back.setEnabled(False)
|
|
301
|
+
self.btn_next.setEnabled(False)
|
|
302
|
+
|
|
303
|
+
@Slot(str)
|
|
304
|
+
def on_update_title(self, title: str):
|
|
305
|
+
"""Update tab title when the page title changes."""
|
|
306
|
+
if not self.output:
|
|
307
|
+
return
|
|
308
|
+
tab = self.output.tab
|
|
309
|
+
if tab is None:
|
|
310
|
+
return
|
|
311
|
+
if self.window and title and title.strip() and title != "about:blank":
|
|
312
|
+
try:
|
|
313
|
+
self.window.controller.ui.tabs.update_title_by_tab(tab, title)
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class BrowserOutput(HtmlOutput):
|
|
319
|
+
def __init__(self, window=None):
|
|
320
|
+
"""
|
|
321
|
+
HTML canvas output
|
|
322
|
+
|
|
323
|
+
:param window: main window
|
|
324
|
+
"""
|
|
325
|
+
super(BrowserOutput, self).__init__(window)
|
|
326
|
+
self.window = window
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# --- Address bar input widget ---
|
|
330
|
+
class AddressLineEdit(QLineEdit):
|
|
331
|
+
"""
|
|
332
|
+
Custom QLineEdit that ensures Enter triggers opening the typed URL
|
|
333
|
+
and prevents default buttons (e.g., Reload) from consuming the key.
|
|
334
|
+
"""
|
|
335
|
+
def __init__(self, on_return_callback=None, *args, **kwargs):
|
|
336
|
+
super().__init__(*args, **kwargs)
|
|
337
|
+
self._on_return_callback = on_return_callback
|
|
338
|
+
|
|
339
|
+
def keyPressEvent(self, event):
|
|
340
|
+
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
|
341
|
+
# Call the assigned handler and stop propagation so default buttons won't trigger.
|
|
342
|
+
if callable(self._on_return_callback):
|
|
343
|
+
self._on_return_callback()
|
|
344
|
+
event.accept()
|
|
345
|
+
return
|
|
346
|
+
super().keyPressEvent(event)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class ToolSignals(QObject):
|
|
350
|
+
url = Signal(str) # url
|
|
351
|
+
closed = Signal() # dialog closed
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.09.22 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
|
|
14
|
-
from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent
|
|
14
|
+
from PySide6.QtCore import Qt, QObject, Signal, Slot, QEvent, QCoreApplication, QEventLoop, QUrl
|
|
15
15
|
from PySide6.QtWebChannel import QWebChannel
|
|
16
16
|
from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEnginePage
|
|
17
17
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
@@ -42,34 +42,131 @@ class HtmlOutput(QWebEngineView):
|
|
|
42
42
|
self.meta = None
|
|
43
43
|
self.tab = None
|
|
44
44
|
self.setMouseTracking(True)
|
|
45
|
+
|
|
46
|
+
# OpenGL widgets
|
|
47
|
+
self._glwidget = None
|
|
48
|
+
self._glwidget_filter_installed = False
|
|
49
|
+
self._unloaded = False
|
|
50
|
+
self._destroyed = False
|
|
51
|
+
|
|
45
52
|
self.installEventFilter(self)
|
|
46
53
|
|
|
54
|
+
def _detach_gl_event_filter(self):
|
|
55
|
+
"""Detach OpenGL widget event filter if installed"""
|
|
56
|
+
if self._glwidget and self._glwidget_filter_installed:
|
|
57
|
+
try:
|
|
58
|
+
self._glwidget.removeEventFilter(self)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self._on_delete_failed(e)
|
|
61
|
+
self._glwidget = None
|
|
62
|
+
self._glwidget_filter_installed = False
|
|
63
|
+
|
|
64
|
+
def _on_delete_failed(self, e):
|
|
65
|
+
"""
|
|
66
|
+
Handle delete failure
|
|
67
|
+
|
|
68
|
+
:param e: Exception instance
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
self.window.core.debug.log(e)
|
|
72
|
+
|
|
73
|
+
def unload(self):
|
|
74
|
+
"""Unload the current page and free resources"""
|
|
75
|
+
try:
|
|
76
|
+
self.hide()
|
|
77
|
+
p = self.page()
|
|
78
|
+
p.triggerAction(QWebEnginePage.Stop)
|
|
79
|
+
p.setUrl(QUrl("about:blank"))
|
|
80
|
+
p.history().clear()
|
|
81
|
+
p.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self._on_delete_failed(e)
|
|
84
|
+
finally:
|
|
85
|
+
self._unloaded = True
|
|
86
|
+
|
|
47
87
|
def on_delete(self):
|
|
48
88
|
"""Clean up on delete"""
|
|
89
|
+
if self._destroyed:
|
|
90
|
+
return
|
|
91
|
+
if not self._unloaded:
|
|
92
|
+
self.unload()
|
|
93
|
+
|
|
94
|
+
self.hide()
|
|
95
|
+
self._detach_gl_event_filter()
|
|
96
|
+
|
|
49
97
|
if self.finder:
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
try:
|
|
99
|
+
self.finder.disconnect()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self._on_delete_failed(e)
|
|
102
|
+
finally:
|
|
103
|
+
self.finder = None
|
|
52
104
|
|
|
53
|
-
self.tab = None
|
|
105
|
+
self.tab = None
|
|
106
|
+
|
|
107
|
+
# disconnect signals
|
|
108
|
+
try:
|
|
109
|
+
self.loadFinished.disconnect(self.on_page_loaded)
|
|
110
|
+
self.customContextMenuRequested.disconnect(self.on_context_menu)
|
|
111
|
+
self.signals.save_as.disconnect()
|
|
112
|
+
self.signals.audio_read.disconnect()
|
|
113
|
+
except Exception as e:
|
|
114
|
+
self._on_delete_failed(e)
|
|
54
115
|
|
|
55
|
-
# delete page
|
|
56
116
|
page = self.page()
|
|
57
|
-
if page:
|
|
58
|
-
if hasattr(page, 'bridge'):
|
|
59
|
-
page.bridge.deleteLater()
|
|
60
|
-
if hasattr(page, 'channel'):
|
|
61
|
-
page.channel.deleteLater()
|
|
62
|
-
if hasattr(page, 'signals') and page.signals:
|
|
63
|
-
page.signals.deleteLater()
|
|
64
|
-
page.deleteLater() # delete page
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
try:
|
|
119
|
+
page.triggerAction(QWebEnginePage.Stop)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
self._on_delete_failed(e)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
page.setUrl(QUrl("about:blank"))
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self._on_delete_failed(e)
|
|
71
127
|
|
|
72
|
-
|
|
128
|
+
try:
|
|
129
|
+
page.history().clear()
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self._on_delete_failed(e)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
page.setLifecycleState(QWebEnginePage.LifecycleState.Discarded)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self._on_delete_failed(e)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
if hasattr(page, "setWebChannel"):
|
|
140
|
+
page.setWebChannel(None)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self._on_delete_failed(e)
|
|
143
|
+
|
|
144
|
+
prof = None
|
|
145
|
+
try:
|
|
146
|
+
prof = page.profile()
|
|
147
|
+
except Exception as e:
|
|
148
|
+
self._on_delete_failed(e)
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
if prof is not None:
|
|
152
|
+
try:
|
|
153
|
+
prof.deleteLater()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self._on_delete_failed(e)
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
self.deleteLater()
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self._on_delete_failed(e)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
QCoreApplication.sendPostedEvents(None, QEvent.DeferredDelete)
|
|
165
|
+
QCoreApplication.processEvents(QEventLoop.AllEvents, 50)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self._on_delete_failed(e)
|
|
168
|
+
|
|
169
|
+
self._destroyed = True
|
|
73
170
|
|
|
74
171
|
def set_tab(self, tab):
|
|
75
172
|
"""
|
|
@@ -251,11 +348,15 @@ class HtmlOutput(QWebEngineView):
|
|
|
251
348
|
:param source: source
|
|
252
349
|
:param event: event
|
|
253
350
|
"""
|
|
254
|
-
if
|
|
255
|
-
|
|
256
|
-
event.child().isWidgetType()):
|
|
351
|
+
if event.type() == QEvent.ChildAdded and source is self and event.child().isWidgetType():
|
|
352
|
+
self._detach_gl_event_filter()
|
|
257
353
|
self._glwidget = event.child()
|
|
258
|
-
|
|
354
|
+
try:
|
|
355
|
+
self._glwidget.installEventFilter(self)
|
|
356
|
+
self._glwidget_filter_installed = True
|
|
357
|
+
except Exception:
|
|
358
|
+
self._glwidget = None
|
|
359
|
+
self._glwidget_filter_installed = False
|
|
259
360
|
elif (event.type() == event.Type.MouseButtonPress):
|
|
260
361
|
if self.tab:
|
|
261
362
|
col_idx = self.tab.column_idx
|
|
@@ -288,6 +389,8 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
288
389
|
if self.window.core.config.has("zoom"):
|
|
289
390
|
self.setZoomFactor(self.window.core.config.get("zoom"))
|
|
290
391
|
|
|
392
|
+
self.loaded = False
|
|
393
|
+
|
|
291
394
|
# bridge Python <> JavaScript
|
|
292
395
|
self.bridge = Bridge(self.window)
|
|
293
396
|
self.channel = QWebChannel(self)
|
|
@@ -306,6 +409,9 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
306
409
|
self.parent.finder.matches = num
|
|
307
410
|
self.parent.finder.on_find_finished()
|
|
308
411
|
|
|
412
|
+
def set_loaded(self, loaded: bool = True):
|
|
413
|
+
self.loaded = loaded
|
|
414
|
+
|
|
309
415
|
def on_view_changed(self):
|
|
310
416
|
"""On view changed"""
|
|
311
417
|
zoom = self.zoomFactor()
|
|
@@ -346,6 +452,37 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
346
452
|
"""
|
|
347
453
|
self.signals.js_message.emit(line_number, message, source_id) # handled in debug controller
|
|
348
454
|
|
|
455
|
+
def cleanup(self):
|
|
456
|
+
"""Cleanup method to release resources"""
|
|
457
|
+
self.loaded = False
|
|
458
|
+
try:
|
|
459
|
+
self.findTextFinished.disconnect()
|
|
460
|
+
self.zoomFactorChanged.disconnect()
|
|
461
|
+
self.selectionChanged.disconnect()
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
if self.bridge:
|
|
466
|
+
try:
|
|
467
|
+
self.bridge.cleanup()
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
self.bridge = None
|
|
471
|
+
|
|
472
|
+
if self.channel:
|
|
473
|
+
try:
|
|
474
|
+
self.channel.unregisterObject("bridge")
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
self.channel = None
|
|
478
|
+
|
|
479
|
+
if self.signals:
|
|
480
|
+
try:
|
|
481
|
+
self.signals.deleteLater()
|
|
482
|
+
except Exception:
|
|
483
|
+
pass
|
|
484
|
+
self.signals = None
|
|
485
|
+
|
|
349
486
|
|
|
350
487
|
class Bridge(QObject):
|
|
351
488
|
"""Bridge between Python and JavaScript"""
|
|
@@ -380,6 +517,17 @@ class Bridge(QObject):
|
|
|
380
517
|
"""
|
|
381
518
|
self.window.controller.chat.render.scroll = pos
|
|
382
519
|
|
|
520
|
+
def cleanup(self):
|
|
521
|
+
"""Cleanup method to release resources"""
|
|
522
|
+
if self.window:
|
|
523
|
+
try:
|
|
524
|
+
self.window = None
|
|
525
|
+
except Exception:
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
# delete the bridge object
|
|
529
|
+
self.deleteLater()
|
|
530
|
+
|
|
383
531
|
|
|
384
532
|
class WebEngineSignals(QObject):
|
|
385
533
|
save_as = Signal(str, str)
|
|
@@ -531,7 +531,7 @@ class CustomWebEnginePage(QWebEnginePage):
|
|
|
531
531
|
return super().acceptNavigationRequest(url, _type, isMainFrame)
|
|
532
532
|
|
|
533
533
|
def javaScriptConsoleMessage(self, level, message, line_number, source_id):
|
|
534
|
-
print("[JS CONSOLE] Line", line_number, ":", message)
|
|
534
|
+
# print("[JS CONSOLE] Line", line_number, ":", message)
|
|
535
535
|
self.signals.js_message.emit(line_number, message, source_id) # handled in debug controller
|
|
536
536
|
|
|
537
537
|
def cleanup(self):
|