diff --git a/examples/swipenavigator/main.qml b/examples/swipenavigator/main.qml new file mode 100644 index 00000000..9326cda7 --- /dev/null +++ b/examples/swipenavigator/main.qml @@ -0,0 +1,27 @@ +import QtQuick 2.0 +import org.kde.kirigami 2.13 as Kirigami + +Kirigami.ApplicationWindow { + Kirigami.SwipeNavigator { + anchors.fill: parent + largeHeader: false + Kirigami.Page { + icon.name: "globe" + title: "World Clocks" + } + Kirigami.Page { + icon.name: "clock" + title: "Alarms" + needsAttention: true + } + Kirigami.Page { + icon.name: "clock" + title: "Stopwatch" + } + Kirigami.Page { + icon.name: "clock" + title: "Timers" + progress: 0.5 + } + } +} diff --git a/src/controls/SwipeNavigator.qml b/src/controls/SwipeNavigator.qml index 2dbc4e3b..93271463 100644 --- a/src/controls/SwipeNavigator.qml +++ b/src/controls/SwipeNavigator.qml @@ -1,209 +1,225 @@ /* * SPDX-FileCopyrightText: 2020 Carson Black * * SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 import org.kde.kirigami 2.12 as Kirigami import "private" as Private // We use a single-column GridLayout to allow using Layout.row for mobile +/** + * SwipeNavigator is a control providing for lateral navigation. + * + * @include swipenavigator/main.qml + */ GridLayout { id: swipeNavigatorRoot rowSpacing: 0 columns: 1 /** * pages: list * * A list of pages to swipe between. */ default property list pages /** * largeHeader: bool * * Whether this SwipeNavigator should use a larger header than normal. Designed * for usage on televisions. */ property bool largeHeader: false /** * layers: StackView * * The StackView holding the core item, allowing users of a SwipeNavigator * in order to push pages on top of the SwipeNavigator. */ property alias layers: stackView - /** - * actions: list - * - * Actions to display in the toolbar where the page switcher resides. - */ - property alias actions: actionToolBar.actions - ToolBar { id: topToolBar padding: 0 bottomPadding: 1 position: Kirigami.Settings.isMobile ? ToolBar.Footer : ToolBar.Header Layout.row: Kirigami.Settings.isMobile ? 1 : 0 states: [ State { name: "small" when: largeBar.implicitWidth > topToolBar.width }, State { name: "large" when: largeBar.implicitWidth <= topToolBar.width } ] - Kirigami.ActionToolBar { - id: actionToolBar + Private.SwipeTabBar { + visible: topToolBar.state == "large" + id: largeBar anchors { top: parent.top bottom: parent.bottom - left: parent.left + horizontalCenter: parent.horizontalCenter } } Private.SwipeTabBar { - visible: topToolBar.state == "large" - id: largeBar - } - - Private.SwipeTabBar { + id: smallBar visible: topToolBar.state == "small" small: true + property bool tooBig: topToolBar.width < this.implicitWidth + parent: tooBig ? _scroll : topToolBar + anchors { + top: tooBig ? undefined : parent.top + bottom: tooBig ? undefined : parent.bottom + horizontalCenter: tooBig ? undefined : parent.horizontalCenter + } + onIndexChanged: { + if (tooBig) { + _scroll.ScrollBar.horizontal.position = xPos/smallBar.width + } + } + } + + ScrollView { + id: _scroll + anchors.fill: parent + contentWidth: smallBar.implicitWidth + clip: false + contentChildren: smallBar.tooBig ? [smallBar] : [] + ScrollBar.horizontal.visible: false } Layout.preferredHeight: { if (swipeNavigatorRoot.largeHeader) { return Kirigami.Units.gridUnit*4 } else if (topToolBar.state == "small") { return Kirigami.Units.gridUnit*3 } return -1 } Layout.fillWidth: true Accessible.role: Accessible.PageTabList } StackView { id: stackView Layout.fillWidth: true Layout.fillHeight: true Layout.row: Kirigami.Settings.isMobile ? 0 : 1 popEnter: Transition { OpacityAnimator { from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } } popExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: 0 to: height/2 duration: Units.longDuration easing.type: Easing.InCubic } } } pushEnter: Transition { ParallelAnimation { //NOTE: It's a PropertyAnimation instead of an Animator because with an animator the item will be visible for an instant before starting to fade PropertyAnimation { property: "opacity" from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Units.longDuration easing.type: Easing.OutCubic } } } pushExit: Transition { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InOutCubic } } replaceEnter: Transition { ParallelAnimation { OpacityAnimator { from: 0 to: 1 duration: Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Units.longDuration easing.type: Easing.OutCubic } } } replaceExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Units.longDuration easing.type: Easing.InCubic } YAnimator { from: 0 to: -height/2 duration: Units.longDuration easing.type: Easing.InOutCubic } } } Kirigami.ColumnView { id: columnView columnResizeMode: Kirigami.ColumnView.SingleColumn contentChildren: swipeNavigatorRoot.pages anchors.fill: parent Component.onCompleted: { columnView.currentIndex = 0 } } } } diff --git a/src/controls/private/SwipeTabBar.qml b/src/controls/private/SwipeTabBar.qml index 0039b6c5..78ffc3fa 100644 --- a/src/controls/private/SwipeTabBar.qml +++ b/src/controls/private/SwipeTabBar.qml @@ -1,210 +1,217 @@ /* * SPDX-FileCopyrightText: 2020 Carson Black * * SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 import org.kde.kirigami 2.12 as Kirigami RowLayout { id: swipeTabBarRoot property bool small: false spacing: 0 - anchors { - top: parent.top - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - } + signal indexChanged(real xPos) Repeater { model: swipeNavigatorRoot.pages Rectangle { + id: tabRoot + Keys.onPressed: { if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { columnView.currentIndex = index } } activeFocusOnTab: true + Connections { + target: columnView + function onCurrentIndexChanged() { + if (index == columnView.currentIndex) { + swipeTabBarRoot.indexChanged(tabRoot.x) + } + } + } + Accessible.name: modelData.title Accessible.description: { if (!!modelData.progress) { if (index == columnView.currentIndex) { return i18nc("Accessibility text for a page tab. Keep the text as concise as possible and don't use a percent sign.", "Current page. Progress: %1 percent.", Math.round(modelData.progress*100)) } else { return i18nc("Accessibility text for a page tab. Keep the text as concise as possible.", "Navigate to %1. Progress: %1 percent.", modelData.title, Math.round(modelData.progress*100)) } } else { if (index == columnView.currentIndex) { return i18nc("Accessibility text for a page tab. Keep the text as concise as possible.", "Current page.") } else if (modelData.needsAttention) { return i18nc("Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible.", "Navigate to %1. Demanding attention.", modelData.title) } else { return i18nc("Accessibility text for a page tab that's requesting the user's attention. Keep the text as concise as possible.", "Navigate to %1.", modelData.title) } } } Accessible.role: Accessible.PageTab Accessible.focusable: true Accessible.onPressAction: columnView.currentIndex = index implicitWidth: largeTitleRow.implicitWidth border { width: activeFocus ? 2 : 0 color: Kirigami.Theme.textColor } color: { if (index == columnView.currentIndex) { return Kirigami.ColorUtils.adjustColor(Kirigami.Theme.activeTextColor, {"alpha": 0.2*255}) } else if (modelData.needsAttention) { return Kirigami.ColorUtils.adjustColor(Kirigami.Theme.negativeTextColor, {"alpha": 0.2*255}) } else { return "transparent" } } Rectangle { Accessible.ignored: true anchors { bottom: parent.bottom left: parent.left right: parent.right } color: { if (index == columnView.currentIndex) { return Kirigami.Theme.activeTextColor } else if (modelData.needsAttention) { return Kirigami.Theme.negativeTextColor } else { return "transparent" } } // Unlike most things, we don't want to scale with the em grid, so we don't use a Unit. height: 2 } Rectangle { Accessible.ignored: true visible: !!modelData.progress anchors { top: parent.top bottom: parent.bottom left: parent.left } width: parent.width * modelData.progress color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.positiveTextColor, {"alpha": 0.2*255}) Rectangle { anchors { bottom: parent.bottom left: parent.left right: parent.right } color: Kirigami.Theme.positiveTextColor // Unlike most things, we don't want to scale with the em grid, so we don't use a Unit. height: 2 } } Rectangle { Accessible.ignored: true visible: !!modelData.progress anchors { top: parent.top bottom: parent.bottom right: parent.right } width: parent.width - (parent.width * modelData.progress) color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.1*255}) Rectangle { anchors { bottom: parent.bottom left: parent.left right: parent.right } color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.1*255}) // Unlike most things, we don't want to scale with the em grid, so we don't use a Unit. height: 2 } } RowLayout { id: largeTitleRow anchors.fill: parent Accessible.ignored: true GridLayout { Layout.margins: Kirigami.Units.largeSpacing Layout.alignment: Qt.AlignVCenter columns: 2 rows: 2 flow: swipeTabBarRoot.small ? GridLayout.TopToBottom : GridLayout.LeftToRight Kirigami.Icon { visible: !!modelData.icon.name source: modelData.icon.name Layout.preferredHeight: swipeNavigatorRoot.largeHeader ? Kirigami.Units.iconSizes.medium : Kirigami.Units.iconSizes.small Layout.preferredWidth: Layout.preferredHeight Layout.alignment: swipeTabBarRoot.small ? Qt.AlignHCenter : (Qt.AlignLeft | Qt.AlignVCenter) } Kirigami.Heading { level: { if (swipeNavigatorRoot.largeHeader) { return 1 } else if (swipeTabBarRoot.small) { return 5 } return 2 } fontSizeMode: { if (swipeNavigatorRoot.largeHeader) { return Text.FixedSize } else if (swipeTabBarRoot.small) { return Text.HorizontalFit } return Text.FixedSize } text: modelData.title // We fall back to Font.Normal, which will override user // font choices. Not ideal, but there doesn't seem to be // a way to reset this property. font.weight: modelData.needsAttention ? Font.Black : Font.Normal Layout.fillWidth: true Layout.alignment: swipeTabBarRoot.small ? Qt.AlignHCenter : (Qt.AlignLeft | Qt.AlignVCenter) } } } MouseArea { id: mouse anchors.fill: parent Accessible.ignored: true onClicked: { columnView.currentIndex = index } } Layout.fillHeight: true Layout.alignment: Qt.AlignHCenter } } }