/* * SPDX-FileCopyrightText: 2016 Marco Martin * * SPDX-License-Identifier: LGPL-2.0-or-later */ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Templates as QT import QtQuick.Controls as QQC2 import org.kde.kirigami.controls as KC import org.kde.kirigami.platform as Platform import org.kde.kirigami.primitives as Primitives import org.kde.kirigami.layouts as KL import "private/globaltoolbar" as GlobalToolBar /*! \qmltype PageRow \inqmlmodule org.kde.kirigami \brief A row-based navigation model that can be used with a set of interlinked information pages. Pages are pushed in the back of the row and the view scrolls until that row is visualized. A PageRow can 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. */ QT.Control { id: root //BEGIN PROPERTIES /*! \qmlproperty int PageRow::depth \brief This property holds the number of pages currently pushed onto the view. */ readonly property alias depth: columnView.count /*! \qmlproperty Page PageRow::lastItem \brief This property holds the last page in the row. */ readonly property Item lastItem: columnView.contentChildren.length > 0 ? columnView.contentChildren[columnView.contentChildren.length - 1] : null /*! \qmlproperty Page PageRow::currentItem \brief This property holds the currently visible/active page. Because of the ability to display multiple pages, it will hold the currently active page. */ readonly property alias currentItem: columnView.currentItem /*! \qmlproperty int PageRow::currentIndex \brief This property holds the index of the currently active page. \sa currentItem */ property alias currentIndex: columnView.currentIndex /*! \qmlproperty Page PageRow::initialPage \brief This property sets the initial page for this PageRow. */ property var initialPage contentItem: columnView /*! \qmlproperty ColumnView PageRow::columnView \brief This property holds the ColumnView that this PageRow owns. Generally, you shouldn't need to change the value of this property. \since 2.12 */ property alias columnView: columnView /*! \qmlproperty list PageRow::items \brief This property holds the present pages in the PageRow. \since 2.6 */ readonly property alias items: columnView.contentChildren /*! \qmlproperty list PageRow::visibleItems \brief This property holds all visible pages in the PageRow, excluding those which are scrolled away. \since 2.6 */ readonly property alias visibleItems: columnView.visibleItems /*! \qmlproperty Item PageRow::leadingVisibleItem \brief This property holds the first page in the PageRow that is at least partially visible. \note Pages before that one (the one contained in the property) will be out of the viewport. \sa ColumnView::leadingVisibleItem \since 2.6 */ readonly property alias leadingVisibleItem: columnView.leadingVisibleItem /*! \qmlproperty Item PageRow::trailingVisibleItem \brief This property holds the last page in the PageRow that is at least partially visible. \note Pages after that one (the one contained in the property) will be out of the viewport. \sa ColumnView::trailingVisibleItem \since 2.6 */ readonly property alias trailingVisibleItem: columnView.trailingVisibleItem /*! \brief This property holds the default width for a column. default: \c{20 * Kirigami.Units.gridUnit} \note Pages can override it using implicitWidth, Layout.fillWidth, Layout.minimumWidth etc. */ property int defaultColumnWidth: Platform.Units.gridUnit * 20 /*! \qmlproperty bool PageRow::interactive \brief This property sets whether it is possible to go back/forward by swiping with a gesture on the content view. default: \c true, except on Android where it conflicts with native back/forward swipe gestures. */ property alias interactive: columnView.interactive /*! \brief This property tells whether the PageRow is wide enough to show multiple pages. \since 5.37 */ readonly property bool wideMode: width >= defaultColumnWidth * 2 && depth >= 2 /*! \qmlproperty bool PageRow::separatorVisible \brief This property sets whether the separators between pages should be displayed. default: \c true \since 5.38 */ property alias separatorVisible: columnView.separatorVisible /*! \qmlproperty var PageRow::globalToolBar \brief This property sets the appearance of an optional global toolbar for the whole PageRow. It's a grouped property comprised of the following properties: \list \li style (Kirigami.ApplicationHeaderStyle): can have the following values: \list \li Auto: Depending on application formfactor, it can behave automatically like other values, such as a Breadcrumb on mobile and ToolBar on desktop. \li Breadcrumb: It will show a breadcrumb of all the page titles in the stack, for easy navigation. \li Titles: Each page will only have its own title on top. \li 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. \li None: No global toolbar will be shown. \endlist \li actualStyle: This will represent the actual style of the toolbar; it can be different from style in the case style is Auto. \li showNavigationButtons: OR flags combination of Kirigami.ApplicationHeaderStyle.ShowBackButton and Kirigami.ApplicationHeaderStyle.ShowForwardButton. \li toolbarActionAlignment (Qt::Alignment): 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). \li minimumHeight: int Minimum height of the header, which will be resized when scrolling. Only in Mobile mode (default: preferredHeight, sliding but no scaling). \li preferredHeight: int The height the toolbar will usually have. \li leftReservedSpace: int, readonly How many pixels of extra space are reserved at the left of the page toolbar (typically for navigation buttons or a drawer handle). \li rightReservedSpace: int, readonly How many pixels of extra space are reserved at the right of the page toolbar (typically for a drawer handle). \endlist \since 5.48 */ readonly property alias globalToolBar: globalToolBar /*! \brief This property assigns a drawer as an internal left sidebar for this PageRow. In this case, when open and not modal, the drawer contents will be in the same layer as the base pagerow. Pushing any other layer on top will cover the sidebar. \since 5.84 */ // TODO KF6: globaldrawer should use actions also used by this sidebar instead of reparenting globaldrawer contents? property OverlayDrawer leftSidebar /*! \qmlproperty QtQuick.Controls.StackView PageRow::layers \brief This property holds 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 /*! \brief This property holds whether to automatically pop pages at the top of the stack if they are not visible. If a user navigates to a previous page on the stack (ex. pressing back button) and pages above it on the stack are not visible, they will be popped if this property is true. \since 5.101 */ property bool popHiddenPages: false //END PROPERTIES //BEGIN FUNCTIONS /*! \qmlmethod Page PageRow::push(var page, var properties) \brief This method pushes a page on the stack. A single page can be defined as an url, a component, or an object. It can also be an array of the above said types, but in that case, the properties' array length must match pages' array length or it must be empty. Failing to comply with the following rules will make the method return null before doing anything. \a page A single page or an array of pages. \a properties A single property object or an array of property objects. Returns The new created page (or the last one if it was an array). */ function push(page, properties): QT.Page { if (!pagesLogic.verifyPages(page, properties)) { console.warn("Pushed pages do not conform to the rules. Please check the documentation."); console.trace(); return null } const item = pagesLogic.insertPage_unchecked(currentIndex + 1, page, properties) currentIndex = depth - 1 return item } /*! \qmlmethod Page PageRow::pushDialogLayer(var page, var properties, var windowProperties) \brief Pushes a page as a new dialog on desktop and as a layer on mobile. \a page A single page defined as either a string url, a component or an object (which will be reparented). The following page can then be closed by calling `Kirigami.PageStack.closeDialog()` at most once to close/hide it when in desktop or mobile mode. \note: You can also call a closeDialog() method directly on the page to close/hide it but this behavior is deprecated since 6.18 and replaced with the attached property. \a properties The properties given when initializing the page. \a windowProperties The properties given to the initialized window on desktop. Returns a newly created page. */ function pushDialogLayer(page, properties = {}, windowProperties = {}): QT.Page { if (!pagesLogic.verifyPages(page, properties)) { console.warn("Page pushed as a dialog or layer does not conform to the rules. Please check the documentation."); console.trace(); return null } let item; if (Platform.Settings.isMobile) { if (QQC2.ApplicationWindow.window.width > Platform.Units.gridUnit * 40) { // open as a QQC2.Dialog const component = pagesLogic.getMobileDialogLayerComponent(); const dialog = component.createObject(QQC2.Overlay.overlay, { width: Qt.binding(() => QQC2.ApplicationWindow.window.width - Platform.Units.gridUnit * 5), height: Qt.binding(() => QQC2.ApplicationWindow.window.height - Platform.Units.gridUnit * 5), x: Platform.Units.gridUnit * 2.5, y: Platform.Units.gridUnit * 2.5, }); if (typeof page === "string") { // url => load component and then load item from component const component = Qt.createComponent(Qt.resolvedUrl(page)); item = component.createObject(dialog.contentItem, properties); component.destroy(); dialog.contentItem.contentItem = item } else if (page instanceof Component) { item = page.createObject(dialog.contentItem, properties); dialog.contentItem.contentItem = item } else if (page instanceof Item) { item = page; page.parent = dialog.contentItem; } else if (typeof page === 'object' && typeof page.toString() === 'string') { // url const component = Qt.createComponent(page); item = component.createObject(dialog.contentItem, properties); component.destroy(); dialog.contentItem.contentItem = item } dialog.title = Qt.binding(() => item.title); // Pushing a PageRow is supported but without PageRow toolbar if (item.globalToolBar && item.globalToolBar.style) { item.globalToolBar.style = KC.ApplicationHeaderStyle.None } Object.defineProperty(item, 'closeDialog', { value: function() { console.warn("Calling closeDialog is deprecated. Use Kirigami.PageStack.closeDialog instead."); dialog.close(); } }); (item as Item).KL.PageStack.closeDialog.connect(() => dialog.close()); dialog.open(); } else { // open as a layer if (root.globalToolBar.style !== KC.ApplicationHeaderStyle.Breadcrumb) { properties.globalToolBarStyle = root.globalToolBar.style } item = layers.push(page, properties); item.KL.PageStack.closeDialog.connect(() => layers.pop()); Object.defineProperty(item, 'closeDialog', { value: function() { console.warn("Calling closeDialog is deprecated. Use Kirigami.PageStack.closeDialog instead."); layers.pop(); } }); } } else { // open as a new window if (!("modality" in windowProperties)) { windowProperties.modality = Qt.WindowModal; } if (!("height" in windowProperties)) { windowProperties.height = Platform.Units.gridUnit * 30; } if (!("width" in windowProperties)) { windowProperties.width = Platform.Units.gridUnit * 50; } if (!("minimumWidth" in windowProperties)) { windowProperties.minimumWidth = Platform.Units.gridUnit * 20; } if (!("minimumHeight" in windowProperties)) { windowProperties.minimumHeight = Platform.Units.gridUnit * 15; } if (!("flags" in windowProperties)) { windowProperties.flags = Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint; } const windowComponent = Qt.createComponent(Qt.resolvedUrl("./ApplicationWindow.qml")); const window = windowComponent.createObject(root, windowProperties); windowComponent.destroy(); item = window.pageStack.push(page, properties); (item as Item).KL.PageStack.closeDialog.connect(() => window.close()); Object.defineProperty(item, 'closeDialog', { value: function() { console.warn("Calling closeDialog is deprecated. Use Kirigami.PageStack.closeDialog instead."); window.close(); } }); window.visibleChanged.connect(() => { if (!window.visible) { window.destroy(); } }); } // Escape is considered a shortcut, so we need to override it. Or else we don't get any events! item.Keys.shortcutOverride.connect(event => event.accepted = (event.key === Qt.Key_Escape)); item.Keys.escapePressed.connect(event => item.closeDialog()); return item; } /*! \qmlmethod Page PageRow::insertPage(int position, var page, var properties) \brief Inserts a new page or a list of new pages at an arbitrary position. A single page can be defined as an url, a component, or an object. It can also be an array of the above said types, but in that case, the properties' array length must match pages' array length or it must be empty. Failing to comply with the following rules will make the method return null before doing anything. \a page A single page or an array of pages. \a properties A single property object or an array of property objects. Returns the new created page (or the last one if it was an array). \since 2.7 */ function insertPage(position, page, properties): QT.Page { if (!pagesLogic.verifyPages(page, properties)) { console.warn("Inserted pages do not conform to the rules. Please check the documentation."); console.trace(); return null } if (position < 0 || position > depth) { console.warn("You are trying to insert a page to an out-of-bounds position. Position will be adjusted accordingly."); console.trace(); position = Math.max(0, Math.min(depth, position)); } return pagesLogic.insertPage_unchecked(position, page, properties) } /*! \qmlmethod void PageRow::movePage(int fromPos, int toPos) 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): void { columnView.moveItem(fromPos, toPos); } /*! \qmlmethod Page PageRow::removePage(var page) \brief Remove the given page. \a page The page can be given both as integer position or by reference Returns the page that has just been removed \since 2.7 */ function removePage(page): QT.Page { if (depth > 0) { return columnView.removeItem(page) as QT.Page; } return null } /*! \qmlmethod Page PageRow::pop(var page) \brief Pops a page off the stack. \a page If page is specified then the stack is unwound to that page, to unwind to the first page specify page as null. Returns the page instance that was popped off the stack. */ function pop(page): QT.Page { return columnView.pop(page) as QT.Page; } /*! \qmlmethod Page PageRow::replace(var page, var properties) \brief Replaces a page on the current index. A single page can be defined as an url, a component, or an object. It can also be an array of the above said types, but in that case, the properties' array length must match pages' array length or it must be empty. Failing to comply with the following rules will make the method return null before doing anything. \a page A single page or an array of pages. \a properties A single property object or an array of property objects. Returns the new created page (or the last one if it was an array). See push() for details. */ function replace(page, properties): QT.Page { if (!pagesLogic.verifyPages(page, properties)) { console.warn("Specified pages do not conform to the rules. Please check the documentation."); console.trace(); return null } // Remove all pages on top of the one being replaced. if (currentIndex >= 0) { columnView.pop(currentIndex); } else { console.warn("There's no page to replace"); } // Figure out if more than one page is being pushed. let pages; let propsArray = []; if (page instanceof Array) { pages = page; page = pages.shift(); } if (properties instanceof Array) { propsArray = properties; properties = propsArray.shift(); } else { propsArray = [properties]; } // Replace topmost page. let pageItem = pagesLogic.initPage(page, properties); if (depth > 0) columnView.replaceItem(depth - 1, pageItem); else { console.log("Calling replace on empty PageRow", pageItem) columnView.addItem(pageItem) } pagePushed(pageItem); // Push any extra defined pages onto the stack. if (pages) { for (const i in pages) { const tPage = pages[i]; const tProps = propsArray[i]; pageItem = pagesLogic.initPage(tPage, tProps); columnView.addItem(pageItem); pagePushed(pageItem); } } currentIndex = depth - 1; return pageItem; } /*! \qmlmethod void PageRow::clear() \brief Clears the page stack. Destroy (or reparent) all the pages contained. */ function clear(): void { columnView.clear(); } /*! \qmlmethod Page PageRow::get(int idx) \a idx the depth of the page we want Returns the page at idx */ function get(idx): QT.Page { return items[idx]; } /*! \qmlmethod void PageRow::flickBack() Go back to the previous index and scroll to the left to show one more column. */ function flickBack(): void { if (depth > 1) { currentIndex = Math.max(0, currentIndex - 1); } } /*! \qmlmethod void PageRow::goBack(var event = null) Acts as if you had pressed the "back" button on Android or did Alt-Left on desktop, "going back" in the layers and page row. Results in a layer being popped if available, or the currentIndex being set to currentIndex-1 if not available. \a event Optional, an event that will be accepted if a page is successfully "backed" on */ function goBack(event = null): void { const backEvent = {accepted: false} if (layersStack.depth >= 1) { try { // app code might be screwy, but we still want to continue functioning if it throws an exception layersStack.currentItem.backRequested(backEvent) } catch (error) {} if (!backEvent.accepted) { if (layersStack.depth > 1) { layersStack.pop() if (event) { event.accepted = true } return } } } if (currentIndex >= 1) { try { // app code might be screwy, but we still want to continue functioning if it throws an exception currentItem.backRequested(backEvent) } catch (error) {} if (!backEvent.accepted) { if (depth > 1) { currentIndex = Math.max(0, currentIndex - 1) if (event) { event.accepted = true } } } } } /*! \qmlmethod void goForward() Acts as if you had pressed the "forward" shortcut on desktop, "going forward" in the page row. Results in the active page becoming the next page in the row from the current active page, i.e. currentIndex + 1. */ function goForward(): void { currentIndex = Math.min(depth - 1, currentIndex + 1) } //END FUNCTIONS //BEGIN signals & signal handlers /*! \brief Emitted when a page has been inserted anywhere. \a position where the page has been inserted \a page the new page \since 2.7 */ signal pageInserted(int position, Item page) /*! \brief Emitted when a page has been pushed to the bottom. \a page the new page \since 2.5 */ signal pagePushed(Item page) /*! \brief Emitted when a page has been removed from the row. \a 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) onLeftSidebarChanged: { if (leftSidebar && !leftSidebar.modal) { modalConnection.onModalChanged(); } } Keys.onReleased: event => { if (event.key === Qt.Key_Back) { this.goBack(event) } } onInitialPageChanged: { if (initialPage) { clear(); push(initialPage, null) } } /* onActiveFocusChanged: { if (activeFocus) { layersStack.currentItem.forceActiveFocus() if (columnView.activeFocus) { print("SSS"+columnView.currentItem) columnView.currentItem.forceActiveFocus(); } } } */ //END signals & signal handlers Connections { id: modalConnection target: root.leftSidebar function onModalChanged(): void { if (root.leftSidebar.modal) { root.leftSidebar.parent = root.QQC2.Overlay.overlay root.leftSidebar.background.parent.parent = null } else { root.leftSidebar.parent = sidebarControl root.leftSidebar.background.parent.parent = sidebarControl } } } // Enforce the parent when we are in sidebar mode Connections { enabled: root.leftSidebar && root.leftSidebar.contentItem && !root.leftSidebar.modal target: root.leftSidebar?.contentItem.parent ?? null function onParentChanged () { root.leftSidebar.contentItem.parent.parent = sidebarControl } } implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding Shortcut { sequences: [ StandardKey.Back ] onActivated: root.goBack() } Shortcut { sequences: [ StandardKey.Forward ] onActivated: root.goForward() } Keys.forwardTo: [currentItem] GlobalToolBar.PageRowGlobalToolBarStyleGroup { id: globalToolBar readonly property int leftReservedSpace: (globalToolBarUI.item as GlobalToolBar.PageRowGlobalToolBarUI)?.leftReservedSpace ?? 0 readonly property int rightReservedSpace: (globalToolBarUI.item as GlobalToolBar.PageRowGlobalToolBarUI)?.rightReservedSpace ?? 0 readonly property int height: globalToolBarUI.height readonly property Item leftHandleAnchor: (globalToolBarUI.item as GlobalToolBar.PageRowGlobalToolBarUI)?.leftHandleAnchor ?? null readonly property Item rightHandleAnchor: (globalToolBarUI.item as GlobalToolBar.PageRowGlobalToolBarUI)?.rightHandleAnchor ?? null } QQC2.StackView { id: layersStack z: 99 Accessible.role: Accessible.Pane anchors { left: parent.left top: parent.top right: parent.right bottom: parent.bottom topMargin: depth < 2 ? globalToolBarUI.height : currentItem.KL.ColumnView.globalHeader?.height ?? 0 bottomMargin: currentItem.KL.ColumnView.globalFooter?.height ?? 0 } // placeholder as initial item initialItem: columnViewLayout onDepthChanged: { if (depth < 2) { return; } let item = layersStack.get(depth - 1) // headers are parented to items in layers, so items can't clip otherwise headers become invisible item.clip = false // For layers reparent the global header to the page const header = item.KL.ColumnView.globalHeader header.parent = item header.anchors.bottom = item.top header.anchors.left = item.left header.anchors.right = item.right const footer = item.KL.ColumnView.globalFooter footer.parent = item footer.anchors.top = item.bottom footer.anchors.left = item.left footer.anchors.right = item.right } function clear(): void { // don't let it kill the main page row const d = layersStack.depth; for (let i = 1; i < d; ++i) { pop(); } } popEnter: Transition { PauseAnimation { duration: Platform.Units.longDuration } PropertyAction { property: "opacity" value: 1 } } popExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Platform.Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: 0 to: height/2 duration: Platform.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: Platform.Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Platform.Units.longDuration easing.type: Easing.OutCubic } } } pushExit: Transition { PauseAnimation { duration: Platform.Units.longDuration } } replaceEnter: Transition { ParallelAnimation { OpacityAnimator { from: 0 to: 1 duration: Platform.Units.longDuration easing.type: Easing.InOutCubic } YAnimator { from: height/2 to: 0 duration: Platform.Units.longDuration easing.type: Easing.OutCubic } } } replaceExit: Transition { ParallelAnimation { OpacityAnimator { from: 1 to: 0 duration: Platform.Units.longDuration easing.type: Easing.InCubic } YAnimator { from: 0 to: -height/2 duration: Platform.Units.longDuration easing.type: Easing.InOutCubic } } } } Loader { id: globalToolBarUI anchors { left: parent.left top: parent.top right: parent.right } z: 100 property QT.Control pageRow: root active: globalToolBar.actualStyle !== KC.ApplicationHeaderStyle.None || (root.leadingVisibleItem && root.leadingVisibleItem.globalToolBarStyle === KC.ApplicationHeaderStyle.ToolBar) visible: active height: active ? implicitHeight : 0 // If load is asynchronous, it will fail to compute the initial implicitHeight // https://bugs.kde.org/show_bug.cgi?id=442660 asynchronous: false source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml"); } QtObject { id: pagesLogic readonly property var componentCache: new Array() property Component __mobileDialogLayerComponent function getMobileDialogLayerComponent() { if (!__mobileDialogLayerComponent) { __mobileDialogLayerComponent = Qt.createComponent(Qt.resolvedUrl("private/MobileDialogLayer.qml")); } return __mobileDialogLayerComponent; } function verifyPages(pages, properties): bool { function validPage(page) { //don't try adding an already existing page if (page instanceof QT.Page && columnView.containsItem(page)) { console.log(`Page ${page} is already in the PageRow`) return false } return page instanceof QT.Page || page instanceof Component || typeof page === 'string' || (typeof page === 'object' && typeof page.toString() === 'string') } // check page/pages that it is/they are valid const pagesIsArr = Array.isArray(pages) && pages.length > 0 let isValidArrOfPages = pagesIsArr; if (pagesIsArr) { for (const page of pages) { if (!validPage(page)) { isValidArrOfPages = false; break; } } } // check properties object/array object validity const isProp = typeof properties === 'object'; const propsIsArr = Array.isArray(properties) && properties.length > 0 let isValidPropArr = propsIsArr; if (propsIsArr) { for (const prop of properties) { if (typeof prop !== 'object') { isValidPropArr = false; break; } } isValidPropArr = isValidPropArr && pages.length === properties.length } return (validPage(pages) || isValidArrOfPages) && (!properties || (isProp || isValidPropArr)) } function insertPage_unchecked(position, page, properties) { columnView.pop(position - 1); // figure out if more than one page is being pushed let pages; let propsArray = []; if (page instanceof Array) { pages = page; page = pages.pop(); } if (properties instanceof Array) { propsArray = properties; properties = propsArray.pop(); } else { propsArray = [properties]; } // push any extra defined pages onto the stack if (pages) { for (const i in pages) { let tPage = pages[i]; let tProps = propsArray[i]; pagesLogic.initAndInsertPage(position, tPage, tProps); ++position; } } // initialize the page const pageItem = pagesLogic.initAndInsertPage(position, page, properties); root.pagePushed(pageItem); return pageItem; } function getPageComponent(page): Component { let 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); } } else if (typeof page === "object" && !(page instanceof Item) && page.toString !== undefined) { // page defined as url (QML value type, not a string) pageComp = pagesLogic.componentCache[page.toString()]; if (!pageComp) { pageComp = pagesLogic.componentCache[page.toString()] = Qt.createComponent(page.toString()); } } return pageComp } function initPage(page, properties): QT.Page { const pageComp = getPageComponent(page, properties); if (pageComp) { // instantiate page from component // Important: The parent needs to be set otherwise a reference needs to be kept around // to avoid the page being garbage collected. page = pageComp.createObject(pagesLogic, properties || {}); if (pageComp.status === Component.Error) { throw new Error("Error while loading page: " + pageComp.errorString()); } } else { // copy properties to the page for (const prop in properties) { if (page.hasOwnProperty(prop)) { page[prop] = properties[prop]; } } } return page; } function initAndInsertPage(position, page, properties): QT.Page { page = initPage(page, properties); columnView.insertItem(position, page); return page; } } Item { Item { id: columnViewLayout anchors { fill: parent // Use this instead Layout.topMargin in RowLayout seems to be unreliable topMargin: -layersStack.y } readonly property alias columnView: columnView // set the pagestack of this and all children to root, otherwise // they would automatically resolve to the layer's stackview KL.PageStack.pageStack: root Item { id: sidebarControl anchors { left: parent.left top: parent.top bottom: parent.bottom } transform: Translate { x: root.mirrored ? -sidebarControl.Primitives.ScenePosition.x : 0 } z: 1 width: visible ? root.leftSidebar.width * root.leftSidebar.position : 0 // Using leftSidebar.position instead of visible because leftSidebar.visible depends from its main item visibility which is our child so would be a loop visible: root.leftSidebar && children.length > 0 } KL.ColumnView { id: columnView anchors { left: sidebarControl.right top: parent.top right: parent.right bottom: parent.bottom } topPadding: (globalToolBarUI.item as GlobalToolBar.PageRowGlobalToolBarUI)?.breadcrumbVisible ? globalToolBarUI.height : 0 Component { id: pageTranslation Translate { id: transitionTransform required property Item page property bool wasDragging readonly property bool active: columnView.moving && columnView.columnResizeMode === KL.ColumnView.SingleColumn && page.background && (page.background?.color.a === 1 ?? true) && (columnView.trailingVisibleItem?.background ?? false) && (columnView.trailingVisibleItem?.background?.color.a === 1 ?? true) readonly property real progress: Math.max(-1, Math.min(1, (page.x - columnView.contentX) / columnView.width)) x: { if (!active) { return 0; } const animDistance = Platform.Units.gridUnit * 4; if (!wasDragging && Math.abs(progress) > 1e-9) { // Cancel the natural full‑width slide and add a partial offset return (columnView.contentX - page.x) + animDistance * progress; } return 0; } // Opacity binding: crossfade both pages. // Fade out when being pushed away, fade in when returning from pop. component OpacityBinding: Binding { target: transitionTransform.page property: "opacity" value: { if (transitionTransform.wasDragging || !transitionTransform.active) { return 1; } var animProgress = transitionTransform.progress; if (animProgress > 0) { return 1 - Math.min(1, animProgress); } else if (animProgress < 0) { return 1 + Math.min(1, animProgress); } return 1; } } // NOTE: We use list here instead of list to work around QTBUG-144092 readonly property list __opacityBindings: [ OpacityBinding { target: transitionTransform.page }, OpacityBinding { target: transitionTransform.page.KL.ColumnView.globalHeader }, OpacityBinding { target: transitionTransform.page.KL.ColumnView.globalFooter } ] readonly property Connections __draggingConnection: Connections { target: columnView function onMovingChanged() { if (columnView.moving) { if (columnView.dragging) { transitionTransform.wasDragging = true; } } else { transitionTransform.wasDragging = false; } } } } } // Internal hidden api for Page readonly property Item __pageRow: root acceptsMouse: Platform.Settings.isMobile columnResizeMode: root.wideMode ? KL.ColumnView.FixedColumns : KL.ColumnView.SingleColumn columnWidth: root.defaultColumnWidth interactive: Qt.platform.os !== 'android' onItemInserted: (position, item) => { item.transform = pageTranslation.createObject(item, {page: item}); item.KL.ColumnView.globalHeader.transform = item.transform; item.KL.ColumnView.globalFooter.transform = item.transform; root.pageInserted(position, item); } onItemRemoved: item => { item.transform = null; item.KL.ColumnView.globalHeader.transform = null; item.KL.ColumnView.globalFooter.transform = null; root.pageRemoved(item); } onVisibleItemsChanged: { // implementation of `popHiddenPages` option if (root.popHiddenPages) { // manually fetch lastItem here rather than use root.lastItem property, since that binding may not have updated yet let lastItem = columnView.contentChildren[columnView.contentChildren.length - 1]; let trailingVisibleItem = columnView.trailingVisibleItem; // pop every page that isn't visible and at the top of the stack while (lastItem && columnView.trailingVisibleItem && lastItem !== columnView.trailingVisibleItem && columnView.containsItem(lastItem)) { root.pop(); } } } } } } }