timeline.qml 76.6 KB
Newer Older
1
2
3
import QtQuick 2.11
import QtQml.Models 2.11
import QtQuick.Controls 1.4 as OLD
4
import QtQuick.Controls.Styles 1.4
5
6
7
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
import QtQuick.Dialogs 1.3
8
import Kdenlive.Controls 1.0
9
10
11
12
13
14
15
import 'Timeline.js' as Logic

Rectangle {
    id: root
    objectName: "timelineview"
    SystemPalette { id: activePalette }
    color: activePalette.window
16
    property bool validMenu: false
17
    property color textColor: activePalette.text
18
    property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active
19

20
    signal clipClicked()
21
    signal mousePosChanged(int position)
22
    signal showClipMenu()
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
    CompositionMenu {
33
34
        id: compositionMenu
    }
35

36
37
38
39
    onDragInProgressChanged: {
        processingDrag(!root.dragInProgress)
    }

40
41
42
43
44
45
46
47
48
49
50
51
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
        return scrollView.flickableItem.contentX
    }

    function goToStart(pos) {
        scrollView.flickableItem.contentX = pos
    }

52
53
54
55
56
57
    function checkDeletion(itemId) {
        if (dragProxy.draggedItem == itemId) {
            endDrag()
        }
    }

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

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

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

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

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

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

137
    function getMousePos() {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
138
        return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
139
140
    }

141
142
143
144
145
146
147
148
    function getScrollPos() {
        return scrollView.flickableItem.contentX
    }

    function setScrollPos(pos) {
        return scrollView.flickableItem.contentX = pos
    }

149
150
151
152
    function getCopiedItemId() {
        return copiedClip
    }

153
    function getMouseTrack() {
154
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
155
156
    }

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

168
169
170
171
172
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
173
        scrollTimer.stop()
174
    }
175

176
    function isDragging() {
177
        return dragInProgress
178
    }
179

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

201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
    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
    }
218
219
220
221
222
223
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
224

225
    property int headerWidth: timeline.headerWidth()
226
    property int activeTool: 0
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
227
    property real baseUnit: fontMetrics.font.pixelSize
228
    property real fontUnit: fontMetrics.font.pointSize * 0.9
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
229
230
    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)
231
    property bool autoScrolling: timeline.autoScroll
232
    property int duration: timeline.duration
233
234
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
235
    property color lockedColor: timeline.lockedColor
Vincent Pinon's avatar
Vincent Pinon committed
236
    property color selectionColor: timeline.selectionColor
237
    property color groupColor: timeline.groupColor
238
239
    property int mainItemId: -1
    property int clipFrame: 0
Nicolas Carion's avatar
Nicolas Carion committed
240
    property int clipBeingDroppedId: -1
241
    property string clipBeingDroppedData
242
243
    property int droppedPosition: -1
    property int droppedTrack: -1
244
    property int clipBeingMovedId: -1
245
    property int consumerPosition: proxy.position
246
    property int spacerGroup: -1
247
248
    property int spacerFrame: -1
    property int spacerClickFrame: -1
249
    property real timeScale: timeline.scaleFactor
250
    property real snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? 10 / Math.sqrt(timeScale) - 0.5 : -1
251
    property var timelineSelection: timeline.selection
252
    property int trackHeight
253
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
254
    property int zoomOnMouse: -1
255
    property int viewActiveTrack: timeline.activeTrack
256
257
    property int wheelAccumulatedDelta: 0
    readonly property int defaultDeltasPerStep: 120
258
    property bool seekingFinished : proxy.seekFinished
259
260
    property int scrollMin: scrollView.flickableItem.contentX / timeline.scaleFactor
    property int scrollMax: scrollMin + scrollView.viewport.width / timeline.scaleFactor
261
    property double dar: 16/9
262
263
264
265

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

267
    //onCurrentTrackChanged: timeline.selection = []
268
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
269
        if (root.zoomOnMouse >= 0) {
270
            scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
271
272
            root.zoomOnMouse = -1
        } else {
273
            scrollView.flickableItem.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
274
        }
275
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
276
        ruler.adjustStepSize()
277
278
279
280
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
281
    }
282

283
284
285
286
    onConsumerPositionChanged: {
        if (autoScrolling) Logic.scrollIfNeeded()
    }

287
288
289
290
291
    onViewActiveTrackChanged: {
        var tk = Logic.getTrackById(timeline.activeTrack)
        if (tk.y < scrollView.flickableItem.contentY) {
            scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3)
        } else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) {
292
            scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
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
    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()
            }
        }
    }

322
323
324
325
326
327
328
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
329
            console.log("Trying to drop composition")
330
            if (clipBeingMovedId == -1) {
331
                console.log("No clip being moved")
332
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
333
334
                var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                droppedPosition = frame
335
                if (track >= 0 && !controller.isAudioTrack(track)) {
336
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
337
                    console.log("Trying to insert",track, frame, clipBeingDroppedData)
338
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
339
                    console.log("id",clipBeingDroppedId)
340
341
342
343
344
345
346
                    continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
347
348
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
349
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
350
351
352
                if (track !=-1) {
                    var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                    if (clipBeingDroppedId >= 0){
353
354
355
356
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
357
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, Math.floor(root.snapping))
358
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
359
                    } else if (!controller.isAudioTrack(track)) {
360
                        frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
361
362
363
364
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    }
365
366
367
                }
            }
        }
368
369
        onExited:{
            if (clipBeingDroppedId != -1) {
370
                controller.requestItemDeletion(clipBeingDroppedId, false)
371
            }
372
            clearDropData()
373
        }
374
375
376
377
378
        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
379
                controller.requestItemDeletion(clipBeingDroppedId, false)
380
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
381
            }
382
            clearDropData()
383
        }
384
385
    }
    DropArea { //Drop area for bin/clips
386
387
388
389
390
391
392
393
394
        /** @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) {
395
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
396
            } else {
397
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
398
399
400

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
401
                    timeline.selectItems(ids)
402
403
404
405
406
407
                    id = ids[0]
                }
            }
            return id
        }

408
409
        property int fakeFrame: -1
        property int fakeTrack: -1
410
411
412
413
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
414
        keys: 'kdenlive/producerslist'
415
        onEntered: {
416
            if (clipBeingMovedId == -1) {
417
                //var track = Logic.getTrackIdFromPos(drag.y)
418
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
419
                if (track >= 0  && track < tracksRepeater.count) {
420
421
                    var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                    droppedPosition = frame
422
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
423
                    //drag.acceptProposedAction()
424
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
425
                    console.log('dropped data: ', clipBeingDroppedData)
426
427
428
429
430
                    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)
431
                        if (clipBeingDroppedId > -1) {
432
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
433
434
435
436
                            fakeTrack = timeline.activeTrack
                        } else {
                            drag.accepted = false
                        }
437
                    }
438
                    continuousScrolling(drag.x + scrollView.flickableItem.contentX)
439
440
441
442
                } else {
                    drag.accepted = false
                }
            }
443
        }
Nicolas Carion's avatar
Nicolas Carion committed
444
445
        onExited:{
            if (clipBeingDroppedId != -1) {
446
                controller.requestItemDeletion(clipBeingDroppedId, false)
Nicolas Carion's avatar
Nicolas Carion committed
447
            }
448
            clearDropData()
Nicolas Carion's avatar
Nicolas Carion committed
449
        }
450
        onPositionChanged: {
451
            if (clipBeingMovedId == -1) {
452
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
453
                if (track >= 0  && track < tracksRepeater.count) {
454
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
455
                    var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
456
                    if (clipBeingDroppedId >= 0) {
457
                        fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
458
459
                        fakeTrack = timeline.activeTrack
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
460
461
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    } else {
462
463
464
465
466
467
                        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)
468
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
469
470
                            fakeTrack = timeline.activeTrack
                        }
471
472
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    }
473
                }
Nicolas Carion's avatar
Nicolas Carion committed
474
            }
475
476
        }
        onDropped: {
477
478
479
            if (clipBeingDroppedId != -1) {
                var frame = controller.getClipPosition(clipBeingDroppedId)
                var track = controller.getClipTrackId(clipBeingDroppedId)
480
481
482
483
                if (!controller.normalEdit()) {
                    frame = fakeFrame
                    track = fakeTrack
                }
484
485
486
487
                /* 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
488
                controller.requestItemDeletion(clipBeingDroppedId, false)
489
490
491

                var binIds = clipBeingDroppedData.split(";")
                if (binIds.length == 1) {
492
493
494
495
496
                    if (controller.normalEdit()) {
                        timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
                    } else {
                        timeline.insertClipZone(clipBeingDroppedData, track, frame)
                    }
497
                } else {
498
499
500
501
502
503
                    if (controller.normalEdit()) {
                        timeline.insertClips(track, frame, binIds, true, true)
                    } else {
                        // TODO
                        console.log('multiple clips insert/overwrite not supported yet')
                    }
504
                }
505
506
                fakeTrack = -1
                fakeFrame = -1
507
            }
508
            clearDropData()
509
510
        }
    }
511
512
513
514
515
516
517
518
519
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
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
    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) {
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
                    var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                    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)
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    } 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
                        }
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    }
                }
            }
        }
        onDropped: {
            var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
            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()
        }
    }
582
    OLD.Menu {
583
        id: menu
584
585
        property int clickedX
        property int clickedY
586
587
        onAboutToHide: {
            timeline.ungrabHack()
588
            editGuideMenu.visible = false
589
        }
590
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
591
            text: i18n("Paste")
592
            iconName: 'edit-paste'
593
594
            visible: copiedClip != -1
            onTriggered: {
595
596
597
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                timeline.pasteItem(frame, track)
598
599
            }
        }
600
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
601
            text: i18n("Insert Space")
602
            onTriggered: {
603
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
604
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
605
606
607
                timeline.insertSpace(track, frame);
            }
        }
608
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
609
            text: i18n("Remove Space On Active Track")
610
            onTriggered: {
611
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
612
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
613
614
615
                timeline.removeSpace(track, frame);
            }
        }
616
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
617
            text: i18n("Remove Space")
618
            onTriggered: {
619
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
620
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
621
622
623
                timeline.removeSpace(track, frame, true);
            }
        }
624
        OLD.MenuItem {
625
            id: addGuideMenu
Yuri Chornoivan's avatar
Yuri Chornoivan committed
626
            text: i18n("Add Guide")
627
            onTriggered: {
628
                timeline.switchGuide(root.consumerPosition);
629
630
            }
        }
631
        GuidesMenu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
632
            title: i18n("Go to guide...")
633
634
635
            menuModel: guidesModel
            enabled: guidesDelegateModel.count > 0
            onGuideSelected: {
636
                proxy.position = assetFrame
637
638
            }
        }
639
        OLD.MenuItem {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
640
            id: editGuideMenu
Yuri Chornoivan's avatar
Yuri Chornoivan committed
641
            text: i18n("Edit Guide")
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
642
643
            visible: false
            onTriggered: {
644
                timeline.editGuide(root.consumerPosition);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
645
646
            }
        }
647
        AssetMenu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
648
            title: i18n("Insert a composition...")
649
            menuModel: transitionModel
650
            isTransition: true
651
            onAssetSelected: {
652
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
653
654
655
656
657
658
659
                var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                var id = timeline.insertComposition(track, frame, assetId, true)
                if (id == -1) {
                    compositionFail.open()
                }
            }
        }
660
        onAboutToShow: {
661
            if (guidesModel.hasMarker(root.consumerPosition)) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
662
                // marker at timeline position
Yuri Chornoivan's avatar
Yuri Chornoivan committed
663
                addGuideMenu.text = i18n("Remove Guide")
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
664
665
                editGuideMenu.visible = true
            } else {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
666
                addGuideMenu.text = i18n("Add Guide")
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
667
            }
668
669
            console.log("pop menu")
        }
670
    }
671
672
673
674
    OLD.Menu {
        id: rulermenu
        property int clickedX
        property int clickedY
675
676
677
678
        onAboutToHide: {
            timeline.ungrabHack()
            editGuideMenu2.visible = false
        }
679
680
        OLD.MenuItem {
            id: addGuideMenu2
Yuri Chornoivan's avatar
Yuri Chornoivan committed
681
            text: i18n("Add Guide")
682
            onTriggered: {
683
                timeline.switchGuide(root.consumerPosition);
684
685
            }
        }
686
        GuidesMenu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
687
            title: i18n("Go to guide...")
688
689
690
            menuModel: guidesModel
            enabled: guidesDelegateModel.count > 0
            onGuideSelected: {
691
                proxy.position = assetFrame
692
693
            }
        }
694
695
        OLD.MenuItem {
            id: editGuideMenu2
Yuri Chornoivan's avatar
Yuri Chornoivan committed
696
            text: i18n("Edit Guide")
697
698
            visible: false
            onTriggered: {
699
                timeline.editGuide(root.consumerPosition);
700
701
            }
        }
702
703
        OLD.MenuItem {
            id: addProjectNote
Yuri Chornoivan's avatar
Yuri Chornoivan committed
704
            text: i18n("Add Project Note")
705
706
707
708
            onTriggered: {
                timeline.triggerAction('add_project_note')
            }
        }
709
        onAboutToShow: {
710
            if (guidesModel.hasMarker(root.consumerPosition)) {
711
                // marker at timeline position
Yuri Chornoivan's avatar
Yuri Chornoivan committed
712
                addGuideMenu2.text = i18n("Remove Guide")
713
714
                editGuideMenu2.visible = true
            } else {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
715
                addGuideMenu2.text = i18n("Add Guide")
716
717
718
719
            }
            console.log("pop menu")
        }
    }
720
721
722
723
724
725
    MessageDialog {
        id: compositionFail
        title: i18n("Timeline error")
        icon: StandardIcon.Warning
        text: i18n("Impossible to add a composition at that position. There might not be enough space")
        standardButtons: StandardButton.Ok
726
    }
727
    OLD.Menu {
728
        id: headerMenu
729
730
731
        property int trackId: -1
        property int thumbsFormat: 0
        property bool audioTrack: false
732
        property bool recEnabled: false
733
734
735
        onAboutToHide: {
            timeline.ungrabHack()
        }
736
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
737
            text: i18n("Add Track")
738
739
740
            onTriggered: {
                timeline.addTrack(timeline.activeTrack)
            }
741
        }
742
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
743
            text: i18n("Delete Track")
744
745
746
            onTriggered: {
                timeline.deleteTrack(timeline.activeTrack)
            }
747
        }
748
749
750
        OLD.MenuItem {
            visible: headerMenu.audioTrack
            id: showRec
Yuri Chornoivan's avatar
Yuri Chornoivan committed
751
            text: i18n("Show Record Controls")
752
753
754
755
756
757
            onTriggered: {
                controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0')
            }
            checkable: true
            checked: headerMenu.recEnabled
        }
758
759
760
        OLD.MenuItem {
            visible: headerMenu.audioTrack
            id: configRec
Yuri Chornoivan's avatar
Yuri Chornoivan committed
761
            text: i18n("Configure Recording")
762
763
764
765
            onTriggered: {
                timeline.showConfig(4,2)
            }
        }
766
        OLD.Menu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
767
            title: i18n("Track thumbnails")
768
769
770
            visible: !headerMenu.audioTrack
                    OLD.ExclusiveGroup { id: thumbStyle }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
771
                        text: i18n("In frame")
772
773
774
775
776
777
                        id: inFrame
                        onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2)
                        checkable: true
                        exclusiveGroup: thumbStyle
                    }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
778
                        text: i18n("In / out frames")
779
780
781
782
783
784
785
                        id: inOutFrame
                        onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 0)
                        checkable: true
                        checked: true
                        exclusiveGroup: thumbStyle
                    }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
786
                        text: i18n("All frames")
787
788
789
790
791
792
                        id: allFrame
                        onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1)
                        checkable: true
                        exclusiveGroup: thumbStyle
                    }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
793
                        text: i18n("No thumbnails")
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
                        id: noFrame
                        onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3)
                        checkable: true
                        exclusiveGroup: thumbStyle
                    }
                onAboutToShow: {
                        switch(headerMenu.thumbsFormat) {
                            case 3:
                                noFrame.checked = true
                                break
                            case 2:
                                inFrame.checked = true
                                break
                            case 1:
                                allFrame.checked = true
                                break
                            default:
                                inOutFrame.checked = true
                                break
                        }
                }
        }
816
    }
817

818
819
    Row {
        Column {
820
            id: headerContainer
821
            width: headerWidth
822
            z: 1
823
            Item {
824
                // Padding between toolbar and track headers.
825
                width: parent.width
826
                height: ruler.height
827
828
829
830
831
                Button {
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : "M"
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
832
833
834
835
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
836
837
838
839
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
840
841
842
843
                    onClicked: {
                        timeline.showMasterEffects()
                    }
                }
844
845
846
            }
            Flickable {
                // Non-slider scroll area for the track headers.
847
                id: headerFlick
848
                contentY: scrollView.flickableItem.contentY
849
850
851
                width: parent.width
                y: ruler.height
                height: root.height - ruler.height
852
                interactive: false
853
                clip: true
854

855
856
857
858
859
                MouseArea {
                    width: trackHeaders.width
                    height: trackHeaders.height
                    acceptedButtons: Qt.NoButton
                    onWheel: {
860
                        var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + ruler.height)
861
862
863
                        scrollView.flickableItem.contentY = Math.max(newScroll, 0)
                    }
                }
864
865
                Column {
                    id: trackHeaders
866
                    spacing: 0
867
868
869
870
871
                    Repeater {
                        id: trackHeaderRepeater
                        model: multitrack
                        TrackHead {
                            trackName: model.name
872
                            thumbsFormat: model.thumbsFormat
873
                            trackTag: model.trackTag
874
                            isDisabled: model.disabled
875
876
                            isComposite: model.composite
                            isLocked: model.locked
877
                            isActive: model.trackActive
878
                            isAudio: model.audio
879
                            showAudioRecord: model.audioRecord
880
881
                            effectNames: model.effectNames
                            isStackEnabled: model.isStackEnabled
882
                            width: headerWidth
883
                            current: item === timeline.activeTrack
884
                            trackId: item
885
                            height: Math.max(collapsedHeight, model.trackHeight)
886
                            onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
887
                            collapsed: height <= collapsedHeight
888
                            onMyTrackHeightChanged: {
889
                                collapsed = myTrackHeight <= collapsedHeight
890
891
                                if (!collapsed) {
                                    controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
892
893
894
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
                                } else {
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
895
                                }
896
897
                                // hack: change property to trigger transition adjustment
                                root.trackHeight = root.trackHeight === 1 ? 0 : 1