1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
|
/*
* Copyright 2012 Canonical Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.0
import Ubuntu.Components 0.1
Item {
id: tabBarStyle
// used to detect when the user is interacting with the tab bar by pressing it
// or dragging the tab bar buttons.
readonly property bool pressed: mouseArea.interacting
// styling properties, public API
property color headerTextColor: Theme.palette.normal.backgroundText
property color headerTextSelectedColor: Theme.palette.selected.backgroundText
// Don't start transitions because of updates to selectionMode before styledItem is completed.
// This fixes bug #1246792: "Disable tabs scrolling animations at startup"
property bool animate: false
Binding {
target: tabBarStyle
property: "animate"
when: styledItem.width > 0
value: styledItem.animate
}
property int headerTextFadeDuration: animate ? 350 : 0
property url indicatorImageSource: "artwork/chevron.png"
property string headerFontSize: "x-large"
property int headerTextStyle: Text.Normal
property color headerTextStyleColor: Theme.palette.normal.backgroundText
property int headerFontWeight: Font.Light
property real headerTextLeftMargin: units.gu(2)
property real headerTextRightMargin: units.gu(2)
property real headerTextBottomMargin: units.gu(2)
property real buttonPositioningVelocity: animate ? 1.0 : -1
// The time of inactivity before leaving selection mode automatically
property int deactivateTime: 5000
/*
The function assures the visuals stay on the selected tab. This can be called
by the stack components holding the tabs (i.e. Tabs, ListView, etc) and only
when the changes happen on the list element values, which is not reported
automaytically through ListModel changes.
*/
function sync() {
buttonView.selectButton(styledItem.selectedIndex);
}
property var tabsModel : styledItem ? styledItem.model : null
Connections {
target: styledItem
onSelectionModeChanged: {
if (!styledItem.selectionMode) {
buttonView.selectButton(styledItem.selectedIndex);
}
}
}
Connections {
target: styledItem
onSelectedIndexChanged: buttonView.selectButton(styledItem.selectedIndex)
}
/*
Prevent events that are not accepted by tab buttons or mouseArea below
from passing through the TabBar.
*/
MouseArea {
anchors.fill: parent
onReleased: {
mouseArea.enteringSelectionMode = false;
}
}
Component {
id: tabButtonRow
Row {
id: theRow
anchors {
top: parent.top
bottom: parent.bottom
}
width: childrenRect.width
property int rowNumber: modelData
Component.onCompleted: {
if (rowNumber === 0) {
buttonView.buttonRow1 = theRow;
} else {
buttonView.buttonRow2 = theRow;
}
}
Repeater {
id: repeater
model: tabsModel
AbstractButton {
id: button
anchors {
top: parent.top
bottom: parent.bottom
}
width: text.paintedWidth + text.anchors.leftMargin + text.anchors.rightMargin
// When the tab bar is in selection mode, show both buttons corresponing to
// the tab index as selected, but when it is not in selection mode only one
// to avoid seeing fading animations of the unselected button when switching
// tabs from outside the tab bar.
property bool selected: (styledItem.selectionMode && buttonView.needsScrolling) ?
styledItem.selectedIndex === index :
buttonView.selectedButtonIndex === button.buttonIndex
property real offset: theRow.rowNumber + 1 - button.x / theRow.width;
property int buttonIndex: index + theRow.rowNumber*repeater.count
// Use opacity 0 to hide instead of setting visibility to false in order to
// make fading work well, and not to mess up width/offset computations
opacity: isVisible() ? 1.0 : 0.0
function isVisible() {
if (selected) return true;
if (!styledItem.selectionMode) return false;
if (buttonView.needsScrolling) return true;
// When we don't need scrolling, we want to avoid showing a button that is fading
// while sliding in from the right side when a new button was selected
var numTabs = tabsModel.count;
var minimum = buttonView.selectedButtonIndex;
var maximum = buttonView.selectedButtonIndex + numTabs - 1;
if (MathUtils.clamp(buttonIndex, minimum, maximum) === buttonIndex) return true;
// working modulus numTabs:
if (buttonIndex < buttonView.selectedButtonIndex - numTabs) return true;
return false;
}
Behavior on opacity {
NumberAnimation {
duration: headerTextFadeDuration
easing.type: Easing.InOutQuad
}
}
Image {
id: indicatorImage
source: indicatorImageSource
anchors {
bottom: parent.bottom
bottomMargin: headerTextBottomMargin
}
x: button.width - width
// FIXME: temporary hack for the chevron's height to match the font size
height: 0.82*sourceSize.height
// The indicator image must be visible after the selected tab button, when the
// tab bar is not in selection mode, or after the "last" button (starting with
// the selected one), when the tab bar is in selection mode.
property bool isLastAfterSelected: index === (styledItem.selectedIndex === 0 ?
repeater.count-1 :
styledItem.selectedIndex - 1)
opacity: (styledItem.selectionMode ? isLastAfterSelected : selected) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: headerTextFadeDuration
easing.type: Easing.InOutQuad
}
}
}
Label {
id: text
color: selected ? headerTextSelectedColor : headerTextColor
Behavior on color {
ColorAnimation {
duration: headerTextFadeDuration
easing.type: Easing.InOutQuad
}
}
anchors {
left: parent.left
leftMargin: headerTextLeftMargin
rightMargin: headerTextRightMargin
baseline: parent.bottom
baselineOffset: -headerTextBottomMargin
}
text: (model.hasOwnProperty("tab") && tab.hasOwnProperty("title")) ? tab.title : title
fontSize: headerFontSize
font.weight: headerFontWeight
style: headerTextStyle
styleColor: headerTextStyleColor
}
onClicked: {
if (mouseArea.enteringSelectionMode) {
mouseArea.enteringSelectionMode = false;
} else if (opacity > 0.0) {
styledItem.selectedIndex = index;
if (!styledItem.alwaysSelectionMode) {
styledItem.selectionMode = false;
}
button.select();
}
}
onPressedChanged: {
// Catch release after a press with a delay that is too
// long to make it a click, but don't unset interacting when
// the user starts dragging. In that case it will be unset in
// buttonView.onDragEnded.
if (!pressed && !buttonView.dragging) {
// unset interacting which was set in mouseArea.onPressed
mouseArea.interacting = false;
}
}
// Select this button
function select() {
buttonView.selectedButtonIndex = button.buttonIndex;
buttonView.updateOffset(button.offset);
}
}
}
}
}
/*!
Used by autopilot tests to determine when an animation finishes moving.
\internal
*/
readonly property alias animating: offsetAnimation.running
PathView {
id: buttonView
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
width: needsScrolling ? parent.width : buttonRowWidth
// set to the width of one tabButtonRow in Component.onCompleted.
property real buttonRowWidth: buttonRow1 ? buttonRow1.width : 0
// set by the delegate when the components are completed.
property Row buttonRow1
property Row buttonRow2
// Track which button was last clicked
property int selectedButtonIndex: -1
delegate: tabButtonRow
model: 2 // The second buttonRow shows the buttons that disappear on the left
property bool needsScrolling: buttonRowWidth > parent.width
interactive: needsScrolling
clip: needsScrolling
highlightRangeMode: PathView.NoHighlightRange
offset: 0
path: Path {
startX: -buttonView.buttonRowWidth/2
PathLine {
x: buttonView.buttonRowWidth*1.5
}
}
// x - y (mod a), for (x - y) <= a
function cyclicDistance(x, y, a) {
var r = x - y;
return Math.min(Math.abs(r), Math.abs(r - a));
}
// Select the closest of the two buttons that represent the given tab index
function selectButton(tabIndex) {
if (!tabsModel || tabIndex < 0 || tabIndex >= tabsModel.count) return;
if (buttonView.buttonRow1 && buttonView.buttonRow2) {
var b1 = buttonView.buttonRow1.children[tabIndex];
var b2 = buttonView.buttonRow2.children[tabIndex];
// find the button with the nearest offset
var d1 = cyclicDistance(b1.offset, buttonView.offset, 2);
var d2 = cyclicDistance(b2.offset, buttonView.offset, 2);
if (d1 < d2) {
b1.select();
} else {
b2.select();
}
}
}
function updateOffset(newOffset) {
if (!newOffset) return; // do not update the offset when its value is NaN
if (offset - newOffset < -1) newOffset = newOffset - 2;
offset = newOffset;
}
Behavior on offset {
SmoothedAnimation {
id: offsetAnimation
velocity: buttonPositioningVelocity
easing.type: Easing.InOutQuad
}
}
onDragEnded: {
// unset interacting which was set in mouseArea.onPressed
mouseArea.interacting = false;
mouseArea.enteringSelectionMode = false;
}
Timer {
id: idleTimer
interval: tabBarStyle.deactivateTime
running: styledItem.selectionMode && !styledItem.alwaysSelectionMode
onTriggered: styledItem.selectionMode = false
function conditionalRestartOrStop() {
if (Qt.application.active &&
styledItem.selectionMode &&
!styledItem.alwaysSelectionMode &&
!mouseArea.interacting) {
idleTimer.restart();
} else {
idleTimer.stop();
}
}
}
// disable the timer when the application is not active and reset
// it when the application is resumed.
Connections {
target: Qt.application
onActiveChanged: idleTimer.conditionalRestartOrStop()
}
Connections {
target: styledItem
onSelectionModeChanged: idleTimer.conditionalRestartOrStop()
}
}
MouseArea {
// a tabBar not in selection mode can be put in selection mode by pressing
id: mouseArea
anchors.fill: parent
// set in onPressed, and unset in button.onPressedChanged or buttonView.onDragEnded
// because after not accepting the mouse, the released event will go to
// the buttonView or individual buttons.
property bool interacting: false
onInteractingChanged: idleTimer.conditionalRestartOrStop()
// When pressing to enter selection mode, a release should not be interpreted
// as a click on a button to select a new tab.
property bool enteringSelectionMode: false
// This MouseArea is always enabled, even when the tab bar is in selection mode,
// so that press events are detected and tabBarStyle.pressed is updated.
onPressed: {
mouseArea.interacting = true;
if (!styledItem.selectionMode) {
mouseArea.enteringSelectionMode = true;
}
styledItem.selectionMode = true;
mouse.accepted = false;
}
}
Component.onCompleted: {
tabBarStyle.sync();
}
}
|