From fe0511df39e5cba3c1bae04a0d7b640c7760d94a Mon Sep 17 00:00:00 2001 From: Rodney Dawes Date: Wed, 12 Aug 2020 10:28:34 -0400 Subject: [PATCH 01/19] Fix errors when running against Qt 5.12. --- src/Morph/Web/MorphWebView.qml | 2 +- src/app/ChromeBase.qml | 7 +++++-- src/app/webbrowser/NavigationBar.qml | 4 ++-- src/app/webbrowser/TabPreview.qml | 2 +- src/app/webbrowser/TabsList.qml | 2 +- src/app/webbrowser/UrlDelegate.qml | 6 +++--- src/app/webcontainer/WebApp.qml | 2 +- 7 files changed, 14 insertions(+), 11 deletions(-) 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/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/webbrowser/NavigationBar.qml b/src/app/webbrowser/NavigationBar.qml index 7b31d4000..a53484786 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 { diff --git a/src/app/webbrowser/TabPreview.qml b/src/app/webbrowser/TabPreview.qml index c5b2e6af6..ee90421f4 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 : "" 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/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml index 81126ed43..2a635c4d6 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -16,7 +16,7 @@ * 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 From 3c9a54ab0dd7a7364fd9b6729303f073a7083025 Mon Sep 17 00:00:00 2001 From: Marius Gripsgard Date: Tue, 8 Sep 2020 20:43:02 +0200 Subject: [PATCH 02/19] Add missing include for std::isnan --- src/app/domain-settings-model.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/domain-settings-model.cpp b/src/app/domain-settings-model.cpp index dfc5796c3..a77f85360 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" From 7949181c492ced30054e279e19b96b3ce7f08334 Mon Sep 17 00:00:00 2001 From: Marius Gripsgard Date: Wed, 9 Sep 2020 01:22:02 +0200 Subject: [PATCH 03/19] Make sure to register qml type --- src/Morph/Web/plugin.cpp | 1 + 1 file changed, 1 insertion(+) 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" From 15cde6abdae02638f02128f70ed391699054192f Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Sun, 26 Jul 2020 16:52:08 +0200 Subject: [PATCH 04/19] Update ZoomControls.qml workaround for zoom changes (https://bugreports.qt.io/browse/QTBUG-84313) --- src/app/ZoomControls.qml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/app/ZoomControls.qml b/src/app/ZoomControls.qml index 8d81a8b5f..6dd9865d7 100644 --- a/src/app/ZoomControls.qml +++ b/src/app/ZoomControls.qml @@ -175,7 +175,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 +286,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 +325,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 +346,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 +357,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 +413,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) { From 2bcc6ab2eb522cadd8b5dae5793cf67e1bf05b0b Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Thu, 30 Jul 2020 16:39:29 +0200 Subject: [PATCH 05/19] set zoom factor to 1.0 in fullscreen mode --- src/app/WebViewImpl.qml | 1 + src/app/ZoomControls.qml | 4 ++++ src/app/webbrowser/Browser.qml | 9 +++++---- src/app/webcontainer/WebApp.qml | 5 +++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml index 25fd571a6..2ea338767 100644 --- a/src/app/WebViewImpl.qml +++ b/src/app/WebViewImpl.qml @@ -702,6 +702,7 @@ WebView { } onFullScreenRequested: function(request) { + webview.zoomFactor = 1.0; request.accept(); } diff --git a/src/app/ZoomControls.qml b/src/app/ZoomControls.qml index 6dd9865d7..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: { diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 26fa7fc42..4f2c85aab 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -850,16 +850,17 @@ 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" + chrome.state = "hidden"; } } onIsFullScreenChanged: { if (browser.currentWebview.isFullScreen) { - chrome.state = "hidden" + chrome.state = "hidden"; } else { - chrome.state = "shown" + browser.currentWebview.zoomController.refresh(); + chrome.state = "shown"; } } } diff --git a/src/app/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml index 2a635c4d6..66ff70d9e 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -418,9 +418,10 @@ Common.BrowserView { onIsFullScreenChanged: { if (webapp.currentWebview.isFullScreen) { - chromeLoader.item.state = "hidden" + chromeLoader.item.state = "hidden"; } else { - chromeLoader.item.state === "shown" + webapp.currentWebview.zoomController.refresh(); + chromeLoader.item.state === "shown"; } } } From 03de1d2848856cd24f840237d634b611078c80ff Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Fri, 31 Jul 2020 14:48:55 +0200 Subject: [PATCH 06/19] set zoomfactor for fullscreen twice because of QTBUG-84313 --- src/app/WebViewImpl.qml | 2 ++ src/app/webbrowser/Browser.qml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml index 2ea338767..e282466cb 100644 --- a/src/app/WebViewImpl.qml +++ b/src/app/WebViewImpl.qml @@ -702,6 +702,8 @@ WebView { } onFullScreenRequested: function(request) { + // twice because of QTBUG-84313 + webview.zoomFactor = 1.0; webview.zoomFactor = 1.0; request.accept(); } diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 4f2c85aab..35256bdf4 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -852,7 +852,7 @@ Common.BrowserView { if (browser.currentWebview.loading && !recentView.visible) { chrome.state = "shown"; } else if (browser.currentWebview.isFullScreen) { - chrome.state = "hidden"; + chrome.state = "hidden" } } onIsFullScreenChanged: { From d9a92a8ed35b6aa90bf4df113e109f0bba3efec3 Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Sat, 1 Aug 2020 05:30:40 +0200 Subject: [PATCH 07/19] do the zoom restore on leaving fullscreen in SebViewImpl --- src/app/WebViewImpl.qml | 11 ++++++++--- src/app/webbrowser/Browser.qml | 1 - src/app/webcontainer/WebApp.qml | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml index e282466cb..cd6c05f70 100644 --- a/src/app/WebViewImpl.qml +++ b/src/app/WebViewImpl.qml @@ -702,9 +702,14 @@ WebView { } onFullScreenRequested: function(request) { - // twice because of QTBUG-84313 - webview.zoomFactor = 1.0; - webview.zoomFactor = 1.0; + if (webview.isFullSceeen) { + webview.zoomController.refresh(); + } + else { + // twice because of QTBUG-84313 + webview.zoomFactor = 1.0; + webview.zoomFactor = 1.0; + } request.accept(); } diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 35256bdf4..a1b80c592 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -859,7 +859,6 @@ Common.BrowserView { if (browser.currentWebview.isFullScreen) { chrome.state = "hidden"; } else { - browser.currentWebview.zoomController.refresh(); chrome.state = "shown"; } } diff --git a/src/app/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml index 66ff70d9e..9f7e0ae60 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -420,7 +420,6 @@ Common.BrowserView { if (webapp.currentWebview.isFullScreen) { chromeLoader.item.state = "hidden"; } else { - webapp.currentWebview.zoomController.refresh(); chromeLoader.item.state === "shown"; } } From a815d7db98718985e17ff6bee44c5a8e538b13a8 Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Sat, 12 Dec 2020 10:24:57 +0100 Subject: [PATCH 08/19] use toggleOn flag for zoom change on fullscreen request --- src/app/WebViewImpl.qml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml index cd6c05f70..85a76e3af 100644 --- a/src/app/WebViewImpl.qml +++ b/src/app/WebViewImpl.qml @@ -702,13 +702,12 @@ WebView { } onFullScreenRequested: function(request) { - if (webview.isFullSceeen) { - webview.zoomController.refresh(); - } - else { + if (request.toggleOn) { // twice because of QTBUG-84313 webview.zoomFactor = 1.0; webview.zoomFactor = 1.0; + } else { + webview.zoomController.refresh(); } request.accept(); } From 761d955e515710f8786d011044f45debf675b24e Mon Sep 17 00:00:00 2001 From: Kugi Eusebio Date: Tue, 8 Dec 2020 00:56:04 +0800 Subject: [PATCH 09/19] added reopen tab in toolbar --- src/app/webbrowser/Browser.qml | 59 +++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index a1b80c592..f921e82e5 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -630,24 +630,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 + } - iconName: browser.incognito ? "private-tab-new" : "add" - color: theme.palette.normal.foregroundText + text: i18n.tr("Reopen Tab") - onClicked: { - recentView.reset() - internal.openUrlInNewTab("", true) + 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 + + onClicked: { + recentView.reset() + internal.openUrlInNewTab("", true) + } } } } @@ -1343,10 +1373,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 @@ -1379,7 +1413,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() From a03abfb9f1be999df4dd0376ceaf427b587d19fe Mon Sep 17 00:00:00 2001 From: Kugi Eusebio Date: Fri, 29 Jan 2021 14:00:34 -0800 Subject: [PATCH 10/19] Fix ubports/ubuntu-touch#1344 for rotated contents on external displays (#430) Set automaticOrientation to false to fix ubports/ubuntu-touch#1344 Fixes https://github.com/ubports/ubuntu-touch/issues/1344 Fixes https://github.com/ubports/morph-browser/issues/445 --- src/app/BrowserView.qml | 2 ++ src/app/BrowserWindow.qml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) 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) From c83deabb40670bdf56c03c24479e447a506988d1 Mon Sep 17 00:00:00 2001 From: Kugi Eusebio Date: Wed, 23 Dec 2020 02:16:46 +0800 Subject: [PATCH 11/19] Fixed #426 --- src/app/webbrowser/TabItem.qml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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() } } From 92067e19542967be263762b895bffdf2efd0d561 Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Mon, 4 Jan 2021 10:08:48 +0100 Subject: [PATCH 12/19] Update browserapplication.cpp move setting of the suru style towards the top of the function --- src/app/browserapplication.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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); From 769cab7a7b5fd16e6ae063b08691065894e0919b Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Wed, 6 Jan 2021 17:46:52 +0100 Subject: [PATCH 13/19] adapt padding for Suru set left / right / top / bottom padding for Suru style according to https://github.com/ubports/morph-browser/pull/442#issuecomment-755411576 --- src/app/webbrowser/TabPreview.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/webbrowser/TabPreview.qml b/src/app/webbrowser/TabPreview.qml index ee90421f4..099b2ac6b 100644 --- a/src/app/webbrowser/TabPreview.qml +++ b/src/app/webbrowser/TabPreview.qml @@ -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 From f7d95206ae298775bcd654bedba98b654df4977c Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Wed, 1 Apr 2020 19:19:31 +0200 Subject: [PATCH 14/19] restore custom user agents (see if they work with QtWebEngine 5.14), which have been removed with commit 2cd10b497c704dc60ea8e6a7407e832e2a1dc28f --- src/Morph/Web/MorphWebContext.qml | 4 +++- src/app/DomainSettingsPage.qml | 6 ++---- src/app/webbrowser/Browser.qml | 17 ++++++++++++++--- src/app/webcontainer/WebViewImplOxide.qml | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 8 deletions(-) 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/app/DomainSettingsPage.qml b/src/app/DomainSettingsPage.qml index 21a67a747..f79aa270d 100644 --- a/src/app/DomainSettingsPage.qml +++ b/src/app/DomainSettingsPage.qml @@ -143,8 +143,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") @@ -232,8 +231,7 @@ FocusScope { Row { spacing: units.gu(1.5) height: units.gu(1) - // deactivated for now - visible: false //item.ListView.isCurrentItem + visible: item.ListView.isCurrentItem Label { text: i18n.tr("custom user agent") diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index f921e82e5..a9d708c2f 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -266,11 +266,22 @@ Common.BrowserView { // handle user agents if (isMainFrame) { - currentWebview.context.__ua.setDesktopMode(browser.settings ? browser.settings.setDesktopMode : false); + 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) : ""; + } + else + { + currentWebview.context.__ua.setDesktopMode(browser.settings ? browser.settings.setDesktopMode : false); + } + console.log("user agent: " + currentWebview.context.httpUserAgent); } - - //currentWebview.showMessage(url) } } diff --git a/src/app/webcontainer/WebViewImplOxide.qml b/src/app/webcontainer/WebViewImplOxide.qml index fd2a04fcf..e011c7c00 100644 --- a/src/app/webcontainer/WebViewImplOxide.qml +++ b/src/app/webcontainer/WebViewImplOxide.qml @@ -335,6 +335,21 @@ 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; + } + console.log("user agent: " + webview.context.httpUserAgent) + } + if (runningLocalApplication && url.indexOf("file://") !== 0) { request.action = WebEngineNavigationRequest.IgnoreRequest; openUrlExternally(url, true); From eb68996a3d4d7448099180bd736be88d55c40b52 Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Thu, 2 Apr 2020 18:28:58 +0200 Subject: [PATCH 15/19] make another navigation request after user agent changes (as in original commit) --- src/app/webbrowser/Browser.qml | 9 +++++++-- src/app/webcontainer/WebViewImplOxide.qml | 11 ++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index a9d708c2f..bb011b545 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -274,13 +274,18 @@ Common.BrowserView { { currentWebview.context.userAgentId = newUserAgentId; currentWebview.context.customUserAgent = (newUserAgentId > 0) ? UserAgentsModel.getUserAgentString(newUserAgentId) : ""; + + // 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); } - - console.log("user agent: " + currentWebview.context.httpUserAgent); } } } diff --git a/src/app/webcontainer/WebViewImplOxide.qml b/src/app/webcontainer/WebViewImplOxide.qml index e011c7c00..ba96ef33e 100644 --- a/src/app/webcontainer/WebViewImplOxide.qml +++ b/src/app/webcontainer/WebViewImplOxide.qml @@ -346,8 +346,17 @@ WebappWebview { 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); } - console.log("user agent: " + webview.context.httpUserAgent) } if (runningLocalApplication && url.indexOf("file://") !== 0) { From aab8799497812ee0d39941fcbe51b17a2efbc680 Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Thu, 11 Jun 2020 15:40:24 +0200 Subject: [PATCH 16/19] DomainSettings now can save "allow" and "deny" preference for location access (previously only "allow" was possible) --- src/app/DomainSettingsPage.qml | 44 ++++++++++++++++-------- src/app/GeolocationPermissionRequest.qml | 13 +++---- src/app/WebViewImpl.qml | 16 +++++++-- src/app/domain-permissions-model.h | 2 +- src/app/domain-settings-model.cpp | 18 +++++----- src/app/domain-settings-model.h | 14 ++++++-- 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/app/DomainSettingsPage.qml b/src/app/DomainSettingsPage.qml index f79aa270d..9895c6623 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 @@ -169,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 @@ -203,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 } } @@ -218,13 +222,16 @@ 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 } } @@ -234,8 +241,10 @@ FocusScope { 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 { @@ -260,6 +269,7 @@ FocusScope { DomainSettingsModel.setUserAgentId(model.domain, 0); } } + anchors.verticalCenter: parent.verticalCenter } } @@ -307,16 +317,22 @@ FocusScope { } } - // 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 + } } } } 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/WebViewImpl.qml b/src/app/WebViewImpl.qml index 85a76e3af..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: 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 a77f85360..95cd1cb33 100644 --- a/src/app/domain-settings-model.cpp +++ b/src/app/domain-settings-model.cpp @@ -111,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(); @@ -130,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(); @@ -215,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(); } @@ -346,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; }; From 13481a5b4c926134c5b22f17c7f6092973f72439 Mon Sep 17 00:00:00 2001 From: Chris Clime Date: Tue, 1 Dec 2020 17:59:34 +0100 Subject: [PATCH 17/19] - use Qt Quick Controls ComboBox for selecting the custom user agent in DomainSettingsPage.qml - edit custom user agent strings and names with new dialog EditCustomUserAgentDialog.qml --- src/app/CustomUserAgentsPage.qml | 133 +++++++------------------- src/app/DomainSettingsPage.qml | 46 +++------ src/app/EditCustomUserAgentDialog.qml | 73 ++++++++++++++ 3 files changed, 123 insertions(+), 129 deletions(-) create mode 100644 src/app/EditCustomUserAgentDialog.qml 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 9895c6623..a19c33c9e 100644 --- a/src/app/DomainSettingsPage.qml +++ b/src/app/DomainSettingsPage.qml @@ -252,17 +252,17 @@ 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 { @@ -273,13 +273,9 @@ FocusScope { } } - /* 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) @@ -289,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; } } } @@ -307,14 +302,12 @@ 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); } Row { @@ -367,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 { @@ -395,10 +381,6 @@ FocusScope { onReload: { customUserAgentsViewLoader.active = false; customUserAgentsViewLoader.active = true; - - if (selectedUserAgent) { - customUserAgentsViewLoader.item.setUserAgentAsCurrentItem(selectedUserAgent) - } } } } 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() + } + } +} From ee7fdcf6816dd4876eaeb405f6918fd008d584ed Mon Sep 17 00:00:00 2001 From: mateosalta Date: Sun, 3 Jan 2021 19:19:55 -0600 Subject: [PATCH 18/19] cover up scaling cracks make sure the window background underneath everything blends in with indicator bar. "painting" the cracks seen when qt scaling rounds poorly on certain gu sizes This happens on the volla phone default scale, and other devices or on different scaling. --- src/app/webbrowser/morph-browser.qml | 2 ++ 1 file changed, 2 insertions(+) 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 From f6f636ecfd38f378a172a4c9c944e39f60c80ca3 Mon Sep 17 00:00:00 2001 From: Kugi Eusebio Date: Wed, 3 Feb 2021 15:57:11 -0800 Subject: [PATCH 19/19] Downloads UX Changes (#415) Originally from #336 - Fixes #193 - Implemented recent downloads dialog in bot the browser and webapp. This is displayed when a new download started instead of the downloads page. - A downloads button is added in the navigation bar which wiggles and turns blue when a download completes. In webapps without the title bar, the recent downloads dialog is displayed instead - Only display the file names in the Downloads page instead of full path. - Added info such as file size and download speed in the Downloads page - Added actions such as copy download link and remove from history --- src/app/DownloadDelegate.qml | 204 ++++++++++++++++++--------- src/app/DownloadsDialog.qml | 196 +++++++++++++++++++++++++ src/app/DownloadsPage.qml | 34 +++-- src/app/FileUtils.js | 38 +++++ src/app/SimpleDownloadDelegate.qml | 164 +++++++++++++++++++++ src/app/webbrowser/Browser.qml | 66 ++++++++- src/app/webbrowser/Chrome.qml | 7 + src/app/webbrowser/NavigationBar.qml | 64 +++++++++ src/app/webcontainer/Chrome.qml | 70 ++++++++- src/app/webcontainer/WebApp.qml | 74 +++++++++- 10 files changed, 830 insertions(+), 87 deletions(-) create mode 100644 src/app/DownloadsDialog.qml create mode 100644 src/app/FileUtils.js create mode 100644 src/app/SimpleDownloadDelegate.qml 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/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/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/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index bb011b545..7402b4424 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 @@ -753,6 +755,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 @@ -1583,6 +1588,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 @@ -1851,7 +1907,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) { @@ -1872,6 +1930,10 @@ Common.BrowserView { { DownloadsModel.setError(downloadIdDataBase, download.interruptReasonString) } + + if (!internal.currentDownloadsDialog) { + chrome.downloadNotify = true + } } Connections { @@ -1894,14 +1956,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 a53484786..627941c18 100644 --- a/src/app/webbrowser/NavigationBar.qml +++ b/src/app/webbrowser/NavigationBar.qml @@ -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 @@ -237,6 +241,66 @@ FocusScope { onTriggered: closeTabRequested() } + + 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 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 9f7e0ae60..3235af014 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -22,6 +22,7 @@ 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() + } } } @@ -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) } }