@dusted/anqst 1.7.2 → 1.7.3
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.
- package/AnQstWebBase/AnQstWebBaseAbi.cmake +1 -0
- package/AnQstWebBase/CMakeLists.txt +116 -0
- package/AnQstWebBase/CMakeUserPresets.json +14 -0
- package/AnQstWebBase/README.md +65 -0
- package/AnQstWebBase/src/AnQstBase93.cpp +91 -0
- package/AnQstWebBase/src/AnQstBase93.h +15 -0
- package/AnQstWebBase/src/AnQstBridgeProxy.cpp +30 -0
- package/AnQstWebBase/src/AnQstBridgeProxy.h +41 -0
- package/AnQstWebBase/src/AnQstHostBridgeFacade.cpp +345 -0
- package/AnQstWebBase/src/AnQstHostBridgeFacade.h +99 -0
- package/AnQstWebBase/src/AnQstWebBaseAbi.h +4 -0
- package/AnQstWebBase/src/AnQstWebBaseAbi.h.in +4 -0
- package/AnQstWebBase/src/AnQstWebHostBase.cpp +1822 -0
- package/AnQstWebBase/src/AnQstWebHostBase.h +227 -0
- package/AnQstWebBase/src/AnQstWidgetDebugDialog.cpp +425 -0
- package/AnQstWebBase/src/AnQstWidgetDebugDialog.h +105 -0
- package/AnQstWebBase/src/AngularHttpBaseServer.cpp +965 -0
- package/AnQstWebBase/src/AngularHttpBaseServer.h +97 -0
- package/AnQstWebBase/src/UI/AnQstWidgetDebugDialog.ui +235 -0
- package/AnQstWebBase/tests/CMakeLists.txt +22 -0
- package/AnQstWebBase/tests/test_AnQstWebHostBase.cpp +1102 -0
- package/dist/src/abi-hash-stamp.js +5 -0
- package/dist/src/abi-hash.js +33 -0
- package/dist/src/app.js +67 -19
- package/dist/src/boundary-codec-render.js +17 -10
- package/dist/src/emit.js +76 -30
- package/dist/src/project.js +11 -1
- package/dist/src/webbase.js +94 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1822 @@
|
|
|
1
|
+
#include "AnQstWebHostBase.h"
|
|
2
|
+
|
|
3
|
+
#include "AnQstBridgeProxy.h"
|
|
4
|
+
#include "AnQstHostBridgeFacade.h"
|
|
5
|
+
#include "AnQstWidgetDebugDialog.h"
|
|
6
|
+
#include "AngularHttpBaseServer.h"
|
|
7
|
+
|
|
8
|
+
#include <QAuthenticator>
|
|
9
|
+
#include <QDesktopServices>
|
|
10
|
+
#include <QContextMenuEvent>
|
|
11
|
+
#include <QDir>
|
|
12
|
+
#include <QDragEnterEvent>
|
|
13
|
+
#include <QDragLeaveEvent>
|
|
14
|
+
#include <QDragMoveEvent>
|
|
15
|
+
#include <QDropEvent>
|
|
16
|
+
#include <QEvent>
|
|
17
|
+
#include <QFile>
|
|
18
|
+
#include <QFileInfo>
|
|
19
|
+
#include <QJsonArray>
|
|
20
|
+
#include <QJsonDocument>
|
|
21
|
+
#include <QKeySequence>
|
|
22
|
+
#include <QLabel>
|
|
23
|
+
#include <QDebug>
|
|
24
|
+
#include <QMimeData>
|
|
25
|
+
#include <QPushButton>
|
|
26
|
+
#include <QProcessEnvironment>
|
|
27
|
+
#include <QShortcut>
|
|
28
|
+
#include <QStringList>
|
|
29
|
+
#include <QTimer>
|
|
30
|
+
#include <QVBoxLayout>
|
|
31
|
+
#include <QWebChannel>
|
|
32
|
+
#include <QWebEngineCertificateError>
|
|
33
|
+
#include <QWebEnginePage>
|
|
34
|
+
#include <QWebEngineScript>
|
|
35
|
+
#include <QWebEngineScriptCollection>
|
|
36
|
+
#include <QWebEngineView>
|
|
37
|
+
|
|
38
|
+
namespace ANQST_WEBBASE_NAMESPACE {
|
|
39
|
+
|
|
40
|
+
namespace {
|
|
41
|
+
static QString boolToString(bool value) {
|
|
42
|
+
return value ? QStringLiteral("true") : QStringLiteral("false");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static void appendDetailValue(QStringList& lines, const QString& label, const QString& value) {
|
|
46
|
+
if (!value.trimmed().isEmpty()) {
|
|
47
|
+
lines.append(label + value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static void appendDetailBlock(QStringList& lines, const QString& label, const QString& value) {
|
|
52
|
+
if (value.trimmed().isEmpty()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
lines.append(label);
|
|
56
|
+
lines.append(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static QString joinDetailLines(const QStringList& lines) {
|
|
60
|
+
return lines.join(QStringLiteral("\n"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static QString normalizeQrcRoot(const QString& root) {
|
|
64
|
+
QString normalized = root.trimmed();
|
|
65
|
+
if (normalized.startsWith(QStringLiteral(":/"))) {
|
|
66
|
+
normalized = QStringLiteral("qrc") + normalized;
|
|
67
|
+
}
|
|
68
|
+
if (!normalized.startsWith(QStringLiteral("qrc:/"))) {
|
|
69
|
+
return QString();
|
|
70
|
+
}
|
|
71
|
+
if (normalized.endsWith('/')) {
|
|
72
|
+
normalized.chop(1);
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static void disableWebEngineSandboxForTrustedHost() {
|
|
78
|
+
// The hosted page is considered trusted at application level.
|
|
79
|
+
qputenv("QTWEBENGINE_DISABLE_SANDBOX", QByteArrayLiteral("1"));
|
|
80
|
+
|
|
81
|
+
QByteArray flags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS");
|
|
82
|
+
if (!flags.contains("--no-sandbox")) {
|
|
83
|
+
if (!flags.trimmed().isEmpty()) {
|
|
84
|
+
flags.append(' ');
|
|
85
|
+
}
|
|
86
|
+
flags.append("--no-sandbox");
|
|
87
|
+
}
|
|
88
|
+
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", flags);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static bool shouldEmitJavaScriptConsoleLevel(QWebEnginePage::JavaScriptConsoleMessageLevel level) {
|
|
92
|
+
return level == QWebEnginePage::WarningMessageLevel ||
|
|
93
|
+
level == QWebEnginePage::ErrorMessageLevel;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static QString javaScriptConsoleLevelToString(QWebEnginePage::JavaScriptConsoleMessageLevel level) {
|
|
97
|
+
switch (level) {
|
|
98
|
+
case QWebEnginePage::InfoMessageLevel:
|
|
99
|
+
return QStringLiteral("info");
|
|
100
|
+
case QWebEnginePage::WarningMessageLevel:
|
|
101
|
+
return QStringLiteral("warning");
|
|
102
|
+
case QWebEnginePage::ErrorMessageLevel:
|
|
103
|
+
return QStringLiteral("error");
|
|
104
|
+
}
|
|
105
|
+
return QStringLiteral("unknown");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static QString renderProcessTerminationStatusToString(QWebEnginePage::RenderProcessTerminationStatus status) {
|
|
109
|
+
switch (status) {
|
|
110
|
+
case QWebEnginePage::NormalTerminationStatus:
|
|
111
|
+
return QStringLiteral("normal");
|
|
112
|
+
case QWebEnginePage::AbnormalTerminationStatus:
|
|
113
|
+
return QStringLiteral("abnormal");
|
|
114
|
+
case QWebEnginePage::CrashedTerminationStatus:
|
|
115
|
+
return QStringLiteral("crashed");
|
|
116
|
+
case QWebEnginePage::KilledTerminationStatus:
|
|
117
|
+
return QStringLiteral("killed");
|
|
118
|
+
}
|
|
119
|
+
return QStringLiteral("unknown");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static QString structuredJavaScriptChannel(const QString& message) {
|
|
123
|
+
if (message.startsWith(QStringLiteral("[AnQst][resource.error]"))) {
|
|
124
|
+
return QStringLiteral("webengine.resource_load_error");
|
|
125
|
+
}
|
|
126
|
+
if (message.startsWith(QStringLiteral("[AnQst][window.error]"))) {
|
|
127
|
+
return QStringLiteral("js.window.error");
|
|
128
|
+
}
|
|
129
|
+
if (message.startsWith(QStringLiteral("[AnQst][unhandledrejection]"))) {
|
|
130
|
+
return QStringLiteral("js.unhandledrejection");
|
|
131
|
+
}
|
|
132
|
+
return QString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static QString normalizedJavaScriptConsoleMessage(const QString& message) {
|
|
136
|
+
const int newlineIndex = message.indexOf(QLatin1Char('\n'));
|
|
137
|
+
if (message.startsWith(QStringLiteral("[AnQst][")) && newlineIndex >= 0) {
|
|
138
|
+
return message.mid(newlineIndex + 1);
|
|
139
|
+
}
|
|
140
|
+
if (message.startsWith(QStringLiteral("[AnQst]["))) {
|
|
141
|
+
return QString();
|
|
142
|
+
}
|
|
143
|
+
return message;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static QString javaScriptConsoleChannel(
|
|
147
|
+
QWebEnginePage::JavaScriptConsoleMessageLevel level,
|
|
148
|
+
const QString& message) {
|
|
149
|
+
const QString structuredChannel = structuredJavaScriptChannel(message);
|
|
150
|
+
if (!structuredChannel.isEmpty()) {
|
|
151
|
+
return structuredChannel;
|
|
152
|
+
}
|
|
153
|
+
switch (level) {
|
|
154
|
+
case QWebEnginePage::WarningMessageLevel:
|
|
155
|
+
return QStringLiteral("js.console.warning");
|
|
156
|
+
case QWebEnginePage::ErrorMessageLevel:
|
|
157
|
+
return QStringLiteral("js.console.error");
|
|
158
|
+
case QWebEnginePage::InfoMessageLevel:
|
|
159
|
+
return QStringLiteral("js.console.info");
|
|
160
|
+
}
|
|
161
|
+
return QStringLiteral("js.console.unknown");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static QString formatJavaScriptConsoleDetail(
|
|
165
|
+
QWebEnginePage::JavaScriptConsoleMessageLevel level,
|
|
166
|
+
const QString& message,
|
|
167
|
+
int lineNumber,
|
|
168
|
+
const QString& sourceId) {
|
|
169
|
+
QStringList lines;
|
|
170
|
+
lines.append(QStringLiteral("JavaScript console %1.").arg(javaScriptConsoleLevelToString(level)));
|
|
171
|
+
appendDetailValue(lines, QStringLiteral("Source: "), sourceId);
|
|
172
|
+
if (lineNumber > 0) {
|
|
173
|
+
lines.append(QStringLiteral("Line: %1").arg(lineNumber));
|
|
174
|
+
}
|
|
175
|
+
appendDetailBlock(lines, QStringLiteral("Message:"), normalizedJavaScriptConsoleMessage(message));
|
|
176
|
+
return joinDetailLines(lines);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static QString formatJavaScriptConsoleLogLine(
|
|
180
|
+
QWebEnginePage::JavaScriptConsoleMessageLevel level,
|
|
181
|
+
const QString& message,
|
|
182
|
+
int lineNumber,
|
|
183
|
+
const QString& sourceId) {
|
|
184
|
+
QStringList parts;
|
|
185
|
+
parts.append(QStringLiteral("[%1]").arg(javaScriptConsoleLevelToString(level)));
|
|
186
|
+
if (!sourceId.trimmed().isEmpty() && lineNumber > 0) {
|
|
187
|
+
parts.append(QStringLiteral("%1:%2").arg(sourceId).arg(lineNumber));
|
|
188
|
+
} else if (!sourceId.trimmed().isEmpty()) {
|
|
189
|
+
parts.append(sourceId);
|
|
190
|
+
} else if (lineNumber > 0) {
|
|
191
|
+
parts.append(QStringLiteral("line: %1").arg(lineNumber));
|
|
192
|
+
}
|
|
193
|
+
const QString messageText = normalizedJavaScriptConsoleMessage(message).trimmed();
|
|
194
|
+
if (!messageText.isEmpty()) {
|
|
195
|
+
parts.append(messageText);
|
|
196
|
+
}
|
|
197
|
+
return parts.join(QStringLiteral(" "));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static QString formatCertificateErrorDetail(const QWebEngineCertificateError& certificateError) {
|
|
201
|
+
QStringList lines;
|
|
202
|
+
lines.append(QStringLiteral("TLS certificate error while loading a request."));
|
|
203
|
+
appendDetailValue(lines, QStringLiteral("URL: "), certificateError.url().toString());
|
|
204
|
+
appendDetailValue(lines, QStringLiteral("Description: "), certificateError.errorDescription());
|
|
205
|
+
lines.append(QStringLiteral("Error code: %1").arg(static_cast<int>(certificateError.error())));
|
|
206
|
+
lines.append(QStringLiteral("Overridable: %1").arg(boolToString(certificateError.isOverridable())));
|
|
207
|
+
lines.append(QStringLiteral("Deferred: %1").arg(boolToString(certificateError.deferred())));
|
|
208
|
+
return joinDetailLines(lines);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class LocalOnlyWebPage final : public QWebEnginePage {
|
|
212
|
+
public:
|
|
213
|
+
explicit LocalOnlyWebPage(QObject* parent = nullptr)
|
|
214
|
+
: QWebEnginePage(parent) {}
|
|
215
|
+
|
|
216
|
+
protected:
|
|
217
|
+
bool acceptNavigationRequest(const QUrl& url, NavigationType type, bool isMainFrame) override {
|
|
218
|
+
Q_UNUSED(type);
|
|
219
|
+
Q_UNUSED(isMainFrame);
|
|
220
|
+
const QObject* host = parent();
|
|
221
|
+
const bool blockRemoteNavigation = host != nullptr
|
|
222
|
+
? host->property("anqstBlockRemoteNavigation").toBool()
|
|
223
|
+
: true;
|
|
224
|
+
const QString scheme = url.scheme().toLower();
|
|
225
|
+
if (blockRemoteNavigation &&
|
|
226
|
+
(scheme == QStringLiteral("http") || scheme == QStringLiteral("https") ||
|
|
227
|
+
scheme == QStringLiteral("ws") || scheme == QStringLiteral("wss"))) {
|
|
228
|
+
if (parent() != nullptr) {
|
|
229
|
+
QMetaObject::invokeMethod(parent(), "handleNavigationPolicyError", Q_ARG(QUrl, url));
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return QWebEnginePage::acceptNavigationRequest(url, type, isMainFrame);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void javaScriptConsoleMessage(
|
|
237
|
+
JavaScriptConsoleMessageLevel level,
|
|
238
|
+
const QString& message,
|
|
239
|
+
int lineNumber,
|
|
240
|
+
const QString& sourceID) override {
|
|
241
|
+
if (parent() != nullptr) {
|
|
242
|
+
QMetaObject::invokeMethod(
|
|
243
|
+
parent(),
|
|
244
|
+
"handleJavaScriptConsoleLine",
|
|
245
|
+
Q_ARG(QString, formatJavaScriptConsoleLogLine(level, message, lineNumber, sourceID)));
|
|
246
|
+
}
|
|
247
|
+
if (shouldEmitJavaScriptConsoleLevel(level) && parent() != nullptr) {
|
|
248
|
+
QMetaObject::invokeMethod(
|
|
249
|
+
parent(),
|
|
250
|
+
"handleWebEngineDiagnostic",
|
|
251
|
+
Q_ARG(QString, javaScriptConsoleChannel(level, message)),
|
|
252
|
+
Q_ARG(QString, formatJavaScriptConsoleDetail(level, message, lineNumber, sourceID)));
|
|
253
|
+
}
|
|
254
|
+
QWebEnginePage::javaScriptConsoleMessage(level, message, lineNumber, sourceID);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
bool certificateError(const QWebEngineCertificateError& certificateError) override {
|
|
258
|
+
if (parent() != nullptr) {
|
|
259
|
+
QMetaObject::invokeMethod(
|
|
260
|
+
parent(),
|
|
261
|
+
"handleWebEngineDiagnostic",
|
|
262
|
+
Q_ARG(QString, QStringLiteral("webengine.certificate_error")),
|
|
263
|
+
Q_ARG(QString, formatCertificateErrorDetail(certificateError)));
|
|
264
|
+
}
|
|
265
|
+
return QWebEnginePage::certificateError(certificateError);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
} // namespace
|
|
270
|
+
|
|
271
|
+
class LocalWebView final : public QWebEngineView {
|
|
272
|
+
public:
|
|
273
|
+
explicit LocalWebView(QWidget* parent = nullptr)
|
|
274
|
+
: QWebEngineView(parent) {}
|
|
275
|
+
|
|
276
|
+
void setContextMenuEnabled(bool enabled) { m_contextMenuEnabled = enabled; }
|
|
277
|
+
|
|
278
|
+
protected:
|
|
279
|
+
void contextMenuEvent(QContextMenuEvent* event) override {
|
|
280
|
+
if (m_contextMenuEnabled) {
|
|
281
|
+
QWebEngineView::contextMenuEvent(event);
|
|
282
|
+
} else {
|
|
283
|
+
event->accept();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private:
|
|
288
|
+
bool m_contextMenuEnabled = true;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
AnQstWebHostBase::AnQstWebHostBase(QWidget* parent)
|
|
292
|
+
: QWidget(parent)
|
|
293
|
+
, m_view(new LocalWebView(this))
|
|
294
|
+
, m_devPlaceholder(new QLabel(this))
|
|
295
|
+
, m_reattachButton(new QPushButton(QStringLiteral("Reattach"), this))
|
|
296
|
+
, m_webChannel(new QWebChannel(this))
|
|
297
|
+
, m_bridgeFacade(new AnQstHostBridgeFacade(this))
|
|
298
|
+
, m_bridgeProxy(new AnQstBridgeProxy(m_bridgeFacade, this))
|
|
299
|
+
, m_devServer(new AngularHttpBaseServer(this))
|
|
300
|
+
, m_contentRootMode(ContentRootMode::Unset)
|
|
301
|
+
, m_bridgeObject(nullptr)
|
|
302
|
+
, m_bridgeAttached(false)
|
|
303
|
+
, m_contentRootSet(false)
|
|
304
|
+
, m_entryPointLoaded(false)
|
|
305
|
+
, m_bridgeBootstrapInstalled(false)
|
|
306
|
+
, m_developmentModeEnabled(false)
|
|
307
|
+
, m_developmentModeAllowLan(false)
|
|
308
|
+
, m_textSelectionEnabled(false)
|
|
309
|
+
, m_scrollbarsEnabled(false)
|
|
310
|
+
, m_debugState()
|
|
311
|
+
, m_remoteNavigationBlocked(true)
|
|
312
|
+
, m_activeDebugDialog(nullptr)
|
|
313
|
+
, m_hoverThrottleTimer(new QTimer(this))
|
|
314
|
+
, m_dragDropFilterInstalled(false)
|
|
315
|
+
{
|
|
316
|
+
disableWebEngineSandboxForTrustedHost();
|
|
317
|
+
setProperty("anqstBlockRemoteNavigation", true);
|
|
318
|
+
|
|
319
|
+
auto* layout = new QVBoxLayout(this);
|
|
320
|
+
layout->setContentsMargins(0, 0, 0, 0);
|
|
321
|
+
layout->addWidget(m_view);
|
|
322
|
+
layout->addWidget(m_devPlaceholder);
|
|
323
|
+
layout->addWidget(m_reattachButton);
|
|
324
|
+
setLayout(layout);
|
|
325
|
+
|
|
326
|
+
m_devPlaceholder->setVisible(false);
|
|
327
|
+
m_devPlaceholder->setStyleSheet(QStringLiteral("background-color: #2e7d32; color: #ffffff; font-weight: 600; padding: 12px;"));
|
|
328
|
+
m_devPlaceholder->setObjectName(QStringLiteral("AnQstDevModePlaceholder"));
|
|
329
|
+
m_devPlaceholder->setWordWrap(true);
|
|
330
|
+
m_devPlaceholder->setAlignment(Qt::AlignCenter);
|
|
331
|
+
m_devPlaceholder->setTextFormat(Qt::RichText);
|
|
332
|
+
m_devPlaceholder->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
|
333
|
+
m_devPlaceholder->setOpenExternalLinks(true);
|
|
334
|
+
m_reattachButton->setVisible(false);
|
|
335
|
+
m_reattachButton->setObjectName(QStringLiteral("AnQstDevModeReattachButton"));
|
|
336
|
+
connect(m_reattachButton, &QPushButton::clicked, this, &AnQstWebHostBase::handleReattachRequested);
|
|
337
|
+
|
|
338
|
+
m_view->setPage(new LocalOnlyWebPage(this));
|
|
339
|
+
auto* page = m_view->page();
|
|
340
|
+
page->setWebChannel(m_webChannel);
|
|
341
|
+
setRemoteNavigationBlocked(true);
|
|
342
|
+
installBridgeBootstrapScript();
|
|
343
|
+
applyTextSelectionPolicy();
|
|
344
|
+
applyScrollbarPolicy();
|
|
345
|
+
m_debugState.provider = AnQstWidgetResourceProvider::Qrc;
|
|
346
|
+
m_debugState.host = AnQstAngularAppHost::Application;
|
|
347
|
+
m_debugState.resourceUrl = QStringLiteral("http://localhost:4200/");
|
|
348
|
+
m_debugState.resourceDir = QDir::currentPath();
|
|
349
|
+
applyDebugBorderHint();
|
|
350
|
+
|
|
351
|
+
connect(m_view, &QWebEngineView::loadFinished, this, &AnQstWebHostBase::handleLoadFinished);
|
|
352
|
+
connect(m_view, &QWebEngineView::renderProcessTerminated, this,
|
|
353
|
+
[this, page](QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode) {
|
|
354
|
+
QStringList lines;
|
|
355
|
+
lines.append(QStringLiteral("WebEngine render process terminated."));
|
|
356
|
+
lines.append(QStringLiteral("Status: %1").arg(renderProcessTerminationStatusToString(terminationStatus)));
|
|
357
|
+
lines.append(QStringLiteral("Exit code: %1").arg(exitCode));
|
|
358
|
+
appendDetailValue(lines, QStringLiteral("Requested URL: "), page->requestedUrl().toString());
|
|
359
|
+
appendDetailValue(lines, QStringLiteral("Current URL: "), m_view->url().toString());
|
|
360
|
+
emitWebEngineError(QStringLiteral("webengine.render_process_terminated"), joinDetailLines(lines));
|
|
361
|
+
});
|
|
362
|
+
connect(page, &QWebEnginePage::authenticationRequired, this,
|
|
363
|
+
[this](const QUrl& requestUrl, QAuthenticator* authenticator) {
|
|
364
|
+
QStringList lines;
|
|
365
|
+
lines.append(QStringLiteral("WebEngine request requires HTTP authentication."));
|
|
366
|
+
appendDetailValue(lines, QStringLiteral("URL: "), requestUrl.toString());
|
|
367
|
+
if (authenticator != nullptr) {
|
|
368
|
+
appendDetailValue(lines, QStringLiteral("Realm: "), authenticator->realm());
|
|
369
|
+
}
|
|
370
|
+
emitWebEngineError(QStringLiteral("webengine.authentication_required"), joinDetailLines(lines));
|
|
371
|
+
});
|
|
372
|
+
connect(page, &QWebEnginePage::proxyAuthenticationRequired, this,
|
|
373
|
+
[this](const QUrl& requestUrl, QAuthenticator* authenticator, const QString& proxyHost) {
|
|
374
|
+
QStringList lines;
|
|
375
|
+
lines.append(QStringLiteral("WebEngine request requires proxy authentication."));
|
|
376
|
+
appendDetailValue(lines, QStringLiteral("URL: "), requestUrl.toString());
|
|
377
|
+
appendDetailValue(lines, QStringLiteral("Proxy host: "), proxyHost);
|
|
378
|
+
if (authenticator != nullptr) {
|
|
379
|
+
appendDetailValue(lines, QStringLiteral("Realm: "), authenticator->realm());
|
|
380
|
+
}
|
|
381
|
+
emitWebEngineError(QStringLiteral("webengine.proxy_authentication_required"), joinDetailLines(lines));
|
|
382
|
+
});
|
|
383
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeOutputUpdated, this, &AnQstWebHostBase::anQstBridge_outputUpdated);
|
|
384
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeSlotInvocationRequested, this, &AnQstWebHostBase::anQstBridge_slotInvocationRequested);
|
|
385
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeOutputUpdated, m_bridgeProxy, &AnQstBridgeProxy::anQstBridge_outputUpdated);
|
|
386
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeSlotInvocationRequested, m_bridgeProxy, &AnQstBridgeProxy::anQstBridge_slotInvocationRequested);
|
|
387
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeHostError, this, &AnQstWebHostBase::onHostError);
|
|
388
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeHostError, m_bridgeProxy, &AnQstBridgeProxy::anQstBridge_hostDiagnostic);
|
|
389
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::slotInvocationResolved, this, &AnQstWebHostBase::slotInvocationResolved);
|
|
390
|
+
|
|
391
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeDropReceived, this, &AnQstWebHostBase::anQstBridge_dropReceived);
|
|
392
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeDropReceived, m_bridgeProxy, &AnQstBridgeProxy::anQstBridge_dropReceived);
|
|
393
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeHoverUpdated, this, &AnQstWebHostBase::anQstBridge_hoverUpdated);
|
|
394
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeHoverUpdated, m_bridgeProxy, &AnQstBridgeProxy::anQstBridge_hoverUpdated);
|
|
395
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeHoverLeft, this, &AnQstWebHostBase::anQstBridge_hoverLeft);
|
|
396
|
+
connect(m_bridgeFacade, &AnQstHostBridgeFacade::bridgeHoverLeft, m_bridgeProxy, &AnQstBridgeProxy::anQstBridge_hoverLeft);
|
|
397
|
+
|
|
398
|
+
m_hoverThrottleTimer->setSingleShot(true);
|
|
399
|
+
connect(m_hoverThrottleTimer, &QTimer::timeout, this, &AnQstWebHostBase::dispatchHoverThrottle);
|
|
400
|
+
|
|
401
|
+
auto* debugShortcut = new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_F12), this);
|
|
402
|
+
debugShortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
|
403
|
+
connect(debugShortcut, &QShortcut::activated, this, &AnQstWebHostBase::handleDebugShortcut);
|
|
404
|
+
|
|
405
|
+
m_devServer->setFacade(m_bridgeFacade);
|
|
406
|
+
connect(m_devServer, &AngularHttpBaseServer::serverError, this, [this](const QVariantMap& payload) {
|
|
407
|
+
emitHostError(
|
|
408
|
+
payload.value(QStringLiteral("code")).toString(),
|
|
409
|
+
QStringLiteral("bridge"),
|
|
410
|
+
QStringLiteral("error"),
|
|
411
|
+
true,
|
|
412
|
+
payload.value(QStringLiteral("message")).toString(),
|
|
413
|
+
payload.value(QStringLiteral("context")).toMap());
|
|
414
|
+
});
|
|
415
|
+
connect(m_devServer, &AngularHttpBaseServer::clientAttached, this, [this](const QString& peer) {
|
|
416
|
+
emitHostError(
|
|
417
|
+
QStringLiteral("HOST_DEV_CLIENT_ATTACHED"),
|
|
418
|
+
QStringLiteral("bridge"),
|
|
419
|
+
QStringLiteral("info"),
|
|
420
|
+
true,
|
|
421
|
+
QStringLiteral("Development browser client attached."),
|
|
422
|
+
{
|
|
423
|
+
{QStringLiteral("peer"), peer},
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
connect(m_devServer, &AngularHttpBaseServer::clientDetached, this, [this]() {
|
|
427
|
+
emitHostError(
|
|
428
|
+
QStringLiteral("HOST_DEV_CLIENT_DETACHED"),
|
|
429
|
+
QStringLiteral("bridge"),
|
|
430
|
+
QStringLiteral("info"),
|
|
431
|
+
true,
|
|
432
|
+
QStringLiteral("Development browser client detached."));
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
bool AnQstWebHostBase::installBridgeBootstrapScript(const QString& scriptSource, bool forceReinstall) {
|
|
437
|
+
if (m_bridgeBootstrapInstalled && !forceReinstall) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
QString source = scriptSource;
|
|
442
|
+
if (source.isNull()) {
|
|
443
|
+
source = loadDefaultBridgeBootstrapScript();
|
|
444
|
+
}
|
|
445
|
+
if (source.trimmed().isEmpty()) {
|
|
446
|
+
emitHostError(
|
|
447
|
+
QStringLiteral("HOST_BRIDGE_BOOTSTRAP_UNAVAILABLE"),
|
|
448
|
+
QStringLiteral("bridge"),
|
|
449
|
+
QStringLiteral("error"),
|
|
450
|
+
false,
|
|
451
|
+
QStringLiteral("Failed to load qwebchannel bootstrap script."));
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
QWebEngineScript bootstrapScript;
|
|
456
|
+
bootstrapScript.setName(QStringLiteral("AnQstBridgeBootstrap"));
|
|
457
|
+
bootstrapScript.setInjectionPoint(QWebEngineScript::DocumentCreation);
|
|
458
|
+
bootstrapScript.setWorldId(QWebEngineScript::MainWorld);
|
|
459
|
+
bootstrapScript.setRunsOnSubFrames(false);
|
|
460
|
+
bootstrapScript.setSourceCode(source);
|
|
461
|
+
m_view->page()->scripts().insert(bootstrapScript);
|
|
462
|
+
m_bridgeBootstrapInstalled = true;
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
bool AnQstWebHostBase::setContentRoot(const QString& rootPath) {
|
|
467
|
+
if (m_contentRootSet) {
|
|
468
|
+
qWarning("AnQstWebHostBase: setContentRoot() can only be called once. Ignoring recall.");
|
|
469
|
+
emitHostError(
|
|
470
|
+
QStringLiteral("HOST_CONTENT_ROOT_RECALL_IGNORED"),
|
|
471
|
+
QStringLiteral("lifecycle"),
|
|
472
|
+
QStringLiteral("warn"),
|
|
473
|
+
true,
|
|
474
|
+
QStringLiteral("setContentRoot() recall ignored."),
|
|
475
|
+
{ { QStringLiteral("providedRoot"), rootPath } });
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (rootPath.trimmed().isEmpty()) {
|
|
480
|
+
emitHostError(
|
|
481
|
+
QStringLiteral("HOST_CONTENT_ROOT_INVALID"),
|
|
482
|
+
QStringLiteral("load"),
|
|
483
|
+
QStringLiteral("error"),
|
|
484
|
+
false,
|
|
485
|
+
QStringLiteral("Content root cannot be empty."));
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const QString normalizedQrcRoot = normalizeQrcRoot(rootPath);
|
|
490
|
+
if (!normalizedQrcRoot.isEmpty()) {
|
|
491
|
+
m_contentRootMode = ContentRootMode::Qrc;
|
|
492
|
+
m_contentRoot = normalizedQrcRoot;
|
|
493
|
+
} else {
|
|
494
|
+
const QFileInfo rootInfo(rootPath);
|
|
495
|
+
if (!rootInfo.exists() || !rootInfo.isDir()) {
|
|
496
|
+
emitHostError(
|
|
497
|
+
QStringLiteral("HOST_CONTENT_ROOT_NOT_FOUND"),
|
|
498
|
+
QStringLiteral("load"),
|
|
499
|
+
QStringLiteral("error"),
|
|
500
|
+
false,
|
|
501
|
+
QStringLiteral("Filesystem content root is missing or not a directory."),
|
|
502
|
+
{ { QStringLiteral("contentRoot"), rootPath } });
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
m_contentRootMode = ContentRootMode::Filesystem;
|
|
506
|
+
m_contentRoot = rootInfo.absoluteFilePath();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
m_contentRootSet = true;
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
bool AnQstWebHostBase::loadEntryPoint(const QString& entryPoint) {
|
|
514
|
+
if (!m_contentRootSet) {
|
|
515
|
+
emitHostError(
|
|
516
|
+
QStringLiteral("HOST_CONTENT_ROOT_UNSET"),
|
|
517
|
+
QStringLiteral("lifecycle"),
|
|
518
|
+
QStringLiteral("error"),
|
|
519
|
+
false,
|
|
520
|
+
QStringLiteral("loadEntryPoint() requires setContentRoot() first."));
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const QUrl targetUrl = resolveAssetPath(entryPoint);
|
|
525
|
+
if (!targetUrl.isValid()) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if ((m_contentRootMode == ContentRootMode::Filesystem && !QFileInfo::exists(targetUrl.toLocalFile())) ||
|
|
530
|
+
(m_contentRootMode == ContentRootMode::Qrc && !QFileInfo::exists(QStringLiteral(":") + targetUrl.path()))) {
|
|
531
|
+
emitHostError(
|
|
532
|
+
QStringLiteral("HOST_LOAD_ENTRY_NOT_FOUND"),
|
|
533
|
+
QStringLiteral("load"),
|
|
534
|
+
QStringLiteral("error"),
|
|
535
|
+
false,
|
|
536
|
+
QStringLiteral("Entry point was not found."),
|
|
537
|
+
{ { QStringLiteral("entryPoint"), entryPoint }, { QStringLiteral("resolvedUrl"), targetUrl.toString() } });
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
m_entryPoint = entryPoint;
|
|
542
|
+
m_entryPointLoaded = false;
|
|
543
|
+
emitOutputSnapshotIfReady();
|
|
544
|
+
if (!m_developmentModeEnabled) {
|
|
545
|
+
m_view->setUrl(targetUrl);
|
|
546
|
+
} else {
|
|
547
|
+
m_entryPointLoaded = true;
|
|
548
|
+
emitOutputSnapshotIfReady();
|
|
549
|
+
}
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
bool AnQstWebHostBase::setBridgeObject(QObject* bridgeObject, const QString& objectName) {
|
|
554
|
+
if (m_bridgeAttached) {
|
|
555
|
+
qWarning("AnQstWebHostBase: setBridgeObject() can only be called once. Ignoring recall.");
|
|
556
|
+
emitHostError(
|
|
557
|
+
QStringLiteral("HOST_BRIDGE_RECALL_IGNORED"),
|
|
558
|
+
QStringLiteral("lifecycle"),
|
|
559
|
+
QStringLiteral("warn"),
|
|
560
|
+
true,
|
|
561
|
+
QStringLiteral("setBridgeObject() recall ignored."),
|
|
562
|
+
{ { QStringLiteral("objectName"), objectName } });
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (bridgeObject == nullptr || objectName.trimmed().isEmpty()) {
|
|
567
|
+
emitHostError(
|
|
568
|
+
QStringLiteral("HOST_BRIDGE_SETUP_FAILED"),
|
|
569
|
+
QStringLiteral("bridge"),
|
|
570
|
+
QStringLiteral("error"),
|
|
571
|
+
false,
|
|
572
|
+
QStringLiteral("Bridge object and object name must be valid."));
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
m_bridgeObject = bridgeObject;
|
|
577
|
+
m_bridgeObjectName = objectName;
|
|
578
|
+
m_webChannel->registerObject(m_bridgeObjectName, m_bridgeProxy);
|
|
579
|
+
m_devServer->setBridgeObjectName(m_bridgeObjectName);
|
|
580
|
+
m_bridgeAttached = true;
|
|
581
|
+
|
|
582
|
+
if (shouldEmitReady()) {
|
|
583
|
+
emit onHostReady();
|
|
584
|
+
emitOutputSnapshotIfReady();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
QUrl AnQstWebHostBase::resolveAssetPath(const QString& relativePath) const {
|
|
591
|
+
if (!m_contentRootSet) {
|
|
592
|
+
const_cast<AnQstWebHostBase*>(this)->emitHostError(
|
|
593
|
+
QStringLiteral("HOST_CONTENT_ROOT_UNSET"),
|
|
594
|
+
QStringLiteral("lifecycle"),
|
|
595
|
+
QStringLiteral("error"),
|
|
596
|
+
false,
|
|
597
|
+
QStringLiteral("resolveAssetPath() requires setContentRoot() first."));
|
|
598
|
+
return QUrl();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const QUrl provided(relativePath);
|
|
602
|
+
if (provided.isValid() && !provided.scheme().isEmpty()) {
|
|
603
|
+
if (isBlockedScheme(provided)) {
|
|
604
|
+
const_cast<AnQstWebHostBase*>(this)->emitHostError(
|
|
605
|
+
QStringLiteral("HOST_POLICY_SCHEME_BLOCKED"),
|
|
606
|
+
QStringLiteral("policy"),
|
|
607
|
+
QStringLiteral("error"),
|
|
608
|
+
true,
|
|
609
|
+
QStringLiteral("Blocked disallowed URL scheme."),
|
|
610
|
+
{ { QStringLiteral("url"), provided.toString() } });
|
|
611
|
+
return QUrl();
|
|
612
|
+
}
|
|
613
|
+
if (provided.isLocalFile() || provided.scheme() == QStringLiteral("qrc")) {
|
|
614
|
+
return provided;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (m_contentRootMode == ContentRootMode::Filesystem) {
|
|
619
|
+
QDir rootDir(m_contentRoot);
|
|
620
|
+
const QString cleanedPath = QDir::cleanPath(rootDir.absoluteFilePath(relativePath));
|
|
621
|
+
return QUrl::fromLocalFile(cleanedPath);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const QString joined = QDir::cleanPath(m_contentRoot + QStringLiteral("/") + relativePath);
|
|
625
|
+
return QUrl(joined);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
void AnQstWebHostBase::setCallHandler(const CallHandler& handler) {
|
|
629
|
+
m_bridgeFacade->setCallHandler(handler);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
void AnQstWebHostBase::setEmitterHandler(const EmitterHandler& handler) {
|
|
633
|
+
m_bridgeFacade->setEmitterHandler(handler);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
void AnQstWebHostBase::setInputHandler(const InputHandler& handler) {
|
|
637
|
+
m_bridgeFacade->setInputHandler(handler);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
void AnQstWebHostBase::setOutputValue(const QString& service, const QString& member, const QVariant& value) {
|
|
641
|
+
m_bridgeFacade->setOutputValue(service, member, value);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
bool AnQstWebHostBase::invokeSlot(const QString& service, const QString& member, const QVariantList& args, QVariant* result, QString* error) {
|
|
645
|
+
return m_bridgeFacade->invokeSlot(service, member, args, result, error);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
void AnQstWebHostBase::setSlotInvocationTimeoutMs(int timeoutMs) {
|
|
649
|
+
m_bridgeFacade->setSlotInvocationTimeoutMs(timeoutMs);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
int AnQstWebHostBase::slotInvocationTimeoutMs() const {
|
|
653
|
+
return m_bridgeFacade->slotInvocationTimeoutMs();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
QString AnQstWebHostBase::contentRoot() const {
|
|
657
|
+
return m_contentRoot;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
AnQstWebHostBase::ContentRootMode AnQstWebHostBase::contentRootMode() const {
|
|
661
|
+
return m_contentRootMode;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
bool AnQstWebHostBase::isBridgeSet() const {
|
|
665
|
+
return m_bridgeAttached;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
void AnQstWebHostBase::anQstBridge_registerSlot(const QString& service, const QString& member) {
|
|
669
|
+
m_bridgeFacade->registerSlot(service, member);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
QVariant AnQstWebHostBase::anQstBridge_call(const QString& service, const QString& member, const QVariantList& args) {
|
|
673
|
+
return m_bridgeFacade->call(service, member, args);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
void AnQstWebHostBase::anQstBridge_emit(const QString& service, const QString& member, const QVariantList& args) {
|
|
677
|
+
m_bridgeFacade->emitMessage(service, member, args);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
void AnQstWebHostBase::anQstBridge_setInput(const QString& service, const QString& member, const QVariant& value) {
|
|
681
|
+
m_bridgeFacade->setInput(service, member, value);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
void AnQstWebHostBase::anQstBridge_resolveSlot(const QString& requestId, bool ok, const QVariant& payload, const QString& error) {
|
|
685
|
+
m_bridgeFacade->resolveSlot(requestId, ok, payload, error);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
void AnQstWebHostBase::handleLoadFinished(bool ok) {
|
|
689
|
+
if (!ok) {
|
|
690
|
+
QStringList lines;
|
|
691
|
+
lines.append(QStringLiteral("Host failed to load entry point."));
|
|
692
|
+
appendDetailValue(lines, QStringLiteral("Entry point: "), m_entryPoint);
|
|
693
|
+
appendDetailValue(lines, QStringLiteral("Requested URL: "), m_view->page()->requestedUrl().toString());
|
|
694
|
+
appendDetailValue(lines, QStringLiteral("Current URL: "), m_view->url().toString());
|
|
695
|
+
emitWebEngineError(QStringLiteral("webengine.load_failed"), joinDetailLines(lines));
|
|
696
|
+
emitHostError(
|
|
697
|
+
QStringLiteral("HOST_LOAD_FAILED"),
|
|
698
|
+
QStringLiteral("load"),
|
|
699
|
+
QStringLiteral("error"),
|
|
700
|
+
false,
|
|
701
|
+
QStringLiteral("Host failed to load entry point."),
|
|
702
|
+
{ { QStringLiteral("entryPoint"), m_entryPoint } });
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
m_entryPointLoaded = true;
|
|
707
|
+
installDragDropEventFilter();
|
|
708
|
+
|
|
709
|
+
if (shouldEmitReady()) {
|
|
710
|
+
emit onHostReady();
|
|
711
|
+
emitOutputSnapshotIfReady();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
void AnQstWebHostBase::handleNavigationPolicyError(const QUrl& blockedUrl) {
|
|
716
|
+
QStringList lines;
|
|
717
|
+
lines.append(QStringLiteral("Navigation blocked by local-content policy."));
|
|
718
|
+
appendDetailValue(lines, QStringLiteral("URL: "), blockedUrl.toString());
|
|
719
|
+
emitWebEngineError(QStringLiteral("webengine.navigation_blocked"), joinDetailLines(lines));
|
|
720
|
+
emitHostError(
|
|
721
|
+
QStringLiteral("HOST_POLICY_SCHEME_BLOCKED"),
|
|
722
|
+
QStringLiteral("policy"),
|
|
723
|
+
QStringLiteral("error"),
|
|
724
|
+
true,
|
|
725
|
+
QStringLiteral("Navigation blocked by local-content policy."),
|
|
726
|
+
{ { QStringLiteral("url"), blockedUrl.toString() } });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
void AnQstWebHostBase::handleNetworkPolicyError(const QUrl& blockedUrl) {
|
|
730
|
+
QStringList lines;
|
|
731
|
+
lines.append(QStringLiteral("Network resource blocked by local-content policy."));
|
|
732
|
+
appendDetailValue(lines, QStringLiteral("URL: "), blockedUrl.toString());
|
|
733
|
+
emitWebEngineError(QStringLiteral("webengine.resource_blocked"), joinDetailLines(lines));
|
|
734
|
+
emitHostError(
|
|
735
|
+
QStringLiteral("HOST_POLICY_SCHEME_BLOCKED"),
|
|
736
|
+
QStringLiteral("policy"),
|
|
737
|
+
QStringLiteral("error"),
|
|
738
|
+
true,
|
|
739
|
+
QStringLiteral("Network resource blocked by local-content policy."),
|
|
740
|
+
{ { QStringLiteral("url"), blockedUrl.toString() } });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
void AnQstWebHostBase::handleWebEngineDiagnostic(const QString& channel, const QString& detail) {
|
|
744
|
+
emitWebEngineError(channel, detail);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
void AnQstWebHostBase::emitHostError(
|
|
748
|
+
const QString& code,
|
|
749
|
+
const QString& category,
|
|
750
|
+
const QString& severity,
|
|
751
|
+
bool recoverable,
|
|
752
|
+
const QString& message,
|
|
753
|
+
const QVariantMap& context) {
|
|
754
|
+
QVariantMap payload;
|
|
755
|
+
payload.insert(QStringLiteral("code"), code);
|
|
756
|
+
payload.insert(QStringLiteral("category"), category);
|
|
757
|
+
payload.insert(QStringLiteral("severity"), severity);
|
|
758
|
+
payload.insert(QStringLiteral("recoverable"), recoverable);
|
|
759
|
+
payload.insert(QStringLiteral("message"), message);
|
|
760
|
+
payload.insert(QStringLiteral("context"), context);
|
|
761
|
+
payload.insert(QStringLiteral("timestamp"), QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs));
|
|
762
|
+
emit onHostError(payload);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
void AnQstWebHostBase::emitWebEngineError(const QString& channel, const QString& detail) {
|
|
766
|
+
emit onWebEngineError(channel, detail);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
bool AnQstWebHostBase::isBlockedScheme(const QUrl& url) const {
|
|
770
|
+
const QString scheme = url.scheme().toLower();
|
|
771
|
+
return scheme == QStringLiteral("http") ||
|
|
772
|
+
scheme == QStringLiteral("https") ||
|
|
773
|
+
scheme == QStringLiteral("ws") ||
|
|
774
|
+
scheme == QStringLiteral("wss");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
bool AnQstWebHostBase::isContentRootSet() const {
|
|
778
|
+
return m_contentRootSet;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
bool AnQstWebHostBase::isEntryPointLoaded() const {
|
|
782
|
+
return m_entryPointLoaded;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
bool AnQstWebHostBase::shouldEmitReady() const {
|
|
786
|
+
return isContentRootSet() && isEntryPointLoaded() && m_bridgeAttached;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
void AnQstWebHostBase::emitOutputSnapshotIfReady() {
|
|
790
|
+
const bool wasDispatchEnabled = m_bridgeFacade->dispatchEnabled();
|
|
791
|
+
m_bridgeFacade->setDispatchEnabled(shouldEmitReady());
|
|
792
|
+
if (shouldEmitReady() && wasDispatchEnabled) {
|
|
793
|
+
m_bridgeFacade->emitOutputSnapshot();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
QString AnQstWebHostBase::loadDefaultBridgeBootstrapScript() const {
|
|
798
|
+
QFile scriptFile(QStringLiteral(":/qtwebchannel/qwebchannel.js"));
|
|
799
|
+
if (!scriptFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
800
|
+
return QString();
|
|
801
|
+
}
|
|
802
|
+
QString script = QString::fromUtf8(scriptFile.readAll());
|
|
803
|
+
script.append(QStringLiteral(R"JS(
|
|
804
|
+
;(() => {
|
|
805
|
+
const anyWindow = window;
|
|
806
|
+
const describeValue = (value) => {
|
|
807
|
+
if (value === undefined) {
|
|
808
|
+
return "undefined";
|
|
809
|
+
}
|
|
810
|
+
if (value === null) {
|
|
811
|
+
return "null";
|
|
812
|
+
}
|
|
813
|
+
if (value instanceof Error) {
|
|
814
|
+
if (typeof value.message === "string" && value.message.length > 0) {
|
|
815
|
+
return value.message;
|
|
816
|
+
}
|
|
817
|
+
return String(value);
|
|
818
|
+
}
|
|
819
|
+
if (typeof value === "string") {
|
|
820
|
+
return value;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const encoded = JSON.stringify(value);
|
|
824
|
+
if (typeof encoded === "string" && encoded.length > 0) {
|
|
825
|
+
return encoded;
|
|
826
|
+
}
|
|
827
|
+
} catch (_jsonError) {
|
|
828
|
+
}
|
|
829
|
+
return String(value);
|
|
830
|
+
};
|
|
831
|
+
const emitStructuredConsoleError = (channel, parts) => {
|
|
832
|
+
const lines = [`[AnQst][${channel}]`];
|
|
833
|
+
for (const part of parts) {
|
|
834
|
+
if (typeof part === "string" && part.length > 0) {
|
|
835
|
+
lines.push(part);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
console.error(lines.join("\n"));
|
|
839
|
+
};
|
|
840
|
+
if (!anyWindow.__anqstRawErrorHooksInstalled) {
|
|
841
|
+
anyWindow.addEventListener(
|
|
842
|
+
"error",
|
|
843
|
+
(event) => {
|
|
844
|
+
const target = event?.target;
|
|
845
|
+
const source = event?.filename || target?.currentSrc || target?.src || target?.href || "";
|
|
846
|
+
const line = Number.isFinite(event?.lineno) ? String(event.lineno) : "";
|
|
847
|
+
const column = Number.isFinite(event?.colno) ? String(event.colno) : "";
|
|
848
|
+
const error = event?.error;
|
|
849
|
+
const stack = error && typeof error.stack === "string" ? error.stack : "";
|
|
850
|
+
const tagName = typeof target?.tagName === "string" ? target.tagName.toLowerCase() : "";
|
|
851
|
+
const parts = [];
|
|
852
|
+
if (target && target !== anyWindow && !error) {
|
|
853
|
+
parts.push("Resource load failure.");
|
|
854
|
+
if (tagName.length > 0) {
|
|
855
|
+
parts.push(`Element: <${tagName}>`);
|
|
856
|
+
}
|
|
857
|
+
if (source.length > 0) {
|
|
858
|
+
parts.push(`Source: ${source}`);
|
|
859
|
+
}
|
|
860
|
+
emitStructuredConsoleError("resource.error", parts);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
parts.push("Unhandled window error.");
|
|
864
|
+
if (typeof event?.message === "string" && event.message.length > 0) {
|
|
865
|
+
parts.push(`Message: ${event.message}`);
|
|
866
|
+
}
|
|
867
|
+
if (source.length > 0) {
|
|
868
|
+
parts.push(`Source: ${source}`);
|
|
869
|
+
}
|
|
870
|
+
if (line.length > 0) {
|
|
871
|
+
parts.push(`Line: ${line}`);
|
|
872
|
+
}
|
|
873
|
+
if (column.length > 0) {
|
|
874
|
+
parts.push(`Column: ${column}`);
|
|
875
|
+
}
|
|
876
|
+
if (stack.length > 0) {
|
|
877
|
+
parts.push("Stack:");
|
|
878
|
+
parts.push(stack);
|
|
879
|
+
}
|
|
880
|
+
emitStructuredConsoleError("window.error", parts);
|
|
881
|
+
},
|
|
882
|
+
true
|
|
883
|
+
);
|
|
884
|
+
anyWindow.addEventListener("unhandledrejection", (event) => {
|
|
885
|
+
const reason = event?.reason;
|
|
886
|
+
const stack = reason && typeof reason.stack === "string" ? reason.stack : "";
|
|
887
|
+
const parts = [
|
|
888
|
+
"Unhandled promise rejection.",
|
|
889
|
+
`Reason: ${describeValue(reason)}`
|
|
890
|
+
];
|
|
891
|
+
if (stack.length > 0) {
|
|
892
|
+
parts.push("Stack:");
|
|
893
|
+
parts.push(stack);
|
|
894
|
+
}
|
|
895
|
+
emitStructuredConsoleError("unhandledrejection", parts);
|
|
896
|
+
});
|
|
897
|
+
anyWindow.__anqstRawErrorHooksInstalled = true;
|
|
898
|
+
}
|
|
899
|
+
const transport = anyWindow?.qt?.webChannelTransport;
|
|
900
|
+
if (!transport || typeof transport !== "object") {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (transport.__anqstMessageGuardInstalled) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
let wrapped = null;
|
|
907
|
+
Object.defineProperty(transport, "onmessage", {
|
|
908
|
+
configurable: true,
|
|
909
|
+
enumerable: true,
|
|
910
|
+
get() {
|
|
911
|
+
return wrapped;
|
|
912
|
+
},
|
|
913
|
+
set(fn) {
|
|
914
|
+
if (typeof fn !== "function") {
|
|
915
|
+
wrapped = fn;
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
wrapped = function guardedQtWebChannelMessage(message) {
|
|
919
|
+
if (message === undefined || message === null || message.data === undefined) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
return fn.call(this, message);
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
transport.__anqstMessageGuardInstalled = true;
|
|
927
|
+
})();
|
|
928
|
+
)JS"));
|
|
929
|
+
return script;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
void AnQstWebHostBase::setContextMenuEnabled(bool enabled) {
|
|
933
|
+
m_view->setContextMenuEnabled(enabled);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
void AnQstWebHostBase::setTextSelectionEnabled(bool enabled) {
|
|
937
|
+
if (m_textSelectionEnabled == enabled) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
m_textSelectionEnabled = enabled;
|
|
941
|
+
applyTextSelectionPolicy();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
void AnQstWebHostBase::setScrollbarsEnabled(bool enabled) {
|
|
945
|
+
if (m_scrollbarsEnabled == enabled) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
m_scrollbarsEnabled = enabled;
|
|
949
|
+
applyScrollbarPolicy();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
void AnQstWebHostBase::applyTextSelectionPolicy() {
|
|
953
|
+
static const QString kScriptName = QStringLiteral("AnQstDisableTextSelection");
|
|
954
|
+
static const QString kDisableJs = QStringLiteral(
|
|
955
|
+
"(function(){"
|
|
956
|
+
"if(!document.getElementById('anqst-no-select')){"
|
|
957
|
+
"var s=document.createElement('style');"
|
|
958
|
+
"s.id='anqst-no-select';"
|
|
959
|
+
"s.textContent='*{user-select:none!important;-webkit-user-select:none!important}';"
|
|
960
|
+
"document.head.appendChild(s);"
|
|
961
|
+
"}"
|
|
962
|
+
"})();"
|
|
963
|
+
);
|
|
964
|
+
static const QString kEnableJs = QStringLiteral(
|
|
965
|
+
"(function(){var el=document.getElementById('anqst-no-select');if(el)el.parentNode.removeChild(el);})();"
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
auto& scripts = m_view->page()->scripts();
|
|
969
|
+
const QWebEngineScript existing = scripts.findScript(kScriptName);
|
|
970
|
+
if (!existing.isNull()) {
|
|
971
|
+
scripts.remove(existing);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!m_textSelectionEnabled) {
|
|
975
|
+
QWebEngineScript script;
|
|
976
|
+
script.setName(kScriptName);
|
|
977
|
+
script.setInjectionPoint(QWebEngineScript::DocumentReady);
|
|
978
|
+
script.setWorldId(QWebEngineScript::MainWorld);
|
|
979
|
+
script.setRunsOnSubFrames(false);
|
|
980
|
+
script.setSourceCode(kDisableJs);
|
|
981
|
+
scripts.insert(script);
|
|
982
|
+
m_view->page()->runJavaScript(kDisableJs);
|
|
983
|
+
} else {
|
|
984
|
+
m_view->page()->runJavaScript(kEnableJs);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
void AnQstWebHostBase::applyScrollbarPolicy() {
|
|
989
|
+
static const QString kScriptName = QStringLiteral("AnQstDisableScrollbars");
|
|
990
|
+
static const QString kDisableJs = QStringLiteral(
|
|
991
|
+
"(function(){"
|
|
992
|
+
"if(!document.getElementById('anqst-no-scrollbars')){"
|
|
993
|
+
"var s=document.createElement('style');"
|
|
994
|
+
"s.id='anqst-no-scrollbars';"
|
|
995
|
+
"s.textContent='html,body{overflow:hidden!important;}::-webkit-scrollbar{width:0!important;height:0!important;}';"
|
|
996
|
+
"document.head.appendChild(s);"
|
|
997
|
+
"}"
|
|
998
|
+
"})();"
|
|
999
|
+
);
|
|
1000
|
+
static const QString kEnableJs = QStringLiteral(
|
|
1001
|
+
"(function(){var el=document.getElementById('anqst-no-scrollbars');if(el)el.parentNode.removeChild(el);})();"
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
auto& scripts = m_view->page()->scripts();
|
|
1005
|
+
const QWebEngineScript existing = scripts.findScript(kScriptName);
|
|
1006
|
+
if (!existing.isNull()) {
|
|
1007
|
+
scripts.remove(existing);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!m_scrollbarsEnabled) {
|
|
1011
|
+
QWebEngineScript script;
|
|
1012
|
+
script.setName(kScriptName);
|
|
1013
|
+
script.setInjectionPoint(QWebEngineScript::DocumentReady);
|
|
1014
|
+
script.setWorldId(QWebEngineScript::MainWorld);
|
|
1015
|
+
script.setRunsOnSubFrames(false);
|
|
1016
|
+
script.setSourceCode(kDisableJs);
|
|
1017
|
+
scripts.insert(script);
|
|
1018
|
+
m_view->page()->runJavaScript(kDisableJs);
|
|
1019
|
+
} else {
|
|
1020
|
+
m_view->page()->runJavaScript(kEnableJs);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
void AnQstWebHostBase::setRemoteNavigationBlocked(bool blocked) {
|
|
1025
|
+
m_remoteNavigationBlocked = blocked;
|
|
1026
|
+
setProperty("anqstBlockRemoteNavigation", blocked);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
bool AnQstWebHostBase::remoteNavigationBlocked() const {
|
|
1030
|
+
return m_remoteNavigationBlocked;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
void AnQstWebHostBase::handleJavaScriptConsoleLine(const QString& line) {
|
|
1034
|
+
appendJsConsoleLine(line);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
void AnQstWebHostBase::executeDebugJavaScript(const QString& source) {
|
|
1038
|
+
if (source.isEmpty()) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
appendJsConsoleCommandHistoryEntry(source);
|
|
1042
|
+
appendJsConsoleLine(QStringLiteral("> %1").arg(source));
|
|
1043
|
+
m_view->page()->runJavaScript(source);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
void AnQstWebHostBase::handleDebugShortcut() {
|
|
1047
|
+
openDebugDialogModeless(currentDebugState());
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
void AnQstWebHostBase::handleReattachRequested() {
|
|
1051
|
+
if (m_debugState.host != AnQstAngularAppHost::Browser) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
DebugDialogResult dialogResult;
|
|
1056
|
+
dialogResult.accepted = true;
|
|
1057
|
+
dialogResult.nextState = m_debugState;
|
|
1058
|
+
dialogResult.nextState.host = AnQstAngularAppHost::Application;
|
|
1059
|
+
dialogResult.openBrowser = false;
|
|
1060
|
+
applyDebugStateChange(m_debugState, dialogResult);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
AnQstWebHostBase::DebugState AnQstWebHostBase::currentDebugState() const {
|
|
1064
|
+
return m_debugState;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
AnQstWebHostBase::DebugDialogResult AnQstWebHostBase::runDebugDialog(const DebugState& initialState) {
|
|
1068
|
+
AnQstWidgetDebugDialog::InitialState dialogState;
|
|
1069
|
+
dialogState.widgetName = debugWidgetName();
|
|
1070
|
+
dialogState.hostMode = initialState.host == AnQstAngularAppHost::Application
|
|
1071
|
+
? AnQstWidgetDebugDialog::HostMode::Application
|
|
1072
|
+
: AnQstWidgetDebugDialog::HostMode::Browser;
|
|
1073
|
+
switch (initialState.provider) {
|
|
1074
|
+
case AnQstWidgetResourceProvider::Qrc:
|
|
1075
|
+
dialogState.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Qrc;
|
|
1076
|
+
break;
|
|
1077
|
+
case AnQstWidgetResourceProvider::Dir:
|
|
1078
|
+
dialogState.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Dir;
|
|
1079
|
+
break;
|
|
1080
|
+
case AnQstWidgetResourceProvider::Http:
|
|
1081
|
+
dialogState.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Http;
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
dialogState.resourceUrl = initialState.resourceUrl;
|
|
1085
|
+
dialogState.resourceDirectory = initialState.resourceDir;
|
|
1086
|
+
dialogState.jsConsoleHistory = m_jsConsoleLines;
|
|
1087
|
+
dialogState.jsConsoleCommandHistory = m_jsConsoleCommandHistory;
|
|
1088
|
+
|
|
1089
|
+
AnQstWidgetDebugDialog dialog(dialogState, this);
|
|
1090
|
+
connect(this, &AnQstWebHostBase::jsConsoleLineAppended, &dialog, &AnQstWidgetDebugDialog::appendJsConsoleLine);
|
|
1091
|
+
connect(&dialog, &AnQstWidgetDebugDialog::jsConsoleCommandSubmitted, this, &AnQstWebHostBase::executeDebugJavaScript);
|
|
1092
|
+
const int dialogCode = dialog.exec();
|
|
1093
|
+
const AnQstWidgetDebugDialog::ResultState dialogResult = dialog.resultState();
|
|
1094
|
+
|
|
1095
|
+
DebugDialogResult result;
|
|
1096
|
+
result.accepted = (dialogCode == QDialog::Accepted) && dialogResult.accepted;
|
|
1097
|
+
result.nextState.host = dialogResult.hostMode == AnQstWidgetDebugDialog::HostMode::Application
|
|
1098
|
+
? AnQstAngularAppHost::Application
|
|
1099
|
+
: AnQstAngularAppHost::Browser;
|
|
1100
|
+
switch (dialogResult.resourceProvider) {
|
|
1101
|
+
case AnQstWidgetDebugDialog::ResourceProvider::Qrc:
|
|
1102
|
+
result.nextState.provider = AnQstWidgetResourceProvider::Qrc;
|
|
1103
|
+
break;
|
|
1104
|
+
case AnQstWidgetDebugDialog::ResourceProvider::Dir:
|
|
1105
|
+
result.nextState.provider = AnQstWidgetResourceProvider::Dir;
|
|
1106
|
+
break;
|
|
1107
|
+
case AnQstWidgetDebugDialog::ResourceProvider::Http:
|
|
1108
|
+
result.nextState.provider = AnQstWidgetResourceProvider::Http;
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
result.nextState.resourceUrl = dialogResult.resourceUrl.trimmed();
|
|
1112
|
+
result.nextState.resourceDir = dialogResult.resourceDirectory.trimmed();
|
|
1113
|
+
result.openBrowser = dialogResult.openBrowserChecked;
|
|
1114
|
+
return result;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
void AnQstWebHostBase::openDebugDialogModeless(const DebugState& initialState) {
|
|
1118
|
+
if (m_activeDebugDialog != nullptr) {
|
|
1119
|
+
m_activeDebugDialog->show();
|
|
1120
|
+
m_activeDebugDialog->raise();
|
|
1121
|
+
m_activeDebugDialog->activateWindow();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
AnQstWidgetDebugDialog::InitialState dialogState;
|
|
1126
|
+
dialogState.widgetName = debugWidgetName();
|
|
1127
|
+
dialogState.hostMode = initialState.host == AnQstAngularAppHost::Application
|
|
1128
|
+
? AnQstWidgetDebugDialog::HostMode::Application
|
|
1129
|
+
: AnQstWidgetDebugDialog::HostMode::Browser;
|
|
1130
|
+
switch (initialState.provider) {
|
|
1131
|
+
case AnQstWidgetResourceProvider::Qrc:
|
|
1132
|
+
dialogState.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Qrc;
|
|
1133
|
+
break;
|
|
1134
|
+
case AnQstWidgetResourceProvider::Dir:
|
|
1135
|
+
dialogState.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Dir;
|
|
1136
|
+
break;
|
|
1137
|
+
case AnQstWidgetResourceProvider::Http:
|
|
1138
|
+
dialogState.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Http;
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
dialogState.resourceUrl = initialState.resourceUrl;
|
|
1142
|
+
dialogState.resourceDirectory = initialState.resourceDir;
|
|
1143
|
+
dialogState.jsConsoleHistory = m_jsConsoleLines;
|
|
1144
|
+
dialogState.jsConsoleCommandHistory = m_jsConsoleCommandHistory;
|
|
1145
|
+
|
|
1146
|
+
auto* dialog = new AnQstWidgetDebugDialog(dialogState, this);
|
|
1147
|
+
dialog->setAttribute(Qt::WA_DeleteOnClose, true);
|
|
1148
|
+
dialog->setModal(false);
|
|
1149
|
+
dialog->setWindowModality(Qt::NonModal);
|
|
1150
|
+
m_activeDebugDialog = dialog;
|
|
1151
|
+
|
|
1152
|
+
connect(this, &AnQstWebHostBase::jsConsoleLineAppended, dialog, &AnQstWidgetDebugDialog::appendJsConsoleLine);
|
|
1153
|
+
connect(dialog, &AnQstWidgetDebugDialog::jsConsoleCommandSubmitted, this, &AnQstWebHostBase::executeDebugJavaScript);
|
|
1154
|
+
connect(dialog, &QDialog::finished, this, [this, dialog, initialState](int dialogCode) {
|
|
1155
|
+
if (dialogCode == QDialog::Accepted) {
|
|
1156
|
+
const AnQstWidgetDebugDialog::ResultState dialogResult = dialog->resultState();
|
|
1157
|
+
DebugDialogResult result;
|
|
1158
|
+
result.accepted = dialogResult.accepted;
|
|
1159
|
+
result.nextState.host = dialogResult.hostMode == AnQstWidgetDebugDialog::HostMode::Application
|
|
1160
|
+
? AnQstAngularAppHost::Application
|
|
1161
|
+
: AnQstAngularAppHost::Browser;
|
|
1162
|
+
switch (dialogResult.resourceProvider) {
|
|
1163
|
+
case AnQstWidgetDebugDialog::ResourceProvider::Qrc:
|
|
1164
|
+
result.nextState.provider = AnQstWidgetResourceProvider::Qrc;
|
|
1165
|
+
break;
|
|
1166
|
+
case AnQstWidgetDebugDialog::ResourceProvider::Dir:
|
|
1167
|
+
result.nextState.provider = AnQstWidgetResourceProvider::Dir;
|
|
1168
|
+
break;
|
|
1169
|
+
case AnQstWidgetDebugDialog::ResourceProvider::Http:
|
|
1170
|
+
result.nextState.provider = AnQstWidgetResourceProvider::Http;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
result.nextState.resourceUrl = dialogResult.resourceUrl.trimmed();
|
|
1174
|
+
result.nextState.resourceDir = dialogResult.resourceDirectory.trimmed();
|
|
1175
|
+
result.openBrowser = dialogResult.openBrowserChecked;
|
|
1176
|
+
applyDebugStateChange(initialState, result);
|
|
1177
|
+
}
|
|
1178
|
+
if (m_activeDebugDialog == dialog) {
|
|
1179
|
+
m_activeDebugDialog = nullptr;
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
connect(dialog, &QObject::destroyed, this, [this, dialog]() {
|
|
1183
|
+
if (m_activeDebugDialog == dialog) {
|
|
1184
|
+
m_activeDebugDialog = nullptr;
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
dialog->show();
|
|
1189
|
+
dialog->raise();
|
|
1190
|
+
dialog->activateWindow();
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
bool AnQstWebHostBase::applyDebugStateChange(const DebugState& previousState, const DebugDialogResult& dialogResult) {
|
|
1194
|
+
if (!dialogResult.accepted) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
DebugState nextState = dialogResult.nextState;
|
|
1198
|
+
nextState.resourceDir = normalizedDirectoryRoot(nextState.resourceDir);
|
|
1199
|
+
if (nextState.resourceDir.isEmpty()) {
|
|
1200
|
+
nextState.resourceDir = normalizedDirectoryRoot(QDir::currentPath());
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (nextState.provider == AnQstWidgetResourceProvider::Dir) {
|
|
1204
|
+
QString normalizedDirectory;
|
|
1205
|
+
if (!ensureDirectoryProviderValid(nextState.resourceDir, &normalizedDirectory)) {
|
|
1206
|
+
emitHostError(
|
|
1207
|
+
QStringLiteral("HOST_WIDGET_DEBUG_DIR_INVALID"),
|
|
1208
|
+
QStringLiteral("debug"),
|
|
1209
|
+
QStringLiteral("error"),
|
|
1210
|
+
true,
|
|
1211
|
+
QStringLiteral("The selected resource directory is invalid or not found."),
|
|
1212
|
+
{
|
|
1213
|
+
{QStringLiteral("directory"), nextState.resourceDir},
|
|
1214
|
+
});
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
nextState.resourceDir = normalizedDirectory;
|
|
1218
|
+
}
|
|
1219
|
+
if (nextState.provider == AnQstWidgetResourceProvider::Http) {
|
|
1220
|
+
QUrl normalizedUrl;
|
|
1221
|
+
if (!ensureHttpProviderValid(nextState.resourceUrl, &normalizedUrl)) {
|
|
1222
|
+
emitHostError(
|
|
1223
|
+
QStringLiteral("HOST_WIDGET_DEBUG_URL_INVALID"),
|
|
1224
|
+
QStringLiteral("debug"),
|
|
1225
|
+
QStringLiteral("error"),
|
|
1226
|
+
true,
|
|
1227
|
+
QStringLiteral("The selected HTTP resource URL is invalid."),
|
|
1228
|
+
{
|
|
1229
|
+
{QStringLiteral("resourceUrl"), nextState.resourceUrl},
|
|
1230
|
+
});
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
nextState.resourceUrl = normalizedUrl.toString();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
bool ok = false;
|
|
1237
|
+
if (nextState.host == AnQstAngularAppHost::Application) {
|
|
1238
|
+
ok = applyApplicationHostState(previousState, nextState);
|
|
1239
|
+
} else {
|
|
1240
|
+
ok = applyBrowserHostState(previousState, nextState, dialogResult.openBrowser);
|
|
1241
|
+
}
|
|
1242
|
+
if (!ok) {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
m_debugState = nextState;
|
|
1247
|
+
m_developmentModeEnabled = (nextState.host == AnQstAngularAppHost::Browser);
|
|
1248
|
+
if (m_developmentModeEnabled) {
|
|
1249
|
+
m_developmentModeUrl = browserUrl();
|
|
1250
|
+
} else {
|
|
1251
|
+
m_developmentModeUrl.clear();
|
|
1252
|
+
}
|
|
1253
|
+
emitOutputSnapshotIfReady();
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
bool AnQstWebHostBase::applyApplicationHostState(const DebugState& previousState, const DebugState& nextState) {
|
|
1258
|
+
bool requiresServer = false;
|
|
1259
|
+
const QUrl entryUrl = resolveEntryPointForProvider(nextState, &requiresServer);
|
|
1260
|
+
if (requiresServer) {
|
|
1261
|
+
const bool mustRestartServer = !m_devServer->isRunning() ||
|
|
1262
|
+
previousState.host != AnQstAngularAppHost::Application ||
|
|
1263
|
+
previousState.provider != nextState.provider ||
|
|
1264
|
+
previousState.resourceUrl != nextState.resourceUrl;
|
|
1265
|
+
if (mustRestartServer) {
|
|
1266
|
+
if (m_devServer->isRunning()) {
|
|
1267
|
+
m_devServer->stop();
|
|
1268
|
+
}
|
|
1269
|
+
if (!configureServerForProvider(nextState)) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
if (!m_devServer->start(m_developmentModeAllowLan)) {
|
|
1273
|
+
emitHostError(
|
|
1274
|
+
QStringLiteral("HOST_WIDGET_DEBUG_SERVER_START_FAILED"),
|
|
1275
|
+
QStringLiteral("bridge"),
|
|
1276
|
+
QStringLiteral("error"),
|
|
1277
|
+
true,
|
|
1278
|
+
QStringLiteral("Failed to start local debug server for Application host."));
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const QUrl proxyUrl(m_devServer->url() + QStringLiteral("/"));
|
|
1283
|
+
if (!proxyUrl.isValid()) {
|
|
1284
|
+
emitHostError(
|
|
1285
|
+
QStringLiteral("HOST_WIDGET_DEBUG_PROXY_URL_INVALID"),
|
|
1286
|
+
QStringLiteral("debug"),
|
|
1287
|
+
QStringLiteral("error"),
|
|
1288
|
+
true,
|
|
1289
|
+
QStringLiteral("Failed to resolve local proxy URL for Application host."));
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
setRemoteNavigationBlocked(false);
|
|
1293
|
+
showEmbeddedView(proxyUrl);
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (!entryUrl.isValid()) {
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
if (m_devServer->isRunning()) {
|
|
1301
|
+
m_devServer->stop();
|
|
1302
|
+
}
|
|
1303
|
+
setRemoteNavigationBlocked(true);
|
|
1304
|
+
showEmbeddedView(entryUrl);
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
bool AnQstWebHostBase::applyBrowserHostState(const DebugState& previousState, const DebugState& nextState, bool openBrowser) {
|
|
1309
|
+
const bool mustRestartServer = !m_devServer->isRunning() ||
|
|
1310
|
+
previousState.host != AnQstAngularAppHost::Browser ||
|
|
1311
|
+
previousState.provider != nextState.provider ||
|
|
1312
|
+
previousState.resourceDir != nextState.resourceDir ||
|
|
1313
|
+
previousState.resourceUrl != nextState.resourceUrl;
|
|
1314
|
+
if (mustRestartServer) {
|
|
1315
|
+
if (m_devServer->isRunning()) {
|
|
1316
|
+
m_devServer->stop();
|
|
1317
|
+
}
|
|
1318
|
+
if (!configureServerForProvider(nextState)) {
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
if (!m_devServer->start(m_developmentModeAllowLan)) {
|
|
1322
|
+
emitHostError(
|
|
1323
|
+
QStringLiteral("HOST_WIDGET_DEBUG_SERVER_START_FAILED"),
|
|
1324
|
+
QStringLiteral("bridge"),
|
|
1325
|
+
QStringLiteral("error"),
|
|
1326
|
+
true,
|
|
1327
|
+
QStringLiteral("Failed to start local debug server for Browser host."));
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const QString url = browserUrl();
|
|
1332
|
+
if (url.isEmpty()) {
|
|
1333
|
+
emitHostError(
|
|
1334
|
+
QStringLiteral("HOST_WIDGET_DEBUG_BROWSER_URL_MISSING"),
|
|
1335
|
+
QStringLiteral("debug"),
|
|
1336
|
+
QStringLiteral("error"),
|
|
1337
|
+
true,
|
|
1338
|
+
QStringLiteral("Browser host URL is unavailable."));
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
setRemoteNavigationBlocked(true);
|
|
1342
|
+
showBrowserPlaceholder(url);
|
|
1343
|
+
m_entryPointLoaded = true;
|
|
1344
|
+
emit developmentModeEnabled(url);
|
|
1345
|
+
if (openBrowser) {
|
|
1346
|
+
openUrlInBrowser(url);
|
|
1347
|
+
}
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
bool AnQstWebHostBase::configureServerForProvider(const DebugState& nextState) {
|
|
1352
|
+
if (m_entryPoint.trimmed().isEmpty()) {
|
|
1353
|
+
emitHostError(
|
|
1354
|
+
QStringLiteral("HOST_WIDGET_DEBUG_ENTRYPOINT_MISSING"),
|
|
1355
|
+
QStringLiteral("debug"),
|
|
1356
|
+
QStringLiteral("error"),
|
|
1357
|
+
true,
|
|
1358
|
+
QStringLiteral("Entry point is missing for debug server configuration."));
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
switch (nextState.provider) {
|
|
1362
|
+
case AnQstWidgetResourceProvider::Qrc:
|
|
1363
|
+
if (!m_contentRootSet || m_contentRootMode != ContentRootMode::Qrc) {
|
|
1364
|
+
emitHostError(
|
|
1365
|
+
QStringLiteral("HOST_WIDGET_DEBUG_QRC_UNAVAILABLE"),
|
|
1366
|
+
QStringLiteral("debug"),
|
|
1367
|
+
QStringLiteral("error"),
|
|
1368
|
+
true,
|
|
1369
|
+
QStringLiteral("QRC resource provider is unavailable for this widget."));
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
return m_devServer->configureContent(AngularHttpBaseServer::ContentRootMode::Qrc, m_contentRoot, m_entryPoint);
|
|
1373
|
+
case AnQstWidgetResourceProvider::Dir: {
|
|
1374
|
+
QString normalizedRoot;
|
|
1375
|
+
if (!ensureDirectoryProviderValid(nextState.resourceDir, &normalizedRoot)) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
return m_devServer->configureContent(AngularHttpBaseServer::ContentRootMode::Filesystem, normalizedRoot, m_entryPoint);
|
|
1379
|
+
}
|
|
1380
|
+
case AnQstWidgetResourceProvider::Http: {
|
|
1381
|
+
QUrl normalizedUrl;
|
|
1382
|
+
if (!ensureHttpProviderValid(nextState.resourceUrl, &normalizedUrl)) {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
return m_devServer->configureProxyTarget(normalizedUrl, m_entryPoint);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
bool AnQstWebHostBase::ensureDirectoryProviderValid(const QString& directoryInput, QString* normalizedRoot) const {
|
|
1392
|
+
if (normalizedRoot != nullptr) {
|
|
1393
|
+
normalizedRoot->clear();
|
|
1394
|
+
}
|
|
1395
|
+
const QString normalized = normalizedDirectoryRoot(directoryInput);
|
|
1396
|
+
if (normalized.isEmpty()) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
const QFileInfo info(normalized);
|
|
1400
|
+
if (!info.exists() || !info.isDir()) {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
if (normalizedRoot != nullptr) {
|
|
1404
|
+
*normalizedRoot = normalized;
|
|
1405
|
+
}
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
bool AnQstWebHostBase::ensureHttpProviderValid(const QString& urlText, QUrl* normalizedUrl) const {
|
|
1410
|
+
if (normalizedUrl != nullptr) {
|
|
1411
|
+
normalizedUrl->clear();
|
|
1412
|
+
}
|
|
1413
|
+
const QString trimmed = urlText.trimmed();
|
|
1414
|
+
if (trimmed.isEmpty()) {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
QUrl url(trimmed);
|
|
1418
|
+
if (!url.isValid() || url.scheme().trimmed().isEmpty() || url.host().trimmed().isEmpty()) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
if (url.scheme().toLower() != QStringLiteral("http")) {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
url.setPath(QStringLiteral("/"));
|
|
1425
|
+
url.setQuery(QString());
|
|
1426
|
+
url.setFragment(QString());
|
|
1427
|
+
if (normalizedUrl != nullptr) {
|
|
1428
|
+
*normalizedUrl = url;
|
|
1429
|
+
}
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
QUrl AnQstWebHostBase::resolveEntryPointForProvider(const DebugState& state, bool* requiresServer) const {
|
|
1434
|
+
if (requiresServer != nullptr) {
|
|
1435
|
+
*requiresServer = false;
|
|
1436
|
+
}
|
|
1437
|
+
if (m_entryPoint.trimmed().isEmpty()) {
|
|
1438
|
+
const_cast<AnQstWebHostBase*>(this)->emitHostError(
|
|
1439
|
+
QStringLiteral("HOST_WIDGET_DEBUG_ENTRYPOINT_MISSING"),
|
|
1440
|
+
QStringLiteral("debug"),
|
|
1441
|
+
QStringLiteral("error"),
|
|
1442
|
+
true,
|
|
1443
|
+
QStringLiteral("Entry point must be configured before applying debug state."));
|
|
1444
|
+
return QUrl();
|
|
1445
|
+
}
|
|
1446
|
+
if (state.provider == AnQstWidgetResourceProvider::Http) {
|
|
1447
|
+
if (requiresServer != nullptr) {
|
|
1448
|
+
*requiresServer = true;
|
|
1449
|
+
}
|
|
1450
|
+
return QUrl();
|
|
1451
|
+
}
|
|
1452
|
+
if (state.provider == AnQstWidgetResourceProvider::Qrc) {
|
|
1453
|
+
return resolveAssetPath(m_entryPoint);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const QString normalizedRoot = normalizedDirectoryRoot(state.resourceDir);
|
|
1457
|
+
const QString absoluteEntry = QDir(normalizedRoot).absoluteFilePath(m_entryPoint);
|
|
1458
|
+
const QFileInfo entryInfo(absoluteEntry);
|
|
1459
|
+
if (!entryInfo.exists()) {
|
|
1460
|
+
const_cast<AnQstWebHostBase*>(this)->emitHostError(
|
|
1461
|
+
QStringLiteral("HOST_WIDGET_DEBUG_ENTRY_NOT_FOUND"),
|
|
1462
|
+
QStringLiteral("debug"),
|
|
1463
|
+
QStringLiteral("error"),
|
|
1464
|
+
true,
|
|
1465
|
+
QStringLiteral("Directory provider entry point was not found."),
|
|
1466
|
+
{
|
|
1467
|
+
{QStringLiteral("entryPoint"), m_entryPoint},
|
|
1468
|
+
{QStringLiteral("resourceDir"), normalizedRoot},
|
|
1469
|
+
});
|
|
1470
|
+
return QUrl();
|
|
1471
|
+
}
|
|
1472
|
+
return QUrl::fromLocalFile(entryInfo.absoluteFilePath());
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
void AnQstWebHostBase::showEmbeddedView(const QUrl& targetUrl) {
|
|
1476
|
+
m_devPlaceholder->setVisible(false);
|
|
1477
|
+
m_reattachButton->setVisible(false);
|
|
1478
|
+
m_view->setVisible(true);
|
|
1479
|
+
m_entryPointLoaded = false;
|
|
1480
|
+
emitOutputSnapshotIfReady();
|
|
1481
|
+
m_view->setUrl(targetUrl);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
void AnQstWebHostBase::showBrowserPlaceholder(const QString& browserUrlText) {
|
|
1485
|
+
m_view->setUrl(QUrl(QStringLiteral("about:blank")));
|
|
1486
|
+
m_view->setVisible(false);
|
|
1487
|
+
m_devPlaceholder->setVisible(true);
|
|
1488
|
+
m_reattachButton->setVisible(true);
|
|
1489
|
+
const QString escapedUrl = browserUrlText.toHtmlEscaped();
|
|
1490
|
+
m_devPlaceholder->setText(
|
|
1491
|
+
QStringLiteral("Debug Browser Host: continue at <a href=\"%1\">%1</a>").arg(escapedUrl));
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
bool AnQstWebHostBase::openUrlInBrowser(const QString& urlText) const {
|
|
1495
|
+
const QUrl url(urlText.trimmed());
|
|
1496
|
+
if (!url.isValid()) {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
return QDesktopServices::openUrl(url);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
QString AnQstWebHostBase::normalizedDirectoryRoot(const QString& directoryInput) const {
|
|
1503
|
+
const QString trimmed = directoryInput.trimmed();
|
|
1504
|
+
if (trimmed.isEmpty()) {
|
|
1505
|
+
return QString();
|
|
1506
|
+
}
|
|
1507
|
+
const QFileInfo directoryInfo(trimmed);
|
|
1508
|
+
if (directoryInfo.isAbsolute()) {
|
|
1509
|
+
return QDir::cleanPath(directoryInfo.absoluteFilePath());
|
|
1510
|
+
}
|
|
1511
|
+
return QDir::cleanPath(QDir::current().absoluteFilePath(trimmed));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
QString AnQstWebHostBase::browserUrl() const {
|
|
1515
|
+
if (!m_devServer->isRunning()) {
|
|
1516
|
+
return QString();
|
|
1517
|
+
}
|
|
1518
|
+
return m_devServer->url() + QStringLiteral("/");
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
QString AnQstWebHostBase::debugWidgetName() const {
|
|
1522
|
+
if (!objectName().trimmed().isEmpty()) {
|
|
1523
|
+
return objectName().trimmed();
|
|
1524
|
+
}
|
|
1525
|
+
return QString::fromUtf8(metaObject()->className());
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
void AnQstWebHostBase::appendJsConsoleLine(const QString& line) {
|
|
1529
|
+
if (line.isEmpty()) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
m_jsConsoleLines.append(line);
|
|
1533
|
+
if (m_jsConsoleLines.size() > 20000) {
|
|
1534
|
+
m_jsConsoleLines.removeFirst();
|
|
1535
|
+
}
|
|
1536
|
+
emit jsConsoleLineAppended(line);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
void AnQstWebHostBase::appendJsConsoleCommandHistoryEntry(const QString& source) {
|
|
1540
|
+
if (source.isEmpty()) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
m_jsConsoleCommandHistory.append(source);
|
|
1544
|
+
if (m_jsConsoleCommandHistory.size() > 20000) {
|
|
1545
|
+
m_jsConsoleCommandHistory.removeFirst();
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
void AnQstWebHostBase::applyDebugBorderHint() {
|
|
1550
|
+
const QString debugHint = QProcessEnvironment::systemEnvironment()
|
|
1551
|
+
.value(QStringLiteral("ANQST_WIDGET_DEBUG"))
|
|
1552
|
+
.trimmed()
|
|
1553
|
+
.toLower();
|
|
1554
|
+
if (debugHint == QStringLiteral("true")) {
|
|
1555
|
+
m_view->setStyleSheet(QStringLiteral("border: 1px solid #6a1b9a;"));
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
m_view->setStyleSheet(QString());
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
bool AnQstWebHostBase::enableDebug() {
|
|
1562
|
+
const DebugState previousState = currentDebugState();
|
|
1563
|
+
const DebugDialogResult dialogResult = runDebugDialog(previousState);
|
|
1564
|
+
if (!dialogResult.accepted) {
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
return applyDebugStateChange(previousState, dialogResult);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
bool AnQstWebHostBase::isDevelopmentModeEnabled() const {
|
|
1571
|
+
return m_debugState.host == AnQstAngularAppHost::Browser;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
QString AnQstWebHostBase::developmentModeUrl() const {
|
|
1575
|
+
if (m_debugState.host != AnQstAngularAppHost::Browser) {
|
|
1576
|
+
return QString();
|
|
1577
|
+
}
|
|
1578
|
+
return browserUrl();
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
void AnQstWebHostBase::setDevelopmentModeAllowLan(bool allowLan) {
|
|
1582
|
+
m_developmentModeAllowLan = allowLan;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
bool AnQstWebHostBase::developmentModeAllowLan() const {
|
|
1586
|
+
return m_developmentModeAllowLan;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
void AnQstWebHostBase::registerDropTarget(const QString& service, const QString& member, const QString& mimeType) {
|
|
1590
|
+
m_dropTargets.insert(mimeType, DragTargetBinding{service, member});
|
|
1591
|
+
installDragDropEventFilter();
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
void AnQstWebHostBase::registerHoverTarget(const QString& service, const QString& member, const QString& mimeType, int throttleIntervalMs) {
|
|
1595
|
+
m_hoverTargets.insert(mimeType, DragTargetBinding{service, member, throttleIntervalMs});
|
|
1596
|
+
installDragDropEventFilter();
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
void AnQstWebHostBase::installDragDropEventFilter() {
|
|
1600
|
+
if (m_dragDropFilterInstalled) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (m_dropTargets.isEmpty() && m_hoverTargets.isEmpty()) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (auto* fp = m_view->focusProxy()) {
|
|
1607
|
+
fp->setAcceptDrops(true);
|
|
1608
|
+
fp->installEventFilter(this);
|
|
1609
|
+
m_dragDropFilterInstalled = true;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
bool AnQstWebHostBase::matchDropMimeType(const QMimeData* mime, QString* matchedMimeType) const {
|
|
1614
|
+
for (auto it = m_dropTargets.constBegin(); it != m_dropTargets.constEnd(); ++it) {
|
|
1615
|
+
if (mime->hasFormat(it.key())) {
|
|
1616
|
+
*matchedMimeType = it.key();
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
for (auto it = m_hoverTargets.constBegin(); it != m_hoverTargets.constEnd(); ++it) {
|
|
1621
|
+
if (mime->hasFormat(it.key())) {
|
|
1622
|
+
*matchedMimeType = it.key();
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
QVariant AnQstWebHostBase::deserializeMimePayload(const QMimeData* mime, const QString& mimeType) {
|
|
1630
|
+
const QByteArray rawData = mime->data(mimeType);
|
|
1631
|
+
if (rawData.isEmpty()) {
|
|
1632
|
+
emitHostError(
|
|
1633
|
+
QStringLiteral("HOST_DRAGDROP_PAYLOAD_INVALID"),
|
|
1634
|
+
QStringLiteral("bridge"),
|
|
1635
|
+
QStringLiteral("error"),
|
|
1636
|
+
true,
|
|
1637
|
+
QStringLiteral("Drag/drop MIME payload is empty."),
|
|
1638
|
+
{
|
|
1639
|
+
{QStringLiteral("mimeType"), mimeType},
|
|
1640
|
+
});
|
|
1641
|
+
return QVariant();
|
|
1642
|
+
}
|
|
1643
|
+
const char transportTag = rawData.at(0);
|
|
1644
|
+
const QByteArray payloadBytes = rawData.mid(1);
|
|
1645
|
+
if (transportTag == 'S') {
|
|
1646
|
+
return QString::fromUtf8(rawData);
|
|
1647
|
+
}
|
|
1648
|
+
if (transportTag != 'A' && transportTag != 'O') {
|
|
1649
|
+
emitHostError(
|
|
1650
|
+
QStringLiteral("HOST_DRAGDROP_PAYLOAD_INVALID"),
|
|
1651
|
+
QStringLiteral("bridge"),
|
|
1652
|
+
QStringLiteral("error"),
|
|
1653
|
+
true,
|
|
1654
|
+
QStringLiteral("Drag/drop MIME payload has an unknown transport tag."),
|
|
1655
|
+
{
|
|
1656
|
+
{QStringLiteral("mimeType"), mimeType},
|
|
1657
|
+
{QStringLiteral("transportTag"), QString::fromLatin1(QByteArray(1, transportTag))},
|
|
1658
|
+
});
|
|
1659
|
+
return QVariant();
|
|
1660
|
+
}
|
|
1661
|
+
QJsonParseError parseError;
|
|
1662
|
+
const QJsonDocument doc = QJsonDocument::fromJson(payloadBytes, &parseError);
|
|
1663
|
+
if (parseError.error != QJsonParseError::NoError) {
|
|
1664
|
+
emitHostError(
|
|
1665
|
+
QStringLiteral("HOST_DRAGDROP_PAYLOAD_INVALID"),
|
|
1666
|
+
QStringLiteral("bridge"),
|
|
1667
|
+
QStringLiteral("error"),
|
|
1668
|
+
true,
|
|
1669
|
+
QStringLiteral("Drag/drop MIME payload is not valid JSON."),
|
|
1670
|
+
{
|
|
1671
|
+
{QStringLiteral("mimeType"), mimeType},
|
|
1672
|
+
{QStringLiteral("detail"), parseError.errorString()},
|
|
1673
|
+
});
|
|
1674
|
+
return QVariant();
|
|
1675
|
+
}
|
|
1676
|
+
if (transportTag == 'A') {
|
|
1677
|
+
if (!doc.isArray()) {
|
|
1678
|
+
emitHostError(
|
|
1679
|
+
QStringLiteral("HOST_DRAGDROP_PAYLOAD_INVALID"),
|
|
1680
|
+
QStringLiteral("bridge"),
|
|
1681
|
+
QStringLiteral("error"),
|
|
1682
|
+
true,
|
|
1683
|
+
QStringLiteral("Drag/drop MIME payload declared a JSON array carrier but did not decode as an array."),
|
|
1684
|
+
{
|
|
1685
|
+
{QStringLiteral("mimeType"), mimeType},
|
|
1686
|
+
});
|
|
1687
|
+
return QVariant();
|
|
1688
|
+
}
|
|
1689
|
+
return QString::fromUtf8(rawData);
|
|
1690
|
+
}
|
|
1691
|
+
if (transportTag == 'O') {
|
|
1692
|
+
if (!doc.isObject()) {
|
|
1693
|
+
emitHostError(
|
|
1694
|
+
QStringLiteral("HOST_DRAGDROP_PAYLOAD_INVALID"),
|
|
1695
|
+
QStringLiteral("bridge"),
|
|
1696
|
+
QStringLiteral("error"),
|
|
1697
|
+
true,
|
|
1698
|
+
QStringLiteral("Drag/drop MIME payload declared a JSON object carrier but did not decode as an object."),
|
|
1699
|
+
{
|
|
1700
|
+
{QStringLiteral("mimeType"), mimeType},
|
|
1701
|
+
});
|
|
1702
|
+
return QVariant();
|
|
1703
|
+
}
|
|
1704
|
+
return QString::fromUtf8(rawData);
|
|
1705
|
+
}
|
|
1706
|
+
return QVariant();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
void AnQstWebHostBase::dispatchHoverThrottle() {
|
|
1710
|
+
if (m_cachedHoverService.isEmpty()) {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
m_bridgeFacade->emitHover(
|
|
1714
|
+
m_cachedHoverService,
|
|
1715
|
+
m_cachedHoverMember,
|
|
1716
|
+
m_cachedHoverPayload,
|
|
1717
|
+
static_cast<double>(m_pendingHoverPos.x()),
|
|
1718
|
+
static_cast<double>(m_pendingHoverPos.y()));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
bool AnQstWebHostBase::eventFilter(QObject* obj, QEvent* event) {
|
|
1722
|
+
if (m_dropTargets.isEmpty() && m_hoverTargets.isEmpty()) {
|
|
1723
|
+
return QWidget::eventFilter(obj, event);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (event->type() == QEvent::DragEnter) {
|
|
1727
|
+
auto* de = static_cast<QDragEnterEvent*>(event);
|
|
1728
|
+
QString matchedMime;
|
|
1729
|
+
if (matchDropMimeType(de->mimeData(), &matchedMime)) {
|
|
1730
|
+
QVariant hoverPayload;
|
|
1731
|
+
if (m_hoverTargets.contains(matchedMime)) {
|
|
1732
|
+
hoverPayload = deserializeMimePayload(de->mimeData(), matchedMime);
|
|
1733
|
+
if (!hoverPayload.isValid()) {
|
|
1734
|
+
de->ignore();
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
de->acceptProposedAction();
|
|
1739
|
+
|
|
1740
|
+
if (m_hoverTargets.contains(matchedMime)) {
|
|
1741
|
+
const DragTargetBinding& binding = m_hoverTargets.value(matchedMime);
|
|
1742
|
+
m_cachedHoverPayload = hoverPayload;
|
|
1743
|
+
m_cachedHoverService = binding.service;
|
|
1744
|
+
m_cachedHoverMember = binding.member;
|
|
1745
|
+
m_pendingHoverPos = de->pos();
|
|
1746
|
+
if (binding.throttleIntervalMs > 0) {
|
|
1747
|
+
m_hoverThrottleTimer->setInterval(binding.throttleIntervalMs);
|
|
1748
|
+
}
|
|
1749
|
+
m_bridgeFacade->emitHover(
|
|
1750
|
+
binding.service, binding.member,
|
|
1751
|
+
m_cachedHoverPayload,
|
|
1752
|
+
static_cast<double>(de->pos().x()),
|
|
1753
|
+
static_cast<double>(de->pos().y()));
|
|
1754
|
+
}
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (event->type() == QEvent::DragMove) {
|
|
1760
|
+
auto* de = static_cast<QDragMoveEvent*>(event);
|
|
1761
|
+
QString matchedMime;
|
|
1762
|
+
if (matchDropMimeType(de->mimeData(), &matchedMime)) {
|
|
1763
|
+
de->acceptProposedAction();
|
|
1764
|
+
if (m_hoverTargets.contains(matchedMime)) {
|
|
1765
|
+
m_pendingHoverPos = de->pos();
|
|
1766
|
+
const DragTargetBinding& binding = m_hoverTargets.value(matchedMime);
|
|
1767
|
+
if (binding.throttleIntervalMs <= 0) {
|
|
1768
|
+
dispatchHoverThrottle();
|
|
1769
|
+
} else if (!m_hoverThrottleTimer->isActive()) {
|
|
1770
|
+
m_hoverThrottleTimer->start();
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (event->type() == QEvent::DragLeave) {
|
|
1778
|
+
m_hoverThrottleTimer->stop();
|
|
1779
|
+
if (!m_cachedHoverService.isEmpty()) {
|
|
1780
|
+
m_bridgeFacade->emitHoverLeft(m_cachedHoverService, m_cachedHoverMember);
|
|
1781
|
+
m_cachedHoverService.clear();
|
|
1782
|
+
m_cachedHoverMember.clear();
|
|
1783
|
+
m_cachedHoverPayload = QVariant();
|
|
1784
|
+
}
|
|
1785
|
+
// Always forward DragLeave to the web surface. Swallowing it breaks HTML5 drag/drop
|
|
1786
|
+
// inside the page when this filter is installed for AnQst Qt→web targets.
|
|
1787
|
+
return QWidget::eventFilter(obj, event);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (event->type() == QEvent::Drop) {
|
|
1791
|
+
auto* de = static_cast<QDropEvent*>(event);
|
|
1792
|
+
m_hoverThrottleTimer->stop();
|
|
1793
|
+
|
|
1794
|
+
if (!m_cachedHoverService.isEmpty()) {
|
|
1795
|
+
m_bridgeFacade->emitHoverLeft(m_cachedHoverService, m_cachedHoverMember);
|
|
1796
|
+
m_cachedHoverService.clear();
|
|
1797
|
+
m_cachedHoverMember.clear();
|
|
1798
|
+
m_cachedHoverPayload = QVariant();
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
QString matchedMime;
|
|
1802
|
+
if (matchDropMimeType(de->mimeData(), &matchedMime) && m_dropTargets.contains(matchedMime)) {
|
|
1803
|
+
const DragTargetBinding& binding = m_dropTargets.value(matchedMime);
|
|
1804
|
+
const QVariant payload = deserializeMimePayload(de->mimeData(), matchedMime);
|
|
1805
|
+
if (!payload.isValid()) {
|
|
1806
|
+
de->ignore();
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
m_bridgeFacade->emitDrop(
|
|
1810
|
+
binding.service, binding.member,
|
|
1811
|
+
payload,
|
|
1812
|
+
static_cast<double>(de->pos().x()),
|
|
1813
|
+
static_cast<double>(de->pos().y()));
|
|
1814
|
+
de->acceptProposedAction();
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
return QWidget::eventFilter(obj, event);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
} // namespace ANQST_WEBBASE_NAMESPACE
|