Warning, /network/angelfish/lib/contents/ui/WebView.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-FileCopyrightText: 2014-2015 Sebastian Kügler <sebas@kde.org>
0002 //
0003 // SPDX-License-Identifier: GPL-2.0-or-later
0004
0005 import QtQuick 2.15
0006 import QtQuick.Controls 2.15 as QQC2
0007 import QtQuick.Layouts 1.15
0008 import QtWebEngine
0009
0010 import org.kde.kirigami 2.19 as Kirigami
0011
0012 import org.kde.angelfish 1.0
0013
0014 WebEngineView {
0015 id: webEngineView
0016
0017 property string errorCode: ""
0018 property string errorString: ""
0019
0020 property bool privateMode: false
0021
0022 property alias userAgent: userAgent
0023
0024 // loadingActive property is set to true when loading is started
0025 // and turned to false only after succesful or failed loading. It
0026 // is possible to set it to false by calling stopLoading method.
0027 //
0028 // The property was introduced as it triggers visibility of the webEngineView
0029 // in the other parts of the code. When using loading that is linked
0030 // to visibility, stop/start loading was observed in some conditions. It looked as if
0031 // there is an internal optimization of webengine in the case of parallel
0032 // loading of several pages that could use visibility as one of the decision
0033 // making parameters.
0034 property bool loadingActive: false
0035
0036 // reloadOnVisible property ensures that the view has been always
0037 // loaded at least once while it is visible. When the view is loaded
0038 // while visible is set to false, there, what appears to be Chromium
0039 // optimizations that can disturb the loading.
0040 property bool reloadOnVisible: true
0041
0042 // Profiles of WebViews are shared among all views of the same type (regular or
0043 // private). However, within each group of tabs, we can have some tabs that are
0044 // using mobile or desktop user agent. To avoid loading a page with the wrong
0045 // user agent, the agent is checked in the beginning of the loading at onLoadingChanged
0046 // handler. If the user agent is wrong, loading is stopped and reloadOnMatchingAgents
0047 // property is set to true. As soon as the agent is correct, the page is loaded.
0048 property bool reloadOnMatchingAgents: false
0049
0050 // Used to follow whether agents match
0051 property bool agentsMatch: profile.httpUserAgent === userAgent.userAgent
0052
0053 // URL that was requested and should be used
0054 // as a base for user interaction. It reflects
0055 // last request (successful or failed)
0056 property url requestedUrl: url
0057
0058 property int findInPageResultIndex
0059 property int findInPageResultCount
0060
0061 // Used to hide certain context menu items
0062 property bool isAppView: false
0063
0064 // Used to track reader mode switch
0065 property bool readerMode: false
0066
0067 // url to keep last url to return from reader mode
0068 property url readerSourceUrl
0069
0070 // string to keep last title to return from reader mode
0071 property string readerTitle
0072
0073 // Used for pdf generated to preview before print
0074 property url printPreviewUrl: ""
0075 property bool generatingPdf: false
0076 property int printedPageOrientation: WebEngineView.Portrait
0077 property int printedPageSizeId: WebEngineView.A4
0078
0079 Shortcut {
0080 enabled: webEngineView.isFullScreen
0081 sequence: "Esc"
0082 onActivated: webEngineView.fullScreenCancelled();
0083 }
0084
0085 // helper function to apply DomDistiller
0086 function readerDistillerRun() {
0087 readerSourceUrl = url
0088 runJavaScript(DomDistiller.script, function() {
0089 runJavaScript(DomDistiller.applyScript, function(result) {
0090 loadHtml(result[2][1])
0091 readerTitle = result[1]
0092 })
0093 })
0094 }
0095
0096 // method to switch reader mode
0097 function readerModeSwitch() {
0098 if (readerMode) {
0099 url = readerSourceUrl
0100 } else {
0101 readerDistillerRun()
0102 }
0103 }
0104
0105 UserAgentGenerator {
0106 id: userAgent
0107 onUserAgentChanged: webEngineView.reload()
0108 }
0109
0110 settings {
0111 autoLoadImages: Settings.webAutoLoadImages
0112 javascriptEnabled: Settings.webJavaScriptEnabled
0113 // Disable builtin error pages in favor of our own
0114 errorPageEnabled: false
0115 // Load larger touch icons
0116 touchIconsEnabled: true
0117 // Disable scrollbars on mobile
0118 showScrollBars: !Kirigami.Settings.isMobile
0119 // Generally allow screen sharing, still needs permission from the user
0120 screenCaptureEnabled: true
0121 // Enables a web page to request that one of its HTML elements be made to occupy the user's entire screen
0122 fullScreenSupportEnabled: true
0123 // Turns on printing of CSS backgrounds when printing a web page
0124 printElementBackgrounds: false
0125 }
0126
0127 focus: true
0128 onLoadingChanged: loadRequest => {
0129 //print("Loading: " + loading);
0130 print(" url: " + loadRequest.url + " " + loadRequest.status)
0131 //print(" icon: " + webEngineView.icon)
0132 //print(" title: " + webEngineView.title)
0133
0134 /* Handle
0135 * - WebEngineView::LoadStartedStatus,
0136 * - WebEngineView::LoadStoppedStatus,
0137 * - WebEngineView::LoadSucceededStatus and
0138 * - WebEngineView::LoadFailedStatus
0139 */
0140 var ec = "";
0141 var es = "";
0142 if (loadRequest.status === WebEngineView.LoadStartedStatus) {
0143 if (profile.httpUserAgent !== userAgent.userAgent) {
0144 //print("Mismatch of user agents, will load later " + loadRequest.url);
0145 reloadOnMatchingAgents = true;
0146 stopLoading();
0147 } else {
0148 loadingActive = true;
0149 }
0150 }
0151 if (loadRequest.status === WebEngineView.LoadSucceededStatus) {
0152 if (!privateMode) {
0153 const request = {
0154 url: currentWebView.url,
0155 title: currentWebView.title,
0156 icon: currentWebView.icon
0157 }
0158
0159 BrowserManager.addToHistory(request);
0160 BrowserManager.updateLastVisited(currentWebView.url);
0161 }
0162
0163 if (typeof AdblockUrlInterceptor === "undefined" || !AdblockUrlInterceptor.adblockSupported) {
0164 return;
0165 }
0166
0167 let script = AdblockUrlInterceptor.getInjectedScript(webEngineView.url)
0168 if (script !== "") {
0169 webEngineView.runJavaScript(script)
0170 }
0171
0172 webEngineView.runJavaScript(
0173 `var elements = document.querySelectorAll("*[id]");
0174 var ids = [];
0175 for (var i in elements) {
0176 if (elements[i].id) {
0177 ids.push(elements[i].id)
0178 }
0179 }
0180 ids
0181 `, (ids) => {
0182 webEngineView.runJavaScript(
0183 `var elements = document.querySelectorAll("*[class]");
0184 var classes = [];
0185 for (var i in elements) {
0186 if (elements[i].className) {
0187 classes.push(elements[i].className);
0188 }
0189 }
0190 classes
0191 `, (classes) => {
0192 let selectors = AdblockUrlInterceptor.getCosmeticFilters(webEngineView.url, classes, ids)
0193
0194 for (var i = 0; i < selectors.length; i++) {
0195 webEngineView.runJavaScript(
0196 `{
0197 let adblockStyleElement = document.createElement("style")
0198 adblockStyleElement.type = "text/css"
0199 adblockStyleElement.textContent = '${selectors[i]} { display: none !important; }'
0200 document.head.appendChild(adblockStyleElement);
0201 }`)
0202 }
0203 })
0204 })
0205 loadingActive = false;
0206 }
0207 if (loadRequest.status === WebEngineView.LoadFailedStatus) {
0208 print("Load failed: " + loadRequest.errorCode + " " + loadRequest.errorString);
0209 print("Load failed url: " + loadRequest.url + " " + url);
0210 ec = loadRequest.errorCode;
0211 es = loadRequest.errorString;
0212 loadingActive = false;
0213
0214 // update requested URL only after its clear that it fails.
0215 // Otherwise, its updated as a part of url property update.
0216 if (requestedUrl !== loadRequest.url)
0217 requestedUrl = loadRequest.url;
0218 }
0219 errorCode = ec;
0220 errorString = es;
0221 }
0222
0223 Component.onCompleted: {
0224 print("WebView completed.");
0225 print("Settings: " + webEngineView.settings);
0226 }
0227
0228 onIconChanged: {
0229 if (icon && !privateMode) {
0230 BrowserManager.updateIcon(url, icon)
0231 }
0232 }
0233 onNewWindowRequested: request => {
0234 if (request.userInitiated) {
0235 tabsModel.newTab(request.requestedUrl.toString())
0236 showPassiveNotification(i18n("Website was opened in a new tab"))
0237 } else {
0238 questionLoader.setSource("NewTabQuestion.qml")
0239 questionLoader.item.url = request.requestedUrl
0240 questionLoader.item.visible = true
0241 }
0242 }
0243 onUrlChanged: {
0244 if (requestedUrl !== url) {
0245 requestedUrl = url;
0246 // poor heuristics to update readerMode accordingly:
0247 readerMode = url.toString().startsWith("data:text/html")
0248 }
0249 }
0250
0251 onFullScreenRequested: request => {
0252 if (request.toggleOn) {
0253 webBrowser.showFullScreen()
0254 const message = i18n("Entered Full Screen Mode")
0255 const actionText = i18n("Exit Full Screen (Esc)")
0256 showPassiveNotification(message, "short", actionText, function() { webEngineView.fullScreenCancelled() });
0257 } else {
0258 webBrowser.showNormal()
0259 }
0260
0261 request.accept()
0262 }
0263
0264 onContextMenuRequested: request => {
0265 request.accepted = true // Make sure QtWebEngine doesn't show its own context menu.
0266 contextMenu.request = request
0267 contextMenu.x = request.position.x
0268 contextMenu.y = request.position.y
0269 contextMenu.open()
0270 }
0271
0272 onAuthenticationDialogRequested: request => {
0273 request.accepted = true
0274 sheetLoader.setSource("AuthSheet.qml")
0275 sheetLoader.item.request = request
0276 sheetLoader.item.open()
0277 }
0278
0279 onFeaturePermissionRequested: (securityOrigin, feature) => {
0280 let newQuestion = rootPage.questions.newPermissionQuestion()
0281 newQuestion.permission = feature
0282 newQuestion.origin = securityOrigin
0283 newQuestion.visible = true
0284 }
0285
0286 onJavaScriptDialogRequested: request => {
0287 request.accepted = true;
0288 sheetLoader.setSource("JavaScriptDialogSheet.qml");
0289 sheetLoader.item.request = request;
0290 sheetLoader.item.open();
0291 }
0292
0293 onFindTextFinished: result => {
0294 findInPageResultIndex = result.activeMatch;
0295 findInPageResultCount = result.numberOfMatches;
0296 }
0297
0298 onVisibleChanged: {
0299 if (visible && reloadOnVisible) {
0300 // see description of reloadOnVisible above for reasoning
0301 reloadOnVisible = false;
0302 reload();
0303 }
0304 }
0305
0306 onAgentsMatchChanged: {
0307 if (agentsMatch && reloadOnMatchingAgents) {
0308 // see description of reloadOnMatchingAgents above for reasoning
0309 reloadOnMatchingAgents = false;
0310 reload();
0311 }
0312 }
0313
0314 onCertificateError: error => {
0315 error.defer();
0316 errorHandler.enqueue(error);
0317 }
0318
0319 function findInPageForward(text) {
0320 findText(text);
0321 }
0322
0323 function stopLoading() {
0324 loadingActive = false;
0325 stop();
0326 }
0327
0328 onPrintRequested: {
0329 printPreviewUrl = "";
0330 generatingPdf = true;
0331 const filePath = BrowserManager.tempDirectory() + "/print-preview.pdf";
0332 printToPdf(filePath, printedPageSizeId, printedPageOrientation);
0333
0334 if (!printPreview.sheetOpen) {
0335 printPreview.open();
0336 }
0337 }
0338
0339 onPdfPrintingFinished: (filePath, success) => {
0340 generatingPdf = false;
0341 printPreviewUrl = "file://" + filePath + "#toolbar=0&view=Fit";
0342 }
0343
0344 PrintPreview {
0345 id: printPreview
0346 }
0347
0348 onLinkHovered: hoveredUrl => hoveredLink.text = hoveredUrl
0349
0350 QQC2.Label {
0351 id: hoveredLink
0352 visible: text.length > 0
0353 z: 2
0354 anchors.bottom: parent.bottom
0355 anchors.left: parent.left
0356 leftPadding: Kirigami.Units.smallSpacing
0357 rightPadding: Kirigami.Units.smallSpacing
0358 color: Kirigami.Theme.textColor
0359 font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
0360
0361 background: Rectangle {
0362 anchors.fill: parent
0363 color: Kirigami.Theme.backgroundColor
0364 }
0365 }
0366
0367 QQC2.Menu {
0368 id: contextMenu
0369 property ContextMenuRequest request
0370 property bool isValidUrl: contextMenu.request && contextMenu.request.linkUrl != "" // not strict equality
0371 property bool isAudio: contextMenu.request && contextMenu.request.mediaType === ContextMenuRequest.MediaTypeAudio
0372 property bool isImage: contextMenu.request && contextMenu.request.mediaType === ContextMenuRequest.MediaTypeImage
0373 property bool isVideo: contextMenu.request && contextMenu.request.mediaType === ContextMenuRequest.MediaTypeVideo
0374 property real playbackRate: 100
0375
0376 onAboutToShow: {
0377 if (webEngineView.settings.javascriptEnabled && (contextMenu.isAudio || contextMenu.isVideo)) {
0378 const point = contextMenu.request.x + ', ' + contextMenu.request.y
0379 const js = 'document.elementFromPoint(' + point + ').playbackRate * 100;'
0380 webEngineView.runJavaScript(js, function(result) { contextMenu.playbackRate = result })
0381 }
0382 }
0383
0384 QQC2.MenuItem {
0385 visible: contextMenu.isAudio || contextMenu.isVideo
0386 height: visible ? implicitHeight : 0
0387 text: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaPaused
0388 ? i18n("Play")
0389 : i18n("Pause")
0390 onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaPlayPause)
0391 }
0392 QQC2.MenuItem {
0393 visible: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaHasAudio
0394 height: visible ? implicitHeight : 0
0395 text: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaMuted
0396 ? i18n("Unmute")
0397 : i18n("Mute")
0398 onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaMute)
0399 }
0400 QQC2.MenuItem {
0401 visible: webEngineView.settings.javascriptEnabled && (contextMenu.isAudio || contextMenu.isVideo)
0402 height: visible ? implicitHeight : 0
0403 contentItem: RowLayout {
0404 QQC2.Label {
0405 Layout.leftMargin: Kirigami.Units.largeSpacing
0406 Layout.fillWidth: true
0407 text: i18n("Speed")
0408 }
0409 QQC2.SpinBox {
0410 Layout.rightMargin: Kirigami.Units.largeSpacing
0411 value: contextMenu.playbackRate
0412 from: 25
0413 to: 1000
0414 stepSize: 25
0415 onValueModified: {
0416 contextMenu.playbackRate = value
0417 const point = contextMenu.request.x + ', ' + contextMenu.request.y
0418 const js = 'document.elementFromPoint(' + point + ').playbackRate = ' + contextMenu.playbackRate / 100 + ';'
0419 webEngineView.runJavaScript(js)
0420 }
0421 textFromValue: function(value, locale) {
0422 return Number(value / 100).toLocaleString(locale, 'f', 2)
0423 }
0424 }
0425 }
0426 }
0427 QQC2.MenuItem {
0428 visible: contextMenu.isAudio || contextMenu.isVideo
0429 height: visible ? implicitHeight : 0
0430 text: i18n("Loop")
0431 checked: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaLoop
0432 onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaLoop)
0433 }
0434 QQC2.MenuItem {
0435 visible: webEngineView.settings.javascriptEnabled && contextMenu.isVideo
0436 height: visible ? implicitHeight : 0
0437 text: webEngineView.isFullScreen ? i18n("Exit fullscreen") : i18n("Fullscreen")
0438 onTriggered: {
0439 const point = contextMenu.request.x + ', ' + contextMenu.request.y
0440 const js = webEngineView.isFullScreen
0441 ? 'document.exitFullscreen()'
0442 : 'document.elementFromPoint(' + point + ').requestFullscreen()'
0443 webEngineView.runJavaScript(js)
0444 }
0445 }
0446 QQC2.MenuItem {
0447 visible: webEngineView.settings.javascriptEnabled && (contextMenu.isAudio || contextMenu.isVideo)
0448 height: visible ? implicitHeight : 0
0449 text: contextMenu.request && contextMenu.request.mediaFlags & ContextMenuRequest.MediaControls
0450 ? i18n("Hide controls")
0451 : i18n("Show controls")
0452 onTriggered: webEngineView.triggerWebAction(WebEngineView.ToggleMediaControls)
0453 }
0454 QQC2.MenuSeparator { visible: contextMenu.isAudio || contextMenu.isVideo }
0455 QQC2.MenuItem {
0456 visible: (contextMenu.isAudio || contextMenu.isVideo) && contextMenu.request.mediaUrl !== currentWebView.url
0457 height: visible ? implicitHeight : 0
0458 text: webEngineView.isAppView
0459 ? contextMenu.isVideo ? i18n("Open video") : i18n("Open audio")
0460 : contextMenu.isVideo ? i18n("Open video in new Tab") : i18n("Open audio in new Tab")
0461 onTriggered: {
0462 if (webEngineView.isAppView) {
0463 Qt.openUrlExternally(contextMenu.request.mediaUrl);
0464 } else {
0465 tabsModel.newTab(contextMenu.request.mediaUrl)
0466 }
0467 }
0468 }
0469 QQC2.MenuItem {
0470 visible: contextMenu.isVideo
0471 height: visible ? implicitHeight : 0
0472 text: i18n("Save video")
0473 onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadMediaToDisk)
0474 }
0475 QQC2.MenuItem {
0476 visible: contextMenu.isVideo
0477 height: visible ? implicitHeight : 0
0478 text: i18n("Copy video Link")
0479 onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyMediaUrlToClipboard)
0480 }
0481 QQC2.MenuItem {
0482 visible: contextMenu.isImage && contextMenu.request.mediaUrl !== currentWebView.url
0483 height: visible ? implicitHeight : 0
0484 text: webEngineView.isAppView ? i18n("Open image") : i18n("Open image in new Tab")
0485 onTriggered: {
0486 if (webEngineView.isAppView) {
0487 Qt.openUrlExternally(contextMenu.request.mediaUrl);
0488 } else {
0489 tabsModel.newTab(contextMenu.request.mediaUrl)
0490 }
0491 }
0492 }
0493 QQC2.MenuItem {
0494 visible: contextMenu.isImage
0495 height: visible ? implicitHeight : 0
0496 text: i18n("Save image")
0497 onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadImageToDisk)
0498 }
0499 QQC2.MenuItem {
0500 visible: contextMenu.isImage
0501 height: visible ? implicitHeight : 0
0502 text: i18n("Copy image")
0503 onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyImageToClipboard)
0504 }
0505 QQC2.MenuItem {
0506 visible: contextMenu.isImage
0507 height: visible ? implicitHeight : 0
0508 text: i18n("Copy image link")
0509 onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyImageUrlToClipboard)
0510 }
0511 QQC2.MenuItem {
0512 visible: contextMenu.request && contextMenu.isValidUrl
0513 height: visible ? implicitHeight : 0
0514 text: webEngineView.isAppView ? i18n("Open link") : i18n("Open link in new Tab")
0515 onTriggered: {
0516 if (webEngineView.isAppView) {
0517 Qt.openUrlExternally(contextMenu.request.linkUrl);
0518 } else {
0519 webEngineView.triggerWebAction(WebEngineView.OpenLinkInNewTab)
0520 }
0521 }
0522 }
0523 QQC2.MenuItem {
0524 visible: contextMenu.request && contextMenu.isValidUrl
0525 height: visible ? implicitHeight : 0
0526 text: i18n("Bookmark link")
0527 onTriggered: {
0528 const bookmark = {
0529 url: contextMenu.request.linkUrl,
0530 title: contextMenu.request.linkText
0531 }
0532 BrowserManager.addBookmark(bookmark)
0533 }
0534 }
0535 QQC2.MenuItem {
0536 visible: contextMenu.request && contextMenu.isValidUrl
0537 height: visible ? implicitHeight : 0
0538 text: i18n("Save link")
0539 onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadLinkToDisk)
0540 }
0541 QQC2.MenuItem {
0542 visible: contextMenu.request && contextMenu.isValidUrl
0543 height: visible ? implicitHeight : 0
0544 text: i18n("Copy link")
0545 onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyLinkToClipboard)
0546 }
0547 QQC2.MenuSeparator { visible: contextMenu.request && contextMenu.isValidUrl }
0548 QQC2.MenuItem {
0549 visible: contextMenu.request && (contextMenu.request.editFlags & ContextMenuRequest.CanCopy) && contextMenu.request.mediaUrl == ""
0550 height: visible ? implicitHeight : 0
0551 text: i18n("Copy")
0552 onTriggered: webEngineView.triggerWebAction(WebEngineView.Copy)
0553 }
0554 QQC2.MenuItem {
0555 visible: contextMenu.request && (contextMenu.request.editFlags & ContextMenuRequest.CanCut)
0556 height: visible ? implicitHeight : 0
0557 text: i18n("Cut")
0558 onTriggered: webEngineView.triggerWebAction(WebEngineView.Cut)
0559 }
0560 QQC2.MenuItem {
0561 visible: contextMenu.request && (contextMenu.request.editFlags & ContextMenuRequest.CanPaste)
0562 height: visible ? implicitHeight : 0
0563 text: i18n("Paste")
0564 onTriggered: webEngineView.triggerWebAction(WebEngineView.Paste)
0565 }
0566 QQC2.MenuItem {
0567 property string fullText: contextMenu.request ? contextMenu.request.selectedText || contextMenu.request.linkText : ""
0568 property string elidedText: fullText.length > 25 ? fullText.slice(0, 25) + "..." : fullText
0569 visible: contextMenu.request && fullText
0570 height: visible ? implicitHeight : 0
0571 text: contextMenu.request && fullText ? i18n('Search for "%1"', elidedText) : ""
0572 onTriggered: {
0573 if (webEngineView.isAppView) {
0574 Qt.openUrlExternally(UrlUtils.urlFromUserInput(Settings.searchBaseUrl + fullText));
0575 } else {
0576 tabsModel.newTab(UrlUtils.urlFromUserInput(Settings.searchBaseUrl + fullText));
0577 }
0578 }
0579 }
0580 QQC2.MenuSeparator { visible: !webEngineView.isAppView && contextMenu.request && contextMenu.request.mediaUrl != "" && !contextMenu.isValidUrl }
0581 QQC2.MenuItem {
0582 visible: !webEngineView.isAppView && contextMenu.request && contextMenu.request.selectedText === ""
0583 height: visible ? implicitHeight : 0
0584 text: i18n("Share page")
0585 onTriggered: {
0586 sheetLoader.setSource("ShareSheet.qml")
0587 sheetLoader.item.url = currentWebView.url
0588 sheetLoader.item.inputTitle = currentWebView.title
0589 Qt.callLater(sheetLoader.item.open)
0590 }
0591 }
0592 QQC2.MenuSeparator { visible: !webEngineView.isAppView }
0593 QQC2.MenuItem {
0594 visible: !webEngineView.isAppView
0595 height: visible ? implicitHeight : 0
0596 text: i18n("View page source")
0597 onTriggered: tabsModel.newTab("view-source:" + webEngineView.url)
0598 }
0599 }
0600 }