Kirigami2

PageRow.qml
1/*
2 * SPDX-FileCopyrightText: 2016 Marco Martin <mart@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7import QtQuick
8import QtQuick.Layouts
9import QtQuick.Templates as QT
10import QtQuick.Controls as QQC2
11import org.kde.kirigami as Kirigami
12import "private/globaltoolbar" as GlobalToolBar
13import "templates" as KT
14
15/**
16 * PageRow implements a row-based navigation model, which can be used
17 * with a set of interlinked information pages. Pages are pushed in the
18 * back of the row and the view scrolls until that row is visualized.
19 * A PageRow can show a single page or a multiple set of columns, depending
20 * on the window width: on a phone a single column should be fullscreen,
21 * while on a tablet or a desktop more than one column should be visible.
22 *
23 * @inherits QtQuick.Templates.Control
24 */
25QT.Control {
26 id: root
27
28//BEGIN PROPERTIES
29 /**
30 * @brief This property holds the number of pages currently pushed onto the view.
31 * @property int depth
32 */
33 readonly property alias depth: columnView.count
34
35 /**
36 * @brief This property holds the last page in the row.
37 * @property Page lastItem
38 */
39 readonly property Item lastItem: columnView.contentChildren.length > 0 ? columnView.contentChildren[columnView.contentChildren.length - 1] : null
40
41 /**
42 * @brief This property holds the currently visible/active page.
43 *
44 * Because of the ability to display multiple pages, it will hold the currently active page.
45 *
46 * @property Page currentItem
47 */
48 readonly property alias currentItem: columnView.currentItem
49
50 /**
51 * @brief This property holds the index of the currently active page.
52 * @see currentItem
53 * @property int currentIndex
54 */
55 property alias currentIndex: columnView.currentIndex
56
57 /**
58 * @brief This property sets the initial page for this PageRow.
59 * @property Page initialPage
60 */
61 property var initialPage
62
63 /**
64 * @brief This property holds the main ColumnView of this Row.
65 * @property ColumnView contentItem
66 */
67 contentItem: columnView
68
69 /**
70 * @brief This property holds the ColumnView that this PageRow owns.
71 *
72 * Generally, you shouldn't need to change the value of this property.
73 *
74 * @property ColumnView columnView
75 * @since 2.12
76 */
77 property alias columnView: columnView
79 /**
80 * @brief This property holds the present pages in the PageRow.
81 * @property list<Page> items
82 * @since 2.6
83 */
84 readonly property alias items: columnView.contentChildren
85
86 /**
87 * @brief This property holds all visible pages in the PageRow,
88 * excluding those which are scrolled away.
89 * @property list<Page> visibleItems
90 * @since 2.6
91 */
92 readonly property alias visibleItems: columnView.visibleItems
93
94 /**
95 * @brief This property holds the first page in the PageRow that is at least partially visible.
96 * @note Pages before that one (the one contained in the property) will be out of the viewport.
97 * @see ColumnView::leadingVisibleItem
98 * @property Item leadingVisibleItem
99 * @since 2.6
100 */
101 readonly property alias leadingVisibleItem: columnView.leadingVisibleItem
102
103 /**
104 * @brief This property holds the last page in the PageRow that is at least partially visible.
105 * @note Pages after that one (the one contained in the property) will be out of the viewport.
106 * @see ColumnView::trailingVisibleItem
107 * @property Item trailingVisibleItem
108 * @since 2.6
109 */
110 readonly property alias trailingVisibleItem: columnView.trailingVisibleItem
111
112 /**
113 * @brief This property holds the default width for a column.
114 *
115 * default: ``20 * Kirigami.Units.gridUnit``
116 *
117 * @note Pages can override it using implicitWidth, Layout.fillWidth, Layout.minimumWidth etc.
118 */
119 property int defaultColumnWidth: Kirigami.Units.gridUnit * 20
120
121 /**
122 * @brief This property sets whether it is possible to go back/forward
123 * by swiping with a gesture on the content view.
124 *
125 * default: ``true``
126 *
127 * @property bool interactive
128 */
129 property alias interactive: columnView.interactive
130
131 /**
132 * @brief This property tells whether the PageRow is wide enough to show multiple pages.
133 * @since 5.37
134 */
135 readonly property bool wideMode: width >= defaultColumnWidth * 2 && depth >= 2
137 /**
138 * @brief This property sets whether the separators between pages should be displayed.
139 *
140 * default: ``true``
141 *
142 * @property bool separatorVisible
143 * @since 5.38
144 */
145 property alias separatorVisible: columnView.separatorVisible
146
147 /**
148 * @brief This property sets the appearance of an optional global toolbar for the whole PageRow.
149 *
150 * It's a grouped property comprised of the following properties:
151 * * style (``Kirigami.ApplicationHeaderStyle``): can have the following values:
152 * * ``Auto``: Depending on application formfactor, it can behave automatically like other values, such as a Breadcrumb on mobile and ToolBar on desktop.
153 * * ``Breadcrumb``: It will show a breadcrumb of all the page titles in the stack, for easy navigation.
154 * * ``Titles``: Each page will only have its own title on top.
155 * * ``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.
156 * * ``None``: No global toolbar will be shown.
157 *
158 * * ``actualStyle``: This will represent the actual style of the toolbar; it can be different from style in the case style is Auto.
159 * * ``showNavigationButtons``: OR flags combination of Kirigami.ApplicationHeaderStyle.ShowBackButton and Kirigami.ApplicationHeaderStyle.ShowForwardButton.
160 * * ``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``).
161 * * ``minimumHeight: int`` Minimum height of the header, which will be resized when scrolling. Only in Mobile mode (default: ``preferredHeight``, sliding but no scaling).
162 * * ``preferredHeight: int`` The height the toolbar will usually have.
163 * * ``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).
164 * * ``rightReservedSpace: int, readonly`` How many pixels of extra space are reserved at the right of the page toolbar (typically for a drawer handle).
165 *
166 * @property org::kde::kirigami::private::globaltoolbar::PageRowGlobalToolBarStyleGroup globalToolBar
167 * @since 5.48
168 */
169 readonly property alias globalToolBar: globalToolBar
171 /**
172 * @brief This property assigns a drawer as an internal left sidebar for this PageRow.
173 *
174 * In this case, when open and not modal, the drawer contents will be in the same layer as the base pagerow.
175 * Pushing any other layer on top will cover the sidebar.
176 *
177 * @since 5.84
178 */
179 // TODO KF6: globaldrawer should use actions also used by this sidebar instead of reparenting globaldrawer contents?
180 property OverlayDrawer leftSidebar
181
182 /**
183 * @brief This property holds the modal layers.
184 *
185 * Sometimes an application needs a modal page that always covers all the rows.
186 * For instance the full screen image of an image viewer or a settings page.
187 *
188 * @property QtQuick.Controls.StackView layers
189 * @since 5.38
190 */
191 property alias layers: layersStack
192
193 /**
194 * @brief This property holds whether to automatically pop pages at the top of the stack if they are not visible.
195 *
196 * If a user navigates to a previous page on the stack (ex. pressing back button) and pages above
197 * it on the stack are not visible, they will be popped if this property is true.
198 *
199 * @since 5.101
200 */
201 property bool popHiddenPages: false
202//END PROPERTIES
203
204//BEGIN FUNCTIONS
205 /**
206 * @brief This method pushes a page on the stack.
207 *
208 * A single page can be defined as an url, a component, or an object. It can
209 * also be an array of the above said types, but in that case, the
210 * properties' array length must match pages' array length or it must be
211 * empty. Failing to comply with the following rules will make the method
212 * return null before doing anything.
213 *
214 * @param page A single page or an array of pages.
215 * @param properties A single property object or an array of property
216 * objects.
217 *
218 * @return The new created page (or the last one if it was an array).
219 */
220 function push(page, properties): QT.Page {
221 if (!pagesLogic.verifyPages(page, properties)) {
222 console.warn("Pushed pages do not conform to the rules. Please check the documentation.");
223 console.trace();
224 return null
225 }
226
227 const item = pagesLogic.insertPage_unchecked(currentIndex + 1, page, properties)
228 currentIndex = depth - 1
229 return item
230 }
231
232 /**
233 * @brief Pushes a page as a new dialog on desktop and as a layer on mobile.
234 *
235 * @param page A single page defined as either a string url, a component or
236 * an object (which will be reparented). The following page gains
237 * `closeDialog()` method allowing to make it indistinguishable to
238 * close/hide it when in desktop or mobile mode. Note that Kiriami supports
239 * calling `closeDialog()` only once.
240 *
241 * @param properties The properties given when initializing the page.
242 * @param windowProperties The properties given to the initialized window on desktop.
243 * @return Returns a newly created page.
244 */
245 function pushDialogLayer(page, properties = {}, windowProperties = {}): QT.Page {
246 if (!pagesLogic.verifyPages(page, properties)) {
247 console.warn("Page pushed as a dialog or layer does not conform to the rules. Please check the documentation.");
248 console.trace();
249 return null
250 }
251 let item;
252 if (Kirigami.Settings.isMobile) {
253 if (QQC2.ApplicationWindow.window.width > Kirigami.Units.gridUnit * 40) {
254 // open as a QQC2.Dialog
255 const component = pagesLogic.getMobileDialogLayerComponent();
256 const dialog = component.createObject(QQC2.Overlay.overlay, {
257 width: Qt.binding(() => QQC2.ApplicationWindow.window.width - Kirigami.Units.gridUnit * 5),
258 height: Qt.binding(() => QQC2.ApplicationWindow.window.height - Kirigami.Units.gridUnit * 5),
259 x: Kirigami.Units.gridUnit * 2.5,
260 y: Kirigami.Units.gridUnit * 2.5,
261 });
262
263 if (typeof page === "string") {
264 // url => load component and then load item from component
265 const component = Qt.createComponent(Qt.resolvedUrl(page));
266 item = component.createObject(dialog.contentItem, properties);
267 component.destroy();
268 dialog.contentItem.contentItem = item
269 } else if (page instanceof Component) {
270 item = page.createObject(dialog.contentItem, properties);
271 dialog.contentItem.contentItem = item
272 } else if (page instanceof Item) {
273 item = page;
274 page.parent = dialog.contentItem;
275 } else if (typeof page === 'object' && typeof page.toString() === 'string') { // url
276 const component = Qt.createComponent(page);
277 item = component.createObject(dialog.contentItem, properties);
278 component.destroy();
279 dialog.contentItem.contentItem = item
280 }
281 dialog.title = Qt.binding(() => item.title);
282
283 // Pushing a PageRow is supported but without PageRow toolbar
284 if (item.globalToolBar && item.globalToolBar.style) {
285 item.globalToolBar.style = Kirigami.ApplicationHeaderStyle.None
286 }
287 Object.defineProperty(item, 'closeDialog', {
288 value: function() {
289 dialog.close();
290 }
291 });
292 dialog.open();
293 } else {
294 // open as a layer
295 properties.globalToolBarStyle = root.globalToolBar.style
296 item = layers.push(page, properties);
297 Object.defineProperty(item, 'closeDialog', {
298 value: function() {
299 layers.pop();
300 }
301 });
302 }
303 } else {
304 // open as a new window
305 if (!("modality" in windowProperties)) {
306 windowProperties.modality = Qt.WindowModal;
307 }
308 if (!("height" in windowProperties)) {
309 windowProperties.height = Kirigami.Units.gridUnit * 30;
310 }
311 if (!("width" in windowProperties)) {
312 windowProperties.width = Kirigami.Units.gridUnit * 50;
313 }
314 if (!("minimumWidth" in windowProperties)) {
315 windowProperties.minimumWidth = Kirigami.Units.gridUnit * 20;
316 }
317 if (!("minimumHeight" in windowProperties)) {
318 windowProperties.minimumHeight = Kirigami.Units.gridUnit * 15;
319 }
320 if (!("flags" in windowProperties)) {
321 windowProperties.flags = Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint;
322 }
323 const windowComponent = Qt.createComponent(Qt.resolvedUrl("./ApplicationWindow.qml"));
324 const window = windowComponent.createObject(root, windowProperties);
325 windowComponent.destroy();
326 item = window.pageStack.push(page, properties);
327 Object.defineProperty(item, 'closeDialog', {
328 value: function() {
329 window.close();
330 }
331 });
332 }
333 item.Keys.escapePressed.connect(event => item.closeDialog());
334 return item;
335 }
336
337 /**
338 * @brief Inserts a new page or a list of new pages at an arbitrary position.
339 *
340 * A single page can be defined as an url, a component, or an object. It can
341 * also be an array of the above said types, but in that case, the
342 * properties' array length must match pages' array length or it must be
343 * empty. Failing to comply with the following rules will make the method
344 * return null before doing anything.
345 *
346 * @param page A single page or an array of pages.
347 * @param properties A single property object or an array of property
348 * objects.
349 *
350 * @return The new created page (or the last one if it was an array).
351 * @since 2.7
352 */
353 function insertPage(position, page, properties): QT.Page {
354 if (!pagesLogic.verifyPages(page, properties)) {
355 console.warn("Inserted pages do not conform to the rules. Please check the documentation.");
356 console.trace();
357 return null
358 }
359
360 if (position < 0 || position > depth) {
361 console.warn("You are trying to insert a page to an out-of-bounds position. Position will be adjusted accordingly.");
362 console.trace();
363 position = Math.max(0, Math.min(depth, position));
364 }
365 return pagesLogic.insertPage_unchecked(position, page, properties)
366 }
367
368 /**
369 * Move the page at position fromPos to the new position toPos
370 * If needed, currentIndex will be adjusted
371 * in order to keep the same current page.
372 * @since 2.7
373 */
374 function movePage(fromPos, toPos): void {
375 columnView.moveItem(fromPos, toPos);
376 }
377
378 /**
379 * @brief Remove the given page.
380 * @param page The page can be given both as integer position or by reference
381 * @return The page that has just been removed
382 * @since 2.7
383 */
384 function removePage(page): QT.Page {
385 if (depth > 0) {
386 return columnView.removeItem(page);
387 }
388 return null
389 }
390
391 /**
392 * @brief Pops a page off the stack.
393 * @param page If page is specified then the stack is unwound to that page,
394 * to unwind to the first page specify page as null.
395 * @return The page instance that was popped off the stack.
396 */
397 function pop(page): QT.Page {
398 return columnView.pop(page);
399 }
400
401 /**
402 * @brief Replaces a page on the current index.
403 *
404 * A single page can be defined as an url, a component, or an object. It can
405 * also be an array of the above said types, but in that case, the
406 * properties' array length must match pages' array length or it must be
407 * empty. Failing to comply with the following rules will make the method
408 * return null before doing anything.
409 *
410 * @param page A single page or an array of pages.
411 * @param properties A single property object or an array of property
412 * objects.
413 *
414 * @return The new created page (or the last one if it was an array).
415 * @see push() for details.
416 */
417 function replace(page, properties): QT.Page {
418 if (!pagesLogic.verifyPages(page, properties)) {
419 console.warn("Specified pages do not conform to the rules. Please check the documentation.");
420 console.trace();
421 return null
422 }
423
424 // Remove all pages on top of the one being replaced.
425 if (currentIndex >= 0) {
426 columnView.pop(currentIndex);
427 } else {
428 console.warn("There's no page to replace");
429 }
430
431 // Figure out if more than one page is being pushed.
432 let pages;
433 let propsArray = [];
434 if (page instanceof Array) {
435 pages = page;
436 page = pages.shift();
437 }
438 if (properties instanceof Array) {
439 propsArray = properties;
440 properties = propsArray.shift();
441 } else {
442 propsArray = [properties];
443 }
444
445 // Replace topmost page.
446 let pageItem = pagesLogic.initPage(page, properties);
447 if (depth > 0)
448 columnView.replaceItem(depth - 1, pageItem);
449 else {
450 console.log("Calling replace on empty PageRow", pageItem)
451 columnView.addItem(pageItem)
452 }
453 pagePushed(pageItem);
454
455 // Push any extra defined pages onto the stack.
456 if (pages) {
457 for (const i in pages) {
458 const tPage = pages[i];
459 const tProps = propsArray[i];
460
461 pageItem = pagesLogic.initPage(tPage, tProps);
462 columnView.addItem(pageItem);
463 pagePushed(pageItem);
464 }
465 }
466
467 currentIndex = depth - 1;
468 return pageItem;
469 }
470
471 /**
472 * @brief Clears the page stack.
473 *
474 * Destroy (or reparent) all the pages contained.
475 */
476 function clear(): void {
477 columnView.clear();
478 }
479
480 /**
481 * @return the page at idx
482 * @param idx the depth of the page we want
483 */
484 function get(idx): QT.Page {
485 return items[idx];
486 }
487
488 /**
489 * Go back to the previous index and scroll to the left to show one more column.
490 */
491 function flickBack(): void {
492 if (depth > 1) {
493 currentIndex = Math.max(0, currentIndex - 1);
494 }
495 }
496
497 /**
498 * Acts as if you had pressed the "back" button on Android or did Alt-Left on desktop,
499 * "going back" in the layers and page row. Results in a layer being popped if available,
500 * or the currentIndex being set to currentIndex-1 if not available.
501 *
502 * @param event Optional, an event that will be accepted if a page is successfully
503 * "backed" on
504 */
505 function goBack(event = null): void {
506 const backEvent = {accepted: false}
507
508 if (layersStack.depth >= 1) {
509 try { // app code might be screwy, but we still want to continue functioning if it throws an exception
510 layersStack.currentItem.backRequested(backEvent)
511 } catch (error) {}
512
513 if (!backEvent.accepted) {
514 if (layersStack.depth > 1) {
515 layersStack.pop()
516 if (event) {
517 event.accepted = true
518 }
519 return
520 }
521 }
522 }
523
524 if (currentIndex >= 1) {
525 try { // app code might be screwy, but we still want to continue functioning if it throws an exception
526 currentItem.backRequested(backEvent)
527 } catch (error) {}
528
529 if (!backEvent.accepted) {
530 if (depth > 1) {
531 currentIndex = Math.max(0, currentIndex - 1)
532 if (event) {
533 event.accepted = true
534 }
535 }
536 }
537 }
538 }
539
540 /**
541 * Acts as if you had pressed the "forward" shortcut on desktop,
542 * "going forward" in the page row. Results in the active page
543 * becoming the next page in the row from the current active page,
544 * i.e. currentIndex + 1.
545 */
546 function goForward(): void {
547 currentIndex = Math.min(depth - 1, currentIndex + 1)
548 }
549//END FUNCTIONS
550
551//BEGIN signals & signal handlers
552 /**
553 * @brief Emitted when a page has been inserted anywhere.
554 * @param position where the page has been inserted
555 * @param page the new page
556 * @since 2.7
557 */
558 signal pageInserted(int position, Item page)
559
560 /**
561 * @brief Emitted when a page has been pushed to the bottom.
562 * @param page the new page
563 * @since 2.5
564 */
565 signal pagePushed(Item page)
566
567 /**
568 * @brief Emitted when a page has been removed from the row.
569 * @param page the page that has been removed: at this point it's still valid,
570 * but may be auto deleted soon.
571 * @since 2.5
572 */
573 signal pageRemoved(Item page)
574
575 onLeftSidebarChanged: {
576 if (leftSidebar && !leftSidebar.modal) {
577 modalConnection.onModalChanged();
578 }
579 }
580
581 Keys.onReleased: event => {
582 if (event.key === Qt.Key_Back) {
583 this.goBack(event)
584 }
585 }
586
587 onInitialPageChanged: {
588 if (initialPage) {
589 clear();
590 push(initialPage, null)
591 }
592 }
593/*
594 onActiveFocusChanged: {
595 if (activeFocus) {
596 layersStack.currentItem.forceActiveFocus()
597 if (columnView.activeFocus) {
598 print("SSS"+columnView.currentItem)
599 columnView.currentItem.forceActiveFocus();
600 }
601 }
602 }
603*/
604//END signals & signal handlers
605
606 Connections {
607 id: modalConnection
608 target: leftSidebar
609 function onModalChanged(): void {
610 if (leftSidebar.modal) {
611 const sidebar = sidebarControl.contentItem;
612 const background = sidebarControl.background;
613 sidebarControl.contentItem = null;
614 leftSidebar.contentItem = sidebar;
615 sidebarControl.background = null;
616 leftSidebar.background = background;
617
618 sidebar.visible = true;
619 background.visible = true;
620 } else {
621 const sidebar = leftSidebar.contentItem
622 const background = leftSidebar.background
623 leftSidebar.contentItem=null
624 sidebarControl.contentItem = sidebar
625 leftSidebar.background=null
626 sidebarControl.background = background
627
628 sidebar.visible = true;
629 background.visible = true;
630 }
631 }
632 }
633
634 implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding
635 implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
636
637 Shortcut {
638 sequences: [ StandardKey.Back ]
639 onActivated: goBack()
640 }
641 Shortcut {
642 sequences: [ StandardKey.Forward ]
643 onActivated: goForward()
644 }
645
646 Keys.forwardTo: [currentItem]
647
648 GlobalToolBar.PageRowGlobalToolBarStyleGroup {
649 id: globalToolBar
650 readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0
651 readonly property int rightReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.rightReservedSpace : 0
652 readonly property int height: globalToolBarUI.height
653 readonly property Item leftHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.leftHandleAnchor : null
654 readonly property Item rightHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.rightHandleAnchor : null
655 }
656
657 QQC2.StackView {
658 id: layerToolbarStack
659 anchors {
660 left: parent.left
661 top: parent.top
662 right: parent.right
663 }
664 z: 100 // 100 is layersStack.z + 1
665 height: currentItem?.implicitHeight ?? 0
666 initialItem: Item {implicitHeight: 0}
667
668 Component {
669 id: emptyToolbar
670 Item {
671 implicitHeight: 0
672 }
673 }
674 popEnter: Transition {
675 OpacityAnimator {
676 from: 0
677 to: 1
678 duration: Kirigami.Units.longDuration
679 easing.type: Easing.InOutCubic
680 }
681 }
682 popExit: Transition {
683 OpacityAnimator {
684 from: 1
685 to: 0
686 duration: Kirigami.Units.longDuration
687 easing.type: Easing.InOutCubic
688 }
689 }
690 pushEnter: Transition {
691 OpacityAnimator {
692 from: 0
693 to: 1
694 duration: Kirigami.Units.longDuration
695 easing.type: Easing.InOutCubic
696 }
697 }
698 pushExit: Transition {
699 OpacityAnimator {
700 from: 1
701 to: 0
702 duration: Kirigami.Units.longDuration
703 easing.type: Easing.InOutCubic
704 }
705 }
706 replaceEnter: Transition {
707 OpacityAnimator {
708 from: 0
709 to: 1
710 duration: Kirigami.Units.longDuration
711 easing.type: Easing.InOutCubic
712 }
713 }
714 replaceExit: Transition {
715 OpacityAnimator {
716 from: 1
717 to: 0
718 duration: Kirigami.Units.longDuration
719 easing.type: Easing.InOutCubic
720 }
721 }
722 }
723
724 QQC2.StackView {
725 id: layerFooterStack
726 anchors {
727 left: parent.left
728 bottom: parent.bottom
729 right: parent.right
730 }
731 z: 100 // 100 is layersStack.z + 1
732 height: currentItem?.implicitHeight ?? 0
733 initialItem: Item {implicitHeight: 0}
734
735 popEnter: Transition {
736 OpacityAnimator {
737 from: 0
738 to: 1
739 duration: Kirigami.Units.longDuration
740 easing.type: Easing.InOutCubic
741 }
742 }
743 popExit: Transition {
744 OpacityAnimator {
745 from: 1
746 to: 0
747 duration: Kirigami.Units.longDuration
748 easing.type: Easing.InOutCubic
749 }
750 }
751 pushEnter: Transition {
752 OpacityAnimator {
753 from: 0
754 to: 1
755 duration: Kirigami.Units.longDuration
756 easing.type: Easing.InOutCubic
757 }
758 }
759 pushExit: Transition {
760 OpacityAnimator {
761 from: 1
762 to: 0
763 duration: Kirigami.Units.longDuration
764 easing.type: Easing.InOutCubic
765 }
766 }
767 replaceEnter: Transition {
768 OpacityAnimator {
769 from: 0
770 to: 1
771 duration: Kirigami.Units.longDuration
772 easing.type: Easing.InOutCubic
773 }
774 }
775 replaceExit: Transition {
776 OpacityAnimator {
777 from: 1
778 to: 0
779 duration: Kirigami.Units.longDuration
780 easing.type: Easing.InOutCubic
781 }
782 }
783 }
784
785 QQC2.StackView {
786 id: layersStack
787 z: 99
788 anchors {
789 left: parent.left
790 top: layerToolbarStack.bottom
791 right: parent.right
792 bottom: layerFooterStack.top
793 }
794 // placeholder as initial item
795 initialItem: columnViewLayout
796
797 onDepthChanged: {
798 let item = layersStack.get(depth - 1)
799
800 if (layerToolbarStack.depth > depth) {
801 while (layerToolbarStack.depth > depth) {
802 layerToolbarStack.pop();
803 }
804 } else if (layerToolbarStack.depth < depth) {
805 for (let i = layerToolbarStack.depth; i < depth; ++i) {
806 const toolBar = layersStack.get(i).Kirigami.ColumnView.globalHeader;
807 layerToolbarStack.push(toolBar || emptyToolbar);
808 }
809 }
810 let toolBarItem = layerToolbarStack.get(layerToolbarStack.depth - 1)
811 if (item.Kirigami.ColumnView.globalHeader != toolBarItem) {
812 const toolBar = item.Kirigami.ColumnView.globalHeader;
813 layerToolbarStack.replace(toolBar ?? emptyToolbar);
814 }
815 // WORKAROUND: the second time the transition on opacity doesn't seem to be executed
816 toolBarItem = layerToolbarStack.get(layerToolbarStack.depth - 1)
817 toolBarItem.opacity = 1;
818
819 if (layerFooterStack.depth > depth) {
820 while (layerFooterStack.depth > depth) {
821 layerFooterStack.pop();
822 }
823 } else if (layerFooterStack.depth < depth) {
824 for (let i = layerFooterStack.depth; i < depth; ++i) {
825 const footer = layersStack.get(i).Kirigami.ColumnView.globalFooter;
826 layerFooterStack.push(footer ?? emptyToolbar);
827 }
828 }
829 let footerItem = layerFooterStack.get(layerFooterStack.depth - 1)
830 if (item.Kirigami.ColumnView.globalHeader != footerItem) {
831 const footer = item.Kirigami.ColumnView.globalFooter;
832 layerFooterStack.replace(footer ?? emptyToolbar);
833 }
834 footerItem = layerFooterStack.get(layerFooterStack.depth - 1)
835 footerItem.opacity = 1;
836 }
837
838 function clear(): void {
839 // don't let it kill the main page row
840 const d = layersStack.depth;
841 for (let i = 1; i < d; ++i) {
842 pop();
843 }
844 }
845
846 popEnter: Transition {
847 OpacityAnimator {
848 from: 0
849 to: 1
850 duration: Kirigami.Units.longDuration
851 easing.type: Easing.InOutCubic
852 }
853 }
854 popExit: Transition {
855 ParallelAnimation {
856 OpacityAnimator {
857 from: 1
858 to: 0
859 duration: Kirigami.Units.longDuration
860 easing.type: Easing.InOutCubic
861 }
862 YAnimator {
863 from: 0
864 to: height/2
865 duration: Kirigami.Units.longDuration
866 easing.type: Easing.InCubic
867 }
868 }
869 }
870
871 pushEnter: Transition {
872 ParallelAnimation {
873 // 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
874 PropertyAnimation {
875 property: "opacity"
876 from: 0
877 to: 1
878 duration: Kirigami.Units.longDuration
879 easing.type: Easing.InOutCubic
880 }
881 YAnimator {
882 from: height/2
883 to: 0
884 duration: Kirigami.Units.longDuration
885 easing.type: Easing.OutCubic
886 }
887 }
888 }
889
890
891 pushExit: Transition {
892 OpacityAnimator {
893 from: 1
894 to: 0
895 duration: Kirigami.Units.longDuration
896 easing.type: Easing.InOutCubic
897 }
898 }
899
900 replaceEnter: Transition {
901 ParallelAnimation {
902 OpacityAnimator {
903 from: 0
904 to: 1
905 duration: Kirigami.Units.longDuration
906 easing.type: Easing.InOutCubic
907 }
908 YAnimator {
909 from: height/2
910 to: 0
911 duration: Kirigami.Units.longDuration
912 easing.type: Easing.OutCubic
913 }
914 }
915 }
916
917 replaceExit: Transition {
918 ParallelAnimation {
919 OpacityAnimator {
920 from: 1
921 to: 0
922 duration: Kirigami.Units.longDuration
923 easing.type: Easing.InCubic
924 }
925 YAnimator {
926 from: 0
927 to: -height/2
928 duration: Kirigami.Units.longDuration
929 easing.type: Easing.InOutCubic
930 }
931 }
932 }
933 }
934
935 Loader {
936 id: globalToolBarUI
937 anchors {
938 left: parent.left
939 top: parent.top
940 right: parent.right
941 }
942 z: 100
943 property QT.Control pageRow: root
944 active: globalToolBar.actualStyle !== Kirigami.ApplicationHeaderStyle.None || (leadingVisibleItem && leadingVisibleItem.globalToolBarStyle === Kirigami.ApplicationHeaderStyle.ToolBar)
945 visible: active
946 height: active ? implicitHeight : 0
947 // If load is asynchronous, it will fail to compute the initial implicitHeight
948 // https://bugs.kde.org/show_bug.cgi?id=442660
949 asynchronous: false
950 source: Qt.resolvedUrl("private/globaltoolbar/PageRowGlobalToolBarUI.qml");
951 }
952
953 QtObject {
954 id: pagesLogic
955 readonly property var componentCache: new Array()
956
957 property Component __mobileDialogLayerComponent
958
959 function getMobileDialogLayerComponent() {
960 if (!__mobileDialogLayerComponent) {
961 __mobileDialogLayerComponent = Qt.createComponent(Qt.resolvedUrl("private/MobileDialogLayer.qml"));
962 }
963 return __mobileDialogLayerComponent;
964 }
965
966 function verifyPages(pages, properties): bool {
967 function validPage(page) {
968 //don't try adding an already existing page
969 if (page instanceof QT.Page && columnView.containsItem(page)) {
970 console.log(`Page ${page} is already in the PageRow`)
971 return false
972 }
973 return page instanceof QT.Page || page instanceof Component || typeof page === 'string'
974 || (typeof page === 'object' && typeof page.toString() === 'string')
975 }
976
977 // check page/pages that it is/they are valid
978 const pagesIsArr = Array.isArray(pages) && pages.length > 0
979 let isValidArrOfPages = pagesIsArr;
980
981 if (pagesIsArr) {
982 for (const page of pages) {
983 if (!validPage(page)) {
984 isValidArrOfPages = false;
985 break;
986 }
987 }
988 }
989
990 // check properties obejct/array object validity
991 const isProp = typeof properties === 'object';
992 const propsIsArr = Array.isArray(properties) && properties.length > 0
993 let isValidPropArr = propsIsArr;
994
995 if (propsIsArr) {
996 for (const prop of properties) {
997 if (typeof prop !== 'object') {
998 isValidPropArr = false;
999 break;
1000 }
1001 }
1002 isValidPropArr = isValidPropArr && pages.length === properties.length
1003 }
1004
1005 return (validPage(pages) || isValidArrOfPages)
1006 && (!properties || (isProp || isValidPropArr))
1007 }
1008
1009 function insertPage_unchecked(position, page, properties) {
1010 columnView.pop(position - 1);
1011
1012 // figure out if more than one page is being pushed
1013 let pages;
1014 let propsArray = [];
1015 if (page instanceof Array) {
1016 pages = page;
1017 page = pages.pop();
1018 }
1019 if (properties instanceof Array) {
1020 propsArray = properties;
1021 properties = propsArray.pop();
1022 } else {
1023 propsArray = [properties];
1024 }
1025
1026 // push any extra defined pages onto the stack
1027 if (pages) {
1028 for (const i in pages) {
1029 let tPage = pages[i];
1030 let tProps = propsArray[i];
1031
1032 pagesLogic.initAndInsertPage(position, tPage, tProps);
1033 ++position;
1034 }
1035 }
1036
1037 // initialize the page
1038 const pageItem = pagesLogic.initAndInsertPage(position, page, properties);
1039
1040 pagePushed(pageItem);
1041
1042 return pageItem;
1043 }
1044
1045 function getPageComponent(page): Component {
1046 let pageComp;
1047
1048 if (page.createObject) {
1049 // page defined as component
1050 pageComp = page;
1051 } else if (typeof page === "string") {
1052 // page defined as string (a url)
1053 pageComp = pagesLogic.componentCache[page];
1054 if (!pageComp) {
1055 pageComp = pagesLogic.componentCache[page] = Qt.createComponent(page);
1056 }
1057 } else if (typeof page === "object" && !(page instanceof Item) && page.toString !== undefined) {
1058 // page defined as url (QML value type, not a string)
1059 pageComp = pagesLogic.componentCache[page.toString()];
1060 if (!pageComp) {
1061 pageComp = pagesLogic.componentCache[page.toString()] = Qt.createComponent(page.toString());
1062 }
1063 }
1064
1065 return pageComp
1066 }
1067
1068 function initPage(page, properties): QT.Page {
1069 const pageComp = getPageComponent(page, properties);
1070
1071 if (pageComp) {
1072 // instantiate page from component
1073 // Important: The parent needs to be set otherwise a reference needs to be kept around
1074 // to avoid the page being garbage collected.
1075 page = pageComp.createObject(pagesLogic, properties || {});
1076
1077 if (pageComp.status === Component.Error) {
1078 throw new Error("Error while loading page: " + pageComp.errorString());
1079 }
1080 } else {
1081 // copy properties to the page
1082 for (const prop in properties) {
1083 if (page.hasOwnProperty(prop)) {
1084 page[prop] = properties[prop];
1085 }
1086 }
1087 }
1088 return page;
1089 }
1090
1091 function initAndInsertPage(position, page, properties): QT.Page {
1092 page = initPage(page, properties);
1093 columnView.insertItem(position, page);
1094 return page;
1095 }
1096 }
1097
1098 RowLayout {
1099 id: columnViewLayout
1100 spacing: 1
1101 readonly property alias columnView: columnView
1102 // set the pagestack of this and all children to root, otherwise
1103 // they would automatically resolve to the layer's stackview
1104 Kirigami.PageStack.pageStack: root
1105 anchors {
1106 fill: parent
1107 topMargin: -layersStack.y
1108 }
1109 QQC2.Control {
1110 id: sidebarControl
1111 Layout.fillHeight: true
1112 visible: contentItem !== null
1113 leftPadding: root.leftSidebar ? root.leftSidebar.leftPadding : 0
1114 topPadding: root.leftSidebar ? root.leftSidebar.topPadding : 0
1115 rightPadding: root.leftSidebar ? root.leftSidebar.rightPadding : 0
1116 bottomPadding: root.leftSidebar ? root.leftSidebar.bottomPadding : 0
1117 }
1118 Kirigami.ColumnView {
1119 id: columnView
1120 Layout.fillWidth: true
1121 Layout.fillHeight: true
1122
1123 topPadding: globalToolBarUI.item && globalToolBarUI.item.breadcrumbVisible
1124 ? globalToolBarUI.height : 0
1125
1126 // Internal hidden api for Page
1127 readonly property Item __pageRow: root
1128 acceptsMouse: Kirigami.Settings.isMobile
1129 columnResizeMode: root.wideMode ? Kirigami.ColumnView.FixedColumns : Kirigami.ColumnView.SingleColumn
1130 columnWidth: root.defaultColumnWidth
1131 interactive: Qt.platform.os !== 'android'
1132
1133 onItemInserted: (position, item) => root.pageInserted(position, item);
1134 onItemRemoved: item => root.pageRemoved(item);
1135
1136 onVisibleItemsChanged: {
1137 // implementation of `popHiddenPages` option
1138 if (root.popHiddenPages) {
1139 // manually fetch lastItem here rather than use root.lastItem property, since that binding may not have updated yet
1140 let lastItem = columnView.contentChildren[columnView.contentChildren.length - 1];
1141 let trailingVisibleItem = columnView.trailingVisibleItem;
1142
1143 // pop every page that isn't visible and at the top of the stack
1144 while (lastItem && columnView.trailingVisibleItem &&
1145 lastItem !== columnView.trailingVisibleItem && columnView.containsItem(lastItem)) {
1146 root.pop();
1147 }
1148 }
1149 }
1150 }
1151 }
1152
1153 Rectangle {
1154 anchors.bottom: parent.bottom
1155 height: Kirigami.Units.smallSpacing
1156 x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width))
1157 width: columnView.width * (columnView.width/columnView.contentWidth)
1158 color: Kirigami.Theme.textColor
1159 opacity: 0
1160 onXChanged: {
1161 opacity = 0.3
1162 scrollIndicatorTimer.restart();
1163 }
1164 Behavior on opacity {
1165 OpacityAnimator {
1166 duration: Kirigami.Units.longDuration
1167 easing.type: Easing.InOutQuad
1168 }
1169 }
1170 Timer {
1171 id: scrollIndicatorTimer
1172 interval: Kirigami.Units.longDuration * 4
1173 onTriggered: parent.opacity = 0;
1174 }
1175 }
1176}
PageRow implements a row-based navigation model, which can be used with a set of interlinked informat...
Definition PageRow.qml:21
OverlayDrawer leftSidebar
This property assigns a drawer as an internal left sidebar for this PageRow.
Definition PageRow.qml:170
Item lastItem
This property holds the last page in the row.
Definition PageRow.qml:35
int defaultColumnWidth
This property holds the default width for a column.
Definition PageRow.qml:112
bool wideMode
This property tells whether the PageRow is wide enough to show multiple pages.
Definition PageRow.qml:127
alias depth
This property holds the number of pages currently pushed onto the view.
Definition PageRow.qml:29
void push(page, properties)
This method pushes a page on the stack.
var initialPage
This property sets the initial page for this PageRow.
Definition PageRow.qml:55
alias visibleItems
This property holds all visible pages in the PageRow, excluding those which are scrolled away.
Definition PageRow.qml:86
bool popHiddenPages
This property holds whether to automatically pop pages at the top of the stack if they are not visibl...
Definition PageRow.qml:189
alias leadingVisibleItem
This property holds the first page in the PageRow that is at least partially visible.
Definition PageRow.qml:95
alias separatorVisible
This property sets whether the separators between pages should be displayed.
Definition PageRow.qml:136
alias currentItem
This property holds the currently visible/active page.
Definition PageRow.qml:44
alias globalToolBar
This property sets the appearance of an optional global toolbar for the whole PageRow.
Definition PageRow.qml:160
alias currentIndex
This property holds the index of the currently active page.
Definition PageRow.qml:50
alias interactive
This property sets whether it is possible to go back/forward by swiping with a gesture on the content...
Definition PageRow.qml:121
alias layers
This property holds the modal layers.
Definition PageRow.qml:180
alias columnView
This property holds the ColumnView that this PageRow owns.
Definition PageRow.qml:71
void pushDialogLayer(page, properties={}, windowProperties={})
Pushes a page as a new dialog on desktop and as a layer on mobile.
alias trailingVisibleItem
This property holds the last page in the PageRow that is at least partially visible.
Definition PageRow.qml:104
alias items
This property holds the present pages in the PageRow.
Definition PageRow.qml:78
Page is a container for all the app pages: everything pushed to the ApplicationWindow's pageStack sho...
Definition Page.qml:19
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
KGuiItem properties()
KGuiItem clear()
const QList< QKeySequence > & replace()
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
bool close()
void destroy(bool destroyWindow, bool destroySubWindows)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 11 2025 11:49:26 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.