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