1/*
2 *  SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
3 *
4 *  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7import QtQuick 2.5
8import QtQuick.Controls 2.0 as QQC2
9import QtQuick.Layouts 1.2
10import "private"
11import org.kde.kirigami 2.4
12
13
14/**
15 * An item that can be used as a title for the application.
16 * Scrolling the main page will make it taller or shorter (through the point of going away)
17 * It's a behavior similar to the typical mobile web browser addressbar
18 * the minimum, preferred and maximum heights of the item can be controlled with
19 * * minimumHeight: default is 0, i.e. hidden
20 * * preferredHeight: default is Units.gridUnit * 1.6
21 * * maximumHeight: default is Units.gridUnit * 3
22 *
23 * To achieve a titlebar that stays completely fixed just set the 3 sizes as the same
24 */
25AbstractApplicationHeader {
26    id: header
27
28    /**
29     * headerStyle: int
30     * The way the separator between pages should be drawn in the header.
31     * Allowed values are:
32     * * Breadcrumb: the pages are hierarchical and the separator will look like a >
33     * * TabBar: the pages are intended to behave like tabbar pages
34     *    and the separator will look limke a dot.
35     *
36     * When the header is in wide screen mode, no separator will be drawn.
37     */
38    property int headerStyle: ApplicationHeaderStyle.Auto
39
40    /**
41     * backButtonEnabled: bool
42     * if true, there will be a back button present that will make the pagerow scroll back when clicked
43     */
44    property bool backButtonEnabled: (!titleList.isTabBar && (!Settings.isMobile || Qt.platform.os == "ios"))
45
46    onBackButtonEnabledChanged: {
47        if (backButtonEnabled && !titleList.backButton) {
48            var component = Qt.createComponent(Qt.resolvedUrl("private/BackButton.qml"));
49            titleList.backButton = component.createObject(navButtons);
50            component = Qt.createComponent(Qt.resolvedUrl("private/ForwardButton.qml"));
51            titleList.forwardButton = component.createObject(navButtons, {"headerFlickable": titleList});
52        } else if (titleList.backButton) {
53            titleList.backButton.destroy();
54            titleList.forwardButton.destroy();
55        }
56    }
57    property Component pageDelegate: Component {
58        Row {
59            height: parent.height
60
61            spacing: Units.smallSpacing
62
63            x: Units.smallSpacing
64
65            Icon {
66                //in tabbar mode this is just a spacer
67                visible: !titleList.wideMode && ((typeof(modelData) != "undefined" && modelData > 0) || titleList.internalHeaderStyle == ApplicationHeaderStyle.TabBar)
68                anchors.verticalCenter: parent.verticalCenter
69                height: Units.iconSizes.small
70                width: height
71                selected: header.background && header.background.color && header.background.color === Theme.highlightColor
72                source: titleList.isTabBar ? "" : (LayoutMirroring.enabled ? "go-next-symbolic-rtl" : "go-next-symbolic")
73            }
74
75            Heading {
76                id: title
77                width: Math.min(parent.width, Math.min(titleList.width, implicitWidth)) + Units.smallSpacing
78                anchors.verticalCenter: parent.verticalCenter
79                opacity: current ? 1 : 0.4
80                //Scaling animate NativeRendering is too slow
81                renderType: Text.QtRendering
82                color: header.background && header.background.color && header.background.color === Theme.highlightColor ? Theme.highlightedTextColor : Theme.textColor
83                elide: Text.ElideRight
84                text: page ? page.title : ""
85                font.pointSize: -1
86                font.pixelSize: Math.max(1, titleList.height * 0.7)
87                verticalAlignment: Text.AlignVCenter
88                wrapMode: Text.NoWrap
89                Rectangle {
90                    anchors {
91                        bottom: parent.bottom
92                        left: parent.left
93                        right: parent.right
94                    }
95                    height: Units.smallSpacing
96                    color: title.color
97                    opacity: 0.6
98                    visible: titleList.isTabBar && current
99                }
100            }
101        }
102    }
103
104    Component.onCompleted: print("Warning: ApplicationHeader is deprecated, remove and use the automatic internal toolbar instead.")
105
106    Rectangle {
107        anchors {
108            verticalCenter: parent.verticalCenter
109        }
110        visible: titleList.x > 0 && !titleList.atXBeginning
111        height: parent.height * 0.7
112        color: Theme.highlightedTextColor
113        width: Math.ceil(Units.smallSpacing / 6)
114        opacity: 0.4
115    }
116
117    QQC2.StackView {
118        id: stack
119        anchors {
120            fill: parent
121            leftMargin: navButtons.width
122            rightMargin: __appWindow.contextDrawer && __appWindow.contextDrawer.handleVisible && __appWindow.contextDrawer.handle && __appWindow.contextDrawer.handle.y == 0 ? __appWindow.contextDrawer.handle.width : 0
123        }
124        initialItem: titleList
125
126        popEnter: Transition {
127            YAnimator {
128                from: -height
129                to: 0
130                duration: Units.longDuration
131                easing.type: Easing.OutCubic
132            }
133        }
134        popExit: Transition {
135            YAnimator {
136                from: 0
137                to: height
138                duration: Units.longDuration
139                easing.type: Easing.OutCubic
140            }
141        }
142
143        pushEnter: Transition {
144            YAnimator {
145                from: height
146                to: 0
147                duration: Units.longDuration
148                easing.type: Easing.OutCubic
149            }
150        }
151
152        pushExit: Transition {
153            YAnimator {
154                from: 0
155                to: -height
156                duration: Units.longDuration
157                easing.type: Easing.OutCubic
158            }
159        }
160
161        replaceEnter: Transition {
162            YAnimator {
163                from: height
164                to: 0
165                duration: Units.longDuration
166                easing.type: Easing.OutCubic
167            }
168        }
169
170        replaceExit: Transition {
171            YAnimator {
172                from: 0
173                to: -height
174                duration: Units.longDuration
175                easing.type: Easing.OutCubic
176            }
177        }
178    }
179    Separator {
180        id: separator
181        height: parent.height * 0.6
182        visible: navButtons.width > 0
183        anchors {
184            verticalCenter: parent.verticalCenter
185            left: navButtons.right
186        }
187    }
188    Separator {
189        height: parent.height * 0.6
190        visible: stack.anchors.rightMargin > 0
191        anchors {
192            verticalCenter: parent.verticalCenter
193            right: parent.right
194            rightMargin: stack.anchors.rightMargin
195        }
196    }
197    Repeater {
198        model: pageRow.layers.depth -1
199        delegate: Loader {
200            asynchronous: true
201            sourceComponent: header.pageDelegate
202            readonly property Page page: pageRow.layers.get(modelData+1)
203            readonly property bool current: true;
204            Component.onCompleted: stack.push(this)
205            Component.onDestruction: stack.pop()
206        }
207    }
208
209    Row {
210        id: navButtons
211        anchors {
212            left: parent.left
213            top: parent.top
214            bottom: parent.bottom
215            topMargin: Units.smallSpacing
216            bottomMargin: Units.smallSpacing
217        }
218        Item {
219            height: parent.height
220            width: (applicationWindow().header && applicationWindow().header.toString().indexOf("ToolBarApplicationHeader") === 0) && __appWindow.globalDrawer && __appWindow.globalDrawer.handleVisible && __appWindow.globalDrawer.handle && __appWindow.globalDrawer.handle.y === 0 ? __appWindow.globalDrawer.handle.width : 0
221        }
222    }
223
224    Flickable {
225        id: titleList
226        readonly property bool wideMode: pageRow.hasOwnProperty("wideMode") ? pageRow.wideMode : __appWindow.wideScreen
227        property int internalHeaderStyle: header.headerStyle == ApplicationHeaderStyle.Auto ? (titleList.wideMode ? ApplicationHeaderStyle.Titles : ApplicationHeaderStyle.Breadcrumb) : header.headerStyle
228        //if scrolling the titlebar should scroll also the pages and vice versa
229        property bool scrollingLocked: (header.headerStyle == ApplicationHeaderStyle.Titles || titleList.wideMode)
230        //uses this to have less strings comparisons
231        property bool scrollMutex
232        property bool isTabBar: header.headerStyle == ApplicationHeaderStyle.TabBar
233
234        property Item backButton
235        property Item forwardButton
236        clip: true
237
238
239        boundsBehavior: Flickable.StopAtBounds
240        readonly property alias model: mainRepeater.model
241        contentWidth: contentItem.width
242        contentHeight: height
243
244        readonly property int currentIndex: pageRow && pageRow.currentIndex !== undefined ? pageRow.currentIndex : 0
245        readonly property int count: mainRepeater.count
246
247        function gotoIndex(idx) {
248            //don't actually scroll in widescreen mode
249            if (titleList.wideMode || contentItem.children.length < 2) {
250                return;
251            }
252            listScrollAnim.running = false
253            var pos = titleList.contentX;
254            var destPos;
255            titleList.contentX = Math.max(((contentItem.children[idx] || {x: 0}).x + (contentItem.children[idx] || {width: 0}).width) - titleList.width, Math.min(titleList.contentX, (contentItem.children[idx] || {x: 0}).x));
256            destPos = titleList.contentX;
257            listScrollAnim.from = pos;
258            listScrollAnim.to = destPos;
259            listScrollAnim.running = true;
260        }
261
262        NumberAnimation {
263            id: listScrollAnim
264            target: titleList
265            property: "contentX"
266            duration: Units.longDuration
267            easing.type: Easing.InOutQuad
268        }
269        Timer {
270            id: contentXSyncTimer
271            interval: 0
272            onTriggered: {
273                titleList.contentX = pageRow.contentItem.contentX - pageRow.contentItem.originX + titleList.originX;
274            }
275        }
276        onCountChanged: contentXSyncTimer.restart();
277        onCurrentIndexChanged: gotoIndex(currentIndex);
278        onModelChanged: gotoIndex(currentIndex);
279        onContentWidthChanged: gotoIndex(currentIndex);
280
281        onContentXChanged: {
282            if (movingHorizontally && !titleList.scrollMutex && titleList.scrollingLocked && !pageRow.contentItem.moving) {
283                titleList.scrollMutex = true;
284                pageRow.contentItem.contentX = titleList.contentX - titleList.originX + pageRow.contentItem.originX;
285                titleList.scrollMutex = false;
286            }
287        }
288        onHeightChanged: {
289            titleList.returnToBounds()
290        }
291        onMovementEnded: {
292            if (titleList.scrollingLocked) {
293                //this will trigger snap as well
294                pageRow.contentItem.flick(0,0);
295            }
296        }
297        onFlickEnded: movementEnded();
298
299        NumberAnimation {
300            id: scrollTopAnimation
301            target: pageRow.currentItem && pageRow.currentItem.flickable ? pageRow.currentItem.flickable : null
302            property: "contentY"
303            to: 0
304            duration: Units.longDuration
305            easing.type: Easing.InOutQuad
306        }
307
308        Row {
309            id: contentItem
310            spacing: 0
311            Repeater {
312                id: mainRepeater
313                model: pageRow.depth
314                delegate: MouseArea {
315                    id: delegate
316                    readonly property int currentIndex: index
317                    readonly property var currentModelData: modelData
318                    clip: true
319
320                    width: {
321                        //more columns shown?
322                        if (titleList.scrollingLocked && delegateLoader.page) {
323                            return delegateLoader.page.width - (index == 0 ? navButtons.width : 0) - (index == pageRow.depth-1  ? stack.anchors.rightMargin : 0);
324                        } else {
325                            return Math.min(titleList.width, delegateLoader.implicitWidth + Units.smallSpacing);
326                        }
327                    }
328
329                    height: titleList.height
330                    onClicked: {
331                        if (pageRow.currentIndex === modelData) {
332                            //scroll up if current otherwise make current
333                            if (!pageRow.currentItem.flickable) {
334                                return;
335                            }
336                            if (pageRow.currentItem.flickable.contentY > -__appWindow.header.height) {
337                                scrollTopAnimation.to = -pageRow.currentItem.flickable.topMargin;
338                                scrollTopAnimation.running = true;
339                            }
340
341                        } else {
342                            pageRow.currentIndex = modelData;
343                        }
344                    }
345
346                    Loader {
347                        id: delegateLoader
348                        height: parent.height
349                        x: titleList.wideMode || headerStyle == ApplicationHeaderStyle.Titles ? (Math.min(delegate.width - implicitWidth, Math.max(0, titleList.contentX - delegate.x))) : 0
350                        width: parent.width - x
351
352                        Connections {
353                            target: delegateLoader.page.Component
354                            function onDestruction() { delegateLoader.sourceComponent = null }
355                        }
356
357                        sourceComponent: header.pageDelegate
358
359                        readonly property Page page: pageRow.get(modelData)
360                        //NOTE: why not use ListViewCurrentIndex? because listview itself resets
361                        //currentIndex in some situations (since here we are using an int as a model,
362                        //even more often) so the property binding gets broken
363                        readonly property bool current: pageRow.currentIndex === index
364                        readonly property int index: parent.currentIndex
365                        readonly property var modelData: parent.currentModelData
366                    }
367                }
368            }
369        }
370        Connections {
371            target: titleList.scrollingLocked ? pageRow.contentItem : null
372            function onContentXChanged() {
373                if (!titleList.dragging && !titleList.movingHorizontally && !titleList.scrollMutex) {
374                    titleList.contentX = pageRow.contentItem.contentX - pageRow.contentItem.originX + titleList.originX;
375                }
376            }
377        }
378    }
379}
380