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 variant icon + + /** + * 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,196 @@ +/* + * 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 + +ColumnLayout { + id: swipeNavigatorRoot + spacing: 0 + + /** + * 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 + + /** + * layerStack: StackView + * + * The StackView holding the core item, allowing users of a SwipeNavigator + * in order to push pages on top of the SwipeNavigator. + */ + property alias layerStack: 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: ToolBar.Header + + 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: swipeNavigatorRoot.largeHeader ? Kirigami.Units.gridUnit*4 : -1 + Layout.fillWidth: true + + Accessible.role: Accessible.PageTabList + } + + StackView { + id: stackView + + Layout.fillWidth: true + Layout.fillHeight: true + + 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,160 @@ +/* + * 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 + } + } + + RowLayout { + id: largeTitleRow + anchors.fill: parent + Accessible.ignored: true + + RowLayout { + Layout.margins: Kirigami.Units.largeSpacing + Layout.alignment: Qt.AlignVCenter + + Kirigami.Icon { + visible: !!modelData.icon + source: modelData.icon + + Layout.preferredHeight: swipeNavigatorRoot.largeHeader ? Kirigami.Units.iconSizes.medium : Kirigami.Units.iconSizes.small + Layout.preferredWidth: Layout.preferredHeight + } + Kirigami.Heading { + level: swipeNavigatorRoot.largeHeader ? 1 : 2 + text: modelData.title + visible: { + if (!swipeTabBarRoot.small) return true + return index == columnView.currentIndex + } + + Layout.alignment: 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 @@ -248,6 +248,7 @@ qmlRegisterSingletonType(uri, 2, 12, "ColorUtils", [] (QQmlEngine*, QJSEngine*) -> QObject* { return new ColorUtils; }); qmlRegisterUncreatableType(uri, 2, 12, "CornersGroup", QStringLiteral("Used as grouped property")); + qmlRegisterType(componentUrl(QStringLiteral("SwipeNavigator.qml")), uri, 2, 12, "SwipeNavigator"); qmlProtectModule(uri, 2); }