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 item = layers.push(page, properties);
296 Object.defineProperty(item, 'closeDialog', {
297 value: function() {
298 layers.pop();
299 }
300 });
302 } else {
303 // open as a new window
304 if (!("modality" in windowProperties)) {
305 windowProperties.modality = Qt.WindowModal;
306 }
307 if (!("height" in windowProperties)) {
308 windowProperties.height = Kirigami.Units.gridUnit * 30;
309 }
310 if (!("width" in windowProperties)) {
311 windowProperties.width = Kirigami.Units.gridUnit * 50;
312 }
313 if (!("minimumWidth" in windowProperties)) {
314 windowProperties.minimumWidth = Kirigami.Units.gridUnit * 20;
315 }
316 if (!("minimumHeight" in windowProperties)) {
317 windowProperties.minimumHeight = Kirigami.Units.gridUnit * 15;
319 if (!("flags" in windowProperties)) {
320 windowProperties.flags = Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint;
321 }
322 const windowComponent = Qt.createComponent(Qt.resolvedUrl("./ApplicationWindow.qml"));
323 const window = windowComponent.createObject(root, windowProperties);
324 windowComponent.destroy();
325 item = window.pageStack.push(page, properties);
326 Object.defineProperty(item, 'closeDialog', {
327 value: function() {
328 window.close();
329 }
330 });
332 item.Keys.escapePressed.connect(event => item.closeDialog());
333 return item;
334 }
335
336 /**
337 * @brief Inserts a new page or a list of new pages at an arbitrary position.
338 *
339 * A single page can be defined as an url, a component, or an object. It can
340 * also be an array of the above said types, but in that case, the
341 * properties' array length must match pages' array length or it must be
342 * empty. Failing to comply with the following rules will make the method
343 * return null before doing anything.
344 *
345 * @param page A single page or an array of pages.
346 * @param properties A single property object or an array of property
347 * objects.
348 *
349 * @return The new created page (or the last one if it was an array).
350 * @since 2.7
351 */
352 function insertPage(position, page, properties): QT.Page {
353 if (!pagesLogic.verifyPages(page, properties)) {
354 console.warn("Inserted pages do not conform to the rules. Please check the documentation.");
355 console.trace();
356 return null
357 }
358
359 if (position < 0 || position > depth) {
360 console.warn("You are trying to insert a page to an out-of-bounds position. Position will be adjusted accordingly.");
361 console.trace();
362 position = Math.max(0, Math.min(depth, position));
363 }
364 return pagesLogic.insertPage_unchecked(position, page, properties)
365 }
366
367 /**
368 * Move the page at position fromPos to the new position toPos
369 * If needed, currentIndex will be adjusted
370 * in order to keep the same current page.
371 * @since 2.7
372 */
373 function movePage(fromPos, toPos): void {
374 columnView.moveItem(fromPos, toPos);
375 }
376
377 /**
378 * @brief Remove the given page.
379 * @param page The page can be given both as integer position or by reference
380 * @return The page that has just been removed
381 * @since 2.7
382 */
383 function removePage(page): QT.Page {
384 if (depth > 0) {
385 return columnView.removeItem(page);
386 }
387 return null
388 }
389
390 /**
391 * @brief Pops a page off the stack.
392 * @param page If page is specified then the stack is unwound to that page,
393 * to unwind to the first page specify page as null.
394 * @return The page instance that was popped off the stack.
395 */
396 function pop(page): QT.Page {
397 return columnView.pop(page);
398 }
399
400 /**
401 * @brief Replaces a page on the current index.
402 *
403 * A single page can be defined as an url, a component, or an object. It can
404 * also be an array of the above said types, but in that case, the
405 * properties' array length must match pages' array length or it must be
406 * empty. Failing to comply with the following rules will make the method
407 * return null before doing anything.
408 *
409 * @param page A single page or an array of pages.
410 * @param properties A single property object or an array of property
411 * objects.
412 *
413 * @return The new created page (or the last one if it was an array).
414 * @see push() for details.
415 */
416 function replace(page, properties): QT.Page {
417 if (!pagesLogic.verifyPages(page, properties)) {
418 console.warn("Specified pages do not conform to the rules. Please check the documentation.");
419 console.trace();
420 return null
421 }
422
423 // Remove all pages on top of the one being replaced.
424 if (currentIndex >= 0) {
425 columnView.pop(currentIndex);
426 } else {
427 console.warn("There's no page to replace");
428 }
429
430 // Figure out if more than one page is being pushed.
431 let pages;
432 let propsArray = [];
433 if (page instanceof Array) {
434 pages = page;
435 page = pages.shift();
436 }
437 if (properties instanceof Array) {
438 propsArray = properties;
439 properties = propsArray.shift();
440 } else {
441 propsArray = [properties];
442 }
443
444 // Replace topmost page.
445 let pageItem = pagesLogic.initPage(page, properties);
446 if (depth > 0)
447 columnView.replaceItem(depth - 1, pageItem);
448 else {
449 console.log("Calling replace on empty PageRow", pageItem)
450 columnView.addItem(pageItem)
451 }
452 pagePushed(pageItem);
453
454 // Push any extra defined pages onto the stack.
455 if (pages) {
456 for (const i in pages) {
457 const tPage = pages[i];
458 const tProps = propsArray[i];
459
460 pageItem = pagesLogic.initPage(tPage, tProps);
461 columnView.addItem(pageItem);
462 pagePushed(pageItem);
463 }
464 }
465
466 currentIndex = depth - 1;
467 return pageItem;
468 }
469
470 /**
471 * @brief Clears the page stack.
472 *
473 * Destroy (or reparent) all the pages contained.
474 */
475 function clear(): void {
476 columnView.clear();
477 }
478
479 /**
480 * @return the page at idx
481 * @param idx the depth of the page we want
482 */
483 function get(idx): QT.Page {
484 return items[idx];
485 }
486
487 /**
488 * Go back to the previous index and scroll to the left to show one more column.
489 */
490 function flickBack(): void {
491 if (depth > 1) {
492 currentIndex = Math.max(0, currentIndex - 1);
493 }
494 }
495
496 /**
497 * Acts as if you had pressed the "back" button on Android or did Alt-Left on desktop,
498 * "going back" in the layers and page row. Results in a layer being popped if available,
499 * or the currentIndex being set to currentIndex-1 if not available.
500 *
501 * @param event Optional, an event that will be accepted if a page is successfully
502 * "backed" on
503 */
504 function goBack(event = null): void {
505 const backEvent = {accepted: false}
506
507 if (layersStack.depth >= 1) {
508 try { // app code might be screwy, but we still want to continue functioning if it throws an exception
509 layersStack.currentItem.backRequested(backEvent)
510 } catch (error) {}
511
512 if (!backEvent.accepted) {
513 if (layersStack.depth > 1) {
514 layersStack.pop()
515 if (event) {
516 event.accepted = true
517 }
518 return
519 }
520 }
521 }
522
523 if (currentIndex >= 1) {
524 try { // app code might be screwy, but we still want to continue functioning if it throws an exception
525 currentItem.backRequested(backEvent)
526 } catch (error) {}
527
528 if (!backEvent.accepted) {
529 if (depth > 1) {
530 currentIndex = Math.max(0, currentIndex - 1)
531 if (event) {
532 event.accepted = true
533 }
534 }
535 }
536 }
537 }
538
539 /**
540 * Acts as if you had pressed the "forward" shortcut on desktop,
541 * "going forward" in the page row. Results in the active page
542 * becoming the next page in the row from the current active page,
543 * i.e. currentIndex + 1.
544 */
545 function goForward(): void {
546 currentIndex = Math.min(depth - 1, currentIndex + 1)
547 }
548//END FUNCTIONS
549
550//BEGIN signals & signal handlers
551 /**
552 * @brief Emitted when a page has been inserted anywhere.
553 * @param position where the page has been inserted
554 * @param page the new page
555 * @since 2.7
556 */
557 signal pageInserted(int position, Item page)
558
559 /**
560 * @brief Emitted when a page has been pushed to the bottom.
561 * @param page the new page
562 * @since 2.5
563 */
564 signal pagePushed(Item page)
565
566 /**
567 * @brief Emitted when a page has been removed from the row.
568 * @param page the page that has been removed: at this point it's still valid,
569 * but may be auto deleted soon.
570 * @since 2.5
571 */
572 signal pageRemoved(Item page)
573
574 onLeftSidebarChanged: {
575 if (leftSidebar && !leftSidebar.modal) {
576 modalConnection.onModalChanged();
577 }
578 }
579
580 Keys.onReleased: event => {
581 if (event.key === Qt.Key_Back) {
582 this.goBack(event)
583 }
584 }
585
586 onInitialPageChanged: {
587 if (initialPage) {
588 clear();
589 push(initialPage, null)
590 }
591 }
592/*
593 onActiveFocusChanged: {
594 if (activeFocus) {
595 layersStack.currentItem.forceActiveFocus()
596 if (columnView.activeFocus) {
597 print("SSS"+columnView.currentItem)
598 columnView.currentItem.forceActiveFocus();
599 }
600 }
601 }
602*/
603//END signals & signal handlers
604
605 Connections {
606 id: modalConnection
607 target: leftSidebar
608 function onModalChanged(): void {
609 if (leftSidebar.modal) {
610 const sidebar = sidebarControl.contentItem;
611 const background = sidebarControl.background;
612 sidebarControl.contentItem = null;
613 leftSidebar.contentItem = sidebar;
614 sidebarControl.background = null;
615 leftSidebar.background = background;
616
617 sidebar.visible = true;
618 background.visible = true;
619 } else {
620 const sidebar = leftSidebar.contentItem
621 const background = leftSidebar.background
622 leftSidebar.contentItem=null
623 sidebarControl.contentItem = sidebar
624 leftSidebar.background=null
625 sidebarControl.background = background
626
627 sidebar.visible = true;
628 background.visible = true;
629 }
630 }
631 }
632
633 implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding
634 implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
635
636 Shortcut {
637 sequences: [ StandardKey.Back ]
638 onActivated: goBack()
639 }
640 Shortcut {
641 sequences: [ StandardKey.Forward ]
642 onActivated: goForward()
643 }
644
645 Keys.forwardTo: [currentItem]
646
647 GlobalToolBar.PageRowGlobalToolBarStyleGroup {
648 id: globalToolBar
649 readonly property int leftReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.leftReservedSpace : 0
650 readonly property int rightReservedSpace: globalToolBarUI.item ? globalToolBarUI.item.rightReservedSpace : 0
651 readonly property int height: globalToolBarUI.height
652 readonly property Item leftHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.leftHandleAnchor : null
653 readonly property Item rightHandleAnchor: globalToolBarUI.item ? globalToolBarUI.item.rightHandleAnchor : null
654 }
655
656 QQC2.StackView {
657 id: layerToolbarStack
658 anchors {
659 left: parent.left
660 top: parent.top
661 right: parent.right
662 }
663 z: 100 // 100 is layersStack.z + 1
664 height: currentItem?.implicitHeight ?? 0
665 initialItem: Item {implicitHeight: 0}
666
667 Component {
668 id: emptyToolbar
669 Item {
670 implicitHeight: 0
671 }
672 }
673 popEnter: Transition {
674 OpacityAnimator {
675 from: 0
676 to: 1
677 duration: Kirigami.Units.longDuration
678 easing.type: Easing.InOutCubic
679 }
680 }
681 popExit: Transition {
682 OpacityAnimator {
683 from: 1
684 to: 0
685 duration: Kirigami.Units.longDuration
686 easing.type: Easing.InOutCubic
687 }
688 }
689 pushEnter: Transition {
690 OpacityAnimator {
691 from: 0
692 to: 1
693 duration: Kirigami.Units.longDuration
694 easing.type: Easing.InOutCubic
695 }
696 }
697 pushExit: Transition {
698 OpacityAnimator {
699 from: 1
700 to: 0
701 duration: Kirigami.Units.longDuration
702 easing.type: Easing.InOutCubic
703 }
704 }
705 replaceEnter: Transition {
706 OpacityAnimator {
707 from: 0
708 to: 1
709 duration: Kirigami.Units.longDuration
710 easing.type: Easing.InOutCubic
711 }
712 }
713 replaceExit: Transition {
714 OpacityAnimator {
715 from: 1
716 to: 0
717 duration: Kirigami.Units.longDuration
718 easing.type: Easing.InOutCubic
719 }
720 }
721 }
722
723 QQC2.StackView {
724 id: layerFooterStack
725 anchors {
726 left: parent.left
727 bottom: parent.bottom
728 right: parent.right
729 }
730 z: 100 // 100 is layersStack.z + 1
731 height: currentItem?.implicitHeight ?? 0
732 initialItem: Item {implicitHeight: 0}
733
734 popEnter: Transition {
735 OpacityAnimator {
736 from: 0
737 to: 1
738 duration: Kirigami.Units.longDuration
739 easing.type: Easing.InOutCubic
740 }
741 }
742 popExit: Transition {
743 OpacityAnimator {
744 from: 1
745 to: 0
746 duration: Kirigami.Units.longDuration
747 easing.type: Easing.InOutCubic
748 }
749 }
750 pushEnter: Transition {
751 OpacityAnimator {
752 from: 0
753 to: 1
754 duration: Kirigami.Units.longDuration
755 easing.type: Easing.InOutCubic
756 }
757 }
758 pushExit: Transition {
759 OpacityAnimator {
760 from: 1
761 to: 0
762 duration: Kirigami.Units.longDuration
763 easing.type: Easing.InOutCubic
764 }
765 }
766 replaceEnter: Transition {
767 OpacityAnimator {
768 from: 0
769 to: 1
770 duration: Kirigami.Units.longDuration
771 easing.type: Easing.InOutCubic
772 }
773 }
774 replaceExit: Transition {
775 OpacityAnimator {
776 from: 1
777 to: 0
778 duration: Kirigami.Units.longDuration
779 easing.type: Easing.InOutCubic
780 }
781 }
782 }
783
784 QQC2.StackView {
785 id: layersStack
786 z: 99
787 anchors {
788 left: parent.left
789 top: layerToolbarStack.bottom
790 right: parent.right
791 bottom: layerFooterStack.top
792 }
793 // placeholder as initial item
794 initialItem: columnViewLayout
795
796 onDepthChanged: {
797 let item = layersStack.get(depth - 1)
798
799 if (layerToolbarStack.depth > depth) {
800 while (layerToolbarStack.depth > depth) {
801 layerToolbarStack.pop();
802 }
803 } else if (layerToolbarStack.depth < depth) {
804 for (let i = layerToolbarStack.depth; i < depth; ++i) {
805 const toolBar = layersStack.get(i).Kirigami.ColumnView.globalHeader;
806 layerToolbarStack.push(toolBar || emptyToolbar);
807 }
808 }
809 let toolBarItem = layerToolbarStack.get(layerToolbarStack.depth - 1)
810 if (item.Kirigami.ColumnView.globalHeader != toolBarItem) {
811 const toolBar = item.Kirigami.ColumnView.globalHeader;
812 layerToolbarStack.replace(toolBar ?? emptyToolbar);
813 }
814 // WORKAROUND: the second time the transition on opacity doesn't seem to be executed
815 toolBarItem = layerToolbarStack.get(layerToolbarStack.depth - 1)
816 toolBarItem.opacity = 1;
817
818 if (layerFooterStack.depth > depth) {
819 while (layerFooterStack.depth > depth) {
820 layerFooterStack.pop();
821 }
822 } else if (layerFooterStack.depth < depth) {
823 for (let i = layerFooterStack.depth; i < depth; ++i) {
824 const footer = layersStack.get(i).Kirigami.ColumnView.globalFooter;
825 layerFooterStack.push(footer ?? emptyToolbar);
826 }
827 }
828 let footerItem = layerFooterStack.get(layerFooterStack.depth - 1)
829 if (item.Kirigami.ColumnView.globalHeader != footerItem) {
830 const footer = item.Kirigami.ColumnView.globalFooter;
831 layerFooterStack.replace(footer ?? emptyToolbar);
832 }
833 footerItem = layerFooterStack.get(layerFooterStack.depth - 1)
834 footerItem.opacity = 1;
835 }
836
837 function clear(): void {
838 // don't let it kill the main page row
839 const d = layersStack.depth;
840 for (let i = 1; i < d; ++i) {
841 pop();
842 }
843 }
844
845 popEnter: Transition {
846 OpacityAnimator {
847 from: 0
848 to: 1
849 duration: Kirigami.Units.longDuration
850 easing.type: Easing.InOutCubic
851 }
852 }
853 popExit: Transition {
854 ParallelAnimation {
855 OpacityAnimator {
856 from: 1
857 to: 0
858 duration: Kirigami.Units.longDuration
859 easing.type: Easing.InOutCubic
860 }
861 YAnimator {
862 from: 0
863 to: height/2
864 duration: Kirigami.Units.longDuration
865 easing.type: Easing.InCubic
866 }
867 }
868 }
869
870 pushEnter: Transition {
871 ParallelAnimation {
872 // 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
873 PropertyAnimation {
874 property: "opacity"
875 from: 0
876 to: 1
877 duration: Kirigami.Units.longDuration
878 easing.type: Easing.InOutCubic
879 }
880 YAnimator {
881 from: height/2
882 to: 0
883 duration: Kirigami.Units.longDuration
884 easing.type: Easing.OutCubic
885 }
886 }
887 }
888
889
890 pushExit: Transition {
891 OpacityAnimator {
892 from: 1
893 to: 0
894 duration: Kirigami.Units.longDuration
895 easing.type: Easing.InOutCubic
896 }
897 }
898
899 replaceEnter: Transition {
900 ParallelAnimation {
901 OpacityAnimator {
902 from: 0
903 to: 1
904 duration: Kirigami.Units.longDuration
905 easing.type: Easing.InOutCubic
906 }
907 YAnimator {
908 from: height/2
909 to: 0
910 duration: Kirigami.Units.longDuration
911 easing.type: Easing.OutCubic
912 }
913 }
914 }
915
916 replaceExit: Transition {
917 ParallelAnimation {
918 OpacityAnimator {
919 from: 1
920 to: 0
921 duration: Kirigami.Units.longDuration
922 easing.type: Easing.InCubic
923 }
924 YAnimator {
925 from: 0
926 to: -height/2
927 duration: Kirigami.Units.longDuration
928 easing.type: Easing.InOutCubic
929 }
930 }
931 }
932 }
933
934 Loader {
935 id: globalToolBarUI
936 anchors {
937 left: parent.left
938 top: parent.top
939 right: parent.right
940 }
941 z: 100
942 property QT.Control pageRow: root
943 active: (!leadingVisibleItem || leadingVisibleItem.globalToolBarStyle !== Kirigami.ApplicationHeaderStyle.None) &&
944 (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 (properties.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 anchors {
1103 fill: parent
1104 topMargin: -layersStack.y
1105 }
1106 QQC2.Control {
1107 id: sidebarControl
1108 Layout.fillHeight: true
1109 visible: contentItem !== null
1110 leftPadding: root.leftSidebar ? root.leftSidebar.leftPadding : 0
1111 topPadding: root.leftSidebar ? root.leftSidebar.topPadding : 0
1112 rightPadding: root.leftSidebar ? root.leftSidebar.rightPadding : 0
1113 bottomPadding: root.leftSidebar ? root.leftSidebar.bottomPadding : 0
1114 }
1115 Kirigami.ColumnView {
1116 id: columnView
1117 Layout.fillWidth: true
1118 Layout.fillHeight: true
1119
1120 topPadding: globalToolBarUI.item && globalToolBarUI.item.breadcrumbVisible
1121 ? globalToolBarUI.height : 0
1122
1123 // Internal hidden api for Page
1124 readonly property Item __pageRow: root
1125 acceptsMouse: Kirigami.Settings.isMobile
1126 columnResizeMode: root.wideMode ? Kirigami.ColumnView.FixedColumns : Kirigami.ColumnView.SingleColumn
1127 columnWidth: root.defaultColumnWidth
1128 interactive: Qt.platform.os !== 'android'
1129
1130 onItemInserted: (position, item) => root.pageInserted(position, item);
1131 onItemRemoved: item => root.pageRemoved(item);
1132
1133 onVisibleItemsChanged: {
1134 // implementation of `popHiddenPages` option
1135 if (root.popHiddenPages) {
1136 // manually fetch lastItem here rather than use root.lastItem property, since that binding may not have updated yet
1137 let lastItem = columnView.contentChildren[columnView.contentChildren.length - 1];
1138 let trailingVisibleItem = columnView.trailingVisibleItem;
1139
1140 // pop every page that isn't visible and at the top of the stack
1141 while (lastItem && columnView.trailingVisibleItem &&
1142 lastItem !== columnView.trailingVisibleItem && columnView.containsItem(lastItem)) {
1143 root.pop();
1144 }
1145 }
1146 }
1147 }
1148 }
1149
1150 Rectangle {
1151 anchors.bottom: parent.bottom
1152 height: Kirigami.Units.smallSpacing
1153 x: (columnView.width - width) * (columnView.contentX / (columnView.contentWidth - columnView.width))
1154 width: columnView.width * (columnView.width/columnView.contentWidth)
1155 color: Kirigami.Theme.textColor
1156 opacity: 0
1157 onXChanged: {
1158 opacity = 0.3
1159 scrollIndicatorTimer.restart();
1160 }
1161 Behavior on opacity {
1162 OpacityAnimator {
1163 duration: Kirigami.Units.longDuration
1164 easing.type: Easing.InOutQuad
1165 }
1166 }
1167 Timer {
1168 id: scrollIndicatorTimer
1169 interval: Kirigami.Units.longDuration * 4
1170 onTriggered: parent.opacity = 0;
1171 }
1172 }
1173}
PageRow implements a row-based navigation model, which can be used with a set of interlinked informat...
Definition PageRow.qml:21
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
QWidget * window(QObject *job)
QAction * replace(const QObject *recvr, const char *slot, QObject *parent)
KGuiItem properties()
KGuiItem clear()
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-2024 The KDE developers.
Generated on Fri Dec 27 2024 11:50:15 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.