diff --git a/src/controls/Page.qml b/src/controls/Page.qml --- a/src/controls/Page.qml +++ b/src/controls/Page.qml @@ -215,6 +215,28 @@ */ readonly property alias overlay: overlayItem + /** + * icon: variant + * + * The icon that represents this page. + */ + property ActionIconGroup icon: ActionIconGroup {} + + /** + * needsAttention: bool + * + * Whether this page needs user attention. + */ + property bool needsAttention + + /** + * progress: real + * + * Progress of a task this page is doing. Set to undefined to indicate + * that there are no ongoing tasks. + */ + property var progress: undefined + /** * titleDelegate: Component * The delegate which will be used to draw the page title. It can be customized to put any kind of Item in there. diff --git a/src/controls/SwipeNavigator.qml b/src/controls/SwipeNavigator.qml new file mode 100644 --- /dev/null +++ b/src/controls/SwipeNavigator.qml @@ -0,0 +1,209 @@ +/* + * 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 +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 + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + } + + Private.SwipeTabBar { + visible: topToolBar.state == "large" + id: largeBar + } + + Private.SwipeTabBar { + visible: topToolBar.state == "small" + small: true + } + + 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 new file mode 100644 --- /dev/null +++ b/src/controls/private/SwipeTabBar.qml @@ -0,0 +1,210 @@ +/* + * 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 + } + + Repeater { + model: swipeNavigatorRoot.pages + Rectangle { + Keys.onPressed: { + if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { + columnView.currentIndex = index + } + } + activeFocusOnTab: true + + 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 + } + } +} diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -258,6 +258,7 @@ // 2.13 qmlRegisterType(uri, 2, 13, "ImageColors"); + qmlRegisterType(componentUrl(QStringLiteral("SwipeNavigator.qml")), uri, 2, 13, "SwipeNavigator"); qmlProtectModule(uri, 2); }