diff --git a/src/Morph/Web/MorphWebContext.qml b/src/Morph/Web/MorphWebContext.qml index da79298cd..ae5dccf48 100644 --- a/src/Morph/Web/MorphWebContext.qml +++ b/src/Morph/Web/MorphWebContext.qml @@ -26,6 +26,8 @@ WebEngineProfile { property alias dataPath: oxideContext.persistentStoragePath property alias maxCacheSizeHint: oxideContext.httpCacheMaximumSize property alias incognito: oxideContext.offTheRecord + property int userAgentId: 0 + property string customUserAgent: "" readonly property string defaultUserAgent: __ua.defaultUA offTheRecord: false @@ -35,7 +37,7 @@ WebEngineProfile { cachePath: cacheLocation maxCacheSizeHint: cacheSizeHint - userAgent: defaultUserAgent + userAgent: (customUserAgent !== "") ? customUserAgent : defaultUserAgent persistentCookiesPolicy: WebEngineProfile.ForcePersistentCookies diff --git a/src/Morph/Web/MorphWebView.qml b/src/Morph/Web/MorphWebView.qml index e288f388a..c06a4e85c 100644 --- a/src/Morph/Web/MorphWebView.qml +++ b/src/Morph/Web/MorphWebView.qml @@ -18,7 +18,7 @@ import QtQuick 2.4 import QtQuick.Window 2.2 -import QtWebEngine 1.5 +import QtWebEngine 1.10 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import "." // QTBUG-34418 diff --git a/src/Morph/Web/plugin.cpp b/src/Morph/Web/plugin.cpp index d405e0f3c..b523ebbfe 100644 --- a/src/Morph/Web/plugin.cpp +++ b/src/Morph/Web/plugin.cpp @@ -232,6 +232,7 @@ void MorphBrowserPlugin::initializeEngine(QQmlEngine* engine, const char* uri) void MorphBrowserPlugin::registerTypes(const char* uri) { Q_ASSERT(uri == QLatin1String("Morph.Web")); + qmlRegisterModule(uri, 0, 1); } #include "plugin.moc" diff --git a/src/app/BrowserView.qml b/src/app/BrowserView.qml index 2d08e632a..f83ebab81 100644 --- a/src/app/BrowserView.qml +++ b/src/app/BrowserView.qml @@ -52,6 +52,8 @@ FocusScope { OrientationHelper { id: contentsItem + automaticOrientation: false + KeyboardRectangle { id: _osk } diff --git a/src/app/BrowserWindow.qml b/src/app/BrowserWindow.qml index cf69c807e..0e8c6293c 100644 --- a/src/app/BrowserWindow.qml +++ b/src/app/BrowserWindow.qml @@ -29,7 +29,6 @@ Window { property var currentWebview: null property bool hasTouchScreen: false - contentOrientation: Screen.orientation minimumWidth: units.gu(40) minimumHeight: units.gu(20) diff --git a/src/app/ChromeBase.qml b/src/app/ChromeBase.qml index 7dcb78d99..aa7c007b8 100644 --- a/src/app/ChromeBase.qml +++ b/src/app/ChromeBase.qml @@ -30,8 +30,8 @@ StyledItem { property alias backgroundColor: backgroundRect.color - property alias loading: progressBar.visible - property alias loadProgress: progressBar.value + property bool loading: false + property real loadProgress: 0.0 states: [ State { @@ -71,6 +71,9 @@ StyledItem { ThinProgressBar { id: progressBar + visible: chrome.loading + value: chrome.loadProgress + anchors { left: parent.left right: parent.right diff --git a/src/app/CustomUserAgentsPage.qml b/src/app/CustomUserAgentsPage.qml index a8837bca4..72faef0c1 100644 --- a/src/app/CustomUserAgentsPage.qml +++ b/src/app/CustomUserAgentsPage.qml @@ -28,22 +28,12 @@ BrowserPage { property bool selectMode signal done() - signal reload(string selectedUserAgent) + signal reload() title: i18n.tr("Custom User Agents") showBackAction: !selectMode - function setUserAgentAsCurrentItem(userAgentName) { - for (var index = 0; index < customUserAgentsListView.count; index++) { - var userAgent = customUserAgentsListView.model.get(index); - if (userAgent.name === userAgentName) { - customUserAgentsListView.currentIndex = index; - return; - } - } - } - leadingActions: [ Action { objectName: "close" @@ -100,20 +90,11 @@ BrowserPage { iconName: "add" visible: !selectMode onTriggered: { - var promptDialog = PopupUtils.open(Qt.resolvedUrl("PromptDialog.qml"), customUserAgentsPage); - promptDialog.title = i18n.tr("New User Agent") - promptDialog.message = i18n.tr("Add the name for the new user agent") - promptDialog.accept.connect(function(text) { - if (text !== "") { - if (UserAgentsModel.contains(text)) { - customUserAgentsPage.setUserAgentAsCurrentItem(text); - } - else { - customUserAgentsListView.currentIndex = -1; - UserAgentsModel.insertEntry(text, ""); - reload(text); - } - } + var addDialog = PopupUtils.open(Qt.resolvedUrl("EditCustomUserAgentDialog.qml"), customUserAgentsPage); + addDialog.title = i18n.tr("New User Agent"); + addDialog.accept.connect(function(userAgentName, userAgentString) { + UserAgentsModel.insertEntry(userAgentName, userAgentString); + reload(); }); } } @@ -140,81 +121,15 @@ BrowserPage { delegate: ListItem { id: item - readonly property bool isCurrentItem: item.ListView.isCurrentItem - //height: isCurrentItem ? layout.height : units.gu(5) - height: layout.height - color: isCurrentItem ? theme.palette.selected.base : theme.palette.normal.background - - MouseArea { - anchors.fill: parent - onClicked: customUserAgentsListView.currentIndex = index - } - - SlotsLayout { - id: layout - width: parent.width - - mainSlot: - - Column { - - spacing: units.gu(2) - - Row { - spacing: units.gu(1.5) - height: item.ListView.isCurrentItem ? units.gu(2) : units.gu(1) - width: parent.width - - Icon { - anchors.verticalCenter: userAgentName.verticalCenter - visible: item.ListView.isCurrentItem - name: "avatar-default-symbolic" - height: units.gu(2) - width: height - } - - Label { - id: userAgentLabel - anchors.verticalCenter: parent.verticalCenter - visible: ! item.ListView.isCurrentItem - width: parent.width - height: units.gu(1) - text: model.name - } - - TextField { - id: userAgentName - visible: item.ListView.isCurrentItem - text: model.name - onFocusChanged: { - if (!focus) { - if (text === "") { - text = model.name; - } - else { - UserAgentsModel.setUserAgentName(model.id, text); - } - } - } - } - - } - TextArea { - visible: item.ListView.isCurrentItem - width: parent.width - text: model.userAgentString - placeholderText: i18n.tr("enter user agent string...") - onFocusChanged: { - if (! focus) { - UserAgentsModel.setUserAgentString(model.id, text); - } - } - } - } + Label { + id: userAgentLabel + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + height: units.gu(1) + text: model.name } - leadingActions: deleteActionList ListItemActions { @@ -231,6 +146,30 @@ BrowserPage { } ] } + + trailingActions: trailingActionList + + ListItemActions { + id: trailingActionList + actions: [ + Action { + objectName: "trailingActionList.edit" + iconName: "edit" + enabled: true + onTriggered: { + var editDialog = PopupUtils.open(Qt.resolvedUrl("EditCustomUserAgentDialog.qml"), customUserAgentsPage); + editDialog.title = i18n.tr("Edit User Agent"); + editDialog.previousUserAgentName = model.name; + editDialog.userAgentName = model.name; + editDialog.userAgentString = model.userAgentString; + editDialog.accept.connect(function(userAgentName, userAgentString) { + UserAgentsModel.setUserAgentString(model.id, userAgentString); + UserAgentsModel.setUserAgentName(model.id, userAgentName); + }); + } + } + ] + } } } diff --git a/src/app/DomainSettingsPage.qml b/src/app/DomainSettingsPage.qml index 21a67a747..a19c33c9e 100644 --- a/src/app/DomainSettingsPage.qml +++ b/src/app/DomainSettingsPage.qml @@ -17,6 +17,7 @@ */ import QtQuick 2.6 +import QtQuick.Controls 2.2 import Qt.labs.settings 1.0 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 @@ -143,8 +144,7 @@ FocusScope { ListItem { id: useragentsMenu z: 3 - // custom user agents deactivated for now - height: 0 //units.gu(6) + height: units.gu(6) color: theme.palette.normal.background ListItemLayout { title.text: i18n.tr("Custom User Agents") @@ -170,6 +170,7 @@ FocusScope { readonly property bool isCurrentItem: item.ListView.isCurrentItem readonly property string domain: model.domain readonly property int userAgentId: model.userAgentId + readonly property int locationPreference: model.allowLocation height: isCurrentItem ? layout.height : units.gu(5) color: isCurrentItem ? ((theme.palette.selected.background.hslLightness > 0.5) ? Qt.darker(theme.palette.selected.background, 1.05) : Qt.lighter(theme.palette.selected.background, 1.5)) : theme.palette.normal.background @@ -204,11 +205,13 @@ FocusScope { Label { width: parent.width * 0.9 text: i18n.tr("allowed to launch other apps") + anchors.verticalCenter: parent.verticalCenter } CheckBox { checked: model.allowCustomUrlSchemes onTriggered: DomainSettingsModel.allowCustomUrlSchemes(model.domain, checked) + anchors.verticalCenter: parent.verticalCenter } } @@ -219,25 +222,29 @@ FocusScope { visible: item.ListView.isCurrentItem Label { - width: parent.width * 0.9 - text: i18n.tr("allowed to access your location") + width: parent.width * 0.5 + text: i18n.tr("access your location") + anchors.verticalCenter: parent.verticalCenter } - CheckBox { - checked: model.allowLocation - onTriggered: DomainSettingsModel.allowLocation(model.domain, checked) + ComboBox { + model: [ i18n.tr("Ask each time"), i18n.tr("Allowed"), i18n.tr("Denied") ] + currentIndex: item.locationPreference + onCurrentIndexChanged: DomainSettingsModel.setLocationPreference(item.domain, currentIndex) + anchors.verticalCenter: parent.verticalCenter } } Row { spacing: units.gu(1.5) height: units.gu(1) - // deactivated for now - visible: false //item.ListView.isCurrentItem + visible: item.ListView.isCurrentItem Label { + width: parent.width * 0.9 text: i18n.tr("custom user agent") opacity: UserAgentsModel.count > 0 ? 1.0 : 0.5 + anchors.verticalCenter: parent.verticalCenter } CheckBox { @@ -245,33 +252,30 @@ FocusScope { enabled: UserAgentsModel.count > 0 checked: model.userAgentId > 0 onTriggered: { - optSelect.selectedIndex = -1; + userAgentSelect.currentIndex = -1; if (checked) { - if( UserAgentsModel.count === 1) + if(UserAgentsModel.count === 1) { - optSelect.selectedIndex = 0; - DomainSettingsModel.setUserAgentId(item.domain, optSelect.model.get(optSelect.selectedIndex).id); + userAgentSelect.currentIndex = 0; + DomainSettingsModel.setUserAgentId(item.domain, userAgentSelect.model.get(userAgentSelect.currentIndex).id); } else { - optSelect.currentlyExpanded = true; + userAgentSelect.onPressedChanged(); } } else { DomainSettingsModel.setUserAgentId(model.domain, 0); } } + anchors.verticalCenter: parent.verticalCenter } } - /* ToDo: Can we do sth. about the following log messages ? - file:///usr/lib/arm-linux-gnueabihf/qt5/qml/Ubuntu/Components/1.3/OptionSelector.qml:330:13: - QML ListView: Binding loop detected for property "itemHeight" - */ - OptionSelector { + ComboBox { - id: optSelect + id: userAgentSelect visible: customUserAgentCheckbox.checked enabled: (UserAgentsModel.count > 1) @@ -281,15 +285,14 @@ FocusScope { sort.property: "name" sort.order: Qt.AscendingOrder } - delegate: OptionSelectorDelegate { - text: model.name - } - + + textRole: "name" + function updateIndex() { for (var i = 0; i < model.count; ++i) { if (item.userAgentId === model.get(i).id) { - selectedIndex = i; + currentIndex = i; } } } @@ -299,26 +302,30 @@ FocusScope { onIsCurrentItemChanged: { if (item.isCurrentItem && (item.userAgentId > 0)) { - optSelect.updateIndex(); + userAgentSelect.updateIndex(); } } } - - onDelegateClicked: { - DomainSettingsModel.setUserAgentId(item.domain, model.get(index).id); - } + + onActivated: DomainSettingsModel.setUserAgentId(item.domain, model.get(index).id); } - // within one label the check if zoom factor is set could not be properly done - Label { - height: units.gu(1) - text: i18n.tr("Zoom: ") + Math.round(model.zoomFactor * 100) + "%" - visible: item.ListView.isCurrentItem && ! isNaN(model.zoomFactor) - } - Label { + Row { + spacing: units.gu(1.5) height: units.gu(1) - text: i18n.tr("Zoom: ") + i18n.tr("not set") - visible: item.ListView.isCurrentItem && isNaN(model.zoomFactor) + visible: item.ListView.isCurrentItem + + // within one label the check if zoom factor is set could not be properly done + Label { + text: i18n.tr("Zoom: ") + Math.round(model.zoomFactor * 100) + "%" + visible: ! isNaN(model.zoomFactor) + anchors.verticalCenter: parent.verticalCenter + } + Label { + text: i18n.tr("Zoom: ") + i18n.tr("not set") + visible: isNaN(model.zoomFactor) + anchors.verticalCenter: parent.verticalCenter + } } } } @@ -353,13 +360,6 @@ FocusScope { horizontalAlignment: Text.AlignHCenter text: i18n.tr("No domain specific settings available") } - - Connections { - target: UserAgentsModel - enabled: ! customUserAgentsViewLoader.active - // the OptionSelector does not properly update the model (duplicate entries instead of new user agents) - onRowCountChanged: reload() - } } Loader { @@ -381,10 +381,6 @@ FocusScope { onReload: { customUserAgentsViewLoader.active = false; customUserAgentsViewLoader.active = true; - - if (selectedUserAgent) { - customUserAgentsViewLoader.item.setUserAgentAsCurrentItem(selectedUserAgent) - } } } } diff --git a/src/app/DownloadDelegate.qml b/src/app/DownloadDelegate.qml index f05b00b65..9a19b2cca 100644 --- a/src/app/DownloadDelegate.qml +++ b/src/app/DownloadDelegate.qml @@ -18,7 +18,9 @@ import QtQuick 2.4 import Ubuntu.Components 1.3 +import QtQuick.Layouts 1.3 import ".." +import "FileUtils.js" as FileUtils ListItem { id: downloadDelegate @@ -31,6 +33,7 @@ ListItem { property string downloadId property var download readonly property int progress: download ? 100 * (download.receivedBytes / download.totalBytes) : -1 + property real speed: 0 property bool paused: download.isPaused property alias incognito: incognitoIcon.visible @@ -40,31 +43,67 @@ ListItem { signal cancelled() height: visible ? layout.height : 0 + + Timer { + id: speedTimer + + property real prevBytes: 0 + + interval: 1000 + running: download && !paused? true : false + repeat: true + onTriggered: { + if (download) { + speed = download.receivedBytes - prevBytes + prevBytes = download.receivedBytes + } + } + } + + MimeData { + id: linkMimeData + + text: model ? model.url : "" + } SlotsLayout { id: layout - Item { + ColumnLayout { SlotsLayout.position: SlotsLayout.Leading - width: units.gu(3) - height: units.gu(3) - - Image { - id: thumbimage - asynchronous: true - anchors.fill: parent - fillMode: Image.PreserveAspectFit - sourceSize.width: width - sourceSize.height: height + spacing: units.gu(1) + + Item { + Layout.alignment: Qt.AlignHCenter + implicitWidth: units.gu(3) + implicitHeight: units.gu(3) + + Image { + id: thumbimage + asynchronous: true + anchors.fill: parent + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + } + + Image { + asynchronous: true + anchors.fill: parent + anchors.margins: units.gu(0.2) + source: "image://theme/%1".arg(downloadDelegate.icon || "save") + visible: thumbimage.status !== Image.Ready + cache: true + } } - Image { - asynchronous: true - anchors.fill: parent - anchors.margins: units.gu(0.2) - source: "image://theme/%1".arg(downloadDelegate.icon || "save") - visible: thumbimage.status !== Image.Ready - cache: true + Label { + Layout.alignment: Qt.AlignHCenter + visible: !progressBar.indeterminateProgress && incomplete + horizontalAlignment: Text.AlignHCenter + // TRANSLATORS: %1 is the percentage of the download completed so far + text: i18n.tr("%1%").arg(progressBar.progress) + opacity: paused ? 0.5 : 1 } } @@ -133,70 +172,55 @@ ListItem { } } - IndeterminateProgressBar { - id: progressBar + ColumnLayout { + visible: incomplete && !error.visible anchors { left: parent.left right: parent.right } - height: units.gu(0.5) - visible: incomplete && !error.visible - progress: downloadDelegate.progress - // Work around UDM bug #1450144 - indeterminateProgress: progress < 0 || progress > 100 - opacity: paused ? 0.5 : 1 - } - } - - Column { - SlotsLayout.position: SlotsLayout.Trailing - spacing: units.gu(1) - width: (incomplete && !error.visible) ? cancelButton.width : 0 - Button { - id: cancelButton - visible: incomplete && !error.visible - text: i18n.tr("Cancel") - onClicked: { - if (download) { - download.cancel() - cancelled() - } + IndeterminateProgressBar { + id: progressBar + Layout.fillWidth: true + implicitHeight: units.gu(0.5) + progress: downloadDelegate.progress + // Work around UDM bug #1450144 + indeterminateProgress: progress < 0 || progress > 100 + opacity: paused ? 0.5 : 1 } - } - Label { - visible: !progressBar.indeterminateProgress && incomplete && !error.visible - width: cancelButton.width - horizontalAlignment: Text.AlignHCenter - textSize: Label.Small - // TRANSLATORS: %1 is the percentage of the download completed so far - text: i18n.tr("%1%").arg(progressBar.progress) - opacity: paused ? 0.5 : 1 - } + RowLayout { + Layout.fillWidth: true + implicitHeight: units.gu(4) - Button { - visible: incomplete && ! paused && ! error.visible - text: i18n.tr("Pause") - width: cancelButton.width - onClicked: { - if (download) { - download.pause() + Label { + horizontalAlignment: Text.AlignHCenter + textSize: Label.Small + text: download ? FileUtils.formatBytes(download.receivedBytes) + " / " + FileUtils.formatBytes(download.totalBytes) : i18n.tr("Unknown") + opacity: paused ? 0.5 : 1 } - } - } - Button { - visible: incomplete && paused && ! error.visible - text: i18n.tr("Resume") - width: cancelButton.width - onClicked: { - if (download) { - download.resume() + Label { + horizontalAlignment: Text.AlignHCenter + textSize: Label.Small + // TRANSLATORS: %1 is the number of bytes i.e. 2bytes, 5KB, 1MB + text: "(" + i18n.tr("%1/s").arg(FileUtils.formatBytes(downloadDelegate.speed)) + ")" + opacity: paused ? 0.5 : 1 } } } } + Icon { + id: iconPauseResume + + implicitWidth: units.gu(4) + implicitHeight: implicitWidth + SlotsLayout.position: SlotsLayout.Trailing + asynchronous: true + name: paused ? "media-preview-start" : "media-preview-pause" + visible: incomplete && !error.visible + color: theme.palette.normal.overlayText + } } Icon { @@ -213,7 +237,8 @@ ListItem { name: "private-browsing" } - leadingActions: error.visible || !incomplete ? deleteActionList : null + leadingActions: deleteActionList + trailingActions: trailingActionList ListItemActions { id: deleteActionList @@ -222,10 +247,49 @@ ListItem { objectName: "leadingAction.delete" iconName: "delete" enabled: error.visible || !incomplete + visible: enabled + text: i18n.tr("Delete File") + onTriggered: { + removed() + } + }, + Action { + objectName: "leadingAction.remove" + iconName: "list-remove" + enabled: !incomplete && !error.visible + visible: enabled + text: i18n.tr("Remove from History") onTriggered: { - removed() + } + }, + Action { + objectName: "leadingAction.cancel" + iconName: "cancel" + enabled: incomplete && !error.visible + visible: enabled + text: i18n.tr("Cancel") + onTriggered: { + if (download) { + download.cancel() + cancelled() + } + } + } + ] + } + ListItemActions { + id: trailingActionList + actions: [ + Action { + objectName: "trailingAction.CopyLink" + iconName: "edit-copy" + enabled: model.url != "" ? true : false + visible: enabled + text: i18n.tr("Copy Download Link") + onTriggered: { + Clipboard.push(linkMimeData) } } ] diff --git a/src/app/DownloadsDialog.qml b/src/app/DownloadsDialog.qml new file mode 100644 index 000000000..9cbec6d44 --- /dev/null +++ b/src/app/DownloadsDialog.qml @@ -0,0 +1,196 @@ +/* + * Copyright 2015-2016 Canonical Ltd. + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtWebEngine 1.10 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 +import QtQuick.Layouts 1.3 +import Ubuntu.Content 1.3 +import webbrowsercommon.private 0.1 + +import "MimeTypeMapper.js" as MimeTypeMapper +import "FileUtils.js" as FileUtils + +Popover { + id: downloadsDialog + + property var downloadsList: [] + property bool isEmpty: downloadsListView.count === 0 + readonly property real controlsHeight: (downloadsDialogColumn.spacing * (downloadsDialogColumn.children.length - 1)) + + (downloadsDialogColumn.anchors.margins * 2) + buttonsBar.height + titleLabel.height + property real maximumWidth: units.gu(60) + property real preferredWidth: browser.width - units.gu(6) + + property real maximumHeight: browser.height - units.gu(6) + property real preferredHeight: downloadsDialogColumn.height + units.gu(2) + + signal showDownloadsPage() + signal preview(string url) + + contentHeight: preferredHeight > maximumHeight ? maximumHeight : preferredHeight + contentWidth: preferredWidth > maximumWidth ? maximumWidth : preferredWidth + + grabDismissAreaEvents: true + + ColumnLayout { + id: downloadsDialogColumn + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: units.gu(1) + } + + spacing: units.gu(2) + + Label { + id: titleLabel + + Layout.fillWidth: true + font.bold: true + text: i18n.tr("Recent Downloads") + } + + Rectangle { + id: emptyLabel + + visible: isEmpty + color: "transparent" + + Layout.preferredHeight: units.gu(10) + Layout.minimumHeight: label.height + Layout.fillWidth: true + + Label { + id: label + text: i18n.tr("No Recent Downloads") + anchors.centerIn: parent + } + } + + ListView { + id: downloadsListView + + visible: !isEmpty + Layout.fillWidth: true + Layout.preferredHeight: downloadsListView.count * units.gu(7) + Layout.maximumHeight: browser.height - controlsHeight - units.gu(7) + Layout.minimumHeight: units.gu(7) + clip: true + + model: downloadsList + + property int selectedIndex: -1 + + delegate: SimpleDownloadDelegate { + download: modelData + title.text: FileUtils.getFilename(modelData.path) + subtitle.text: if (cancelled) { + i18n.tr("Cancelled") + } else { + // TRANSLATORS: %1 is the percentage of the download completed so far + (error ? modelData.interruptReasonString : (incomplete ? i18n.tr("%1%").arg(progress) : i18n.tr("Completed"))) + + " - " + FileUtils.formatBytes(download.receivedBytes) + } + + image: !incomplete && thumbnailLoader.status == Loader.Ready + && (modelData.mimeType.indexOf("image") === 0 + || modelData.mimeType.indexOf("video") === 0) + ? "image://thumbnailer/file://" + modelData.path : "" + icon: MimeDatabase.iconForMimetype(modelData.mimeType) + + onClicked: { + /* TODO: Enable once content picker in a popover is merged */ + /*if (!incomplete && !error) { + var properties = {"path": download.path, "contentType": MimeTypeMapper.mimeTypeToContentType(download.mimeType), "mimeType": download.mimeType, "downloadUrl": download.url} + var exportDialog = PopupUtils.open(Qt.resolvedUrl("ContentExportDialog.qml"), downloadsDialog.parent, properties) + exportDialog.preview.connect(downloadsDialog.preview) + } else {*/ + if (download) { + if (paused) { + download.resume() + } else { + download.pause() + } + } + //} + } + + onRemove: downloadsListView.removeItem(index) + onCancel: DownloadsModel.cancelDownload(download.id) + onDeleted: { + if (!incomplete) { + DownloadsModel.deleteDownload(download.path) + } + } + } + + Keys.onDeletePressed: { + currentItem.removeItem(currentItem.download) + } + + function removeItem(index) { + downloadsList.splice(index, 1); + model = downloadsList + forceLayout() + } + + function clear() { + downloadsList.splice(0, downloadsList.length); + model = downloadsList + forceLayout() + } + } + + Item { + id: buttonsBar + + Layout.fillWidth: true + implicitHeight: clearButton.height + + Button { + id: clearButton + visible: !isEmpty + objectName: "downloadsDialog.clearButton" + text: i18n.tr("Clear") + onClicked: { + downloadsListView.clear() + } + } + + Button { + id: viewButton + objectName: "downloadsDialog.viewButton" + anchors.right: parent.right + text: i18n.tr("View All") + color: theme.palette.normal.activity + onClicked: { + showDownloadsPage() + downloadsDialog.destroy() + } + } + } + } + + Loader { + id: thumbnailLoader + source: "Thumbnailer.qml" + } +} diff --git a/src/app/DownloadsPage.qml b/src/app/DownloadsPage.qml index 900d8cfe7..20875e22c 100644 --- a/src/app/DownloadsPage.qml +++ b/src/app/DownloadsPage.qml @@ -16,14 +16,16 @@ * along with this program. If not, see . */ -import QtQuick 2.4 -import QtWebEngine 1.5 +import QtQuick 2.9 +import QtWebEngine 1.10 import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 import Ubuntu.Content 1.3 import webbrowsercommon.private 0.1 import "MimeTypeMapper.js" as MimeTypeMapper import "UrlUtils.js" as UrlUtils +import "FileUtils.js" as FileUtils BrowserPage { id: downloadsItem @@ -117,7 +119,7 @@ BrowserPage { }, Action { iconName: "edit" - visible: !selectMode && !pickingMode && !exportPeerPicker.visible + visible: !selectMode && !pickingMode enabled: downloadsListView.count > 0 onTriggered: { selectMode = true @@ -207,9 +209,11 @@ BrowserPage { } delegate: DownloadDelegate { + id: downloadDelegate + download: ActiveDownloadsSingleton.currentDownloads[model.downloadId] downloadId: model.downloadId - title: getDisplayPath(model.path) + title: FileUtils.getFilename(model.path) url: model.url image: model.complete && thumbnailLoader.status == Loader.Ready && (model.mimetype.indexOf("image") === 0 @@ -238,12 +242,22 @@ BrowserPage { } onClicked: { - if (model.complete && !selectMode) { - exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype); - exportPeerPicker.visible = true; - exportPeerPicker.path = model.path; - exportPeerPicker.mimeType = model.mimetype; - exportPeerPicker.downloadUrl = model.url; + if (!selectMode) { + if (model.complete) { + exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype); + exportPeerPicker.visible = true; + exportPeerPicker.path = model.path; + exportPeerPicker.mimeType = model.mimetype; + exportPeerPicker.downloadUrl = model.url; + } else { + if (download) { + if (paused) { + download.resume() + } else { + download.pause() + } + } + } } } diff --git a/src/app/EditCustomUserAgentDialog.qml b/src/app/EditCustomUserAgentDialog.qml new file mode 100644 index 000000000..45fa31848 --- /dev/null +++ b/src/app/EditCustomUserAgentDialog.qml @@ -0,0 +1,73 @@ +/* + * Copyright 2020 UBports Foundation + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import Ubuntu.Components 1.3 as UITK +import Ubuntu.Components.Popups 1.3 as Popups +import Ubuntu.Content 1.3 +import webbrowsercommon.private 0.1 + +Popups.Dialog { + id: editCustomUserAgent + + property string previousUserAgentName: "" + property alias userAgentName: editUserAgentName.text + property alias userAgentString: editUserAgentString.text + + readonly property bool userAgentNameAlreadyTaken: (userAgentName !== previousUserAgentName) && UserAgentsModel.contains(userAgentName) + + signal accept(string userAgentName, string userAgentString) + signal cancel() + + onAccept: hide() + onCancel: hide() + + UITK.Label { + visible: userAgentNameAlreadyTaken + text: i18n.tr("this user agent name is already taken") + color: theme.palette.normal.negative + } + + UITK.TextField { + id: editUserAgentName + placeholderText: i18n.tr("Add the name for the user agent") + inputMethodHints: Qt.ImhNoPredictiveText + } + + UITK.TextArea { + id: editUserAgentString + placeholderText: i18n.tr("enter user agent string...") + inputMethodHints: Qt.ImhNoPredictiveText + } + + Row { + spacing: units.gu(2) + + UITK.Button { + text: i18n.tr("OK") + color: theme.palette.normal.positive + enabled: (userAgentName !== "") && ! userAgentNameAlreadyTaken + onClicked: accept(userAgentName, userAgentString) + } + + UITK.Button { + text: i18n.tr("Cancel") + onClicked: cancel() + } + } +} diff --git a/src/app/FileUtils.js b/src/app/FileUtils.js new file mode 100644 index 000000000..bea44d930 --- /dev/null +++ b/src/app/FileUtils.js @@ -0,0 +1,38 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ +/*jslint node: true */ +.pragma library +'use strict'; + +function formatBytes(bytes, decimals) { + decimals = decimals ? decimals : 2 + if (bytes === 0) return '0 B'; + + var k = 1000; + var dm = decimals < 0 ? 0 : decimals; + var sizes = ["B", "KB", "MB", "GB", "TB"]; + + var i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +function getFilename(path) { + return path.replace(/^.*[\\\/]/, '') +} + diff --git a/src/app/GeolocationPermissionRequest.qml b/src/app/GeolocationPermissionRequest.qml index 67c808500..c8af4a6fe 100644 --- a/src/app/GeolocationPermissionRequest.qml +++ b/src/app/GeolocationPermissionRequest.qml @@ -24,7 +24,7 @@ Dialog { id: dialog property string securityOrigin - property bool showAllowPermanentlyCheckBox + property bool showRememberDecisionCheckBox title: i18n.tr("Permission Request") text: securityOrigin + "
" + i18n.tr("This page wants to know your device’s location.") @@ -32,28 +32,29 @@ Dialog { signal allow() signal allowPermanently() signal reject() + signal rejectPermanently() onAllow: { PopupUtils.close(dialog); } onAllowPermanently: { PopupUtils.close(dialog); } onReject: { PopupUtils.close(dialog); } + onRejectPermanently: { PopupUtils.close(dialog); } ListItemLayout { - visible: showAllowPermanentlyCheckBox + visible: showRememberDecisionCheckBox title.text: i18n.tr("Remember decision") CheckBox { - id: allowPermanentlyCheckBox + id: rememberDecisionCheckBox } } Button { objectName: "allow" text: i18n.tr("Allow") color: theme.palette.normal.positive - onClicked: allowPermanentlyCheckBox.checked ? allowPermanently() : allow() + onClicked: rememberDecisionCheckBox.checked ? allowPermanently() : allow() } Button { objectName: "deny" text: i18n.tr("Deny") - enabled: ! allowPermanentlyCheckBox.checked - onClicked: reject() + onClicked: rememberDecisionCheckBox.checked ? rejectPermanently() : reject() } } diff --git a/src/app/SimpleDownloadDelegate.qml b/src/app/SimpleDownloadDelegate.qml new file mode 100644 index 000000000..4f62c9d1e --- /dev/null +++ b/src/app/SimpleDownloadDelegate.qml @@ -0,0 +1,164 @@ +/* + * Copyright 2021 UBports Foundation + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtWebEngine 1.10 +import Ubuntu.Components 1.3 + +ListItem { + id: simpleDownloadDelegate + + property var download + property alias title: listItemlayout.title + property alias subtitle: listItemlayout.subtitle + property string icon + property alias image: thumbimage.source + + readonly property bool incomplete: !download.isFinished + readonly property bool error: download.interruptReason > WebEngineDownloadItem.NoReason + readonly property bool cancelled: download.interruptReason == WebEngineDownloadItem.UserCanceled + readonly property bool paused: download.isPaused + readonly property int progress: download ? 100 * (download.receivedBytes / download.totalBytes) : -1 + + signal cancel() + signal remove() + signal deleted() + + divider.visible: false + + MimeData { + id: linkMimeData + + text: download.url + } + + ListItemLayout { + id: listItemlayout + + subtitle.opacity: paused && !cancelled && !error ? 0.5 : 1 + + Item { + + SlotsLayout.position: SlotsLayout.Leading + implicitWidth: units.gu(3) + implicitHeight: implicitWidth + + Image { + id: thumbimage + + asynchronous: true + anchors.fill: parent + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + } + + Image { + asynchronous: true + anchors.fill: parent + anchors.margins: units.gu(0.2) + source: "image://theme/%1".arg(simpleDownloadDelegate.icon || "save") + visible: thumbimage.status !== Image.Ready + cache: true + } + } + + Icon { + id: iconError + + implicitWidth: units.gu(4) + implicitHeight: implicitWidth + SlotsLayout.position: SlotsLayout.Last + asynchronous: true + name: "dialog-warning-symbolic" + visible: error && !cancelled + color: theme.palette.normal.negative + } + + Icon { + id: iconPauseResume + + implicitWidth: units.gu(4) + implicitHeight: implicitWidth + SlotsLayout.position: SlotsLayout.Trailing + asynchronous: true + name: paused ? "media-preview-start" : "media-preview-pause" + visible: incomplete && !error + color: theme.palette.normal.backgroundText + } + } + + leadingActions: deleteActionList + trailingActions: trailingActionList + + ListItemActions { + id: deleteActionList + actions: [ + Action { + objectName: "leadingAction.delete" + iconName: "delete" + enabled: error || !incomplete + visible: enabled + text: i18n.tr("Delete File") + onTriggered: { + deleted() + remove() + } + }, + Action { + objectName: "leadingAction.remove" + iconName: "list-remove" + enabled: !incomplete && !error + visible: enabled + text: i18n.tr("Remove from List") + onTriggered: { + remove() + } + }, + Action { + objectName: "leadingAction.cancel" + iconName: "cancel" + text: i18n.tr("Cancel") + enabled: incomplete && !error + visible: enabled + onTriggered: { + if (download) { + download.cancel() + cancel() + } + } + } + ] + } + + ListItemActions { + id: trailingActionList + actions: [ + Action { + objectName: "trailingAction.CopyLink" + iconName: "edit-copy" + enabled: download.url != "" ? true : false + visible: enabled + text: i18n.tr("Copy Download Link") + onTriggered: { + Clipboard.push(linkMimeData) + } + } + ] + } +} diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml index 25fd571a6..c49a47009 100644 --- a/src/app/WebViewImpl.qml +++ b/src/app/WebViewImpl.qml @@ -196,21 +196,31 @@ WebView { case WebEngineView.Geolocation: var domain = UrlUtils.extractHost(securityOrigin); + var locationPreference = DomainSettingsModel.getLocationPreference(domain); - if (DomainSettingsModel.isLocationAllowed(domain)) + if (locationPreference === DomainSettingsModel.AllowLocationAccess) { grantFeaturePermission(securityOrigin, feature, true); return; } + if (locationPreference === DomainSettingsModel.DenyLocationAccess) + { + grantFeaturePermission(securityOrigin, feature, false); + return; + } + var geoPermissionDialog = PopupUtils.open(Qt.resolvedUrl("GeolocationPermissionRequest.qml"), this); geoPermissionDialog.securityOrigin = securityOrigin; - geoPermissionDialog.showAllowPermanentlyCheckBox = (domain !== "") && ! incognito + geoPermissionDialog.showRememberDecisionCheckBox = (domain !== "") && ! incognito geoPermissionDialog.allow.connect(function() { grantFeaturePermission(securityOrigin, feature, true); }); geoPermissionDialog.allowPermanently.connect(function() { grantFeaturePermission(securityOrigin, feature, true); - DomainSettingsModel.allowLocation(domain, true); + DomainSettingsModel.setLocationPreference(domain, DomainSettingsModel.AllowLocationAccess); }) geoPermissionDialog.reject.connect(function() { grantFeaturePermission(securityOrigin, feature, false); }); + geoPermissionDialog.rejectPermanently.connect(function() { grantFeaturePermission(securityOrigin, feature, false); + DomainSettingsModel.setLocationPreference(domain, DomainSettingsModel.DenyLocationAccess); + }) break; case WebEngineView.MediaAudioCapture: @@ -702,6 +712,13 @@ WebView { } onFullScreenRequested: function(request) { + if (request.toggleOn) { + // twice because of QTBUG-84313 + webview.zoomFactor = 1.0; + webview.zoomFactor = 1.0; + } else { + webview.zoomController.refresh(); + } request.accept(); } diff --git a/src/app/ZoomControls.qml b/src/app/ZoomControls.qml index 8d81a8b5f..c666c450b 100644 --- a/src/app/ZoomControls.qml +++ b/src/app/ZoomControls.qml @@ -163,6 +163,10 @@ UbuntuShape { } } + function refresh() { + internal.setWebviewZoomFactor(controller.currentZoomFactor); + } + // If current domain has changed, we have to forget about previous zoom factors and update page zoom. // This also means, that loading is in progress, fit to widt updates will be done there. onCurrentDomainChanged: { @@ -175,7 +179,7 @@ UbuntuShape { // To keep webview.zoomFactor in sync with currentZoomFactor. onCurrentZoomFactorChanged: { //console.log("[ZC] controller.onCurrentZoomFactorChanged: %1".arg(controller.currentZoomFactor)); - webview.zoomFactor = controller.currentZoomFactor; + internal.setWebviewZoomFactor(controller.currentZoomFactor); } } @@ -286,6 +290,16 @@ UbuntuShape { //console.log("[ZC] currentZoomFactor: %1".arg(controller.currentZoomFactor)); } + function setWebviewZoomFactor(newZoomFactor) { + if (Math.abs(webview.zoomFactor - newZoomFactor) > 0.01) { + //https://bugreports.qt.io/browse/QTBUG-84313 + // zoom is not set reliably on the first change + // set it twice so that changes are not ignored + webview.zoomFactor = newZoomFactor; + webview.zoomFactor = newZoomFactor; + } + } + function updateFitToWidth() { //console.log("[ZC] internal.updateFitToWidth called"); @@ -315,7 +329,7 @@ UbuntuShape { //console.log("[ZC] zooming to default and autofitting"); // Automatic fit to width is done from defaultZoomFactor - webview.zoomFactor = controller.defaultZoomFactor; + internal.setWebviewZoomFactor(controller.defaultZoomFactor); // Wait, to be sure that any page layout change (css, js, ...) after previous zoom or width change takes effect. internal.updateFitToWidthTimer.restart(); } @@ -336,7 +350,7 @@ UbuntuShape { if (width === null || width <= 0) { //console.log("[ZC] no scrollWidth"); // Sync zoom factors in case they are out of sync. - webview.zoomFactor = currentZoomFactor; + internal.setWebviewZoomFactor(controller.currentZoomFactor); return; } @@ -347,7 +361,7 @@ UbuntuShape { if (Math.abs(controller.currentZoomFactor - controller.fitToWidthZoomFactor) < 0.1) { //console.log("[ZC] not autofitting, close to currentZoomFactor"); // Sync zoom factors in case they are out of sync. - webview.zoomFactor = controller.currentZoomFactor; + internal.setWebviewZoomFactor(controller.currentZoomFactor); return; } @@ -403,7 +417,7 @@ UbuntuShape { // This is a workaround, because sometimes a page is not zoomed after loading (happens after manual url change), // although the webview.zoomFactor (and currentZoomFactor) is correctly set. - webview.zoomFactor = controller.currentZoomFactor; + internal.setWebviewZoomFactor(controller.currentZoomFactor); // End of workaround. if (webview.visible === false) { diff --git a/src/app/browserapplication.cpp b/src/app/browserapplication.cpp index 253348a38..0c44835e4 100644 --- a/src/app/browserapplication.cpp +++ b/src/app/browserapplication.cpp @@ -185,6 +185,12 @@ bool BrowserApplication::initialize(const QString& qmlFileSubPath qputenv("UBUNTU_WEBVIEW_DEVTOOLS_HOST", devtoolsHost.toUtf8()); qputenv("UBUNTU_WEBVIEW_DEVTOOLS_PORT", devtoolsPort.toUtf8()); } + + // set suru style + if (qgetenv("QT_QUICK_CONTROLS_STYLE") == QString()) + { + qputenv("QT_QUICK_CONTROLS_STYLE", "Suru"); + } const char* uri = "webbrowsercommon.private"; qmlRegisterSingletonType(uri, 0, 1, "BrowserUtils", BrowserUtils_singleton_factory); @@ -229,12 +235,6 @@ bool BrowserApplication::initialize(const QString& qmlFileSubPath } QQmlProperty::write(m_object, QStringLiteral("hasTouchScreen"), hasTouchScreen); - // set suru style - if (qgetenv("QT_QUICK_CONTROLS_STYLE") == QString()) - { - qputenv("QT_QUICK_CONTROLS_STYLE", "Suru"); - } - inputMethodHandler * handler = new inputMethodHandler(); this->installEventFilter(handler); diff --git a/src/app/domain-permissions-model.h b/src/app/domain-permissions-model.h index 5834cb9c2..62904db58 100644 --- a/src/app/domain-permissions-model.h +++ b/src/app/domain-permissions-model.h @@ -32,6 +32,7 @@ class DomainPermissionsModel : public QAbstractListModel Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged) Q_PROPERTY(bool whiteListMode READ whiteListMode WRITE setWhiteListMode NOTIFY whiteListModeChanged) + Q_ENUMS(DomainPermission) Q_ENUMS(Roles) public: @@ -43,7 +44,6 @@ class DomainPermissionsModel : public QAbstractListModel Blocked = 1, Whitelisted = 2 }; - Q_ENUMS(DomainPermission) enum Roles { Domain = Qt::UserRole + 1, diff --git a/src/app/domain-settings-model.cpp b/src/app/domain-settings-model.cpp index dfc5796c3..95cd1cb33 100644 --- a/src/app/domain-settings-model.cpp +++ b/src/app/domain-settings-model.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #define CONNECTION_NAME "morph-browser-domainsettings" @@ -110,7 +111,7 @@ void DomainSettingsModel::createOrAlterDatabaseSchema() { QSqlQuery createQuery(m_database); QString query = QLatin1String("CREATE TABLE IF NOT EXISTS domainsettings " - "(domain VARCHAR NOT NULL UNIQUE, domainWithoutSubdomain VARCHAR, allowCustomUrlSchemes BOOL, allowLocation BOOL, " + "(domain VARCHAR NOT NULL UNIQUE, domainWithoutSubdomain VARCHAR, allowCustomUrlSchemes BOOL, allowLocation INTEGER, " "userAgentId INTEGER, zoomFactor REAL, PRIMARY KEY(domain), FOREIGN KEY(userAgentId) REFERENCES useragents(id)); "); createQuery.prepare(query); createQuery.exec(); @@ -129,7 +130,7 @@ void DomainSettingsModel::populateFromDatabase() entry.domain = populateQuery.value("domain").toString(); entry.domainWithoutSubdomain = populateQuery.value("domainWithoutSubdomain").toString(); entry.allowCustomUrlSchemes = populateQuery.value("allowCustomUrlSchemes").toBool(); - entry.allowLocation = populateQuery.value("allowLocation").toBool(); + entry.allowLocation = static_cast(populateQuery.value("allowLocation").toInt()); entry.userAgentId = populateQuery.value("userAgentId").toInt(); entry.zoomFactor = populateQuery.value("zoomFactor").isNull() ? std::numeric_limits::quiet_NaN() : populateQuery.value("zoomFactor").toDouble(); @@ -214,33 +215,33 @@ void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allo } } -bool DomainSettingsModel::isLocationAllowed(const QString& domain) const +DomainSettingsModel::AllowLocationPreference DomainSettingsModel::getLocationPreference(const QString& domain) const { int index = getIndexForDomain(domain); if (index == -1) { - return false; + return AllowLocationPreference::AskForLocationAccess; } return m_entries[index].allowLocation; } -void DomainSettingsModel::allowLocation(const QString& domain, bool allow) +void DomainSettingsModel::setLocationPreference(const QString& domain, DomainSettingsModel::AllowLocationPreference preference) { insertEntry(domain); int index = getIndexForDomain(domain); if (index != -1) { DomainSetting& entry = m_entries[index]; - if (entry.allowLocation == allow) { + if (entry.allowLocation == preference) { return; } - entry.allowLocation = allow; + entry.allowLocation = preference; Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector() << AllowLocation); QSqlQuery query(m_database); static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowLocation=? WHERE domain=?;"); query.prepare(updateStatement); - query.addBindValue(allow); + query.addBindValue(entry.allowLocation); query.addBindValue(domain); query.exec(); } @@ -345,7 +346,7 @@ void DomainSettingsModel::insertEntry(const QString &domain) entry.domain = domain; entry.domainWithoutSubdomain = DomainUtils::getDomainWithoutSubdomain(domain); entry.allowCustomUrlSchemes = false; - entry.allowLocation = false; + entry.allowLocation = AllowLocationPreference::AskForLocationAccess; entry.userAgentId = 0; entry.zoomFactor = std::numeric_limits::quiet_NaN(); m_entries.append(entry); diff --git a/src/app/domain-settings-model.h b/src/app/domain-settings-model.h index d1009a7f9..d3271584c 100644 --- a/src/app/domain-settings-model.h +++ b/src/app/domain-settings-model.h @@ -23,6 +23,7 @@ #include #include + class DomainSettingsModel : public QAbstractListModel { Q_OBJECT @@ -31,12 +32,19 @@ class DomainSettingsModel : public QAbstractListModel Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged) Q_PROPERTY(double defaultZoomFactor READ defaultZoomFactor WRITE setDefaultZoomFactor) + Q_ENUMS(AllowLocationPreference) Q_ENUMS(Roles) public: DomainSettingsModel(QObject* parent=0); ~DomainSettingsModel(); + enum AllowLocationPreference { + AskForLocationAccess = 0, + AllowLocationAccess = 1, + DenyLocationAccess = 2 + }; + enum Roles { Domain = Qt::UserRole + 1, DomainWithoutSubdomain, @@ -61,8 +69,8 @@ class DomainSettingsModel : public QAbstractListModel Q_INVOKABLE void deleteAndResetDataBase(); Q_INVOKABLE bool areCustomUrlSchemesAllowed(const QString& domain); Q_INVOKABLE void allowCustomUrlSchemes(const QString& domain, bool allow); - Q_INVOKABLE bool isLocationAllowed(const QString& domain) const; - Q_INVOKABLE void allowLocation(const QString& domain, bool allow); + Q_INVOKABLE AllowLocationPreference getLocationPreference(const QString& domain) const; + Q_INVOKABLE void setLocationPreference(const QString& domain, AllowLocationPreference preference); Q_INVOKABLE int getUserAgentId(const QString& domain) const; Q_INVOKABLE void setUserAgentId(const QString& domain, int userAgentId); Q_INVOKABLE void removeUserAgentIdFromAllDomains(int userAgentId); @@ -84,7 +92,7 @@ class DomainSettingsModel : public QAbstractListModel QString domain; QString domainWithoutSubdomain; bool allowCustomUrlSchemes; - bool allowLocation; + AllowLocationPreference allowLocation; int userAgentId; double zoomFactor; }; diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 8f2750e5d..565018faf 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -25,6 +25,8 @@ import Qt.labs.settings 1.0 import Morph.Web 0.1 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 +import Ubuntu.Content 1.3 +import QtQuick.Controls 2.2 as QQC2 import webbrowserapp.private 0.1 import webbrowsercommon.private 0.1 import "../actions" as Actions @@ -266,11 +268,27 @@ Common.BrowserView { // handle user agents if (isMainFrame) { - currentWebview.context.__ua.setDesktopMode(browser.settings ? browser.settings.setDesktopMode : false); - console.log("user agent: " + currentWebview.context.httpUserAgent); - } + currentWebview.hideContextMenu(); + var newUserAgentId = (UserAgentsModel.count > 0) ? DomainSettingsModel.getUserAgentId(requestDomain) : 0; + + // change of the custom user agent + if (newUserAgentId !== currentWebview.context.userAgentId) + { + currentWebview.context.userAgentId = newUserAgentId; + currentWebview.context.customUserAgent = (newUserAgentId > 0) ? UserAgentsModel.getUserAgentString(newUserAgentId) : ""; - //currentWebview.showMessage(url) + // for some reason when letting through the request, another navigation request will take us back to the + // to the previous page. Therefore we block it first and navigate to the new url with the correct user agent. + request.action = WebEngineNavigationRequest.IgnoreRequest; + currentWebview.url = url; + return; + } + else + { + currentWebview.context.__ua.setDesktopMode(browser.settings ? browser.settings.setDesktopMode : false); + console.log("user agent: " + currentWebview.context.httpUserAgent); + } + } } } @@ -639,24 +657,54 @@ Common.BrowserView { onClicked: recentView.closeAndSwitchToTab(0) } + + Row { + id: rightActionsRow + + spacing: units.gu(2) + height: parent.height - units.gu(2) - ToolbarAction { - objectName: "newTabButton" anchors { right: parent.right rightMargin: units.gu(2) verticalCenter: parent.verticalCenter } - height: parent.height - units.gu(2) - text: i18n.tr("New Tab") + ToolbarAction { + objectName: "reopenTabButton" + visible: !incognito && internal.closedTabHistory.length > 0 + anchors { + top: parent.top + bottom: parent.bottom + } + + text: i18n.tr("Reopen Tab") + + iconName: "undo" + color: theme.palette.normal.foregroundText + + onClicked: { + recentView.reset() + internal.undoCloseTab() + } + } + + ToolbarAction { + objectName: "newTabButton" + anchors { + top: parent.top + bottom: parent.bottom + } + + text: i18n.tr("New Tab") - iconName: browser.incognito ? "private-tab-new" : "add" - color: theme.palette.normal.foregroundText + iconName: browser.incognito ? "private-tab-new" : "add" + color: theme.palette.normal.foregroundText - onClicked: { - recentView.reset() - internal.openUrlInNewTab("", true) + onClicked: { + recentView.reset() + internal.openUrlInNewTab("", true) + } } } } @@ -716,6 +764,9 @@ Common.BrowserView { // QtWebEngine icons are provided as e.g. image://favicon/https://duckduckgo.com/favicon.ico else internal.addBookmark(tab.url, tab.title, (UrlUtils.schemeIs(tab.icon, "image") && UrlUtils.hostIs(tab.icon, "favicon")) ? tab.icon.toString().substring(("image://favicon/").length) : tab.icon) } + onToggleDownloads: { + internal.showDownloadsDialog() + } onWebviewChanged: bookmarked = isCurrentUrlBookmarked() Connections { target: chrome.tab @@ -859,16 +910,16 @@ Common.BrowserView { target: browser.currentWebview onLoadingChanged: { if (browser.currentWebview.loading && !recentView.visible) { - chrome.state = "shown" + chrome.state = "shown"; } else if (browser.currentWebview.isFullScreen) { chrome.state = "hidden" } } onIsFullScreenChanged: { if (browser.currentWebview.isFullScreen) { - chrome.state = "hidden" + chrome.state = "hidden"; } else { - chrome.state = "shown" + chrome.state = "shown"; } } } @@ -1352,10 +1403,14 @@ Common.BrowserView { if (tab) { if (!incognito && tab.url.toString().length > 0) { - closedTabHistory.push({ + + // For triggering property change on closedTabHistory + var temp = closedTabHistory.slice() + temp.push({ state: serializeTabState(tab), index: index }) + closedTabHistory = temp.slice() } // When moving a tab between windows don't close the tab as it has been moved @@ -1388,7 +1443,10 @@ Common.BrowserView { function undoCloseTab() { if (!incognito && closedTabHistory.length > 0) { - var tabInfo = closedTabHistory.pop() + // For triggering property change on closedTabHistory + var temp = closedTabHistory.slice() + var tabInfo = temp.pop() + closedTabHistory = temp.slice() var tab = restoreTabState(tabInfo.state) addTab(tab, true, tabInfo.index) tab.load() @@ -1539,6 +1597,57 @@ Common.BrowserView { internal.currentBookmarkOptionsDialog = PopupUtils.open(Qt.resolvedUrl("BookmarkOptions.qml"), location, properties) } + + property var recentDownloads: [] + property var currentDownloadsDialog: null + function showDownloadsDialog(caller) { + if (!internal.currentDownloadsDialog) { + chrome.downloadNotify = false + if (caller === undefined) caller = chrome.downloadsButtonPlaceHolder + var properties = {"downloadsList": recentDownloads} + + internal.currentDownloadsDialog = PopupUtils.open(Qt.resolvedUrl("../DownloadsDialog.qml"), + caller, properties) + } + } + + function addNewDownload(download) { + recentDownloads.unshift(download) + chrome.showDownloadButton = Qt.binding( + function(){ + if (browser.wide) { + return true; + } else { + if (internal.currentDownloadsDialog) { + if (internal.currentDownloadsDialog.isEmpty) { + return false; + } else { + return true; + } + } else { + if (recentDownloads.length > 0) { + return true; + } else { + return false; + } + } + } + }) + if (internal.currentDownloadsDialog) { + internal.currentDownloadsDialog.downloadsList = recentDownloads + } + } + } + + Connections { + target: internal.currentDownloadsDialog + + onShowDownloadsPage: showDownloadsPage() + + onPreview: { + PopupUtils.close(internal.currentDownloadsDialog); + currentWebview.url = url; + } } // Work around https://launchpad.net/bugs/1502675 by delaying the switch to @@ -1807,7 +1916,9 @@ Common.BrowserView { console.log("adding download with id " + downloadIdDataBase) Common.ActiveDownloadsSingleton.currentDownloads[downloadIdDataBase] = download DownloadsModel.add(downloadIdDataBase, (download.url.toString().indexOf("file://%1/pdf_tmp".arg(cacheLocation)) === 0) ? "" : download.url, download.path, download.mimeType, incognito) - downloadsViewLoader.active = true + + internal.addNewDownload(download) + internal.showDownloadsDialog() } function setDownloadComplete(download) { @@ -1828,6 +1939,10 @@ Common.BrowserView { { DownloadsModel.setError(downloadIdDataBase, download.interruptReasonString) } + + if (!internal.currentDownloadsDialog) { + chrome.downloadNotify = true + } } Connections { @@ -1850,14 +1965,12 @@ Common.BrowserView { console.log("a download was requested with path %1".arg(download.path)) download.accept(); - browser.showDownloadsPage(); browser.startDownload(download); } onDownloadFinished: { console.log(incognito? "download finished" : "a download was finished with path %1.".arg(download.path)); - browser.showDownloadsPage(); browser.setDownloadComplete(download); // delete pdf in cache / close the tab diff --git a/src/app/webbrowser/Chrome.qml b/src/app/webbrowser/Chrome.qml index 9ad0be601..8db1f7006 100644 --- a/src/app/webbrowser/Chrome.qml +++ b/src/app/webbrowser/Chrome.qml @@ -32,6 +32,9 @@ ChromeBase { property alias bookmarked: navigationBar.bookmarked signal closeTabRequested() signal toggleBookmark() + signal toggleDownloads() + property bool showDownloadButton: false + property bool downloadNotify: false property alias drawerActions: navigationBar.drawerActions property alias drawerOpen: navigationBar.drawerOpen property alias requestedUrl: navigationBar.requestedUrl @@ -45,6 +48,7 @@ ChromeBase { property alias showFaviconInAddressBar: navigationBar.showFaviconInAddressBar property alias availableHeight: navigationBar.availableHeight readonly property alias bookmarkTogglePlaceHolder: navigationBar.bookmarkTogglePlaceHolder + readonly property alias downloadsButtonPlaceHolder: navigationBar.downloadsButtonPlaceHolder property bool touchEnabled: true readonly property real tabsBarHeight: tabsBar.height + tabsBar.anchors.topMargin + content.anchors.topMargin property BrowserWindow thisWindow @@ -112,6 +116,8 @@ ChromeBase { loading: chrome.loading fgColor: theme.palette.normal.backgroundText iconColor: (incognito && !showTabsBar) ? theme.palette.normal.baseText : fgColor + showDownloadButton: chrome.showDownloadButton + downloadNotify: chrome.downloadNotify focus: true @@ -124,6 +130,7 @@ ChromeBase { onCloseTabRequested: chrome.closeTabRequested() onToggleBookmark: chrome.toggleBookmark() + onToggleDownloads: chrome.toggleDownloads() } } diff --git a/src/app/webbrowser/NavigationBar.qml b/src/app/webbrowser/NavigationBar.qml index c3dfd5a9c..b6df9a850 100644 --- a/src/app/webbrowser/NavigationBar.qml +++ b/src/app/webbrowser/NavigationBar.qml @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -import QtQuick 2.4 +import QtQuick 2.7 import Ubuntu.Components 1.3 -import QtWebEngine 1.7 +import QtWebEngine 1.10 import ".." FocusScope { @@ -31,6 +31,9 @@ FocusScope { property alias bookmarked: addressbar.bookmarked signal closeTabRequested() signal toggleBookmark() + signal toggleDownloads() + property bool showDownloadButton: false + property bool downloadNotify: false property list drawerActions readonly property bool drawerOpen: internal.openDrawer property alias requestedUrl: addressbar.requestedUrl @@ -42,6 +45,7 @@ FocusScope { property alias incognito: addressbar.incognito property alias showFaviconInAddressBar: addressbar.showFavicon readonly property alias bookmarkTogglePlaceHolder: addressbar.bookmarkTogglePlaceHolder + readonly property alias downloadsButtonPlaceHolder: downloadsButton property color fgColor: theme.palette.normal.baseText property color iconColor: theme.palette.normal.baseText property real availableHeight @@ -220,6 +224,66 @@ FocusScope { onTriggered: internal.webview.findController.next() } + ChromeButton { + id: downloadsButton + objectName: "downloadsButton" + + visible: showDownloadButton && !tabListMode + iconName: "save" + iconSize: 0.5 * height + iconColor: downloadNotify ? theme.palette.normal.focus : root.iconColor + + height: root.height + width: height * 0.8 + + anchors.verticalCenter: parent.verticalCenter + + Connections { + target: root + + onDownloadNotifyChanged: { + if (downloadNotify) { + shakeAnimation.start() + } + } + } + + Behavior on iconColor { + ColorAnimation { duration: UbuntuAnimation.BriskDuration } + } + + SequentialAnimation { + id: shakeAnimation + + loops: 4 + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 350 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Clockwise + to: 10 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 0 + duration: 50 + } + } + + onTriggered: { + toggleDownloads() + } + } + ChromeButton { id: drawerButton objectName: "drawerButton" diff --git a/src/app/webbrowser/TabItem.qml b/src/app/webbrowser/TabItem.qml index ed0a5e23d..43fd42a1f 100644 --- a/src/app/webbrowser/TabItem.qml +++ b/src/app/webbrowser/TabItem.qml @@ -115,15 +115,13 @@ Item { anchors.bottom: parent.bottom anchors.right: parent.right acceptedButtons: Qt.AllButtons - onPressed: { + + onClicked: { if (mouse.button === Qt.LeftButton) { tabItem.selected() } else if (mouse.button === Qt.RightButton) { tabItem.contextMenu() - } - } - onClicked: { - if ((mouse.buttons === 0) && (mouse.button === Qt.MiddleButton)) { + } else if ((mouse.buttons === 0) && (mouse.button === Qt.MiddleButton)) { tabItem.closed() } } diff --git a/src/app/webbrowser/TabPreview.qml b/src/app/webbrowser/TabPreview.qml index c5b2e6af6..099b2ac6b 100644 --- a/src/app/webbrowser/TabPreview.qml +++ b/src/app/webbrowser/TabPreview.qml @@ -24,7 +24,7 @@ QQC2.SwipeDelegate { id: tabPreview property alias title: chrome.title - property alias icon: chrome.icon + property alias tabIcon: chrome.icon property alias incognito: chrome.incognito property var tab readonly property url url: tab ? tab.url : "" @@ -32,7 +32,10 @@ QQC2.SwipeDelegate { background: Rectangle { color: "transparent" } - padding: 0 + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 swipe.enabled: true swipe.behind: Rectangle { width: tabPreview.width diff --git a/src/app/webbrowser/TabsList.qml b/src/app/webbrowser/TabsList.qml index 85dc6a2be..3786ba7a1 100644 --- a/src/app/webbrowser/TabsList.qml +++ b/src/app/webbrowser/TabsList.qml @@ -106,7 +106,7 @@ Item { sourceComponent: TabPreview { title: delegate.title - icon: delegate.icon + tabIcon: delegate.icon incognito: tabslist.incognito tab: model.tab diff --git a/src/app/webbrowser/UrlDelegate.qml b/src/app/webbrowser/UrlDelegate.qml index b1d51d6c6..ee44e2262 100644 --- a/src/app/webbrowser/UrlDelegate.qml +++ b/src/app/webbrowser/UrlDelegate.qml @@ -48,18 +48,18 @@ ListItem { Loader { id: headerComponentLoader - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter visible: status == Loader.Ready } Favicon { id: icon - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter } Column { Layout.fillWidth: true - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter Label { id: title diff --git a/src/app/webbrowser/morph-browser.qml b/src/app/webbrowser/morph-browser.qml index f84a6b2d4..119aae939 100644 --- a/src/app/webbrowser/morph-browser.qml +++ b/src/app/webbrowser/morph-browser.qml @@ -96,6 +96,8 @@ QtObject { BrowserWindow { id: window + color: "#111111" + property alias incognito: browser.incognito readonly property alias model: browser.tabsModel readonly property var tabsModel: browser.tabsModel diff --git a/src/app/webcontainer/Chrome.qml b/src/app/webcontainer/Chrome.qml index 83100a697..07d2ddc7a 100644 --- a/src/app/webcontainer/Chrome.qml +++ b/src/app/webcontainer/Chrome.qml @@ -26,6 +26,10 @@ ChromeBase { property var webview: null property bool navigationButtonsVisible: false property bool accountSwitcher: false + signal toggleDownloads() + property bool showDownloadButton: false + property bool downloadNotify: false + readonly property alias downloadsButtonPlaceHolder: downloadsButton loading: webview && webview.loading && webview.loadProgress !== 100 loadProgress: loading ? webview.loadProgress : 0 @@ -39,6 +43,8 @@ ChromeBase { reloadButton.iconColor = color; settingsButton.iconColor = color; accountsButton.iconColor = color; + + downloadsButton.iconColor = Qt.binding(function(){ return downloadNotify ? theme.palette.normal.focus : color}) } signal chooseAccount() @@ -141,7 +147,7 @@ ChromeBase { width: visible ? height : 0 anchors { - right: settingsButton.left + right: downloadsButton.left verticalCenter: parent.verticalCenter } @@ -149,6 +155,68 @@ ChromeBase { onTriggered: chrome.webview.reload() } + ChromeButton { + id: downloadsButton + objectName: "downloadsButton" + + visible: chrome.navigationButtonsVisible && showDownloadButton + iconName: "save" + iconSize: 0.6 * height + + height: parent.height + width: visible ? height : 0 + + anchors { + right: settingsButton.left + verticalCenter: parent.verticalCenter + } + + Connections { + target: root + + onDownloadNotifyChanged: { + if (downloadNotify) { + shakeAnimation.start() + } + } + } + + Behavior on iconColor { + ColorAnimation { duration: UbuntuAnimation.BriskDuration } + } + + SequentialAnimation { + id: shakeAnimation + + loops: 4 + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 350 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Clockwise + to: 10 + duration: 50 + } + + RotationAnimation { + target: downloadsButton + direction: RotationAnimation.Counterclockwise + to: 0 + duration: 50 + } + } + + onTriggered: { + toggleDownloads() + } + } + ChromeButton { id: settingsButton objectName: "settingsButton" diff --git a/src/app/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml index 81126ed43..3235af014 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -16,12 +16,13 @@ * along with this program. If not, see . */ -import QtQuick 2.5 +import QtQuick 2.7 import QtWebEngine 1.10 import Qt.labs.settings 1.0 import webbrowsercommon.private 0.1 import Morph.Web 0.1 import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 import Ubuntu.Unity.Action 1.1 as UnityActions import "../actions" as Actions import ".." as Common @@ -60,6 +61,9 @@ Common.BrowserView { property bool chromeVisible: false readonly property bool chromeless: !chromeVisible && !backForwardButtonsVisible && !accountSwitcher readonly property real themeColorTextContrastFactor: 3.0 + + property var recentDownloads: [] + property var currentDownloadsDialog: null signal chooseAccount() @@ -175,7 +179,14 @@ Common.BrowserView { console.log("adding download with id " + downloadIdDataBase) Common.ActiveDownloadsSingleton.currentDownloads[downloadIdDataBase] = download DownloadsModel.add(downloadIdDataBase, download.url, download.path, download.mimeType, false) - downloadsViewLoader.active = true + + addNewDownload(download) + + if (webapp.chromeless) { + showDownloadsDialog(anchorItem) + } else { + showDownloadsDialog() + } } function setDownloadComplete(download) { @@ -196,6 +207,62 @@ Common.BrowserView { { DownloadsModel.setError(downloadIdDataBase, download.interruptReasonString) } + + if (!currentDownloadsDialog && chromeLoader.item) { + if (!webapp.chromeless) { + chromeLoader.item.downloadNotify = true + + if (!chromeLoader.item.navigationButtonsVisible) { + showDownloadsDialog() + } + } else { + showDownloadsDialog(anchorItem) + } + } + } + + function showDownloadsDialog(caller) { + if (!currentDownloadsDialog && chromeLoader.item) { + if (!webapp.chromeless) { + chromeLoader.item.downloadNotify = false + if (caller === undefined) caller = chromeLoader.item.downloadsButtonPlaceHolder + } else { + if (caller === undefined) caller = webapp + } + var properties = {"downloadsList": recentDownloads} + currentDownloadsDialog = PopupUtils.open(Qt.resolvedUrl("../DownloadsDialog.qml"), + caller, properties) + } + } + + function addNewDownload(download) { + recentDownloads.unshift(download) + if (chromeLoader.item && !webapp.chromeless) { + chromeLoader.item.showDownloadButton = true + } + if (currentDownloadsDialog) { + currentDownloadsDialog.downloadsList = recentDownloads + } + } + + Connections { + target: currentDownloadsDialog + onShowDownloadsPage: showDownloadsPage() + onPreview: { + PopupUtils.close(currentDownloadsDialog); + webapp.currentWebview.url = url; + } + } + + /* Only used for anchoring the downloads dialog to the top when chromeless */ + Item { + id: anchorItem + anchors { + top: parent.top + right: parent.right + } + height: units.gu(2) + width: height } Item { @@ -306,6 +373,9 @@ Common.BrowserView { y: webapp.currentWebview ? containerWebView.currentWebview.locationBarController.offset : 0 onChooseAccount: webapp.chooseAccount() + onToggleDownloads: { + webapp.showDownloadsDialog() + } } } @@ -418,9 +488,9 @@ Common.BrowserView { onIsFullScreenChanged: { if (webapp.currentWebview.isFullScreen) { - chromeLoader.item.state = "hidden" + chromeLoader.item.state = "hidden"; } else { - chromeLoader.item.state === "shown" + chromeLoader.item.state === "shown"; } } } @@ -434,14 +504,12 @@ Common.BrowserView { console.log("a download was requested with path %1".arg(download.path)) download.accept(); - webapp.showDownloadsPage(); webapp.startDownload(download); } onDownloadFinished: { console.log("a download was finished with path %1.".arg(download.path)) - webapp.showDownloadsPage() webapp.setDownloadComplete(download) } } diff --git a/src/app/webcontainer/WebViewImplOxide.qml b/src/app/webcontainer/WebViewImplOxide.qml index fd2a04fcf..ba96ef33e 100644 --- a/src/app/webcontainer/WebViewImplOxide.qml +++ b/src/app/webcontainer/WebViewImplOxide.qml @@ -335,6 +335,30 @@ WebappWebview { return; } + // handle user agents + if (isMainFrame) + { + var newUserAgentId = (UserAgentsModel.count > 0) ? DomainSettingsModel.getUserAgentId(requestDomain) : 0; + + // change of the custom user agent + if (newUserAgentId !== webview.context.userAgentId) + { + webview.context.userAgentId = newUserAgentId; + webview.context.userAgent = (newUserAgentId > 0) ? UserAgentsModel.getUserAgentString(newUserAgentId) + : localUserAgentOverride ? localUserAgentOverride : webview.context.defaultUserAgent; + + // for some reason when letting through the request, another navigation request will take us back to the + // to the previous page. Therefore we block it first and navigate to the new url with the correct user agent. + request.action = WebEngineNavigationRequest.IgnoreRequest; + webview.url = url; + return; + } + else + { + console.log("user agent: " + webview.context.httpUserAgent); + } + } + if (runningLocalApplication && url.indexOf("file://") !== 0) { request.action = WebEngineNavigationRequest.IgnoreRequest; openUrlExternally(url, true);