diff --git a/autotests/tabsmodeltest.cpp b/autotests/tabsmodeltest.cpp index 959a5d2..707c064 100644 --- a/autotests/tabsmodeltest.cpp +++ b/autotests/tabsmodeltest.cpp @@ -1,132 +1,132 @@ /* * Copyright 2020 Jonah Brüchert * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include "tabsmodel.h" class TabsModelTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { m_tabsModel = new TabsModel(); } void testInitialTabExists() { QCOMPARE(m_tabsModel->tabs().count(), 1); // Current tab should be initial tab QCOMPARE(m_tabsModel->currentTab(), 0); QCOMPARE(m_tabsModel->tab(0).url(), "about:blank"); } void testNewTab() { m_tabsModel->newTab("https://kde.org"); QCOMPARE(m_tabsModel->tabs().count(), 2); qDebug() << m_tabsModel->tab(1).url() << m_tabsModel->tab(0).isMobile(); QCOMPARE(m_tabsModel->tab(1).url(), "https://kde.org"); // newly created tab should be current tab now QCOMPARE(m_tabsModel->currentTab(), 1); } void testCurrentTab() { QCOMPARE(m_tabsModel->tabs().at(m_tabsModel->currentTab()).url(), "https://kde.org"); } void testCloseTab() { // Close initial tab, keep kde.org one m_tabsModel->closeTab(0); QCOMPARE(m_tabsModel->tabs().count(), 1); // Check tabs moved properly QCOMPARE(m_tabsModel->tabs().at(0).url(), "https://kde.org"); } void testLoad() { - m_tabsModel->load("https://debian.org"); + m_tabsModel->setUrl(0, "https://debian.org"); // Number of tabs must not change QCOMPARE(m_tabsModel->tabs().count(), 1); QCOMPARE(m_tabsModel->tabs().at(0).url(), "https://debian.org"); } void testRowCountMatches() { QCOMPARE(m_tabsModel->tabs().count(), m_tabsModel->rowCount()); } void testCloseCurrentTab() { // // Case 1: There is only one tab, a new one should be created // QCOMPARE(m_tabsModel->tabs().count(), 1); m_tabsModel->setCurrentTab(0); m_tabsModel->closeTab(0); // Check whether a new empty tab was created (count must not be less than one) QCOMPARE(m_tabsModel->tabs().count(), 1); QCOMPARE(m_tabsModel->tabs().at(0).url(), "about:blank"); // // Case 2: There are multiple tabs // m_tabsModel->newTab("second"); m_tabsModel->newTab("third"); QCOMPARE(m_tabsModel->tabs(), QVector({TabState("about:blank", true), TabState("second", true), TabState("third", true)})); // current tab is 2 // close tab "second" m_tabsModel->closeTab(1); // current tab should now be 0, since we reset to first tab if the current tab is closed QCOMPARE(m_tabsModel->currentTab(), 0); // "second" is indeed gone QCOMPARE(m_tabsModel->tabs(), QVector({TabState("about:blank", true), TabState("third", true)})); } void testSetTab() { - m_tabsModel->setTab(0, QStringLiteral("https://debian.org")); + m_tabsModel->setUrl(0, QStringLiteral("https://debian.org")); QCOMPARE(m_tabsModel->tabs(), QVector({TabState("https://debian.org", true), TabState("third", true)})); } void testPrivateMode() { // private mode is off by default QCOMPARE(m_tabsModel->privateMode(), false); m_tabsModel->setPrivateMode(true); QCOMPARE(m_tabsModel->privateMode(), true); m_tabsModel->setPrivateMode(false); QCOMPARE(m_tabsModel->privateMode(), false); } private: TabsModel *m_tabsModel; }; QTEST_MAIN(TabsModelTest); #include "tabsmodeltest.moc" diff --git a/src/contents/ui/ListWebView.qml b/src/contents/ui/ListWebView.qml index d76b6e2..eb88c99 100644 --- a/src/contents/ui/ListWebView.qml +++ b/src/contents/ui/ListWebView.qml @@ -1,72 +1,83 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Controls 2.0 import QtQml.Models 2.1 - import QtWebEngine 1.6 + +import org.kde.kirigami 2.7 as Kirigami import org.kde.mobile.angelfish 1.0 Repeater { id: tabs clip: true - property bool activeTabs: true + property bool activeTabs: false property bool privateTabsMode: false property alias currentIndex: tabsModel.currentTab property var currentItem property alias tabsModel: tabsModel model: TabsModel { id: tabsModel + isMobileDefault: Kirigami.Settings.isMobile privateMode: privateTabsMode Component.onCompleted: tabsModel.loadInitialTabs() } delegate: WebView { id: webView anchors { bottom: tabs.bottom top: tabs.top } privateMode: tabs.privateTabsMode url: model.pageurl + userAgent.isMobile: model.isMobile width: tabs.width property bool showView: index === tabs.currentIndex visible: showView && tabs.activeTabs x: 0 onShowViewChanged: { if (showView) { tabs.currentItem = webView } } + onUrlChanged: { - tabs.tabsModel.setTab(index, url) + tabsModel.setUrl(index, url); + } + + Connections { + target: webView.userAgent + onUserAgentChanged: { + tabsModel.setIsMobile(index, webView.userAgent.isMobile); + } } } } diff --git a/src/contents/ui/Navigation.qml b/src/contents/ui/Navigation.qml index bc69752..4f3ee5e 100644 --- a/src/contents/ui/Navigation.qml +++ b/src/contents/ui/Navigation.qml @@ -1,205 +1,203 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Layouts 1.0 import QtWebEngine 1.4 import QtQuick.Controls 2.0 as Controls import org.kde.kirigami 2.5 as Kirigami import org.kde.mobile.angelfish 1.0 - - Item { id: navigation property bool navigationShown: true property int expandedHeight: Kirigami.Units.gridUnit * 3 property int buttonSize: Kirigami.Units.gridUnit * 2 signal activateUrlEntry; Behavior on height { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad} } Rectangle { anchors.fill: parent; color: Kirigami.Theme.backgroundColor; } RowLayout { id: layout anchors.fill: parent anchors.leftMargin: Kirigami.Units.gridUnit / 2 anchors.rightMargin: Kirigami.Units.gridUnit / 2 visible: navigationShown spacing: Kirigami.Units.smallSpacing Kirigami.Theme.inherit: true Controls.ToolButton { icon.name: "open-menu-symbolic" Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize Kirigami.Theme.inherit: true onClicked: globalDrawer.open() } Controls.ToolButton { icon.name: "tab-duplicate" Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize Kirigami.Theme.inherit: true onClicked: { pageStack.push(Qt.resolvedUrl("Tabs.qml")) } } Controls.ToolButton { id: backButton Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize visible: currentWebView.canGoBack && !Kirigami.Settings.isMobile icon.name: "go-previous" Kirigami.Theme.inherit: true onClicked: currentWebView.goBack() } Controls.ToolButton { id: forwardButton Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize visible: currentWebView.canGoForward && !Kirigami.Settings.isMobile icon.name: "go-next" Kirigami.Theme.inherit: true onClicked: currentWebView.goForward() } Item { id: labelItem Layout.fillWidth: true Layout.preferredHeight: layout.height property string scheme: UrlUtils.urlScheme(currentWebView.url) Controls.ToolButton { id: schemeIcon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter icon.name: { if (labelItem.scheme === "https") return "lock"; if (labelItem.scheme === "http") return "unlock"; return ""; } visible: icon.name height: buttonSize * 0.75 width: visible ? buttonSize * 0.75 : 0 Kirigami.Theme.inherit: true background: Rectangle { implicitWidth: schemeIcon.width implicitHeight: schemeIcon.height color: "transparent" } } Controls.Label { anchors.left: schemeIcon.right anchors.right: parent.right anchors.top: parent.top height: parent.height text: { if (labelItem.scheme === "http" || labelItem.scheme === "https") { var h = UrlUtils.urlHostPort(currentWebView.url); var p = UrlUtils.urlPath(currentWebView.url); if (p === "/") p = "" return '%1%2'.arg(h).arg(p); } return currentWebView.url; } textFormat: Text.StyledText elide: Text.ElideRight verticalAlignment: Text.AlignVCenter } MouseArea { anchors.fill: parent onClicked: activateUrlEntry() } } Controls.ToolButton { id: reloadButton Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize visible: !Kirigami.Settings.isMobile icon.name: currentWebView.loading ? "process-stop" : "view-refresh" Kirigami.Theme.inherit: true onClicked: currentWebView.loading ? currentWebView.stop() : currentWebView.reload() } Controls.ToolButton { id: optionsButton property string targetState: "overview" Layout.fillWidth: false Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize icon.name: "overflow-menu" Kirigami.Theme.inherit: true onClicked: contextDrawer.open() } } states: [ State { name: "shown" when: navigationShown PropertyChanges { target: navigation; height: expandedHeight} }, State { name: "hidden" when: !navigationShown PropertyChanges { target: navigation; height: 0} } ] } diff --git a/src/contents/ui/NavigationEntrySheet.qml b/src/contents/ui/NavigationEntrySheet.qml index 916ba56..3914ad6 100644 --- a/src/contents/ui/NavigationEntrySheet.qml +++ b/src/contents/ui/NavigationEntrySheet.qml @@ -1,155 +1,155 @@ /*************************************************************************** * * * Copyright 2019 Simon Schmeisser * * Copyright 2019 Jonah Brüchert * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.7 import QtQuick.Controls 2.2 as Controls import QtQuick.Layouts 1.2 import org.kde.kirigami 2.5 as Kirigami import org.kde.mobile.angelfish 1.0 import "regex-weburl.js" as RegexWebUrl Controls.Drawer { id: overlay dragMargin: 0 edge: Qt.BottomEdge width: parent.width bottomPadding: 0 topPadding: 0 rightPadding: 0 leftPadding: 0 property int buttonSize: Kirigami.Units.gridUnit * 2 property int fullHeight: 0.9 * rootPage.height property bool openedState: false property Item urlInput property Item listView contentHeight: fullHeight - topPadding - bottomPadding contentWidth: parent.width - rightPadding - leftPadding contentItem: Item { width: parent.width height: parent.height RowLayout { id: editRow anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter height: Kirigami.Units.gridUnit * 3 width: parent.width - Kirigami.Units.gridUnit Controls.ToolButton { Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize icon.name: "window-minimize" Kirigami.Theme.inherit: true onClicked: overlay.close() } Controls.TextField { id: urlInput Layout.fillWidth: true clip: true focus: false text: currentWebView.url selectByMouse: true Kirigami.Theme.inherit: true onActiveFocusChanged: if (activeFocus) selectAll() onAccepted: applyUrl() onTextChanged: urlFilter.setFilterFixedString(text) Keys.onEscapePressed: if (overlay.sheetOpen) overlay.close() Component.onCompleted: overlay.urlInput = urlInput function applyUrl() { if (text.match(RegexWebUrl.re_weburl)) { - tabs.tabsModel.load(UrlUtils.urlFromUserInput(text)) + currentWebView.url = UrlUtils.urlFromUserInput(text); } else { - tabs.tabsModel.load(UrlUtils.urlFromUserInput(BrowserManager.searchBaseUrl + text)) + currentWebView.url = UrlUtils.urlFromUserInput(BrowserManager.searchBaseUrl + text); } overlay.close(); } } Controls.ToolButton { Layout.preferredWidth: buttonSize Layout.preferredHeight: buttonSize icon.name: "go-next" Kirigami.Theme.inherit: true onClicked: urlInput.applyUrl(); } } ListView { id: listView anchors { bottom: parent.bottom left: parent.left right: parent.right top: editRow.bottom } boundsBehavior: Flickable.StopAtBounds clip: true delegate: UrlDelegate { showRemove: false onClicked: overlay.close() highlightText: urlInput.text width: parent.width } model: UrlFilterProxyModel { id: urlFilter sourceModel: BrowserManager.history } Component.onCompleted: overlay.listView = listView } } onOpened: { // check if the drawer was just slightly slided if (openedState) return; openedState = true; urlInput.text = currentWebView.url; urlInput.forceActiveFocus(); urlInput.selectAll(); listView.positionViewAtBeginning(); } onClosed: { openedState = false; currentWebView.forceActiveFocus(); } } diff --git a/src/contents/ui/Settings.qml b/src/contents/ui/Settings.qml index a096401..48657d2 100644 --- a/src/contents/ui/Settings.qml +++ b/src/contents/ui/Settings.qml @@ -1,126 +1,124 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Controls 2.4 as Controls import QtQuick.Layouts 1.11 import org.kde.kirigami 2.7 as Kirigami import org.kde.mobile.angelfish 1.0 -import org.kde.mobile.angelfish 1.0 - Kirigami.ScrollablePage { title: i18n("Settings") topPadding: 0 bottomPadding: 0 leftPadding: 0 rightPadding: 0 Kirigami.ColumnView.fillWidth: false background: Rectangle { Kirigami.Theme.colorSet: Kirigami.Theme.View color: Kirigami.Theme.backgroundColor } ColumnLayout { id: settingsPage spacing: 0 Kirigami.AbstractListItem { width: parent.width Controls.CheckBox { text: i18n("Enable JavaScript") Layout.fillWidth: true onCheckedChanged: { var settings = currentWebView.settings; settings.javascriptEnabled = checked; // FIXME: save to config } Component.onCompleted: { checked = currentWebView.settings.javascriptEnabled; } } } Kirigami.AbstractListItem { width: parent.width Controls.CheckBox { text: i18n("Load images") Layout.fillWidth: true onCheckedChanged: { var settings = currentWebView.settings; settings.autoLoadImages = checked; // FIXME: save to config } Component.onCompleted: { checked = currentWebView.settings.autoLoadImages; } } } InputSheet { id: homePagePopup title: i18n("Homepage") description: i18n("Website that should be loaded on startup") placeholderText: BrowserManager.homepage onAccepted: { if (homePagePopup.text !== "") BrowserManager.homepage = UrlUtils.urlFromUserInput(homePagePopup.text) } } InputSheet { id: searchEnginePopup title: i18n("Search Engine") description: i18n("Base URL of your preferred search engine") placeholderText: BrowserManager.searchBaseUrl onAccepted: { if (searchEnginePopup.text !== "") BrowserManager.searchBaseUrl = UrlUtils.urlFromUserInput(searchEnginePopup.text); } } Kirigami.BasicListItem { text: i18n("Homepage") Layout.fillWidth: true onClicked: { homePagePopup.open() } } Kirigami.BasicListItem { text: i18n("Search Engine") Layout.fillWidth: true onClicked: { searchEnginePopup.open() } } Item { Layout.fillHeight: true } } } diff --git a/src/contents/ui/Tabs.qml b/src/contents/ui/Tabs.qml index e653ed0..488da68 100644 --- a/src/contents/ui/Tabs.qml +++ b/src/contents/ui/Tabs.qml @@ -1,230 +1,230 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Controls 2.0 as Controls import QtGraphicalEffects 1.0 //import QtWebEngine 1.0 import QtQuick.Layouts 1.0 import org.kde.kirigami 2.7 as Kirigami import org.kde.mobile.angelfish 1.0 Kirigami.ScrollablePage { id: tabsRoot title: rootPage.privateMode ? i18n("Private Tabs") : i18n("Tabs") leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 Kirigami.ColumnView.fillWidth: false actions.main: Kirigami.Action { icon.name: "list-add" text: i18n("New") onTriggered: { // Somewhat weird behaviour, consider always just opening "about:blank" - tabs.tabsModel.newTab(rootPage.privateMode ? "about:blank" : BrowserManager.homepage, Kirigami.Settings.isMobile) + tabs.tabsModel.newTab(rootPage.privateMode ? "about:blank" : BrowserManager.homepage) pageStack.pop() } } property int itemHeight: Kirigami.Units.gridUnit * 6 property int itemWidth: { if (!landscapeMode) return width; // using grid width to take into account its scrollbar var n = Math.floor((grid.width - Kirigami.Units.largeSpacing) / (landscapeMinWidth + Kirigami.Units.largeSpacing)); return Math.floor(grid.width / n) - Kirigami.Units.largeSpacing; } property int landscapeMinWidth: Kirigami.Units.gridUnit * 12 property bool landscapeMode: grid.width > landscapeMinWidth * 2 + 3 * Kirigami.Units.largeSpacing //Rectangle { anchors.fill: parent; color: "brown"; opacity: 0.5; } GridView { id: grid anchors.fill: parent anchors.bottomMargin: Kirigami.Units.largeSpacing anchors.leftMargin: landscapeMode ? Kirigami.Units.largeSpacing / 2 : 0 // second half comes from item anchors.rightMargin: landscapeMode ? Kirigami.Units.largeSpacing / 2 : 0 // second half comes from item anchors.topMargin: Kirigami.Units.largeSpacing model: tabs.model cellWidth: itemWidth + (landscapeMode ? Kirigami.Units.largeSpacing : 0) cellHeight: itemHeight + Kirigami.Units.largeSpacing delegate: Item { // taking care of spacing width: grid.cellWidth height: grid.cellHeight Item { id: tabItem anchors.centerIn: parent width: itemWidth height: itemHeight Image { anchors.fill: parent clip: true fillMode: Image.PreserveAspectCrop source: tabs.itemAt(index) ? tabs.itemAt(index).thumb.source : "" verticalAlignment: Image.AlignTop LinearGradient { id: grad anchors.fill: parent cached: true start: Qt.point(0,0) end: Qt.point(0,height) gradient: Gradient { GradientStop { position: Math.max(0.25, 1 - 1.5 * (1 - label.y / itemHeight)); color: "transparent"; } GradientStop { position: Math.max(0.25, label.y / itemHeight); color: Kirigami.Theme.backgroundColor; } } } } // ShaderEffectSource requires that corresponding WebEngineView is // visible. Which is probably not the best practice as it seems to keep // all the views active. // ShaderEffectSource { // id: shaderItem // //live: true // anchors.fill: parent // sourceRect: Qt.rect(0, 0, width, height) // sourceItem: { // tabs.itemAt(index); // } // //opacity: tabs.currentIndex == index ? 1 : 0.0 // Behavior on height { // SequentialAnimation { // ScriptAction { // script: { // print("Animation start"); // // switch to tabs // } // } // NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } // NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad; target: contentView; property: opacity } // ScriptAction { // script: { // print("Animation done"); // contentView.state = "hidden" // } // } // } // } // Behavior on width { // NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad} // } // LinearGradient { // id: grad // anchors.fill: parent // cached: true // start: Qt.point(0,0) // end: Qt.point(0,height) // gradient: Gradient { // GradientStop { position: Math.max(0.25, 1 - 1.5*(1-label.y/itemHeight)); color: "transparent"; } // GradientStop { position: Math.max(0.25, label.y/itemHeight); color: Kirigami.Theme.backgroundColor; } // } // } // } Rectangle { // border around a tile anchors.fill: parent; border.color: Kirigami.Theme.textColor border.width: webBrowser.borderWidth color: "transparent" opacity: tabs.currentIndex === index ? 0.75 : 0.3 } Rectangle { // selection indicator anchors.fill: parent color: mouse.pressed ? Kirigami.Theme.highlightColor : "transparent" opacity: 0.2 } MouseArea { id: mouse anchors.fill: parent onClicked: { print("Switch from " + tabs.currentIndex + " to tab " + index); tabs.currentIndex = index; pageStack.pop() } } Controls.ToolButton { icon.name: "window-close" height: Kirigami.gridUnit width: height anchors.right: parent.right anchors.rightMargin: Kirigami.Units.smallSpacing + Kirigami.Units.largeSpacing + (tabsRoot.landscapeMode ? 0 : tabsRoot.width-grid.width) anchors.top: parent.top anchors.topMargin: Kirigami.Units.smallSpacing onClicked: tabs.tabsModel.closeTab(index) } Column { id: label anchors { left: tabItem.left right: tabItem.right bottom: tabItem.bottom bottomMargin: Kirigami.Units.smallSpacing leftMargin: Kirigami.Units.largeSpacing rightMargin: Kirigami.Units.largeSpacing } spacing: 0 Kirigami.Heading { elide: Text.ElideRight level: 4 text: tabs.itemAt(index) != null ? tabs.itemAt(index).title : "" width: label.width } Controls.Label { elide: Text.ElideRight font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.5 text: tabs.itemAt(index) != null ? tabs.itemAt(index).url : "" width: label.width } } } } } Component.onCompleted: grid.currentIndex = tabs.currentIndex } diff --git a/src/contents/ui/UrlDelegate.qml b/src/contents/ui/UrlDelegate.qml index 18497eb..c028b17 100644 --- a/src/contents/ui/UrlDelegate.qml +++ b/src/contents/ui/UrlDelegate.qml @@ -1,97 +1,95 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Controls 2.0 as Controls import QtQuick.Layouts 1.3 import org.kde.kirigami 2.5 as Kirigami Kirigami.SwipeListItem { id: urlDelegate property bool showRemove: true property string highlightText property var regex: new RegExp(highlightText, 'i') property string highlightedText: "$&" height: Kirigami.Units.gridUnit * 3 Kirigami.Theme.colorSet: Kirigami.Theme.View onClicked: { - tabs.tabsModel.load(url) - //tabs.newTab(url) - //contentView.state = "hidden" + currentWebView.url = url; } signal removed RowLayout { Kirigami.Theme.inherit: true Item { Layout.preferredHeight: parent.height Layout.preferredWidth: parent.height Image { anchors.fill: parent fillMode: Image.PreserveAspectFit source: model.icon ? model.icon : "" } Image { source: preview == undefined ? "" : preview } } ColumnLayout { Layout.fillWidth: true // title Controls.Label { text: title ? title.replace(regex, highlightedText) : "" elide: Qt.ElideRight maximumLineCount: 1 Layout.fillWidth: true } // url Controls.Label { text: url ? url.replace(regex, highlightedText) : "" opacity: 0.6 elide: Qt.ElideRight maximumLineCount: 1 Layout.fillWidth: true } } } actions: [ Kirigami.Action { icon.name: "list-remove" visible: urlDelegate.showRemove onTriggered: urlDelegate.removed(); } ] } diff --git a/src/contents/ui/WebView.qml b/src/contents/ui/WebView.qml index 3c8411a..89498fb 100644 --- a/src/contents/ui/WebView.qml +++ b/src/contents/ui/WebView.qml @@ -1,253 +1,252 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.3 import QtQuick.Controls 2.4 as Controls import QtQuick.Window 2.1 import QtQuick.Layouts 1.3 import QtWebEngine 1.7 import org.kde.kirigami 2.4 as Kirigami import org.kde.mobile.angelfish 1.0 WebEngineView { id: webEngineView property string errorCode: "" property string errorString: "" property bool privateMode: false property alias userAgent: userAgent property alias thumb: thumb - property bool reloadOnVisible: false + property bool reloadOnVisible: true UserAgentGenerator { id: userAgent - isMobile: model.isMobile - onUserAgentChanged: tabs.tabsModel.setTab(index, webEngineView.url, isMobile) + onUserAgentChanged: webEngineView.reload() } Image { id: thumb visible: false } Timer { id: snaphotTimer interval: 1000 repeat: false onTriggered: { if (webEngineView.visible) grabThumb(); } } profile { offTheRecord: privateMode httpUserAgent: userAgent.userAgent onDownloadRequested: { // if we don't accept the request right away, it will be deleted download.accept() // therefore just stop the download again as quickly as possible, // and ask the user for confirmation download.pause() questionLoader.setSource("DownloadQuestion.qml") questionLoader.item.download = download questionLoader.item.visible = true } onDownloadFinished: { if (download.state === WebEngineDownloadItem.DownloadCompleted) { showPassiveNotification(i18n("Download finished")) } else if (download.state === WebEngineDownloadItem.DownloadInterrupted) { showPassiveNotification(i18n("Download failed")) console.log("Download interrupt reason: " + download.interruptReason) } else if (download.state === WebEngineDownloadItem.DownloadCancelled) { console.log("Download cancelled by the user") } } } settings { // Disable builtin error pages in favor of our own errorPageEnabled: false // Disable scrollbars on mobile showScrollBars: !Kirigami.Settings.isMobile } // Custom context menu Controls.Menu { property var request id: contextMenu Controls.MenuItem { enabled: contextMenu.request != null && (contextMenu.request.editFlags & ContextMenuRequest.CanCopy) != 0 text: i18n("Copy") onTriggered: webEngineView.triggerWebAction(WebEngineView.Copy) } Controls.MenuItem { enabled: contextMenu.request != null && (contextMenu.request.editFlags & ContextMenuRequest.CanCut) != 0 text: i18n("Cut") onTriggered: webEngineView.triggerWebAction(WebEngineView.Cut) } Controls.MenuItem { enabled: contextMenu.request != null && (contextMenu.request.editFlags & ContextMenuRequest.CanPaste) != 0 text: i18n("Paste") onTriggered: webEngineView.triggerWebAction(WebEngineView.Paste) } Controls.MenuItem { enabled: contextMenu.request !== null && contextMenu.request.linkUrl !== "" text: i18n("Copy Url") onTriggered: webEngineView.triggerWebAction(WebEngineView.CopyLinkToClipboard) } Controls.MenuItem { text: i18n("View source") onTriggered: tabsModel.newTab("view-source:" + webEngineView.url) } Controls.MenuItem { text: i18n("Download") onTriggered: webEngineView.triggerWebAction(WebEngineView.DownloadLinkToDisk) } Controls.MenuItem { enabled: contextMenu.request !== null && contextMenu.request.linkUrl !== "" text: i18n("Open in new Tab") onTriggered: webEngineView.triggerWebAction(WebEngineView.OpenLinkInNewTab) } } focus: true onLoadingChanged: { //print("Loading: " + loading); print(" url: " + loadRequest.url) //print(" icon: " + webEngineView.icon) //print(" title: " + webEngineView.title) /* Handle * - WebEngineView::LoadStartedStatus, * - WebEngineView::LoadStoppedStatus, * - WebEngineView::LoadSucceededStatus and * - WebEngineView::LoadFailedStatus */ var ec = ""; var es = ""; if (loadRequest.status === WebEngineView.LoadStartedStatus) { thumb.source = ""; } if (loadRequest.status === WebEngineView.LoadSucceededStatus) { if (!privateMode) { addHistoryEntry(); } grabThumb(); } if (loadRequest.status === WebEngineView.LoadFailedStatus) { print("Load failed: " + loadRequest.errorCode + " " + loadRequest.errorString); ec = loadRequest.errorCode; es = loadRequest.errorString; thumb.source = ""; } errorCode = ec; errorString = es; } Component.onCompleted: { print("WebView completed."); var settings = webEngineView.settings; print("Settings: " + settings); } onIconChanged: { if (icon) BrowserManager.history.updateIcon(url, icon) } onNewViewRequested: { if (request.userInitiated) { tabsModel.newTab(request.requestedUrl.toString()) showPassiveNotification(i18n("Website was opened in a new tab")) } else { questionLoader.setSource("NewTabQuestion.qml") questionLoader.item.url = request.requestedUrl questionLoader.item.visible = true } } onUrlChanged: thumb.source = "" onFullScreenRequested: { request.accept() if (webBrowser.visibility !== Window.FullScreen) webBrowser.showFullScreen() else webBrowser.showNormal() } onContextMenuRequested: { request.accepted = true // Make sure QtWebEngine doesn't show its own context menu. contextMenu.request = request contextMenu.x = request.x contextMenu.y = request.y contextMenu.open() } onAuthenticationDialogRequested: { request.accepted = true sheetLoader.setSource("AuthSheet.qml") sheetLoader.item.request = request sheetLoader.item.open() } onFeaturePermissionRequested: { questionLoader.setSource("PermissionQuestion.qml") questionLoader.item.permission = feature questionLoader.item.origin = securityOrigin questionLoader.item.visible = true } onVisibleChanged: { // set user agent to the current displayed tab // this ensures that we follow mobile preference // of the current webview. also update the current // snapshot image with short delay to be sure that // all kirigami pages have moved into place if (visible) { profile.httpUserAgent = Qt.binding(function() { return userAgent.userAgent; }); snaphotTimer.start(); if (reloadOnVisible) { reloadOnVisible = false; reload(); } } } function grabThumb() { webEngineView.grabToImage(function(result) { webEngineView.thumb.source = result.url; }); } } diff --git a/src/contents/ui/webbrowser.qml b/src/contents/ui/webbrowser.qml index 2faefb8..a8cb660 100644 --- a/src/contents/ui/webbrowser.qml +++ b/src/contents/ui/webbrowser.qml @@ -1,352 +1,354 @@ /*************************************************************************** * * * Copyright 2014-2015 Sebastian Kügler * * * * This program 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; either version 2 of the License, or * * (at your option) any later version. * * * * This program 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * * * ***************************************************************************/ import QtQuick 2.1 import QtWebEngine 1.6 import QtQuick.Window 2.3 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.7 as Kirigami import org.kde.mobile.angelfish 1.0 import QtQuick.Layouts 1.2 Kirigami.ApplicationWindow { id: webBrowser title: i18n("Angelfish Web Browser") /** Pointer to the currently active view. * * Browser-level functionality should use this to refer to the current * view, rather than looking up views in the mode, as far as possible. */ property Item currentWebView: tabs.currentItem // Pointer to the currently active list of tabs. // // As there are private and normal tabs, switch between // them according to the current mode. property ListWebView tabs: rootPage.privateMode ? privateTabs : regularTabs onCurrentWebViewChanged: { print("Current WebView is now : " + tabs.currentIndex); } property int borderWidth: Math.round(Kirigami.Units.gridUnit / 18); property color borderColor: Kirigami.Theme.highlightColor; width: Kirigami.Units.gridUnit * 20 height: Kirigami.Units.gridUnit * 30 /** * Add page of currently active webview to history */ function addHistoryEntry() { //print("Adding history"); var request = new Object;// FIXME request.url = currentWebView.url; request.title = currentWebView.title; request.icon = currentWebView.icon; request.lastVisited = new Date(); BrowserManager.addToHistory(request); } pageStack.globalToolBar.showNavigationButtons: { if (pageStack.depth <= 1) return Kirigami.ApplicationHeaderStyle.None; if (pageStack.currentIndex === pageStack.depth - 1) return Kirigami.ApplicationHeaderStyle.ShowBackButton; // not used so far, but maybe in future return (Kirigami.ApplicationHeaderStyle.ShowBackButton | Kirigami.ApplicationHeaderStyle.ShowForwardButton); } globalDrawer: Kirigami.GlobalDrawer { id: globalDrawer handleVisible: false actions: [ Kirigami.Action { icon.name: "tab-duplicate" onTriggered: { pageStack.push(Qt.resolvedUrl("Tabs.qml")) } text: i18n("Tabs") }, Kirigami.Action { icon.name: "view-private" onTriggered: { rootPage.privateMode ? rootPage.privateMode = false : rootPage.privateMode = true } text: rootPage.privateMode ? i18n("Leave private mode") : i18n("Private mode") }, Kirigami.Action { icon.name: "bookmarks" onTriggered: { pageStack.push(Qt.resolvedUrl("Bookmarks.qml")) } text: i18n("Bookmarks") }, Kirigami.Action { icon.name: "view-history" onTriggered: { pageStack.push(Qt.resolvedUrl("History.qml")) } text: i18n("History") }, Kirigami.Action { icon.name: "configure" text: i18n("Settings") onTriggered: { pageStack.push(Qt.resolvedUrl("Settings.qml")) } } ] } contextDrawer: Kirigami.ContextDrawer { id: contextDrawer handleVisible: false } // Main Page pageStack.initialPage: Kirigami.Page { id: rootPage leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None Kirigami.ColumnView.fillWidth: true Kirigami.ColumnView.pinned: true Kirigami.ColumnView.preventStealing: true + // Required to enforce active tab reload + // on start. As a result, mixed isMobile + // tabs will work correctly + property bool initialized: false + property bool privateMode: false ListWebView { id: regularTabs anchors { top: parent.top left: parent.left right: parent.right bottom: navigation.top } - activeTabs: !rootPage.privateMode + activeTabs: rootPage.initialized && !rootPage.privateMode } ListWebView { id: privateTabs anchors { top: parent.top left: parent.left right: parent.right bottom: navigation.top } - activeTabs: rootPage.privateMode + activeTabs: rootPage.initialized && rootPage.privateMode privateTabsMode: true } ErrorHandler { id: errorHandler errorString: currentWebView.errorString errorCode: currentWebView.errorCode anchors { top: parent.top left: parent.left right: parent.right bottom: navigation.top } visible: currentWebView.errorCode !== "" } Loader { id: questionLoader anchors.bottom: navigation.top anchors.left: parent.left anchors.right: parent.right } // Container for the progress bar Item { id: progressItem height: Math.round(Kirigami.Units.gridUnit / 6) z: navigation.z + 1 anchors { top: tabs.bottom topMargin: -Math.round(height / 2) left: tabs.left right: tabs.right } opacity: currentWebView.loading ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad; } } Rectangle { color: Kirigami.Theme.highlightColor width: Math.round((currentWebView.loadProgress / 100) * parent.width) anchors { top: parent.top left: parent.left bottom: parent.bottom } } } Loader { id: sheetLoader } // The menu at the bottom right contextualActions: [ Kirigami.Action { icon.name: "edit-find" shortcut: "Ctrl+F" onTriggered: { if (!sheetLoader.item || !sheetLoader.item.sheetOpen) { sheetLoader.setSource("FindInPageSheet.qml") sheetLoader.item.open() } } text: i18n("Find in page") }, Kirigami.Action { icon.name: "document-share" text: i18n("Share page") onTriggered: { sheetLoader.setSource("ShareSheet.qml") sheetLoader.item.url = currentWebView.url sheetLoader.item.title = currentWebView.title sheetLoader.item.open() } }, Kirigami.Action { enabled: currentWebView.canGoBack icon.name: "go-previous" text: i18n("Go previous") onTriggered: { currentWebView.goBack() } }, Kirigami.Action { enabled: currentWebView.canGoForward icon.name: "go-next" text: i18n("Go forward") onTriggered: { currentWebView.goForward() } }, Kirigami.Action { icon.name: currentWebView.loading ? "process-stop" : "view-refresh" text: currentWebView.loading ? i18n("Stop loading") : i18n("Refresh") onTriggered: { currentWebView.loading ? currentWebView.stop() : currentWebView.reload() } }, Kirigami.Action { icon.name: "bookmarks" text: i18n("Add bookmark") onTriggered: { print("Adding bookmark"); var request = new Object;// FIXME request.url = currentWebView.url; request.title = currentWebView.title; request.icon = currentWebView.icon; request.bookmarked = true; BrowserManager.addBookmark(request); } }, Kirigami.Action { icon.name: "computer" text: i18n("Show desktop site") checkable: true checked: !currentWebView.userAgent.isMobile onTriggered: { - if (currentWebView.userAgent.isMobile) { - currentWebView.userAgent.isMobile = false - } else { - currentWebView.userAgent.isMobile = true - } - currentWebView.reload() + currentWebView.userAgent.isMobile = !currentWebView.userAgent.isMobile; } } ] // Bottom navigation bar Navigation { id: navigation navigationShown: !webappcontainer && webBrowser.visibility !== Window.FullScreen Kirigami.Theme.colorSet: rootPage.privateMode ? Kirigami.Theme.Complementary : Kirigami.Theme.Window layer.enabled: navigation.visible layer.effect: DropShadow { verticalOffset: - 1 color: Kirigami.Theme.disabledTextColor samples: 10 spread: 0.1 cached: true // element is static } anchors { bottom: parent.bottom left: parent.left right: parent.right } onActivateUrlEntry: urlEntry.open() } NavigationEntrySheet { id: urlEntry } // Thin line above navigation Rectangle { height: webBrowser.borderWidth color: webBrowser.borderColor anchors { left: parent.left bottom: navigation.top right: parent.right } } } Connections { target: webBrowser.pageStack onCurrentIndexChanged: { // drop all sub pages as soon as the browser window is the // focussed one if (webBrowser.pageStack.currentIndex === 0) webBrowser.pageStack.pop(); } } + + Component.onCompleted: rootPage.initialized = true } diff --git a/src/tabsmodel.cpp b/src/tabsmodel.cpp index 9de7723..1f793cd 100644 --- a/src/tabsmodel.cpp +++ b/src/tabsmodel.cpp @@ -1,383 +1,386 @@ /* * Copyright 2020 Jonah Brüchert * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "tabsmodel.h" #include #include #include #include #include #include #include "browsermanager.h" TabsModel::TabsModel(QObject *parent) : QAbstractListModel(parent) { connect(this, &TabsModel::currentTabChanged, [this] { qDebug() << "Current tab changed to" << m_currentTab; }); // The fallback tab must not be saved, it would overwrite our actual data. m_tabsReadOnly = true; // Make sure model always contains at least one tab createEmptyTab(); } QHash TabsModel::roleNames() const { return { { RoleNames::UrlRole, QByteArrayLiteral("pageurl") }, { RoleNames::IsMobileRole, QByteArrayLiteral("isMobile") } }; } QVariant TabsModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_tabs.count()) { return {}; } switch(role) { case RoleNames::UrlRole: return m_tabs.at(index.row()).url(); case RoleNames::IsMobileRole: return m_tabs.at(index.row()).isMobile(); } return {}; } int TabsModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : m_tabs.count(); } -/** - * @brief TabsModel::setTabUrl sets the properties of a tab at a given index. - * It should be used in cases were a reload of the web engine after the url change - * is not wanted, e.g if this function is already triggered by a load of the web engine. - * @param index - * @param url - * @param isMobile - */ -void TabsModel::setTab(int index, const QString &url, bool isMobile) -{ - if (index < 0 && index >= m_tabs.count()) - return; // index out of bounds - - m_tabs[index].setUrl(url); - m_tabs[index].setIsMobile(isMobile); - - saveTabs(); - - tabsChanged(); -} - /** * @brief TabsModel::tab returns the tab at the given index * @param index * @return tab at the index */ TabState TabsModel::tab(int index) { if (index < 0 && index >= m_tabs.count()) return {}; // index out of bounds return m_tabs.at(index); } /** * @brief TabsModel::loadInitialTabs sets up the tabs that should already be open when starting the browser * This includes the configured homepage, an url passed on the command line (usually by another app) and tabs * which were still open when the browser was last closed. * * @warning It is impossible to save any new tabs until this function was called. */ void TabsModel::loadInitialTabs() { if (!m_privateMode) { loadTabs(); } m_tabsReadOnly = false; if (!m_privateMode) { if (AngelFish::BrowserManager::instance()->initialUrl().isEmpty()) { if (m_tabs.first().url() == QStringLiteral("about:blank")) - load(AngelFish::BrowserManager::instance()->homepage()); + setUrl(0, AngelFish::BrowserManager::instance()->homepage()); } else { if (m_tabs.first().url() == QStringLiteral("about:blank")) - load(AngelFish::BrowserManager::instance()->initialUrl()); + setUrl(0, AngelFish::BrowserManager::instance()->initialUrl()); else newTab(AngelFish::BrowserManager::instance()->initialUrl()); } } } /** * @brief TabsModel::currentTab returns the index of the tab that is currently visible to the user * @return index */ int TabsModel::currentTab() const { return m_currentTab; } /** * @brief TabsModel::setCurrentTab sets the tab that is currently visible to the user * @param index */ void TabsModel::setCurrentTab(int index) { if (index >= m_tabs.count()) return; m_currentTab = index; currentTabChanged(); saveTabs(); } QVector TabsModel::tabs() const { return m_tabs; } /** * @brief TabsModel::loadTabs restores tabs saved in tabs.json * @return whether any tabs were restored */ bool TabsModel::loadTabs() { if (!m_privateMode) { beginResetModel(); QString input = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/angelfish/tabs.json"); QFile inputFile(input); if (!inputFile.exists()) { return false; } if (!inputFile.open(QIODevice::ReadOnly)) { qDebug() << "Failed to load tabs from disk"; } const auto tabsStorage = QJsonDocument::fromJson(inputFile.readAll()).object(); m_tabs.clear(); for (const auto tab : tabsStorage.value(QLatin1String("tabs")).toArray()) { m_tabs.append(TabState::fromJson(tab.toObject())); } qDebug() << "loaded from file:" << m_tabs.count() << input; m_currentTab = tabsStorage.value(QLatin1String("currentTab")).toInt(); // Make sure model always contains at least one tab if (m_tabs.count() == 0) { createEmptyTab(); } endResetModel(); - tabsChanged(); currentTabChanged(); - inputFile.close(); - return true; } return false; } /** * @brief TabsModel::saveTabs saves the current state of the model to disk * @return whether the tabs could be saved */ bool TabsModel::saveTabs() const { // only save if not in private mode if (!m_privateMode && !m_tabsReadOnly) { QString outputDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/angelfish/"); QFile outputFile(outputDir + QStringLiteral("tabs.json")); if (!QDir(outputDir).mkpath(".")) { qDebug() << "Destdir doesn't exist and I can't create it: " << outputDir; return false; } if (!outputFile.open(QIODevice::WriteOnly)) { qDebug() << "Failed to write tabs to disk"; } auto document = QJsonDocument(); auto tabsStorage = QJsonObject(); QJsonArray tabsArray; for (const auto &tab : m_tabs) { tabsArray.append(tab.toJson()); } qDebug() << "Wrote to file" << outputFile.fileName() << "(" << tabsArray.count() << "urls" << ")"; tabsStorage.insert(QLatin1String("tabs"), tabsArray); tabsStorage.insert(QLatin1String("currentTab"), m_currentTab); document.setObject(tabsStorage); outputFile.write(document.toJson()); - outputFile.close(); return true; } return false; } +bool TabsModel::isMobileDefault() const +{ + return m_isMobileDefault; +} + +void TabsModel::setIsMobileDefault(bool def) +{ + m_isMobileDefault = def; + isMobileDefaultChanged(); + + // used in initialization of the tab + if (m_tabs.count() == 1) { + setIsMobile(0, def); + } +} + bool TabsModel::privateMode() const { return m_privateMode; } void TabsModel::setPrivateMode(bool privateMode) { m_privateMode = privateMode; privateModeChanged(); } /** * @brief TabsModel::createEmptyTab convinience function for opening a tab containing "about:blank" */ void TabsModel::createEmptyTab() { newTab(QStringLiteral("about:blank")); }; /** * @brief TabsModel::newTab * @param url * @param isMobile */ -void TabsModel::newTab(const QString &url, bool isMobile) { +void TabsModel::newTab(const QString &url) { beginInsertRows({}, m_tabs.count(), m_tabs.count()); - m_tabs.append(TabState(url, isMobile)); + m_tabs.append(TabState(url, m_isMobileDefault)); endInsertRows(); // Switch to last tab m_currentTab = m_tabs.count() - 1; currentTabChanged(); + saveTabs(); } /** * @brief TabsModel::closeTab removes the tab at the index, handles moving the tabs after it and sets a new currentTab * @param index */ void TabsModel::closeTab(int index) { if (index < 0 && index >= m_tabs.count()) return; // index out of bounds if (m_tabs.count() <= 1) { // create new tab before removing the last one // to avoid linking all signals to null object createEmptyTab(); // now we have (tab_to_remove, "about:blank) // 0 will be the correct current tab index after tab_to_remove is gone m_currentTab = 0; // index to remove index = 0; } if (m_currentTab > index) { // decrease index if it's after the removed tab m_currentTab--; } if (m_currentTab == index) { // handle the removal of current tab // Just reset to first tab m_currentTab = 0; } beginRemoveRows({}, index, index); m_tabs.removeAt(index); endRemoveRows(); currentTabChanged(); saveTabs(); } +void TabsModel::setIsMobile(int index, bool isMobile) +{ + qDebug() << "Setting isMobile:" << index << isMobile << "tabs open" << m_tabs.count(); + if (index < 0 && index >= m_tabs.count()) + return; // index out of bounds + + m_tabs[index].setIsMobile(isMobile); + + QModelIndex mindex = createIndex(index, index); + dataChanged(mindex, mindex, { RoleNames::IsMobileRole }); + saveTabs(); +} -/** - * Load a url in the current tab - */ -void TabsModel::load(const QString &url) { - qDebug() << "Loading url:" << url; - qDebug() << "current tab" << m_currentTab << "tabs open" << m_tabs.count(); - m_tabs[m_currentTab].setUrl(url); +void TabsModel::setUrl(int index, const QString &url) +{ + qDebug() << "Setting URL:" << index << url << "tabs open" << m_tabs.count(); + if (index < 0 && index >= m_tabs.count()) + return; // index out of bounds - QModelIndex index = createIndex(m_currentTab, m_currentTab); - dataChanged(index, index); + m_tabs[index].setUrl(url); - tabsChanged(); + QModelIndex mindex = createIndex(index, index); + dataChanged(mindex, mindex, { RoleNames::UrlRole }); + saveTabs(); } QString TabState::url() const { return m_url; } void TabState::setUrl(const QString &url) { m_url = url; } bool TabState::isMobile() const { return m_isMobile; } void TabState::setIsMobile(bool isMobile) { m_isMobile = isMobile; } TabState TabState::fromJson(const QJsonObject &obj) { TabState tab; tab.setUrl(obj.value(QStringLiteral("url")).toString()); tab.setIsMobile(obj.value(QStringLiteral("isMobile")).toBool()); return tab; } TabState::TabState(const QString &url, const bool isMobile) { setIsMobile(isMobile); setUrl(url); } bool TabState::operator==(const TabState &other) const { return (m_url == other.url() && m_isMobile == other.isMobile()); } QJsonObject TabState::toJson() const { QJsonObject obj; obj.insert(QStringLiteral("url"), m_url); obj.insert(QStringLiteral("isMobile"), m_isMobile); return obj; } diff --git a/src/tabsmodel.h b/src/tabsmodel.h index 12599d7..1248ce9 100644 --- a/src/tabsmodel.h +++ b/src/tabsmodel.h @@ -1,99 +1,105 @@ /* * Copyright 2020 Jonah Brüchert * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef TABSMODEL_H #define TABSMODEL_H #include #include class TabState { public: static TabState fromJson(const QJsonObject &obj); QJsonObject toJson() const; TabState() = default; TabState(const QString &url, const bool isMobile); bool operator==(const TabState &other) const; bool isMobile() const; void setIsMobile(bool isMobile); QString url() const; void setUrl(const QString &url); private: QString m_url; bool m_isMobile; }; class TabsModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int currentTab READ currentTab WRITE setCurrentTab NOTIFY currentTabChanged) + Q_PROPERTY(bool isMobileDefault READ isMobileDefault WRITE setIsMobileDefault NOTIFY isMobileDefaultChanged) Q_PROPERTY(bool privateMode READ privateMode WRITE setPrivateMode NOTIFY privateModeChanged) enum RoleNames { UrlRole = Qt::UserRole + 1, IsMobileRole }; public: explicit TabsModel(QObject *parent = nullptr); QHash roleNames() const override; QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int currentTab() const; void setCurrentTab(int index); QVector tabs() const; - Q_INVOKABLE void setTab(int index, const QString &url, bool isMobile = true); Q_INVOKABLE TabState tab(int index); Q_INVOKABLE void loadInitialTabs(); - Q_INVOKABLE void newTab(const QString &url, bool isMobile = true); + Q_INVOKABLE void newTab(const QString &url); Q_INVOKABLE void createEmptyTab(); Q_INVOKABLE void closeTab(int index); - Q_INVOKABLE void load(const QString &url); + + Q_INVOKABLE void setUrl(int index, const QString &url); + Q_INVOKABLE void setIsMobile(int index, bool isMobile); + + bool isMobileDefault() const; + void setIsMobileDefault(bool def); bool privateMode() const; void setPrivateMode(bool privateMode); protected: bool loadTabs(); bool saveTabs() const; private: int m_currentTab = 0; QVector m_tabs {}; bool m_privateMode = false; bool m_tabsReadOnly = false; + bool m_isMobileDefault = false; signals: void currentTabChanged(); - void tabsChanged(); + void isMobileDefaultChanged(); void privateModeChanged(); }; #endif // TABSMODEL_H