~zsombi/ubuntu-ui-toolkit/popover-dismiss-buttons

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();
    }
}