diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 90def0605..d0bef8595 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -126,6 +126,13 @@ Common.BrowserView { internal.switchToTab(tabsModel.count - 1, true); } + function openRecentView() { + recentView.state = "shown" + if (!browser.wide) { + recentToolbar.state = "shown" + } + } + signal newWindowRequested(bool incognito) signal newWindowFromTab(var tab, var callback) signal openLinkInNewWindowRequested(url url, bool incognito) @@ -572,13 +579,18 @@ Common.BrowserView { id: recentView objectName: "recentView" + z: browser.wide ? 1 : 0 anchors.fill: parent visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown") onVisibleChanged: { if (visible) { - + forceActiveFocus() currentWebview.hideContextMenu(); - chrome.state = "hidden"; + tabslist.reset() + + if (!browser.wide) { + chrome.state = "hidden"; + } } else { chrome.state = "shown"; @@ -594,11 +606,60 @@ Common.BrowserView { internal.switchToTab(index, false) } - Keys.onEscapePressed: closeAndSwitchToTab(0) + Keys.onEscapePressed: { + if (browser.wide) { + recentView.reset() + } else { + closeAndSwitchToTab(0) + } + } + + Keys.onPressed: { + if (event.text.trim() !== "") { + tabslist.focusInput(); + tabslist.searchText = event.text; + } + switch (event.key) { + case Qt.Key_Right: + case Qt.Key_Left: + case Qt.Key_Down: + tabslist.view.forceActiveFocus() + break; + case Qt.Key_Up: + tabslist.focusInput(); + break; + } + + event.accepted = true; + } + + Rectangle { + id: backgroundRec + + anchors.fill: parent + color: UbuntuColors.jet + opacity: 0.5 + visible: browser.wide + + MouseArea { + anchors.fill: parent + preventStealing: true + onClicked: recentView.reset() + } + } TabsList { id: tabslist - anchors.fill: parent + + anchors { + top: parent.top + topMargin: !browser.wide ? 0 : + browser.height > units.gu(90) ? chrome.height : units.gu(2) + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + + width: browser.wide ? browser.width * 0.7 : parent.width model: tabsModel readonly property real delegateMinHeight: units.gu(20) delegateHeight: { @@ -628,8 +689,8 @@ Common.BrowserView { objectName: "recentToolbar" anchors { - left: parent.left - right: parent.right + left: tabslist.left + right: tabslist.right } height: units.gu(7) state: "hidden" @@ -771,6 +832,7 @@ Common.BrowserView { onSwitchToTab: internal.switchToTab(index, true) onRequestNewTab: internal.openUrlInNewTab("", makeCurrent, true, index) onTabClosed: internal.closeTab(index, moving) + onOpenRecentView: browser.openRecentView() onFindInPageModeChanged: { if (!chrome.findInPageMode) internal.resetFocus() @@ -994,9 +1056,8 @@ Common.BrowserView { } onWideChanged: { - if (wide) { - recentView.reset() - } else { + recentView.reset() + if (!wide) { // In narrow mode, the tabslist is a stack: the current tab is always at the top. tabsModel.move(tabsModel.currentIndex, 0) } @@ -1847,6 +1908,20 @@ Common.BrowserView { onActivated: currentWebview.zoomController.resetSaveFit() } + // Ctrl+W: Open and search tabs list + Shortcut { + sequence: "Ctrl+Space" + enabled: currentWebview || recentView.visible + onActivated: { + console.log(); + if (recentView.visible) { + recentView.reset() + } else { + browser.openRecentView() + } + } + } + Loader { id: contentHandlerLoader source: "../ContentHandler.qml" diff --git a/src/app/webbrowser/Chrome.qml b/src/app/webbrowser/Chrome.qml index 8db1f7006..5f82a4f0e 100644 --- a/src/app/webbrowser/Chrome.qml +++ b/src/app/webbrowser/Chrome.qml @@ -58,6 +58,7 @@ ChromeBase { signal switchToTab(int index) signal requestNewTab(int index, bool makeCurrent) signal tabClosed(int index, bool moving) + signal openRecentView backgroundColor: incognito ? UbuntuColors.darkGrey : theme.palette.normal.background @@ -107,6 +108,7 @@ ChromeBase { onRequestNewTab: chrome.requestNewTab(index, makeCurrent) onTabClosed: chrome.tabClosed(index, moving) + onOpenRecentView: chrome.openRecentView() } } diff --git a/src/app/webbrowser/TabPreview.qml b/src/app/webbrowser/TabPreview.qml index 099b2ac6b..fa21f8897 100644 --- a/src/app/webbrowser/TabPreview.qml +++ b/src/app/webbrowser/TabPreview.qml @@ -26,6 +26,7 @@ QQC2.SwipeDelegate { property alias title: chrome.title property alias tabIcon: chrome.icon property alias incognito: chrome.incognito + property real chromeHeight property var tab readonly property url url: tab ? tab.url : "" @@ -60,6 +61,7 @@ QQC2.SwipeDelegate { right: parent.right } tabWidth: units.gu(26) + height: tabPreview.chromeHeight onSelected: tabPreview.selected() onClosed: tabPreview.closed() @@ -76,7 +78,7 @@ QQC2.SwipeDelegate { visible: !tab.loadingPreview - height: parent.height + height: parent.height - chrome.height clip: true Rectangle { diff --git a/src/app/webbrowser/TabsBar.qml b/src/app/webbrowser/TabsBar.qml index 28ba11749..81bdaf66c 100644 --- a/src/app/webbrowser/TabsBar.qml +++ b/src/app/webbrowser/TabsBar.qml @@ -52,6 +52,7 @@ Extras.TabsBar { signal requestNewTab(int index, bool makeCurrent) signal tabClosed(int index, bool moving) + signal openRecentView onContextMenu: PopupUtils.open(contextualOptionsComponent, tabDelegate, {"targetIndex": index}) @@ -65,6 +66,11 @@ Extras.TabsBar { } actions: [ + Action { + iconName: "search" + objectName: "searchTabButton" + onTriggered: tabsBar.openRecentView() + }, Action { // FIXME: icon from theme is fuzzy at many GUs // iconSource: Qt.resolvedUrl("Tabs/tab_add.png") diff --git a/src/app/webbrowser/TabsList.qml b/src/app/webbrowser/TabsList.qml index 3786ba7a1..a90f1f29f 100644 --- a/src/app/webbrowser/TabsList.qml +++ b/src/app/webbrowser/TabsList.qml @@ -18,26 +18,39 @@ import QtQuick 2.4 import Ubuntu.Components 1.3 +import QtQml.Models 2.2 Item { id: tabslist property real delegateHeight property real chromeHeight - property alias model: repeater.model - readonly property int count: repeater.count + property real tabChromeHeight: units.gu(5) + property alias model: filteredModel.model + readonly property int count: model.count + property alias searchText: searchField.text + property alias view: list.item property bool incognito + property bool searchMode: false signal scheduleTabSwitch(int index) signal tabSelected(int index) signal tabClosed(int index) function reset() { - flickable.contentY = 0 + tabslist.view.contentY = 0 + searchText = "" } readonly property bool animating: selectedAnimation.running + Connections { + // WORKAROUND: Repeater items in listNarrowComponent stay hidden when switching from wide to narrow layout + // if the model is direcly assigned in its definition. This solves that issue. + target: browser + onWideChanged: if (!target.wide) searchText = " " + } + TabChrome { id: invisibleTabChrome visible: false @@ -46,69 +59,261 @@ Item { Rectangle { id: backrect width: parent.width - height: dealayBackground.running ? invisibleTabChrome.height : parent.height + height: delayBackground.running ? invisibleTabChrome.height : parent.height color: theme.palette.normal.base + visible: !browser.wide } onVisibleChanged: { - if (visible) - dealayBackground.start() + if (visible) { + delayBackground.start() + + if (browser.wide) { + searchMode = true + } else { + searchMode = false + } + } else { + if (browser.wide) { + tabslist.view.focus = false + } + } } Timer { - id: dealayBackground + id: delayBackground interval: 300 } - Flickable { - id: flickable + function focusInput() { + searchMode = true + searchField.selectAll(); + searchField.forceActiveFocus() + } - anchors.fill: parent + function selectFirstItem() { + var firstItem = matchGroup.get(0) + if (browser.wide) { + tabslist.tabSelected(firstItem.itemsIndex) + } else { + tabslist.selectAndAnimateTab(firstItem.itemsIndex, firstItem.index) + } + } + + Loader { + id: dragLoader - flickableDirection: Flickable.VerticalFlick - boundsBehavior: Flickable.StopAtBounds + readonly property real dragThreshold: units.gu(15) - contentWidth: width - contentHeight: model ? (model.count - 1) * delegateHeight + height : 0 + active: tabslist.view && !browser.wide + asynchronous: true + sourceComponent: Connections{ + target: tabslist.view - Repeater { - id: repeater + onVerticalOvershootChanged: { + if(target.verticalOvershoot < 0 && target.dragging){ + if(-target.verticalOvershoot >= dragThreshold){ + tabslist.searchMode = true + tabslist.focusInput() + } + } + } + } + } - delegate: Loader { - id: delegate + Rectangle { + id: searchRec - asynchronous: true + anchors { + top: parent.top + left: parent.left + right: parent.right + } - width: flickable.contentWidth + height: units.gu(6) + color: browser.wide ? "transparent" : theme.palette.normal.background + opacity: tabslist.searchMode ? 1 : tabslist.view.verticalOvershoot < 0 ? -tabslist.view.verticalOvershoot / dragLoader.dragThreshold : 0 + Behavior on opacity { + UbuntuNumberAnimation { + duration: UbuntuAnimation.FastDuration + } + } - height: flickable.height + TextField { + id: searchField - y: Math.max(flickable.contentY, index * delegateHeight) - Behavior on y { - enabled: !flickable.moving && !selectedAnimation.running - UbuntuNumberAnimation { - duration: UbuntuAnimation.BriskDuration + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + margins: units.gu(1) + } + placeholderText: i18n.tr("Search Tabs") + inputMethodHints: Qt.ImhNoPredictiveText + primaryItem: Icon { + height: parent.height * 0.5 + width: height + name: "search" + } + + KeyNavigation.down: tabslist.view + onTextChanged: searchDelay.restart() + onAccepted: tabslist.selectFirstItem() + + Timer { + id: searchDelay + interval: 300 + onTriggered: filteredModel.update(searchField.text) + } + } + } + + Label { + id: resultsLabel + + text: searchDelay.running ? i18n.tr("Loading...") : i18n.tr("No results") + textSize: Label.Large + font.weight: Font.DemiBold + color: browser.wide ? UbuntuColors.porcelain : theme.palette.normal.baseText + anchors { + top: searchRec.bottom + horizontalCenter: parent.horizontalCenter + margins: units.gu(3) + } + visible: filteredModel.count == 0 + } + + Loader { + id: list + + asynchronous: true + anchors.fill: parent + anchors.topMargin: tabslist.searchMode ? searchRec.height : 0 + sourceComponent: browser.wide ? listWideComponent : listNarrowComponent + + Behavior on anchors.topMargin { + enabled: !browser.wide + UbuntuNumberAnimation { + duration: UbuntuAnimation.SnapDuration + } + } + } + + DelegateModel { + id: filteredModel + + function update(searchText) { + if (items.count > 0) { + items.setGroups(0, items.count, ["items"]); + } + + if (searchText) { + filterOnGroup = "match" + var match = []; + var searchTextUpper = searchText.toUpperCase() + var titleUpper + var urlUpper + var item + + for (var i = 0; i < items.count; ++i) { + item = items.get(i); + titleUpper = item.model.title.toUpperCase() + urlUpper = item.model.url.toString().toUpperCase() + + if (titleUpper.indexOf(searchTextUpper) > -1 || urlUpper.indexOf(searchTextUpper) > -1 ) { + match.push(item); } } - opacity: selectedAnimation.running && (index > selectedAnimation.index) ? 0 : 1 - Behavior on opacity { - UbuntuNumberAnimation { - duration: UbuntuAnimation.FastDuration + for (i = 0; i < match.length; ++i) { + item = match[i]; + item.inMatch = true; + } + } else { + filterOnGroup = "items" + } + } + + groups: [ + DelegateModelGroup { + id: matchGroup + + name: "match" + includeByDefault: false + } + ] + + delegate: Package { + id: packageDelegate + + Item { + id: gridDelegate + + Package.name: "grid" + + property int tabIndex: index + + width: tabslist.view.cellWidth + height: tabslist.view.cellHeight + clip: true + + TabPreview { + property real horizontalMargin: units.gu(1) + property real verticalMargin: horizontalMargin * ((gridDelegate.height - tabslist.tabChromeHeight) / gridDelegate.width) + + title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab")) + tabIcon: model.icon + incognito: tabslist.incognito + tab: model.tab + chromeHeight: tabslist.tabChromeHeight + + anchors { + fill: parent + leftMargin: horizontalMargin + rightMargin: horizontalMargin + topMargin: verticalMargin + bottomMargin: verticalMargin } + + onSelected: tabslist.tabSelected(index) + onClosed: tabslist.tabClosed(index) } + } + Loader { + id: listDelegate + + property int groupIndex: filteredModel.filterOnGroup === "match" ? packageDelegate.DelegateModel.matchIndex : index readonly property string title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab")) readonly property string icon: model.icon - active: (index >= 0) && ((flickable.contentY + flickable.height + delegateHeight / 2) >= (index * delegateHeight)) + Package.name: "list" + + asynchronous: true + width: tabslist.view.contentWidth + height: tabslist.view.height + (tabslist.searchMode ? searchRec.height : 0) + opacity: selectedAnimation.running && (groupIndex > selectedAnimation.listIndex) ? 0 : 1 + Behavior on opacity { + UbuntuNumberAnimation { + duration: UbuntuAnimation.FastDuration + } + } + y: Math.max(tabslist.view.contentY, groupIndex * delegateHeight) + Behavior on y { + enabled: !tabslist.view.moving && !selectedAnimation.running + UbuntuNumberAnimation { + duration: UbuntuAnimation.BriskDuration + } + } - visible: flickable.contentY < ((index + 1) * delegateHeight) + active: (groupIndex >= 0) && ((tabslist.view.contentY + tabslist.view.height + delegateHeight / 2) >= (groupIndex * delegateHeight)) + visible: tabslist.view.contentY < ((groupIndex + 1) * delegateHeight) sourceComponent: TabPreview { - title: delegate.title - tabIcon: delegate.icon + title: listDelegate.title + tabIcon: listDelegate.icon incognito: tabslist.incognito tab: model.tab + chromeHeight: tabslist.tabChromeHeight /* Binding { // Change the height of the location bar controller @@ -120,41 +325,105 @@ Item { value: invisibleTabChrome.height } */ - onSelected: tabslist.selectAndAnimateTab(index) + onSelected: tabslist.selectAndAnimateTab(index, groupIndex) onClosed: tabslist.tabClosed(index) } } } + } + + Component { + id: listWideComponent + + GridView { + id: gridView + + property int columnCount: switch (true) { + case tabslist.width >= units.gu(100): + 3 + break; + case tabslist.width >= units.gu(60): + 2 + break; + default: + 1 + break; + } + + clip: true + model: filteredModel.parts.grid + cellWidth: (tabslist.width) / columnCount + cellHeight: ((cellWidth * (browser.height - tabslist.chromeHeight)) / browser.width) + tabslist.tabChromeHeight + highlight: Component { + Item { + z: 10 + width: gridView.cellWidth + height: gridView.cellHeight + opacity: 0.4 + visible: gridView.activeFocus + + Rectangle { + anchors.fill: parent + color: theme.palette.normal.focus + } + } + } + + Keys.onEnterPressed: tabslist.tabSelected(currentItem.tabIndex) + Keys.onReturnPressed: tabslist.tabSelected(currentItem.tabIndex) + } + } + + Component { + id: listNarrowComponent + + Flickable { + id: flickable + + anchors.fill: parent - PropertyAnimation { - id: selectedAnimation - property int index: 0 - target: flickable - property: "contentY" - to: index * delegateHeight - duration: UbuntuAnimation.FastDuration - onStopped: { - // Delay switching the tab until after the animation has completed. - delayedTabSelection.index = index - delayedTabSelection.start() + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.DragOverBounds + contentWidth: width + contentHeight: filteredModel ? (filteredModel.count - 1) * delegateHeight + height : 0 + + Repeater { + id: repeater + + model: filteredModel.parts.list } } + } + + Timer { + id: delayedTabSelection + interval: 1 + property int index: 0 + onTriggered: tabslist.tabSelected(index) + } - Timer { - id: delayedTabSelection - interval: 1 - property int index: 0 - onTriggered: tabslist.tabSelected(index) + PropertyAnimation { + id: selectedAnimation + property int tabIndex: 0 + property int listIndex: 0 + target: tabslist.view + property: "contentY" + to: listIndex * delegateHeight + duration: UbuntuAnimation.FastDuration + onStopped: { + // Delay switching the tab until after the animation has completed. + delayedTabSelection.index = tabIndex + delayedTabSelection.start() } } - function selectAndAnimateTab(index) { - // Animate tab into full view - if (index == 0) { + function selectAndAnimateTab(tabIndex, listIndex) { + if (tabIndex == 0) { tabSelected(0) } else { - selectedAnimation.index = index - scheduleTabSwitch(index) + selectedAnimation.tabIndex = tabIndex + selectedAnimation.listIndex = listIndex ? listIndex : tabIndex + scheduleTabSwitch(tabIndex) selectedAnimation.start() } }