timeline.qml 66.8 KB
Newer Older
1 2 3
import QtQuick 2.11
import QtQml.Models 2.11
import QtQuick.Controls 2.4
4
import Kdenlive.Controls 1.0
5 6 7 8 9 10 11
import 'Timeline.js' as Logic

Rectangle {
    id: root
    objectName: "timelineview"
    SystemPalette { id: activePalette }
    color: activePalette.window
12
    property bool validMenu: false
13
    property color textColor: activePalette.text
14
    property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active
15

16
    signal clipClicked()
17
    signal mousePosChanged(int position)
18
    signal showClipMenu()
19
    signal showCompositionMenu()
20
    signal showTimelineMenu()
21 22
    signal showRulerMenu()
    signal showHeaderMenu()
23 24
    signal zoomIn(bool onMouse)
    signal zoomOut(bool onMouse)
25
    signal processingDrag(bool dragging)
26

27 28
    FontMetrics {
        id: fontMetrics
29
        font: smallFont
30
    }
31

32 33 34 35
    onDragInProgressChanged: {
        processingDrag(!root.dragInProgress)
    }

36 37 38 39 40
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
41
        return scrollView.contentX
42 43 44
    }

    function goToStart(pos) {
45
        scrollView.contentX = pos
46 47
    }

48 49 50 51
    function checkDeletion(itemId) {
        if (dragProxy.draggedItem == itemId) {
            endDrag()
        }
52 53 54
        if (itemId == mainItemId) {
            mainItemId = -1
        }
55 56
    }

57 58
    function updatePalette() {
        root.color = activePalette.window
59
        root.textColor = activePalette.text
60 61 62
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
    }
63

64
    function moveSelectedTrack(offset) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
65 66
        var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
        var newTrack = cTrack + offset
67 68 69 70 71 72 73
        var max = tracksRepeater.count;
        if (newTrack < 0) {
            newTrack = max - 1;
        } else if (newTrack >= max) {
            newTrack = 0;
        }
        console.log('Setting curr tk: ', newTrack, 'MAX: ',max)
74
        timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
75 76
    }

77
    function zoomByWheel(wheel) {
78
        if (wheel.modifiers & Qt.AltModifier) {
79
            // Seek to next snap
80 81 82 83 84 85
            if (wheel.angleDelta.x > 0) {
                timeline.triggerAction('monitor_seek_snap_backward')
            } else {
                timeline.triggerAction('monitor_seek_snap_forward')
            }
        } else if (wheel.modifiers & Qt.ControlModifier) {
86
            root.wheelAccumulatedDelta += wheel.angleDelta.y;
87
            // Zoom
88
            if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) {
89
                root.zoomIn(true);
90 91
                root.wheelAccumulatedDelta = 0;
            } else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) {
92
                root.zoomOut(true);
93
                root.wheelAccumulatedDelta = 0;
94
            }
95 96
        } else if (wheel.modifiers & Qt.ShiftModifier) {
            // Vertical scroll
97 98
            var newScroll = Math.min(scrollView.contentY - wheel.angleDelta.y, trackHeaders.height - tracksArea.height + scrollView.ScrollBar.horizontal.height + ruler.height)
            scrollView.contentY = Math.max(newScroll, 0)
99
        } else {
100
            // Horizontal scroll
101 102
            var newScroll = Math.min(scrollView.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.ScrollBar.vertical.width))
            scrollView.contentX = Math.max(newScroll, 0)
103
        }
104
        wheel.accepted = true
105 106
    }

107 108
    function continuousScrolling(x) {
        // This provides continuous scrolling at the left/right edges.
109
        if (x > scrollView.contentX + scrollView.width - 50) {
110 111 112 113
            scrollTimer.item = clip
            scrollTimer.backwards = false
            scrollTimer.start()
        } else if (x < 50) {
114
            scrollView.contentX = 0;
115
            scrollTimer.stop()
116
        } else if (x < scrollView.contentX + 50) {
117 118 119 120 121 122 123
            scrollTimer.item = clip
            scrollTimer.backwards = true
            scrollTimer.start()
        } else {
            scrollTimer.stop()
        }
    }
124 125 126
    function getTrackYFromId(a_track) {
        return Logic.getTrackYFromId(a_track)
    }
127

128 129 130 131
    function getTrackYFromMltIndex(a_track) {
        return Logic.getTrackYFromMltIndex(a_track)
    }

132 133 134 135
    function getTracksCount() {
        return Logic.getTracksList()
    }

136
    function getMousePos() {
137 138 139 140 141
        if (tracksArea.containsMouse) {
            return (scrollView.contentX + tracksArea.mouseX) / timeline.scaleFactor
        } else {
            return -1;
        }
142 143
    }

144
    function getScrollPos() {
145
        return scrollView.contentX
146 147 148
    }

    function setScrollPos(pos) {
149
        return scrollView.contentX = pos
150 151
    }

152 153 154 155
    function getCopiedItemId() {
        return copiedClip
    }

156
    function getMouseTrack() {
157
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.contentY)
158 159
    }

160 161 162
    function getTrackColor(audio, header) {
        var col = activePalette.alternateBase
        if (audio) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
163
            col = Qt.tint(col, "#06FF00CC")
164 165
        }
        if (header) {
166
            col = Qt.darker(col, 1.05)
167
        }
168
        return col
169 170
    }

171 172 173 174 175
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
176
        scrollTimer.stop()
177
    }
178

179
    function isDragging() {
180
        return dragInProgress
181
    }
182

183
    function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
184
        dragProxy.x = itemObject.modelStart * timeScale
185
        dragProxy.y = itemCoord.y
186
        dragProxy.width = itemObject.clipDuration * timeScale
187 188 189 190 191 192
        dragProxy.height = itemCoord.height
        dragProxy.masterObject = itemObject
        dragProxy.draggedItem = itemId
        dragProxy.sourceTrack = itemTrack
        dragProxy.sourceFrame = itemPos
        dragProxy.isComposition = isComposition
193
        dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
194 195 196 197 198 199 200
    }
    function endDrag() {
        dragProxy.draggedItem = -1
        dragProxy.x = 0
        dragProxy.y = 0
        dragProxy.width = 0
        dragProxy.height = 0
201
        dragProxy.verticalOffset = 0
202 203
    }

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    function getItemAtPos(tk, posx, isComposition) {
        var track = Logic.getTrackById(tk)
        var container = track.children[0]
        var tentativeClip = undefined
        //console.log('TESTING ITMES OK TK: ', tk, ', POS: ', posx, ', CHILREN: ', container.children.length, ', COMPO: ', isComposition)
        for (var i = 0 ; i < container.children.length; i++) {
            if (container.children[i].children.length == 0 || container.children[i].children[0].children.length == 0) {
                continue
            }
            tentativeClip = container.children[i].children[0].childAt(posx, 1)
            if (tentativeClip && tentativeClip.clipId && (tentativeClip.isComposition == isComposition)) {
                //console.log('found item with id: ', tentativeClip.clipId, ' IS COMPO: ', tentativeClip.isComposition)
                break
            }
        }
        return tentativeClip
    }
221 222 223 224 225 226
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
227

228
    property int headerWidth: timeline.headerWidth()
229
    property int activeTool: 0
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
230
    property real baseUnit: fontMetrics.font.pixelSize
231
    property real fontUnit: fontMetrics.font.pointSize * 0.9
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
232 233
    property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
    property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3)
234
    property bool autoScrolling: timeline.autoScroll
235
    property int duration: timeline.duration
236 237
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
238
    property color titleColor: timeline.titleColor
Sashmita Raghav's avatar
Sashmita Raghav committed
239
    property color imageColor: timeline.imageColor
240
    property color slideshowColor: timeline.slideshowColor
241
    property color lockedColor: timeline.lockedColor
Vincent Pinon's avatar
Vincent Pinon committed
242
    property color selectionColor: timeline.selectionColor
243
    property color groupColor: timeline.groupColor
244
    property int mainItemId: -1
245
    property int mainFrame: 0
Nicolas Carion's avatar
Nicolas Carion committed
246
    property int clipBeingDroppedId: -1
247
    property string clipBeingDroppedData
248 249
    property int droppedPosition: -1
    property int droppedTrack: -1
250
    property int clipBeingMovedId: -1
251
    property int consumerPosition: proxy.position
252
    property int spacerGroup: -1
253 254
    property int spacerFrame: -1
    property int spacerClickFrame: -1
255
    property real timeScale: timeline.scaleFactor
256
    property real snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? baseUnit / (timeScale > 1 ? Math.sqrt(timeScale) : timeScale) : -1
257
    property var timelineSelection: timeline.selection
258
    property int trackHeight
259
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
260
    property int zoomOnMouse: -1
261
    property int viewActiveTrack: timeline.activeTrack
262 263
    property int wheelAccumulatedDelta: 0
    readonly property int defaultDeltasPerStep: 120
264
    property bool seekingFinished : proxy.seekFinished
265
    property int scrollMin: scrollView.contentX / timeline.scaleFactor
266
    property int scrollMax: scrollMin + scrollView.contentItem.width / timeline.scaleFactor
267
    property double dar: 16/9
268
    property int collapsedHeight: baseUnit * 1.8
269 270 271 272

    onSeekingFinishedChanged : {
        playhead.opacity = seekingFinished ? 1 : 0.5
    }
273

274
    //onCurrentTrackChanged: timeline.selection = []
275
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
276
        if (root.zoomOnMouse >= 0) {
277
            scrollView.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
278 279
            root.zoomOnMouse = -1
        } else {
280
            scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
281
        }
282
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
283
        ruler.adjustStepSize()
284 285 286 287
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
288
        console.log('GOT SCALE: ', timeScale, ' - SNAPPING: ', snapping)
289
    }
290

291 292 293 294
    onConsumerPositionChanged: {
        if (autoScrolling) Logic.scrollIfNeeded()
    }

295 296
    onViewActiveTrackChanged: {
        var tk = Logic.getTrackById(timeline.activeTrack)
297 298 299
        if (tk.y < scrollView.contentY) {
            scrollView.contentY = Math.max(0, tk.y - scrollView.height / 3)
        } else if (tk.y + tk.height > scrollView.contentY + scrollView.contentItem.height) {
300 301 302 303
            var newY = Math.min(trackHeaders.height - scrollView.height + scrollView.ScrollBar.horizontal.height, tk.y - scrollView.height / 3)
            if (newY >= 0) {
                scrollView.contentY = newY
            }
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
    onActiveToolChanged: {
        if (root.activeTool == 2) {
            // Spacer activated
            endDrag()
        } else if (root.activeTool == 0) {
            var tk = getMouseTrack()
            if (tk < 0) {
                console.log('........ MOUSE OUTSIDE TRAKS\n\n.........')
                return
            }
            var pos = getMousePos() * timeline.scaleFactor
            var sourceTrack = Logic.getTrackById(tk)
            var allowComposition = tracksArea.mouseY- sourceTrack.y > sourceTrack.height / 2
            var tentativeItem = undefined
            if (allowComposition) {
                tentativeItem = getItemAtPos(tk, pos, true)
            }
            if (!tentativeItem) {
                tentativeItem = getItemAtPos(tk, pos, false)
            }
            if (tentativeItem) {
                tentativeItem.updateDrag()
            }
        }
    }

333 334 335 336 337 338 339
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
340
            console.log("Trying to drop composition")
341
            if (clipBeingMovedId == -1) {
342
                console.log("No clip being moved")
343 344
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY)
                var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
345
                droppedPosition = frame
346
                if (track >= 0 && !controller.isAudioTrack(track)) {
347
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
348
                    console.log("Trying to insert",track, frame, clipBeingDroppedData)
349
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
350
                    console.log("id",clipBeingDroppedId)
351
                    continuousScrolling(drag.x + scrollView.contentX)
352 353 354 355 356 357
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
358 359
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
360
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY)
361
                if (track !=-1) {
362
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
363
                    if (clipBeingDroppedId >= 0){
364 365 366 367
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
368
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, Math.floor(root.snapping))
369
                        continuousScrolling(drag.x + scrollView.contentX)
370
                    } else if (!controller.isAudioTrack(track)) {
371
                        frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
372 373
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
374
                        continuousScrolling(drag.x + scrollView.contentX)
375
                    }
376 377 378
                }
            }
        }
379 380
        onExited:{
            if (clipBeingDroppedId != -1) {
381
                controller.requestItemDeletion(clipBeingDroppedId, false)
382
            }
383
            clearDropData()
384
        }
385 386 387 388 389
        onDropped: {
            if (clipBeingDroppedId != -1) {
                var frame = controller.getCompositionPosition(clipBeingDroppedId)
                var track = controller.getCompositionTrackId(clipBeingDroppedId)
                // we simulate insertion at the final position so that stored undo has correct value
390
                controller.requestItemDeletion(clipBeingDroppedId, false)
391
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
392
            }
393
            clearDropData()
394
        }
395 396
    }
    DropArea { //Drop area for bin/clips
397 398 399 400 401 402 403 404 405
        /** @brief local helper function to handle the insertion of multiple dragged items */
        function insertAndMaybeGroup(track, frame, droppedData) {
            var binIds = droppedData.split(";")
            if (binIds.length == 0) {
                return -1
            }

            var id = -1
            if (binIds.length == 1) {
406
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
407
            } else {
408
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
409 410 411

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
412
                    timeline.selectItems(ids)
413 414 415 416 417 418
                    id = ids[0]
                }
            }
            return id
        }

419 420
        property int fakeFrame: -1
        property int fakeTrack: -1
421 422 423 424
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
425
        keys: 'kdenlive/producerslist'
426
        onEntered: {
427
            if (clipBeingMovedId == -1) {
428
                //var track = Logic.getTrackIdFromPos(drag.y)
429
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY)
430
                if (track >= 0  && track < tracksRepeater.count) {
431
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
432
                    droppedPosition = frame
433
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
434
                    //drag.acceptProposedAction()
435
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
436
                    console.log('dropped data: ', clipBeingDroppedData)
437 438 439 440 441
                    if (controller.normalEdit()) {
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
                    } else {
                        // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
442
                        if (clipBeingDroppedId > -1) {
443
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
444 445 446 447
                            fakeTrack = timeline.activeTrack
                        } else {
                            drag.accepted = false
                        }
448
                    }
449
                    continuousScrolling(drag.x + scrollView.contentX)
450 451 452 453
                } else {
                    drag.accepted = false
                }
            }
454
        }
Nicolas Carion's avatar
Nicolas Carion committed
455 456
        onExited:{
            if (clipBeingDroppedId != -1) {
457
                controller.requestItemDeletion(clipBeingDroppedId, false)
Nicolas Carion's avatar
Nicolas Carion committed
458
            }
459
            clearDropData()
Nicolas Carion's avatar
Nicolas Carion committed
460
        }
461
        onPositionChanged: {
462
            if (clipBeingMovedId == -1) {
463
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY)
464
                if (track >= 0  && track < tracksRepeater.count) {
465
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
466
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
467
                    if (clipBeingDroppedId >= 0) {
468
                        fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
469 470
                        fakeTrack = timeline.activeTrack
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
471
                        continuousScrolling(drag.x + scrollView.contentX)
472
                    } else {
473 474 475 476 477 478
                        frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
                        if (controller.normalEdit()) {
                            clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
                            clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
479
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
480 481
                            fakeTrack = timeline.activeTrack
                        }
482
                        continuousScrolling(drag.x + scrollView.contentX)
483
                    }
484
                }
Nicolas Carion's avatar
Nicolas Carion committed
485
            }
486 487
        }
        onDropped: {
488 489 490
            if (clipBeingDroppedId != -1) {
                var frame = controller.getClipPosition(clipBeingDroppedId)
                var track = controller.getClipTrackId(clipBeingDroppedId)
491 492 493 494
                if (!controller.normalEdit()) {
                    frame = fakeFrame
                    track = fakeTrack
                }
495 496 497 498
                /* We simulate insertion at the final position so that stored undo has correct value
                 * NOTE: even if dropping multiple clips, requesting the deletion of the first one is
                 * enough as internally it will request the group deletion
                 */
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
499
                controller.requestItemDeletion(clipBeingDroppedId, false)
500 501 502

                var binIds = clipBeingDroppedData.split(";")
                if (binIds.length == 1) {
503 504 505 506 507
                    if (controller.normalEdit()) {
                        timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
                    } else {
                        timeline.insertClipZone(clipBeingDroppedData, track, frame)
                    }
508
                } else {
509 510 511 512 513 514
                    if (controller.normalEdit()) {
                        timeline.insertClips(track, frame, binIds, true, true)
                    } else {
                        // TODO
                        console.log('multiple clips insert/overwrite not supported yet')
                    }
515
                }
516 517
                fakeTrack = -1
                fakeFrame = -1
518
            }
519
            clearDropData()
520 521
        }
    }
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
    DropArea { //Drop area for urls (direct drop from file manager)
        /** @brief local helper function to handle the insertion of multiple dragged items */
        property int fakeFrame: -1
        property int fakeTrack: -1
        property var droppedUrls: []
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'text/uri-list'
        onEntered: {
            drag.accepted = true
            droppedUrls.length = 0
            for(var i in drag.urls){
                var url = drag.urls[i]
                droppedUrls.push(Qt.resolvedUrl(url))
            }
        }
        onExited:{
            if (clipBeingDroppedId != -1) {
                controller.requestItemDeletion(clipBeingDroppedId, false)
            }
            clearDropData()
        }
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
548
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY)
549 550
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
551
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
552 553 554 555
                    if (clipBeingDroppedId >= 0) {
                        //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
                        fakeTrack = timeline.activeTrack
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
556
                        continuousScrolling(drag.x + scrollView.contentX)
557 558 559 560 561 562 563 564 565 566
                    } else {
                        frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
                        if (controller.normalEdit()) {
                            //clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
                            //clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
                            //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
                            fakeTrack = timeline.activeTrack
                        }
567
                        continuousScrolling(drag.x + scrollView.contentX)
568 569 570 571 572
                    }
                }
            }
        }
        onDropped: {
573
            var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
            var track = timeline.activeTrack
            //var binIds = clipBeingDroppedData.split(";")
            //if (binIds.length == 1) {
                if (controller.normalEdit()) {
                    timeline.urlDropped(droppedUrls, frame, track)
                } else {
                    //timeline.insertClipZone(clipBeingDroppedData, track, frame)
                }
            /*} else {
                if (controller.normalEdit()) {
                    timeline.insertClips(track, frame, binIds, true, true)
                } else {
                    // TODO
                    console.log('multiple clips insert/overwrite not supported yet')
                }
            }*/
            clearDropData()
        }
    }
593

594 595
    Row {
        Column {
596
            id: headerContainer
597
            width: headerWidth
598
            z: 1
599
            Item {
600
                // Padding between toolbar and track headers.
601
                width: parent.width
602
                height: ruler.height
603
                Button {
604
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : i18nc("Initial for Master", "M")
605 606 607
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
608 609 610 611
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
612 613 614 615
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
616 617 618 619
                    onClicked: {
                        timeline.showMasterEffects()
                    }
                }
620 621 622
            }
            Flickable {
                // Non-slider scroll area for the track headers.
623
                id: headerFlick
624
                contentY: scrollView.contentY
625 626 627
                width: parent.width
                y: ruler.height
                height: root.height - ruler.height
628
                interactive: false
629
                clip: true
630

631 632 633 634 635
                MouseArea {
                    width: trackHeaders.width
                    height: trackHeaders.height
                    acceptedButtons: Qt.NoButton
                    onWheel: {
636 637
                        var newScroll = Math.min(scrollView.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.ScrollBar.horizontal.height + ruler.height)
                        scrollView.contentY = Math.max(newScroll, 0)
638 639
                    }
                }
640 641
                Column {
                    id: trackHeaders
642
                    spacing: 0
643 644 645 646 647
                    Repeater {
                        id: trackHeaderRepeater
                        model: multitrack
                        TrackHead {
                            trackName: model.name
648
                            thumbsFormat: model.thumbsFormat
649
                            trackTag: model.trackTag
650
                            isDisabled: model.disabled
651 652
                            isComposite: model.composite
                            isLocked: model.locked
653
                            isActive: model.trackActive
654
                            isAudio: model.audio
655
                            showAudioRecord: model.audioRecord
656 657
                            effectNames: model.effectNames
                            isStackEnabled: model.isStackEnabled
658
                            width: headerWidth
659
                            current: item === timeline.activeTrack
660
                            trackId: item
661
                            height: Math.max(collapsedHeight, model.trackHeight)
662
                            onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
663
                            collapsed: height <= collapsedHeight
664
                            onMyTrackHeightChanged: {
665
                                collapsed = myTrackHeight <= collapsedHeight
666 667
                                if (!collapsed) {
                                    controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
668 669 670
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
                                } else {
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
671
                                }
672 673
                                // hack: change property to trigger transition adjustment
                                root.trackHeight = root.trackHeight === 1 ? 0 : 1
674
                            }
675 676 677 678 679 680
                        }
                    }
                }
                Column {
                    id: trackHeadersResizer
                    spacing: 0
681
                    width: root.baseUnit / 2
682 683 684
                    Rectangle {
                        id: resizer
                        height: trackHeaders.height
685
                        width: root.baseUnit / 2
686 687 688 689 690 691 692 693 694 695 696 697 698
                        x: root.headerWidth - 2
                        color: 'red'
                        opacity: 0
                        Drag.active: headerMouseArea.drag.active
                        Drag.proposedAction: Qt.MoveAction

                        MouseArea {
                            id: headerMouseArea
                            anchors.fill: parent
                            hoverEnabled: true
                            cursorShape: Qt.SizeHorCursor
                            drag.target: parent
                            drag.axis: Drag.XAxis
699
                            drag.minimumX: 2 * baseUnit
700 701 702 703 704
                            property double startX
                            property double originalX
                            drag.smoothed: false

                            onPressed: {
705
                                root.autoScrolling = false
706 707
                            }
                            onReleased: {
708
                                root.autoScrolling = timeline.autoScroll
709 710 711 712 713 714 715 716 717 718 719 720
                                parent.opacity = 0
                            }
                            onEntered: parent.opacity = 0.5
                            onExited: parent.opacity = 0
                            onPositionChanged: {
                                if (mouse.buttons === Qt.LeftButton) {
                                    parent.opacity = 0.5
                                    headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
                                    timeline.setHeaderWidth(headerWidth)
                                }
                            }
                        }
721 722 723 724 725 726
                    }
                }
            }
        }
        MouseArea {
            id: tracksArea
727 728
            property real clickX
            property real clickY
729
            width: root.width - root.headerWidth
730
            height: root.height
731
            x: root.headerWidth
732
            property bool shiftPress: false
733 734
            // This provides continuous scrubbing and scimming at the left/right edges.
            hoverEnabled: true
735 736
            preventStealing: true
            acceptedButtons: Qt.AllButtons
737
            cursorShape: root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
738
            onWheel: {
739 740 741 742 743 744 745 746 747
                if (wheel.modifiers & Qt.AltModifier) {
                    // Alt + wheel = seek to next snap point
                    if (wheel.angleDelta.x > 0) {
                        timeline.triggerAction('monitor_seek_snap_backward')
                    } else {
                        timeline.triggerAction('monitor_seek_snap_forward')
                    }
                } else {
                    var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
748
                    proxy.position = wheel.angleDelta.y > 0 ? Math.max(root.consumerPosition - delta, 0) : Math.min(root.consumerPosition + delta, timeline.fullDuration - 1)
749
                }
750
            }
751
            onPressed: {
752
                focus = true
753 754
                shiftPress = (mouse.modifiers & Qt.ShiftModifier) && (mouse.y > ruler.height)
                if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && (mouse.modifiers & Qt.ControlModifier) && !shiftPress)) {
755 756 757 758
                    clickX = mouseX
                    clickY = mouseY
                    return
                }
759
                if (root.activeTool === 0 && shiftPress && mouse.y > ruler.height) {
760 761 762 763 764 765 766 767
                        // rubber selection
                        rubberSelect.x = mouse.x + tracksArea.x
                        rubberSelect.y = mouse.y
                        rubberSelect.originX = mouse.x
                        rubberSelect.originY = rubberSelect.y
                        rubberSelect.width = 0
                        rubberSelect.height = 0
                } else if (mouse.button & Qt.LeftButton) {
768 769
                    if (root.activeTool === 1) {
                        // razor tool
770
                        var y = mouse.y - ruler.height + scrollView.contentY
771
                        if (y >= 0) {
772
                            timeline.cutClipUnderCursor((scrollView.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId)
773
                        }
774
                    }
775 776 777 778
                    if (dragProxy.draggedItem > -1) {
                        mouse.accepted = false
                        return
                    }
779
                    if (root.activeTool === 2 && mouse.y > ruler.height) {
780
                        // spacer tool
781 782
                        var y = mouse.y - ruler.height + scrollView.contentY
                        var frame = (scrollView.contentX + mouse.x) / timeline.scaleFactor
783
                        var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1
784 785 786 787 788 789
                        spacerGroup = timeline.requestSpacerStartOperation(track, frame)
                        if (spacerGroup > -1) {
                            drag.axis = Drag.XAxis
                            Drag.active = true
                            Drag.proposedAction = Qt.MoveAction
                            spacerClickFrame = frame
790
                            spacerFrame = controller.getItemPosition(spacerGroup)
791
                        }
792
                    } else if (root.activeTool === 0 || mouse.y <= ruler.height) {
793
                        if (mouse.y > ruler.height) {
794
                            controller.requestClearSelection();
795
                        }
796
                        proxy.position = Math.min((scrollView.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
797
                    }
798
                } else if (mouse.button & Qt.RightButton) {
799
                    if (mouse.y > ruler.height) {
800 801
                        timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.contentY)).trackInternalId
                        root.mainFrame = Math.floor((mouse.x + scrollView.contentX) / timeline.scaleFactor)
802
                        root.showTimelineMenu()