diff --git a/Mainpage.dox b/Mainpage.dox index db97bae6..2a0d66d6 100644 --- a/Mainpage.dox +++ b/Mainpage.dox @@ -1,199 +1,201 @@ /* * This file is part of Kirigami * SPDX-FileCopyrightText: 2016 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ /** \mainpage kirigami \section overview Introduction Kirigami is a set of QtQuick components for building adaptable UIs based on Qt Quick Controls 2. Its goal is to enable creation of applications that look and feel great on mobile as well as desktop devices and follow the KDE Human Interface Guidelines. The target of those components is anybody that wants to do an application using QtQuick as its main UI, especially if targeting a mobile platform, without adding many dependencies. They work on a variety of platforms, such as Plasma Mobile, Desktop Linux, Android, iOS and Windows. It is a Tier-1 KDE Framework starting with KDE Frameworks 5.37. \section components Main Components - \link org::kde::kirigami::ApplicationWindow ApplicationWindow \endlink - \link org::kde::kirigami::Action Action \endlink - \link org::kde::kirigami::GlobalDrawer GlobalDrawer \endlink - \link org::kde::kirigami::ContextDrawer ContextDrawer \endlink - \link org::kde::kirigami::OverlayDrawer OverlayDrawer \endlink - \link org::kde::kirigami::Page Page \endlink - \link org::kde::kirigami::ScrollablePage ScrollablePage \endlink - \link org::kde::kirigami::OverlaySheet OverlaySheet \endlink +- \link PageRouter PageRouter \endlink +- \link PageRoute PageRoute \endlink - \link org::kde::kirigami::Theme Theme \endlink - \link org::kde::kirigami::Units Units \endlink - \link org::kde::kirigami::Icon Icon \endlink - \link org::kde::kirigami::AbstractApplicationHeader AbstractApplicationHeader \endlink - \link org::kde::kirigami::AbstractApplicationWindow AbstractApplicationWindow \endlink - \link org::kde::kirigami::AbstractListItem AbstractListItem \endlink - \link org::kde::kirigami::ApplicationHeader ApplicationHeader \endlink - \link org::kde::kirigami::BasicListItem BasicListItem \endlink - \link org::kde::kirigami::ListSectionHeader ListSectionHeader \endlink - \link org::kde::kirigami::SwipeListItem SwipeListItem \endlink - \link org::kde::kirigami::Heading Heading \endlink - \link org::kde::kirigami::Label Label \endlink \section example Minimal Example @code import QtQuick 2.1 import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.4 as Kirigami Kirigami.ApplicationWindow { id: root header: Kirigami.ApplicationHeader {} globalDrawer: Kirigami.GlobalDrawer { title: "Hello App" titleIcon: "applications-graphics" actions: [ Kirigami.Action { text: "View" iconName: "view-list-icons" Kirigami.Action { text: "action 1" } Kirigami.Action { text: "action 2" } Kirigami.Action { text: "action 3" } }, Kirigami.Action { text: "action 3" }, Kirigami.Action { text: "action 4" } ] } contextDrawer: Kirigami.ContextDrawer { id: contextDrawer } pageStack.initialPage: mainPageComponent Component { id: mainPageComponent Kirigami.ScrollablePage { title: "Hello" actions { main: Kirigami.Action { iconName: sheet.sheetOpen ? "dialog-cancel" : "document-edit" onTriggered: { print("Action button in buttons page clicked"); sheet.sheetOpen = !sheet.sheetOpen } } left: Kirigami.Action { iconName: "go-previous" onTriggered: { print("Left action triggered") } } right: Kirigami.Action { iconName: "go-next" onTriggered: { print("Right action triggered") } } contextualActions: [ Kirigami.Action { text:"Action for buttons" iconName: "bookmarks" onTriggered: print("Action 1 clicked") }, Kirigami.Action { text:"Action 2" iconName: "folder" enabled: false }, Kirigami.Action { text: "Action for Sheet" visible: sheet.sheetOpen } ] } Kirigami.OverlaySheet { id: sheet onSheetOpenChanged: page.actions.main.checked = sheetOpen QQC2.Label { wrapMode: Text.WordWrap text: "Lorem ipsum dolor sit amet" } } //Page contents... } } } @endcode \section deployment Deployment CMake is recomended for both building Kirigami and the project using it, QMake is supported as well, so we can have several configurations, depending what is the host build system and how the deployment needs to be done. Kirigami can be built in two ways: both as a module or statically linked in the application, leading to four combinations: * Kirigami built as a module with CMake * Kirigami statically built with CMake (needed to link statically from applications built with CMake) * Kirigami built as a module with QMake * Kirigami statically built with QMake (needed to link statically from applications built with QMake) The simplest and recomended way to use Kirigami is to just use the module provided by the Linux distribution, or build it as a module and deploy it together the main application. For example when building an application on Android with CMake, if Kirigami for Android is built and installed in the same temporary directory before the application, the create-apk- step of the application will include the Kirigami files as well in the APK. If QMake needs to be used, it's recomended to follow the schema of the example app present in the folder examples/minimalqmake of the Kirigami source code. It will use Kirigami statically linked only on Android, while on desktop systems it will use the version provided by the distribution. What platforms use the static version and what the dynamic one can be freely adjusted. The application needs to have a folder called "3rdparty" containing clones of two KDE repositories: kirigami and breeze-icons (available at git://anongit.kde.org/kirigami.git and git://anongit.kde.org/breeze-icons.git). The relevant part in the .pro file is: @code android: { include(3rdparty/kirigami/kirigami.pri) } @endcode While the main.cpp file will have something like: @code #include #include #ifdef Q_OS_ANDROID #include "./3rdparty/kirigami/src/kirigamiplugin.h" #endif int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; #ifdef Q_OS_ANDROID KirigamiPlugin::getInstance().registerTypes(); #endif .... } @endcode @authors Marco Martin \
Sebastian Kuegler \
Aleix Pol Gonzalez \
Dirk Hohndel \
@maintainers Marco Martin \ @licenses @lgpl */ // DOXYGEN_SET_RECURSIVE = YES // DOXYGEN_SET_EXCLUDE_PATTERNS += *_p.h */private/* */examples/* // DOXYGEN_SET_PROJECT_NAME = Kirigami // vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 31765a28..0187ee36 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,33 +1,34 @@ if(NOT Qt5QuickTest_FOUND) message(STATUS "Qt5QuickTest not found, autotests will not be built.") return() endif() add_executable(qmltest qmltest.cpp) target_link_libraries(qmltest Qt5::QuickTest) macro(kirigami_add_tests) if (WIN32) set(_extra_args -platform offscreen) endif() foreach(test ${ARGV}) add_test(NAME ${test} COMMAND qmltest ${_extra_args} -import ${CMAKE_BINARY_DIR}/bin -input ${test} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) endforeach() endmacro() kirigami_add_tests( tst_keynavigation.qml tst_listskeynavigation.qml tst_pagerow.qml tst_icon.qml tst_actiontoolbar.qml + tst_pagerouter.qml pagepool/tst_pagepool.qml ) diff --git a/autotests/tst_pagerouter.qml b/autotests/tst_pagerouter.qml new file mode 100644 index 00000000..288dd757 --- /dev/null +++ b/autotests/tst_pagerouter.qml @@ -0,0 +1,110 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami +import QtTest 1.0 + +Kirigami.PageRow { + id: root + TestCase { + name: "PageRouterGeneralTests" + function test_10_init() { + compare(router.currentRoutes().length, 1) + } + function test_20_navigate() { + router.navigateToRoute(["home", "login"]) + compare(router.currentRoutes().length, 2) + } + function test_30_data() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}]) + compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true) + compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false) + } + function test_40_cache_works() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) + compare(router.currentRoutes().length, 3) + } + function test_50_push() { + router.pushRoute("home") + compare(router.currentRoutes().length, 4) + } + function test_60_pop() { + router.popRoute() + compare(router.currentRoutes().length, 3) + } + function test_70_bring_to_view() { + router.bringToView("home") + compare(root.columnView.currentIndex, 0) + router.bringToView({"route": "login", "data": "red"}) + compare(root.columnView.currentIndex, 1) + router.bringToView({"route": "login", "data": "blue"}) + compare(root.columnView.currentIndex, 2) + } + function test_80_routeactive() { + compare(router.routeActive(["home"]), true) + compare(router.routeActive(["home", "login"]), true) + compare(router.routeActive(["home", {"route": "login", "data": "red"}]), true) + compare(router.routeActive(["home", {"route": "login", "data": "blue"}]), false) + } + function test_90_initial_route() { + router.initialRoute = "login" + compare(router.routeActive(["login"]), false) + compare(router.currentRoutes().length, 3) + } + function test_100_navigation_two() { + router.navigateToRoute(["home", {"route": "login", "data": "red"}, {"route": "login", "data": "blue"}]) + compare(router.currentRoutes().length, 3) + router.navigateToRoute(["home"]) + compare(router.currentRoutes().length, 1) + compare(router.pageStack.count, 1) + } + } + Kirigami.PageRouter { + id: router + initialRoute: "home" + pageStack: root.columnView + + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Welcome" + } + QQC2.Button { + text: "Red Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}]) + } + QQC2.Button { + text: "Blue Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}]) + } + } + } + } + } + Kirigami.PageRoute { + name: "login" + cache: true + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Login" + } + Rectangle { + height: 50 + width: 50 + color: Kirigami.PageRouter.data + } + QQC2.Button { + text: "Back to Home" + onClicked: Kirigami.PageRouter.navigateToRoute("home") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/docs/pics/PageRouterModel.svg b/docs/pics/PageRouterModel.svg new file mode 100644 index 00000000..cedea9e3 --- /dev/null +++ b/docs/pics/PageRouterModel.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/pics/PageRouterNavigate.svg b/docs/pics/PageRouterNavigate.svg new file mode 100644 index 00000000..e128ba36 --- /dev/null +++ b/docs/pics/PageRouterNavigate.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/pics/PageRouterPop.svg b/docs/pics/PageRouterPop.svg new file mode 100644 index 00000000..ee9bb4d2 --- /dev/null +++ b/docs/pics/PageRouterPop.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/pics/PageRouterPush.svg b/docs/pics/PageRouterPush.svg new file mode 100644 index 00000000..a74a1e95 --- /dev/null +++ b/docs/pics/PageRouterPush.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/examples/PageRoute.qml b/examples/PageRoute.qml new file mode 100644 index 00000000..6287f31b --- /dev/null +++ b/examples/PageRoute.qml @@ -0,0 +1,8 @@ +Kirigami.PageRoute { + name: "routeOne" + Component { + Kirigami.Page { + // Page contents... + } + } +} \ No newline at end of file diff --git a/examples/PageRouter.qml b/examples/PageRouter.qml new file mode 100644 index 00000000..34128397 --- /dev/null +++ b/examples/PageRouter.qml @@ -0,0 +1,55 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 as QQC2 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + initialRoute: "home" + pageStack: applicaionWindow.pageStack.columnView + + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Welcome" + } + QQC2.Button { + text: "Red Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "red"}]) + } + QQC2.Button { + text: "Blue Login" + onClicked: Kirigami.PageRouter.navigateToRoute(["home", {"route": "login", "data": "blue"}]) + } + } + } + } + } + Kirigami.PageRoute { + name: "login" + cache: true + Component { + Kirigami.Page { + Column { + Kirigami.Heading { + text: "Login" + } + Rectangle { + height: 50 + width: 50 + color: Kirigami.PageRouter.data + } + QQC2.Button { + text: "Back to Home" + onClicked: Kirigami.PageRouter.navigateToRoute("home") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/examples/PageRouterCachePagesDo.qml b/examples/PageRouterCachePagesDo.qml new file mode 100644 index 00000000..22248fab --- /dev/null +++ b/examples/PageRouterCachePagesDo.qml @@ -0,0 +1,23 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + initialRoute: "home" + pageStack: applicaionWindow.pageStack.columnView + + // home can be pushed onto the route twice + Component.onCompleted: navigateToRoute("home", "home") + + Kirigami.PageRoute { + name: "home" + cache: false + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/PageRouterCachePagesDont.qml b/examples/PageRouterCachePagesDont.qml new file mode 100644 index 00000000..1f893909 --- /dev/null +++ b/examples/PageRouterCachePagesDont.qml @@ -0,0 +1,23 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + initialRoute: "home" + pageStack: applicaionWindow.pageStack.columnView + + // home can't be pushed onto the route twice + Component.onCompleted: navigateToRoute("home", "home") + + Kirigami.PageRoute { + name: "home" + cache: true + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/PageRouterColumnView.qml b/examples/PageRouterColumnView.qml new file mode 100644 index 00000000..e631e1cf --- /dev/null +++ b/examples/PageRouterColumnView.qml @@ -0,0 +1,20 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + pageStack: applicaionWindow.pageStack.columnView + + Kirigami.PageRoute { + name: "home" + Component { + Kirigami.Page { + // Page contents... + } + } + } + initialRoute: "home" + Component.onCompleted: navigateToRoute("home", "home") + } +} \ No newline at end of file diff --git a/examples/PageRouterInitialRoute.qml b/examples/PageRouterInitialRoute.qml new file mode 100644 index 00000000..5989124b --- /dev/null +++ b/examples/PageRouterInitialRoute.qml @@ -0,0 +1,27 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + initialRoute: "home" + pageStack: applicaionWindow.pageStack.columnView + + Kirigami.PageRoute { + name: "home" + Component { + Kirigami.Page { + // This page will show up when starting the application + } + } + } + Kirigami.PageRoute { + name: "login" + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/examples/PageRouterRoutes.qml b/examples/PageRouterRoutes.qml new file mode 100644 index 00000000..2b50b3b7 --- /dev/null +++ b/examples/PageRouterRoutes.qml @@ -0,0 +1,34 @@ +import QtQuick 2.12 +import org.kde.kirigami 2.12 as Kirigami + +Kirigami.ApplicationWindow { + id: applicaionWindow + Kirigami.PageRouter { + pageStack: applicaionWindow.pageStack.columnView + + Kirigami.PageRoute { + name: "routeOne" + Component { + Kirigami.Page { + // Page contents... + } + } + } + Kirigami.PageRoute { + name: "routeTwo" + Component { + Kirigami.Page { + // Page contents... + } + } + } + Kirigami.PageRoute { + name: "routeThree" + Component { + Kirigami.Page { + // Page contents... + } + } + } + } +} \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ad634143..f6b8f125 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,120 +1,121 @@ project(kirigami) if (BUILD_SHARED_LIBS) ecm_create_qm_loader(kirigami_QM_LOADER libkirigami2plugin_qt) else() set(KIRIGAMI_STATIC_FILES libkirigami/basictheme.cpp libkirigami/platformtheme.cpp libkirigami/tabletmodewatcher.cpp libkirigami/kirigamipluginfactory.cpp) endif() include_directories(${CMAKE_CURRENT_SOURCE_DIR}/libkirigami ${CMAKE_CURRENT_BINARY_DIR}/libkirigami) set(kirigami_SRCS kirigamiplugin.cpp columnview.cpp enums.cpp delegaterecycler.cpp icon.cpp settings.cpp formlayoutattached.cpp pagepool.cpp scenepositionattached.cpp mnemonicattached.cpp wheelhandler.cpp shadowedrectangle.cpp shadowedtexture.cpp colorutils.cpp + pagerouter.cpp scenegraph/shadowedrectanglenode.cpp scenegraph/shadowedrectanglematerial.cpp scenegraph/shadowedborderrectanglematerial.cpp scenegraph/paintedrectangleitem.cpp scenegraph/shadowedtexturenode.cpp scenegraph/shadowedtexturematerial.cpp scenegraph/shadowedbordertexturematerial.cpp ${kirigami_QM_LOADER} ${KIRIGAMI_STATIC_FILES} ) qt5_add_resources(SHADERS scenegraph/shaders/shaders.qrc) add_subdirectory(libkirigami) if(NOT BUILD_SHARED_LIBS) # `rcc` is a bit dumb and isn't designed to use auto generated files, to # avoid poluting the source directory, use absolute paths set(kirigami_QML_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../) # First, pre-process the QRC to add the files associated with the right Qt # version. configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/../kirigami.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/../kirigami.qrc @ONLY ) # When using the static library, all QML files need to be shipped within the # .a file. qt5_add_resources( RESOURCES ${CMAKE_CURRENT_BINARY_DIR}/../kirigami.qrc ) if (UNIX AND NOT ANDROID AND NOT(APPLE) AND NOT(DISABLE_DBUS)) qt5_add_dbus_interface(kirigami_SRCS libkirigami/org.kde.KWin.TabletModeManager.xml tabletmodemanager_interface) endif() endif() add_library(kirigamiplugin ${kirigami_SRCS} ${RESOURCES} ${SHADERS}) if(NOT BUILD_SHARED_LIBS) SET_TARGET_PROPERTIES(kirigamiplugin PROPERTIES AUTOMOC_MOC_OPTIONS -Muri=org.kde.kirigami) if (UNIX AND NOT ANDROID AND NOT(APPLE) AND NOT(DISABLE_DBUS)) set(Kirigami_EXTRA_LIBS Qt5::DBus) else() set(Kirigami_EXTRA_LIBS "") endif() else() set(Kirigami_EXTRA_LIBS KF5::Kirigami2) endif() target_link_libraries(kirigamiplugin PUBLIC Qt5::Core PRIVATE ${Kirigami_EXTRA_LIBS} Qt5::Qml Qt5::Quick Qt5::QuickControls2 Qt5::Concurrent ) if (BUILD_SHARED_LIBS) add_custom_target(copy_to_bin ALL COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2/ COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/controls ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2/ COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/styles ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2/styles COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_BINARY_DIR}/bin/org/kde/kirigami.2/ ) install(DIRECTORY controls/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) if (DESKTOP_ENABLED) install(DIRECTORY styles/org.kde.desktop DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/styles) endif() install(DIRECTORY styles/Material DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2/styles) install(FILES ${platformspecific} DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME Kirigami2 LIB_NAME KF5Kirigami2 DEPS "core qml quick svg" FILENAME_VAR PRI_FILENAME ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) endif() install(TARGETS kirigamiplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigami.2) diff --git a/src/controls/PageRow.qml b/src/controls/PageRow.qml index e96dc993..8afc033c 100644 --- a/src/controls/PageRow.qml +++ b/src/controls/PageRow.qml @@ -1,613 +1,624 @@ /* * SPDX-FileCopyrightText: 2016 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick 2.5 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import QtQuick.Templates 2.0 as T import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.7 import "private/globaltoolbar" as GlobalToolBar import "templates" as KT /** * PageRow implements a row-based navigation model, which can be used * with a set of interlinked information pages. Items are pushed in the * back of the row and the view scrolls until that row is visualized. * A PageRowcan show a single page or a multiple set of columns, depending * on the window width: on a phone a single column should be fullscreen, * while on a tablet or a desktop more than one column should be visible. * @inherit QtQuick.Templates.Control */ T.Control { id: root //BEGIN PROPERTIES /** * This property holds the number of items currently pushed onto the view */ property alias depth: columnView.count /** * The last Page in the Row */ readonly property Item lastItem: columnView.contentChildren.length > 0 ? columnView.contentChildren[columnView.contentChildren.length - 1] : null /** * The currently visible Item */ property alias currentItem: columnView.currentItem /** * the index of the currently visible Item */ property alias currentIndex: columnView.currentIndex /** * The initial item when this PageRow is created */ property variant initialPage /** * The main ColumnView of this Row */ contentItem: columnView + /** + * columnView: Kirigami::ColumnView + * + * The ColumnView that this PageRow owns. + * Generally, you shouldn't need to change + * the value of this. + * + * @since 2.12 + */ + property alias columnView: columnView + /** * items: list * All the items that are present in the PageRow * @since 2.6 */ property alias items: columnView.contentChildren; /** * visibleItems: list * All pages which are visible in the PageRow, excluding those which are scrolled away * @since 2.6 */ property alias visibleItems: columnView.visibleItems /** * firstVisibleItem: Item * The first at least partially visible page in the PageRow, pages before that one will be out of the viewport * @since 2.6 */ property alias firstVisibleItem: columnView.firstVisibleItem /** * lastVisibleItem: Item * The last at least partially visible page in the PageRow, pages after that one will be out of the viewport * @since 2.6 */ property alias lastVisibleItem: columnView.lastVisibleItem /** * The default width for a column * default is wide enough for 30 grid units. * Pages can override it with their Layout.fillWidth, * implicitWidth Layout.minimumWidth etc. */ property int defaultColumnWidth: Units.gridUnit * 20 /** * interactive: bool * If true it will be possible to go back/forward by dragging the * content themselves with a gesture. * Otherwise the only way to go back will be programmatically * default: true */ property alias interactive: columnView.interactive /** * wideMode: bool * If true, the PageRow is wide enough that willshow more than one column at once * @since 5.37 */ readonly property bool wideMode: root.width >= root.defaultColumnWidth*2 && depth >= 2 /** * separatorVisible: bool * True if the separator between pages should be visible * default: true * @since 5.38 */ property alias separatorVisible: columnView.separatorVisible /** * globalToolBar: grouped property * Controls the appearance of an optional global toolbar for the whole PageRow. * It's a grouped property comprised of the following properties: * * style (Kirigami.ApplicationHeaderStyle): can have the following values: * * Auto: depending on application formfactor, it can behave automatically like other values, such as a Breadcrumb on mobile and ToolBar on desktop * * Breadcrumb: it will show a breadcrumb of all the page titles in the stack, for easy navigation * * Titles: each page will only have its own tile on top * * TabBar: the global toolbar will look like a TabBar to select the pages * * ToolBar: each page will have the title on top together buttons and menus to represent all of the page actions: not available on Mobile systems. * * None: no global toolbar will be shown * * * actualStyle: this will represent the actual style of the toolbar: it can be different from style in the case style is Auto * * showNavigationButtons: OR flags combination of ApplicationHeaderStyle.ShowBackButton and ApplicationHeaderStyle.ShowForwardButton * * toolbarActionAlignment: How to horizontally align the actions when using the ToolBar style. Note that anything but Qt.AlignRight will cause the title to be hidden (default: Qt.AlignRight) * * minimumHeight (int): minimum height of the header, which will be resized when scrolling, only in Mobile mode (default: preferredHeight, sliding but no scaling) * * preferredHeight (int): the height the toolbar will usually have * * maximumHeight (int): the height the toolbar will have in mobile mode when the app is in reachable mode (default: preferredHeight * 1.5) * * leftReservedSpace (int, readonly): how many pixels are reserved at the left of the page toolbar (for navigation buttons or drawer handle) * * rightReservedSpace (int, readonly): how many pixels are reserved at the right of the page toolbar (drawer handle) * * @since 5.48 */ readonly property alias globalToolBar: globalToolBar implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding //END PROPERTIES //BEGIN FUNCTIONS /** * Pushes a page on the stack. * The page can be defined as a component, item or string. * If an item is used then the page will get re-parented. * If a string is used then it is interpreted as a url that is used to load a page * component. * The last pushed page will become the current item. * * @param page The page can also be given as an array of pages. * In this case all those pages will * be pushed onto the stack. The items in the stack can be components, items or * strings just like for single pages. * Additionally an object can be used, which specifies a page and an optional * properties property. * This can be used to push multiple pages while still giving each of * them properties. * When an array is used the transition animation will only be to the last page. * * @param properties The properties argument is optional and allows defining a * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps * @return The new created page (or the last one if it was an array) */ function push(page, properties) { var item = insertPage(depth, page, properties); currentIndex = depth - 1; return item; } /** * Inserts a new page or a list of new at an arbitrary position * The page can be defined as a component, item or string. * If an item is used then the page will get re-parented. * If a string is used then it is interpreted as a url that is used to load a page * component. * The current Page will not be changed, currentIndex will be adjusted * accordingly if needed to keep the same current page. * * @param page The page can also be given as an array of pages. * In this case all those pages will * be pushed onto the stack. The items in the stack can be components, items or * strings just like for single pages. * Additionally an object can be used, which specifies a page and an optional * properties property. * This can be used to push multiple pages while still giving each of * them properties. * When an array is used the transition animation will only be to the last page. * * @param properties The properties argument is optional and allows defining a * map of properties to set on the page. If page is actually an array of pages, properties should also be an array of key/value maps * @return The new created page (or the last one if it was an array) * @since 2.7 */ function insertPage(position, page, properties) { if (!page) { return null } //don't push again things already there if (page.createObject === undefined && typeof page != "string" && columnView.containsItem(page)) { print("The item " + page + " is already in the PageRow"); return null; } position = Math.max(0, Math.min(depth, position)); columnView.pop(columnView.currentItem); // figure out if more than one page is being pushed var pages; var propsArray = []; if (page instanceof Array) { pages = page; page = pages.pop(); //compatibility with pre-qqc1 api, can probably be removed if (page.createObject === undefined && page.parent === undefined && typeof page != "string") { properties = properties || page.properties; page = page.page; } } if (properties instanceof Array) { propsArray = properties; properties = propsArray.pop(); } else { propsArray = [properties]; } // push any extra defined pages onto the stack if (pages) { var i; for (i = 0; i < pages.length; i++) { var tPage = pages[i]; var tProps = propsArray[i]; //compatibility with pre-qqc1 api, can probably be removed if (tPage.createObject === undefined && tPage.parent === undefined && typeof tPage != "string") { if (columnView.containsItem(tPage)) { print("The item " + page + " is already in the PageRow"); continue; } tProps = tPage.properties; tPage = tPage.page; } var pageItem = pagesLogic.initAndInsertPage(position, tPage, tProps); ++position; } } // initialize the page var pageItem = pagesLogic.initAndInsertPage(position, page, properties); pagePushed(pageItem); return pageItem; } /** * Move the page at position fromPos to the new position toPos * If needed, currentIndex will be adjusted * in order to keep the same current page. * @since 2.7 */ function movePage(fromPos, toPos) { columnView.moveItem(fromPos, toPos); } /** * Remove the given page * @param page The page can be given both as integer position or by reference * @return The page that has just been removed * @since 2.7 */ function removePage(page) { if (depth == 0) { return null; } return columnView.removeItem(page); } /** * Pops a page off the stack. * @param page If page is specified then the stack is unwound to that page, * to unwind to the first page specify * page as null. * @return The page instance that was popped off the stack. */ function pop(page) { if (depth == 0) { return null; } return columnView.pop(page); } /** * Emitted when a page has been inserted anywhere * @param position where the page has been inserted * @param page the new page * @since 2.7 */ signal pageInserted(int position, Item page) /** * Emitted when a page has been pushed to the bottom * @param page the new page * @since 2.5 */ signal pagePushed(Item page) /** * Emitted when a page has been removed from the row. * @param page the page that has been removed: at this point it's still valid, * but may be auto deleted soon. * @since 2.5 */ signal pageRemoved(Item page) /** * Replaces a page on the stack. * @param page The page can also be given as an array of pages. * In this case all those pages will * be pushed onto the stack. The items in the stack can be components, items or * strings just like for single pages. * the current page and all pagest after it in the stack will be removed. * Additionally an object can be used, which specifies a page and an optional * properties property. * This can be used to push multiple pages while still giving each of * them properties. * When an array is used the transition animation will only be to the last page. * @param properties The properties argument is optional and allows defining a * map of properties to set on the page. * @see push() for details. */ function replace(page, properties) { if (currentIndex >= 1) { pop(columnView.contentChildren[currentIndex-1]); } else if (currentIndex == 0) { pop(); } else { console.warn("There's no page to replace"); } return push(page, properties); } /** * Clears the page stack. * Destroy (or reparent) all the pages contained. */ function clear() { return columnView.clear(); } /** * @return the page at idx * @param idx the depth of the page we want */ function get(idx) { return columnView.contentChildren[idx]; } /** * go back to the previous index and scroll to the left to show one more column */ function flickBack() { if (depth > 1) { currentIndex = Math.max(0, currentIndex - 1); } } /** * layers: QtQuick.Controls.PageStack * Access to the modal layers. * Sometimes an application needs a modal page that always covers all the rows. * For instance the full screen image of an image viewer or a settings page. * @since 5.38 */ property alias layers: layersStack //END FUNCTIONS onInitialPageChanged: { if (initialPage) { clear(); push(initialPage, null) } } /* onActiveFocusChanged: { if (activeFocus) { layersStack.currentItem.forceActiveFocus() if (columnView.activeFocus) { print("SSS"+columnView.currentItem) columnView.currentItem.forceActiveFocus(); } } } */ Keys.forwardTo: [currentItem] GlobalToolBar.PageRowGlobalToolBarStyleGroup { id: globalToolBar readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0 readonly property int rightReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.rightReservedSpace : 0 readonly property int height: globalToolBarUI.height readonly property Item leftHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.leftHandleAnchor : null readonly property Item rightHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.rightHandleAnchor : null } QQC2.StackView { id: layersStack z: 99 anchors { fill: parent } //placeholder as initial item initialItem: columnView function clear () { //don't let it kill the main page row var d = root.depth; for (var i = 1; i < d; ++i) { pop(); } } 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 } } } } Loader { id: globalToolBarUI anchors { left: parent.left top: parent.top right: parent.right } z: 100 active: globalToolBar.actualStyle != ApplicationHeaderStyle.None || (firstVisibleItem && firstVisibleItem.globalToolBarStyle == ApplicationHeaderStyle.ToolBar) visible: active height: active ? implicitHeight : 0 source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml"); } QtObject { id: pagesLogic readonly property var componentCache: new Array() function initAndInsertPage(position, page, properties) { var pageComp; if (page.createObject) { // page defined as component pageComp = page; } else if (typeof page == "string") { // page defined as string (a url) pageComp = pagesLogic.componentCache[page]; if (!pageComp) { pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page); } } if (pageComp) { // instantiate page from component // FIXME: parent directly to columnView or root? page = pageComp.createObject(null, properties || {}); columnView.insertItem(position, page); if (pageComp.status === Component.Error) { throw new Error("Error while loading page: " + pageComp.errorString()); } } else { // copy properties to the page for (var prop in properties) { if (properties.hasOwnProperty(prop)) { page[prop] = properties[prop]; } } columnView.insertItem(position, page); } return page; } } ColumnView { id: columnView topPadding: globalToolBarUI.item && globalToolBarUI.item.breadcrumbVisible ? globalToolBarUI.height : 0 // Internal hidden api for Page readonly property Item __pageRow: root columnResizeMode: root.wideMode ? ColumnView.FixedColumns : ColumnView.SingleColumn columnWidth: root.defaultColumnWidth onItemInserted: root.pageInserted(position, item); onItemRemoved: root.pageRemoved(item); } Rectangle { anchors.bottom: parent.bottom height: Units.smallSpacing x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width)) width: columnView.width * (columnView.width/columnView.contentWidth) color: Theme.textColor opacity: 0 onXChanged: { opacity = 0.3 scrollIndicatorTimer.restart(); } Behavior on opacity { OpacityAnimator { duration: Units.longDuration easing.type: Easing.InOutQuad } } Timer { id: scrollIndicatorTimer interval: Units.longDuration * 4 onTriggered: parent.opacity = 0; } } } diff --git a/src/kirigamiplugin.cpp b/src/kirigamiplugin.cpp index 45ac0787..4903c02b 100644 --- a/src/kirigamiplugin.cpp +++ b/src/kirigamiplugin.cpp @@ -1,261 +1,265 @@ /* * SPDX-FileCopyrightText: 2009 Alan Alpert * SPDX-FileCopyrightText: 2010 Ménard Alexis * SPDX-FileCopyrightText: 2010 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kirigamiplugin.h" #include "columnview.h" #include "enums.h" #include "icon.h" #include "settings.h" #include "formlayoutattached.h" #include "mnemonicattached.h" #include "delegaterecycler.h" #include "pagepool.h" #include "scenepositionattached.h" #include "wheelhandler.h" #include "shadowedrectangle.h" #include "shadowedtexture.h" #include "colorutils.h" +#include "pagerouter.h" #include #include #include #include #include #include #include "libkirigami/platformtheme.h" static QString s_selectedStyle; //Q_INIT_RESOURCE(kirigami); #ifdef KIRIGAMI_BUILD_TYPE_STATIC #include #endif class CopyHelperPrivate : public QObject { Q_OBJECT public: Q_INVOKABLE static void copyTextToClipboard(const QString& text) { qGuiApp->clipboard()->setText(text); } }; // we can't do this in the plugin object directly, as that can live in a different thread // and event filters are only allowed in the same thread as the filtered object class LanguageChangeEventFilter : public QObject { Q_OBJECT public: bool eventFilter(QObject *receiver, QEvent *event) override { if (event->type() == QEvent::LanguageChange && receiver == QCoreApplication::instance()) { emit languageChangeEvent(); } return QObject::eventFilter(receiver, event); } Q_SIGNALS: void languageChangeEvent(); }; KirigamiPlugin::KirigamiPlugin(QObject *parent) : QQmlExtensionPlugin(parent) { auto filter = new LanguageChangeEventFilter; filter->moveToThread(QCoreApplication::instance()->thread()); QCoreApplication::instance()->installEventFilter(filter); connect(filter, &LanguageChangeEventFilter::languageChangeEvent, this, &KirigamiPlugin::languageChangeEvent); } QUrl KirigamiPlugin::componentUrl(const QString &fileName) const { for (const QString &style : qAsConst(m_stylesFallbackChain)) { const QString candidate = QStringLiteral("styles/") + style + QLatin1Char('/') + fileName; if (QFile::exists(resolveFilePath(candidate))) { #ifdef KIRIGAMI_BUILD_TYPE_STATIC return QUrl(QStringLiteral("qrc:/org/kde/kirigami/styles/") + style + QLatin1Char('/') + fileName); #else return QUrl(resolveFileUrl(candidate)); #endif } } #ifdef KIRIGAMI_BUILD_TYPE_STATIC return QUrl(QStringLiteral("qrc:/org/kde/kirigami/") + fileName); #else return QUrl(resolveFileUrl(fileName)); #endif } void KirigamiPlugin::registerTypes(const char *uri) { #if defined(Q_OS_ANDROID) && QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QResource::registerResource(QStringLiteral("assets:/android_rcc_bundle.rcc")); #endif Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.kirigami")); const QString style = QQuickStyle::name(); if (QIcon::themeName().isEmpty() && !qEnvironmentVariableIsSet("XDG_CURRENT_DESKTOP")) { QIcon::setThemeSearchPaths({resolveFilePath(QStringLiteral(".")), QStringLiteral(":/icons")}); QIcon::setThemeName(QStringLiteral("breeze-internal")); } #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) //org.kde.desktop.plasma is a couple of files that fall back to desktop by purpose if ((style.isEmpty() || style == QStringLiteral("org.kde.desktop.plasma")) && QFile::exists(resolveFilePath(QStringLiteral("/styles/org.kde.desktop")))) { m_stylesFallbackChain.prepend(QStringLiteral("org.kde.desktop")); } #elif defined(Q_OS_ANDROID) if (!m_stylesFallbackChain.contains(QLatin1String("Material"))) { m_stylesFallbackChain.prepend(QStringLiteral("Material")); } #else // do we have an iOS specific style? if (!m_stylesFallbackChain.contains(QLatin1String("Material"))) { m_stylesFallbackChain.prepend(QStringLiteral("Material")); } #endif if (!style.isEmpty() && QFile::exists(resolveFilePath(QStringLiteral("/styles/") + style)) && !m_stylesFallbackChain.contains(style)) { m_stylesFallbackChain.prepend(style); //if we have plasma deps installed, use them for extra integration if (style == QStringLiteral("org.kde.desktop") && QFile::exists(resolveFilePath(QStringLiteral("/styles/org.kde.desktop.plasma")))) { m_stylesFallbackChain.prepend(QStringLiteral("org.kde.desktop.plasma")); } } else { #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) m_stylesFallbackChain.prepend(QStringLiteral("org.kde.desktop")); #endif } //At this point the fallback chain will be selected->org.kde.desktop->Fallback s_selectedStyle = m_stylesFallbackChain.first(); qmlRegisterSingletonType(uri, 2, 0, "Settings", [](QQmlEngine *e, QJSEngine*) -> QObject* { Settings *settings = Settings::self(); //singleton managed internally, qml should never delete it e->setObjectOwnership(settings, QQmlEngine::CppOwnership); settings->setStyle(s_selectedStyle); return settings; } ); qmlRegisterUncreatableType(uri, 2, 0, "ApplicationHeaderStyle", QStringLiteral("Cannot create objects of type ApplicationHeaderStyle")); //old legacy retrocompatible Theme qmlRegisterSingletonType(componentUrl(QStringLiteral("Theme.qml")), uri, 2, 0, "Theme"); qmlRegisterSingletonType(componentUrl(QStringLiteral("Units.qml")), uri, 2, 0, "Units"); qmlRegisterType(componentUrl(QStringLiteral("Action.qml")), uri, 2, 0, "Action"); qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationHeader.qml")), uri, 2, 0, "AbstractApplicationHeader"); qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationWindow.qml")), uri, 2, 0, "AbstractApplicationWindow"); qmlRegisterType(componentUrl(QStringLiteral("AbstractListItem.qml")), uri, 2, 0, "AbstractListItem"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationHeader.qml")), uri, 2, 0, "ApplicationHeader"); qmlRegisterType(componentUrl(QStringLiteral("ToolBarApplicationHeader.qml")), uri, 2, 0, "ToolBarApplicationHeader"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationWindow.qml")), uri, 2, 0, "ApplicationWindow"); qmlRegisterType(componentUrl(QStringLiteral("BasicListItem.qml")), uri, 2, 0, "BasicListItem"); qmlRegisterType(componentUrl(QStringLiteral("OverlayDrawer.qml")), uri, 2, 0, "OverlayDrawer"); qmlRegisterType(componentUrl(QStringLiteral("ContextDrawer.qml")), uri, 2, 0, "ContextDrawer"); qmlRegisterType(componentUrl(QStringLiteral("GlobalDrawer.qml")), uri, 2, 0, "GlobalDrawer"); qmlRegisterType(componentUrl(QStringLiteral("Heading.qml")), uri, 2, 0, "Heading"); qmlRegisterType(componentUrl(QStringLiteral("Separator.qml")), uri, 2, 0, "Separator"); qmlRegisterType(componentUrl(QStringLiteral("PageRow.qml")), uri, 2, 0, "PageRow"); qmlRegisterType(uri, 2, 0, "Icon"); qmlRegisterType(componentUrl(QStringLiteral("Label.qml")), uri, 2, 0, "Label"); //TODO: uncomment for 2.3 release //qmlRegisterTypeNotAvailable(uri, 2, 3, "Label", "Label type not supported anymore, use QtQuick.Controls.Label 2.0 instead"); qmlRegisterType(componentUrl(QStringLiteral("OverlaySheet.qml")), uri, 2, 0, "OverlaySheet"); qmlRegisterType(componentUrl(QStringLiteral("Page.qml")), uri, 2, 0, "Page"); qmlRegisterType(componentUrl(QStringLiteral("ScrollablePage.qml")), uri, 2, 0, "ScrollablePage"); qmlRegisterType(componentUrl(QStringLiteral("SplitDrawer.qml")), uri, 2, 0, "SplitDrawer"); qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem.qml")), uri, 2, 0, "SwipeListItem"); //2.1 qmlRegisterType(componentUrl(QStringLiteral("AbstractItemViewHeader.qml")), uri, 2, 1, "AbstractItemViewHeader"); qmlRegisterType(componentUrl(QStringLiteral("ItemViewHeader.qml")), uri, 2, 1, "ItemViewHeader"); qmlRegisterType(componentUrl(QStringLiteral("AbstractApplicationItem.qml")), uri, 2, 1, "AbstractApplicationItem"); qmlRegisterType(componentUrl(QStringLiteral("ApplicationItem.qml")), uri, 2, 1, "ApplicationItem"); //2.2 //Theme changed from a singleton to an attached property qmlRegisterUncreatableType(uri, 2, 2, "Theme", QStringLiteral("Cannot create objects of type Theme, use it as an attached property")); //2.3 qmlRegisterType(componentUrl(QStringLiteral("FormLayout.qml")), uri, 2, 3, "FormLayout"); qmlRegisterUncreatableType(uri, 2, 3, "FormData", QStringLiteral("Cannot create objects of type FormData, use it as an attached property")); qmlRegisterUncreatableType(uri, 2, 3, "MnemonicData", QStringLiteral("Cannot create objects of type MnemonicData, use it as an attached property")); //2.4 qmlRegisterType(componentUrl(QStringLiteral("AbstractCard.qml")), uri, 2, 4, "AbstractCard"); qmlRegisterType(componentUrl(QStringLiteral("Card.qml")), uri, 2, 4, "Card"); qmlRegisterType(componentUrl(QStringLiteral("CardsListView.qml")), uri, 2, 4, "CardsListView"); qmlRegisterType(componentUrl(QStringLiteral("CardsGridView.qml")), uri, 2, 4, "CardsGridView"); qmlRegisterType(componentUrl(QStringLiteral("CardsLayout.qml")), uri, 2, 4, "CardsLayout"); qmlRegisterType(componentUrl(QStringLiteral("InlineMessage.qml")), uri, 2, 4, "InlineMessage"); qmlRegisterUncreatableType(uri, 2, 4, "MessageType", QStringLiteral("Cannot create objects of type MessageType")); qmlRegisterType(uri, 2, 4, "DelegateRecycler"); //2.5 qmlRegisterType(componentUrl(QStringLiteral("ListItemDragHandle.qml")), uri, 2, 5, "ListItemDragHandle"); qmlRegisterType(componentUrl(QStringLiteral("ActionToolBar.qml")), uri, 2, 5, "ActionToolBar"); qmlRegisterUncreatableType(uri, 2, 5, "ScenePosition", QStringLiteral("Cannot create objects of type ScenePosition, use it as an attached property")); //2.6 qmlRegisterType(componentUrl(QStringLiteral("AboutPage.qml")), uri, 2, 6, "AboutPage"); qmlRegisterType(componentUrl(QStringLiteral("LinkButton.qml")), uri, 2, 6, "LinkButton"); qmlRegisterType(componentUrl(QStringLiteral("UrlButton.qml")), uri, 2, 6, "UrlButton"); qmlRegisterSingletonType("org.kde.kirigami.private", 2, 6, "CopyHelperPrivate", [] (QQmlEngine*, QJSEngine*) -> QObject* { return new CopyHelperPrivate; }); //2.7 qmlRegisterType(uri, 2, 7, "ColumnView"); qmlRegisterType(componentUrl(QStringLiteral("ActionTextField.qml")), uri, 2, 7, "ActionTextField"); //2.8 qmlRegisterType(componentUrl(QStringLiteral("SearchField.qml")), uri, 2, 8, "SearchField"); qmlRegisterType(componentUrl(QStringLiteral("PasswordField.qml")), uri, 2, 8, "PasswordField"); //2.9 qmlRegisterType(uri, 2, 9, "WheelHandler"); qmlRegisterUncreatableType(uri, 2, 9, "WheelEvent", QStringLiteral("Cannot create objects of type WheelEvent.")); //2.10 qmlRegisterType(componentUrl(QStringLiteral("ListSectionHeader.qml")), uri, 2, 10, "ListSectionHeader"); // 2.11 qmlRegisterType(uri, 2, 11, "PagePool"); qmlRegisterType(componentUrl(QStringLiteral("PagePoolAction.qml")), uri, 2, 11, "PagePoolAction"); //TODO: remove qmlRegisterType(componentUrl(QStringLiteral("SwipeListItem2.qml")), uri, 2, 11, "SwipeListItem2"); // 2.12 qmlRegisterType(uri, 2, 12, "ShadowedRectangle"); qmlRegisterType(uri, 2, 12, "ShadowedTexture"); qmlRegisterType(componentUrl(QStringLiteral("ShadowedImage.qml")), uri, 2, 12, "ShadowedImage"); qmlRegisterUncreatableType(uri, 2, 12, "BorderGroup", QStringLiteral("Used as grouped property")); qmlRegisterUncreatableType(uri, 2, 12, "ShadowGroup", QStringLiteral("Used as grouped property")); qmlRegisterSingletonType(uri, 2, 12, "ColorUtils", [] (QQmlEngine*, QJSEngine*) -> QObject* { return new ColorUtils; }); qmlRegisterUncreatableType(uri, 2, 12, "CornersGroup", QStringLiteral("Used as grouped property")); + qmlRegisterType(uri, 2, 12, "PageRouter"); + qmlRegisterType(uri, 2, 12, "PageRoute"); + qmlRegisterUncreatableType(uri, 2, 12, "PageRouterAttached", QStringLiteral("PageRouterAttached cannot be created")); qmlProtectModule(uri, 2); } void KirigamiPlugin::initializeEngine(QQmlEngine *engine, const char *uri) { Q_UNUSED(uri); connect(this, &KirigamiPlugin::languageChangeEvent, engine, &QQmlEngine::retranslate); } #include "kirigamiplugin.moc" diff --git a/src/pagerouter.cpp b/src/pagerouter.cpp new file mode 100644 index 00000000..e69972e7 --- /dev/null +++ b/src/pagerouter.cpp @@ -0,0 +1,387 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include +#include +#include +#include +#include +#include "pagerouter.h" + +ParsedRoute parseRoute(QJSValue value) +{ + if (value.isUndefined()) { + return ParsedRoute{QString(), QVariant(), false, nullptr}; + } else if (value.isString()) { + return ParsedRoute{ + value.toString(), + QVariant(), + false, + nullptr + }; + } else { + return ParsedRoute{ + value.property(QStringLiteral("route")).toString(), + value.property(QStringLiteral("data")).toVariant(), + false, + nullptr + }; + } +} + +QList parseRoutes(QJSValue values) +{ + QList ret; + if (values.isArray()) { + for (auto route : values.toVariant().toList()) { + if (route.toString() != QString()) { + ret << ParsedRoute{ + route.toString(), + QVariant(), + false, + nullptr + }; + } else if (route.canConvert()) { + auto map = route.value(); + ret << ParsedRoute{ + map.value(QStringLiteral("route")).toString(), + map.value(QStringLiteral("data")), + false, + nullptr + }; + } + } + } else { + ret << parseRoute(values); + } + return ret; +} + +PageRouter::PageRouter(QQuickItem *parent) : QObject(parent) +{ + connect(this, &PageRouter::pageStackChanged, [=]() { + connect(m_pageStack, &ColumnView::itemRemoved, [=](QQuickItem *item) { + QList toRemove; + for (auto route : m_currentRoutes) { + if (route.item == qobject_cast(item)) { + if (!route.cache) { + route.item->deleteLater(); + } + } + } + for (auto route : toRemove) { + m_currentRoutes.removeAll(route); + } + }); + connect(m_pageStack, &ColumnView::currentIndexChanged, this, &PageRouter::currentIndexChanged); + }); +} + +QQmlListProperty PageRouter::routes() +{ + return QQmlListProperty(this, nullptr, appendRoute, routeCount, route, clearRoutes); +} + +void PageRouter::appendRoute(QQmlListProperty* prop, PageRoute* route) +{ + auto router = qobject_cast(prop->object); + router->m_routes.append(route); +} + +int PageRouter::routeCount(QQmlListProperty* prop) +{ + auto router = qobject_cast(prop->object); + return router->m_routes.length(); +} + +PageRoute* PageRouter::route(QQmlListProperty* prop, int index) +{ + auto router = qobject_cast(prop->object); + return router->m_routes[index]; +} + +void PageRouter::clearRoutes(QQmlListProperty* prop) +{ + auto router = qobject_cast(prop->object); + router->m_routes.clear(); +} + +PageRouter::~PageRouter() {} + +void PageRouter::classBegin() +{ + +} + +void PageRouter::componentComplete() +{ + if (m_pageStack == nullptr) { + qCritical() << "PageRouter should be created with a ColumnView. Not doing so is undefined behaviour, and is likely to result in a crash upon further interaction."; + } else { + Q_EMIT pageStackChanged(); + m_currentRoutes.clear(); + push(parseRoute(initialRoute())); + } +} + +bool PageRouter::routesContainsKey(const QString &key) +{ + for (auto route : m_routes) { + if (route->name() == key) return true; + } + return false; +} + +QQmlComponent* PageRouter::routesValueForKey(const QString &key) +{ + for (auto route : m_routes) { + if (route->name() == key) return route->component(); + } + return nullptr; +} + +bool PageRouter::routesCacheForKey(const QString &key) +{ + for (auto route : m_routes) { + if (route->name() == key) return route->cache(); + } + return false; +} + +void PageRouter::push(ParsedRoute route) +{ + if (!routesContainsKey(route.name)) { + qCritical() << "Route" << route.name << "not defined"; + return; + } + if (routesCacheForKey(route.name)) { + for (auto cachedRoute : m_cachedRoutes) { + if (cachedRoute.name == route.name && cachedRoute.data == route.data) { + m_currentRoutes << cachedRoute; + m_pageStack->addItem(qobject_cast(cachedRoute.item)); + return; + } + } + } + auto context = qmlContext(this); + auto component = routesValueForKey(route.name); + auto createAndPush = [component, context, route, this]() { + // We use beginCreate and completeCreate to allow + // for a PageRouterAttached to find its parent + // on construction time. + auto item = component->beginCreate(context); + item->setParent(this); + auto clone = route; + clone.item = item; + clone.cache = routesCacheForKey(route.name); + m_currentRoutes << clone; + if (routesCacheForKey(route.name)) { + m_cachedRoutes << clone; + } + component->completeCreate(); + m_pageStack->addItem(qobject_cast(item)); + m_pageStack->setCurrentIndex(m_currentRoutes.length()-1); + }; + + if (component->status() == QQmlComponent::Ready) { + createAndPush(); + } else if (component->status() == QQmlComponent::Loading) { + connect(component, &QQmlComponent::statusChanged, [=](QQmlComponent::Status status) { + // Loading can only go to Ready or Error. + if (status != QQmlComponent::Ready) { + qCritical() << "Failed to push route:" << component->errors(); + } + createAndPush(); + }); + } else { + qCritical() << "Failed to push route:" << component->errors(); + } +} + +QJSValue PageRouter::initialRoute() const +{ + return m_initialRoute; +} + +void PageRouter::setInitialRoute(QJSValue value) +{ + m_initialRoute = value; +} + +void PageRouter::navigateToRoute(QJSValue route) +{ + auto incomingRoutes = parseRoutes(route); + QList resolvedRoutes; + + if (incomingRoutes.length() <= m_currentRoutes.length()) { + resolvedRoutes = m_currentRoutes.mid(0, incomingRoutes.length()); + } else { + resolvedRoutes = m_currentRoutes; + resolvedRoutes.reserve(incomingRoutes.length()-m_currentRoutes.length()); + } + + for (int i = 0; i < incomingRoutes.length(); i++) { + auto current = resolvedRoutes.value(i); + auto incoming = incomingRoutes.at(i); + if (i >= resolvedRoutes.length()) { + resolvedRoutes.append(incoming); + } else if (current != incoming) { + resolvedRoutes.replace(i, incoming); + } + } + + for (auto route : m_currentRoutes) { + if (!resolvedRoutes.contains(route)) { + if (!route.cache) { + route.item->deleteLater(); + } + } + } + + m_pageStack->clear(); + m_currentRoutes.clear(); + for (auto toPush : resolvedRoutes) { + push(toPush); + } +} + +void PageRouter::bringToView(QJSValue route) +{ + if (route.isNumber()) { + auto index = route.toNumber(); + m_pageStack->setCurrentIndex(index); + } else { + auto parsed = parseRoute(route); + auto index = 0; + for (auto currentRoute : m_currentRoutes) { + if (currentRoute.name == parsed.name && currentRoute.data == parsed.data) { + m_pageStack->setCurrentIndex(index); + return; + } + index++; + } + qWarning() << "Route" << parsed.name << "with data" << parsed.data << "is not on the current stack of routes."; + } +} + +bool PageRouter::routeActive(QJSValue route) +{ + auto parsed = parseRoutes(route); + if (parsed.length() > m_currentRoutes.length()) { + return false; + } + for (int i = 0; i < parsed.length(); i++) { + if (parsed[i].name != m_currentRoutes[i].name) { + return false; + } + if (parsed[i].data.isValid()) { + if (parsed[i].data != m_currentRoutes[i].data) { + return false; + } + } + } + return true; +} + +void PageRouter::pushRoute(QJSValue route) +{ + push(parseRoute(route)); +} + +void PageRouter::popRoute() +{ + m_pageStack->pop(qobject_cast(m_currentRoutes.last().item)); + if (!m_currentRoutes.last().cache) { + m_currentRoutes.last().item->deleteLater(); + } + m_currentRoutes.removeLast(); +} + +QVariant PageRouter::dataFor(QObject *object) +{ + auto pointer = object; + while (pointer != nullptr) { + for (auto route : m_currentRoutes) { + if (route.item == pointer) { + return route.data; + } + } + pointer = pointer->parent(); + } + return QVariant(); +} + +bool PageRouter::isActive(QObject *object) +{ + auto pointer = object; + while (pointer != nullptr) { + auto index = 0; + for (auto route : m_currentRoutes) { + if (route.item == pointer) { + return m_pageStack->currentIndex() == index; + } + index++; + } + pointer = pointer->parent(); + } + qWarning() << "Object" << object << "not in current routes"; + return false; +} + +PageRouterAttached* PageRouter::qmlAttachedProperties(QObject *object) +{ + auto attached = new PageRouterAttached(object); + auto pointer = object; + // Climb the parent tree to find our parent PageRouter + while (pointer != nullptr) { + auto casted = qobject_cast(pointer); + if (casted != nullptr) { + attached->m_router = casted; + connect(casted, &PageRouter::currentIndexChanged, attached, &PageRouterAttached::isCurrentChanged); + break; + } + pointer = pointer->parent(); + } + if (attached->m_router.isNull()) { + qCritical() << "PageRouterAttached could not find a parent PageRouter"; + } + return attached; +} + +QVariant PageRouterAttached::data() const +{ + if (m_router) { + return m_router->dataFor(parent()); + } else { + qCritical() << "PageRouterAttached does not have a parent PageRouter"; + return QVariant(); + } +} + +bool PageRouterAttached::isCurrent() const +{ + if (m_router) { + return m_router->isActive(parent()); + } else { + qCritical() << "PageRouterAttached does not have a parent PageRouter"; + return false; + } +} + +QJSValue PageRouter::currentRoutes() const +{ + auto engine = qjsEngine(this); + auto ret = engine->newArray(m_currentRoutes.length()); + for (int i = 0; i < m_currentRoutes.length(); ++i) { + auto object = engine->newObject(); + object.setProperty(QStringLiteral("route"), m_currentRoutes[i].name); + object.setProperty(QStringLiteral("data"), engine->toScriptValue(m_currentRoutes[i].data)); + ret.setProperty(i, object); + } + return ret; +} + +PageRouterAttached::PageRouterAttached(QObject *parent) : QObject(parent) {} \ No newline at end of file diff --git a/src/pagerouter.h b/src/pagerouter.h new file mode 100644 index 00000000..ffd03cba --- /dev/null +++ b/src/pagerouter.h @@ -0,0 +1,465 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include "columnview.h" + +struct ParsedRoute { + QString name; + QVariant data; + bool cache; + QObject* item; + bool operator==(const ParsedRoute& rhs) + { + return name == rhs.name && data == rhs.data && item == rhs.item && cache == rhs.cache; + } + bool operator!=(const ParsedRoute& rhs) + { + return name != rhs.name && data != rhs.data && item != rhs.item && cache != rhs.cache; + } +}; + +/** + * Item representing a route the PageRouter can navigate to. + * + * @include PageRoute.qml + * + * @see PageRouter + */ +class PageRoute : public QObject +{ + Q_OBJECT + + /** + * @brief The name of this route. + * + * This name should be unique per PageRoute in a PageRouter. + * When two PageRoutes have the same name, the one listed first + * in the PageRouter will be used. + */ + Q_PROPERTY(QString name MEMBER m_name READ name) + + /** + * @brief The page component of this route. + * + * This should be an instance of Component with a Kirigami::Page inside + * of it. + */ + Q_PROPERTY(QQmlComponent* component MEMBER m_component READ component) + + /** + * @brief Whether pages generated by this route should be cached or not. + * + * This should be an instance of Component with a Kirigami::Page inside + * of it. + * + * This will not work: + * + * @include PageRouterCachePagesDont.qml + * + * This will work: + * + * @include PageRouterCachePagesDo.qml + * + */ + Q_PROPERTY(bool cache MEMBER m_cache READ cache) + + Q_CLASSINFO("DefaultProperty", "component") + +private: + QString m_name; + QQmlComponent* m_component; + bool m_cache = false; + +public: + QQmlComponent* component() { return m_component; }; + QString name() { return m_name; }; + bool cache() { return m_cache; }; +}; + +class PageRouterAttached; + +/** + * An item managing pages and data of a ColumnView using named routes. + * + *

+ * + * ## Using a PageRouter + * + * Applications typically manage their contents via elements called "pages" or "screens." + * In Kirigami, these are called @link org::kde::kirigami::Page Pages @endlink and are + * arranged in @link PageRoute routes @endlink using a PageRouter to manage them. The PageRouter + * manages a stack of @link org::kde::kirigami::Page Pages @endlink created from a pool of potential + * @link PageRoute PageRoutes @endlink. + * + * Unlike most traditional stacks, a PageRouter provides functions for random access to its pages + * with navigateToRoute and routeActive. + * + * When your user interface fits the stack paradigm and is likely to use random access navigation, + * using the PageRouter is appropriate. For simpler navigation, it is more appropriate to avoid + * the overhead of a PageRouter by using a @link org::kde::kirigami::PageRow PageRow @endlink + * instead. + * + *

+ * + * ## Navigation Model + * + * A PageRouter draws from a pool of @link PageRoute PageRoutes @endlink in order to construct + * its stack. + * + * @image html PageRouterModel.svg width=50% + * + *

+ * + * You can push pages onto this stack... + * + * @image html PageRouterPush.svg width=50% + * + * ...or pop them off... + * + * @image html PageRouterPop.svg width=50% + * + * ...or navigate to an arbitrary collection of pages. + * + * @image html PageRouterNavigate.svg width=50% + * + *

+ * + * Components are able to query the PageRouter about the currently active routes + * on the stack. This is useful for e.g. a card indicating that the page it takes + * the user to is currently active. + * + *

+ * + * ## Example + * + * @include PageRouter.qml + * + * @see PageRouterAttached + * @see PageRoute + */ +class PageRouter : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /** + * @brief The named routes a PageRouter can navigate to. + * + * @include PageRouterRoutes.qml + */ + Q_PROPERTY(QQmlListProperty routes READ routes) + + Q_CLASSINFO("DefaultProperty", "routes") + + /** + * @brief The initial route. + * + * `initialRoute` is the page that the PageRouter will push upon + * creation. Changing it after creation will cause the PageRouter to reset + * its state. Not providing an `initialRoute` will result in undefined + * behavior. + * + * @include PageRouterInitialRoute.qml + */ + Q_PROPERTY(QJSValue initialRoute READ initialRoute WRITE setInitialRoute NOTIFY initialRouteChanged) + + /** + * @brief The ColumnView being puppeted by the PageRouter. + * + * All PageRouters should be created with a ColumnView, and creating one without + * a ColumnView is undefined behaviour. + * + * @warning You should **not** directly interact with a ColumnView being puppeted + * by a PageRouter. Instead, use a PageRouter's functions to manipulate the + * ColumnView. + * + * @include PageRouterColumnView.qml + */ + Q_PROPERTY(ColumnView* pageStack MEMBER m_pageStack NOTIFY pageStackChanged) + +private: + /** + * @brief The routes the PageRouter is aware of. + * + * Generally, this should not be mutated from C++, only read. + */ + QList m_routes; + + /** + * @brief The PageRouter being puppeted. + * + * m_pageRow is the ColumnView this PageRouter puppets. + */ + ColumnView* m_pageStack = nullptr; + + /** + * @brief The route that the PageRouter will load on completion. + * + * m_initialRoute is the raw QJSValue from QML that will be + * parsed into a ParsedRoute struct on construction. + * Generally, this should not be mutated from C++, only read. + */ + QJSValue m_initialRoute; + + /** + * @brief The current routes pushed on the PageRow. + * + * Generally, the state of m_pageRow and m_currentRoutes + * should be kept in sync. Undesirable behaviour will result + * from desynchronisation of the two. + */ + QList m_currentRoutes; + + /** + * @brief Cached routes. + * + * A list of ParsedRoutes with instantiated items. + */ + QList m_cachedRoutes; + + /** + * @brief Helper function to push a route. + * + * This function has the shared logic between + * navigateToRoute and pushRoute. + */ + void push(ParsedRoute route); + + /** + * @brief Helper function to access whether m_routes has a key. + * + * This function abstracts the QJSValue. + */ + bool routesContainsKey(const QString &key); + + /** + * @brief Helper function to access the component of a key for m_routes. + * + * The return value will be a nullptr if @p key does not exist in + * m_routes. + */ + QQmlComponent *routesValueForKey(const QString &key); + + /** + * @brief Helper function to access the cache status of a key for m_routes. + * + * The return value will be false if @p key does not exist in + * m_routes. + */ + bool routesCacheForKey(const QString &key); + + static void appendRoute(QQmlListProperty* list, PageRoute*); + static int routeCount(QQmlListProperty* list); + static PageRoute* route(QQmlListProperty* list, int); + static void clearRoutes(QQmlListProperty* list); + + QVariant dataFor(QObject* object); + bool isActive(QObject* object); + + friend class PageRouterAttached; + +protected: + void classBegin() override; + void componentComplete() override; + +public: + PageRouter(QQuickItem *parent = nullptr); + ~PageRouter(); + + QQmlListProperty routes(); + + QJSValue initialRoute() const; + void setInitialRoute(QJSValue initialRoute); + + /** + * @brief Navigate to the given route. + * + * Calling `navigateToRoute` causes the PageRouter to replace currently + * active pages with the new route. + * + * @param route The given route for the PageRouter to navigate to. + * A route is an array of variants or a single item. A string item will be interpreted + * as a page without associated data. An object item will be interpreted + * as follows: + * @code{.js} + * { + * "route": "/home" // The named page of the route. + * "data": QtObject {} // The data to pass to the page. + * } + * @endcode + * Navigating to a route not defined in a PageRouter's routes is undefined + * behavior. + * + * @code{.qml} + * Button { + * text: "Login" + * onClicked: { + * Kirigami.PageRouter.navigateToRoute(["/home", "/login"]) + * } + * } + * @endcode + */ + Q_INVOKABLE void navigateToRoute(QJSValue route); + + /** + * @brief Check whether the current route is on the stack. + * + * `routeActive` will return true if the given route + * is on the stack. + * + * @param route The given route to check for. + * + * `routeActive` returns true for partial routes like + * the following: + * + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login", "/google"]) + * Kirigami.PageRouter.routeActive(["/home", "/login"]) // returns true + * @endcode + * + * This only works from the root page, e.g. the following will return false: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login", "/google"]) + * Kirigami.PageRouter.routeActive(["/login", "/google"]) // returns false + * @endcode + */ + Q_INVOKABLE bool routeActive(QJSValue route); + + /** + * @brief Appends a route to the currently navigated route. + * + * Calling `pushRoute` will append the given @p route to the currently navigated + * routes. See navigateToRoute() if you want to replace the items currently on + * the PageRouter. + * + * @param route The given route to push. + * + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login"]) + * // The PageRouter is navigated to /home/login + * Kirigami.PageRouter.pushRoute("/google") + * // The PageRouter is navigated to /home/login/google + * @endcode + */ + Q_INVOKABLE void pushRoute(QJSValue route); + + /** + * @brief Pops the last page on the router. + * + * Calling `popRoute` will result in the last page on the router getting popped. + * You should not call this function when there is only one page on the router. + * + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/login"]) + * // The PageRouter is navigated to /home/login + * Kirigami.PageRouter.popRoute() + * // The PageRouter is navigated to /home + * @endcode + */ + Q_INVOKABLE void popRoute(); + + /** + * @brief Shifts keyboard focus and view to a given index on the PageRouter's stack. + * + * @param view The view to bring to focus. If this is an integer index, the PageRouter will + * navigate to the given index. If it's a route specifier, the PageRouter will navigate + * to the first route matching it. + * + * Navigating to route by index: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/browse", "/apps", "/login"]) + * Kirigami.PageRouter.bringToView(1) + * @endcode + * + * Navigating to route by name: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute(["/home", "/browse", "/apps", "/login"]) + * Kirigami.PageRouter.bringToView("/browse") + * @endcode + * + * Navigating to route by data: + * @code{.js} + * Kirigami.PageRouter.navigateToRoute([{"route": "/page", "data": "red"}, + * {"route": "/page", "data": "blue"}, + * {"route": "/page", "data": "green"}, + * {"route": "/page", "data": "yellow"}]) + * Kirigami.PageRouter.bringToView({"route": "/page", "data": "blue"}) + * @endcode + */ + Q_INVOKABLE void bringToView(QJSValue route); + + /** + * @brief Returns a QJSValue corresponding to the current pages on the stack. + * + * The returned value is in the same form as the input to navigateToRoute. + */ + Q_INVOKABLE QJSValue currentRoutes() const; + + static PageRouterAttached *qmlAttachedProperties(QObject *object); + +Q_SIGNALS: + void routesChanged(); + void initialRouteChanged(); + void pageStackChanged(); + void currentIndexChanged(); +}; + +/** + * Attached object allowing children of a PageRouter to access its functions + * without requiring the children to have the parent PageRouter's id. + * + * @see PageRouter + */ +class PageRouterAttached : public QObject +{ + Q_OBJECT + + Q_PROPERTY(PageRouter *router READ router NOTIFY routerChanged) + /** + * The data for the page this item belongs to. Accessing this property + * outside of a PageRouter will result in undefined behavior. + */ + Q_PROPERTY(QVariant data READ data MEMBER m_data NOTIFY dataChanged) + + /** + * Whether the page this item belongs to is the current index of the ColumnView. + * Accessing this property outside of a PageRouter will result in undefined behaviour. + */ + Q_PROPERTY(bool isCurrent READ isCurrent NOTIFY isCurrentChanged) + +private: + explicit PageRouterAttached(QObject *parent = nullptr); + + QPointer m_router; + QVariant m_data; + + friend class PageRouter; + +public: + PageRouter* router() const { return m_router; }; + QVariant data() const; + bool isCurrent() const; + /// @see PageRouter::navigateToRoute() + Q_INVOKABLE void navigateToRoute(QJSValue route) { m_router->navigateToRoute(route); }; + /// @see PageRouter::routeActive() + Q_INVOKABLE bool routeActive(QJSValue route) { return m_router->routeActive(route); }; + /// @see PageRouter::pushRoute() + Q_INVOKABLE void pushRoute(QJSValue route) { m_router->pushRoute(route); }; + /// @see PageRouter::popRoute() + Q_INVOKABLE void popRoute() { m_router->popRoute(); }; + // @see PageRouter::bringToView() + Q_INVOKABLE void bringToView(QJSValue route) { m_router->bringToView(route); }; + +Q_SIGNALS: + void routerChanged(); + void dataChanged(); + void isCurrentChanged(); +}; + +QML_DECLARE_TYPEINFO(PageRouter, QML_HAS_ATTACHED_PROPERTIES)