timeline.qml 92.7 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
import 'Timeline.js' as Logic
6
import com.enums 1.0
7
8
9
10
11
12

Rectangle {
    id: root
    objectName: "timelineview"
    SystemPalette { id: activePalette }
    color: activePalette.window
13
    property bool validMenu: false
14
    property color textColor: activePalette.text
15
    property var groupTrimData
Vincent Pinon's avatar
Vincent Pinon committed
16
    property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active || groupTrimData !== undefined
17

18
    signal clipClicked()
19
    signal mousePosChanged(int position)
20
    signal showClipMenu(int cid)
21
    signal showCompositionMenu()
22
    signal showTimelineMenu()
23
24
    signal showRulerMenu()
    signal showHeaderMenu()
25
    signal showTargetMenu(int ix)
26
27
    signal zoomIn(bool onMouse)
    signal zoomOut(bool onMouse)
28
    signal processingDrag(bool dragging)
Sashmita Raghav's avatar
Sashmita Raghav committed
29
    signal showSubtitleClipMenu()
30

31
32
33
34
    // Zoombar properties
    property double zoomStart: scrollView.visibleArea.xPosition
    property double zoomBarWidth: scrollView.visibleArea.widthRatio

35
36
    FontMetrics {
        id: fontMetrics
37
        font: miniFont
38
    }
39

40
41
42
43
    onDragInProgressChanged: {
        processingDrag(!root.dragInProgress)
    }

44
45
46
47
    function endBinDrag() {
        clipDropArea.processDrop()
    }

48
49
50
51
52
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
53
        return scrollView.contentX
54
55
56
    }

    function goToStart(pos) {
57
        scrollView.contentX = pos
58
59
    }

60
61
62
63
64
65
66
67
    function switchSubtitleTrack() {
        if (subtitleTrack.height > root.collapsedHeight) {
            subtitleTrack.height = root.collapsedHeight
        } else {
            subtitleTrack.height = 5 * root.baseUnit
        }
    }
    
68
69
70
71
72
    function highlightSub(ix) {
        var currentSub = subtitlesRepeater.itemAt(ix)
        currentSub.editText()
    }
    
73
    function checkDeletion(itemId) {
Vincent Pinon's avatar
Vincent Pinon committed
74
        if (dragProxy.draggedItem === itemId) {
75
76
            endDrag()
        }
Vincent Pinon's avatar
Vincent Pinon committed
77
        if (itemId === mainItemId) {
78
79
            mainItemId = -1
        }
80
    }
Pistos Pi's avatar
Pistos Pi committed
81

82
    function getActiveTrackStreamPos() {
83
84
        // Return the relative y click position, to display the context menu
        return Logic.getTrackYFromId(timeline.activeTrack) + rulercontainer.height - scrollView.contentY
85
    }
86

87
88
    function updatePalette() {
        root.color = activePalette.window
89
        root.textColor = activePalette.text
90
91
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
92
        // Disable caching for track header icons
93
        root.paletteUnchanged = false
94
    }
95

96
    function moveSelectedTrack(offset) {
97
98
99
100
101
102
103
104
105
106
107
        var newTrack
        if (timeline.activeTrack < 0 ) {
            if (offset <0) {
                newTrack = -2
            } else {
                newTrack = max
            }
        } else {
            var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
            newTrack = cTrack + offset
        }
108
109
        var max = tracksRepeater.count;
        if (newTrack < 0) {
Vincent Pinon's avatar
Vincent Pinon committed
110
            if (showSubtitles && newTrack === -1) {
111
112
113
                timeline.activeTrack = -2
                return
            }
114
115
            newTrack = max - 1;
        } else if (newTrack >= max) {
116
117
118
119
            if (showSubtitles) {
                timeline.activeTrack = -2
                return
            }
120
121
            newTrack = 0;
        }
122
        timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
123
124
    }

125
    function zoomByWheel(wheel) {
126
        if (wheel.modifiers & Qt.AltModifier) {
127
            // Seek to next snap
128
129
130
131
132
133
            if (wheel.angleDelta.x > 0) {
                timeline.triggerAction('monitor_seek_snap_backward')
            } else {
                timeline.triggerAction('monitor_seek_snap_forward')
            }
        } else if (wheel.modifiers & Qt.ControlModifier) {
134
            root.wheelAccumulatedDelta += wheel.angleDelta.y;
135
            // Zoom
136
            if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) {
137
                root.zoomIn(true);
138
139
                root.wheelAccumulatedDelta = 0;
            } else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) {
140
                root.zoomOut(true);
141
                root.wheelAccumulatedDelta = 0;
142
            }
143
        } else if (wheel.modifiers & Qt.ShiftModifier) {
Pistos Pi's avatar
Pistos Pi committed
144
            if (scrollVertically) {
Pistos Pi's avatar
Pistos Pi committed
145
                horizontalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
146
            } else {
Pistos Pi's avatar
Pistos Pi committed
147
                verticalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
148
            }
149
        } else {
Pistos Pi's avatar
Pistos Pi committed
150
            if (scrollVertically) {
Pistos Pi's avatar
Pistos Pi committed
151
                verticalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
152
            } else {
Pistos Pi's avatar
Pistos Pi committed
153
                horizontalScroll(wheel)
Pistos Pi's avatar
Pistos Pi committed
154
            }
155
        }
156
        wheel.accepted = true
157
158
    }

Pistos Pi's avatar
Pistos Pi committed
159
160
161
162
163
164
165
166
167
168
169
    function horizontalScroll(wheel) {
        var newScroll = Math.min(
          scrollView.contentX - wheel.angleDelta.y,
          timeline.fullDuration * root.timeScale - scrollView.width
        )
        scrollView.contentX = Math.max(newScroll, 0)
    }

    function verticalScroll(wheel) {
        var newScroll = Math.min(
            scrollView.contentY - wheel.angleDelta.y,
170
            trackHeaders.height + subtitleTrackHeader.height - tracksArea.height + horZoomBar.height + ruler.height
Pistos Pi's avatar
Pistos Pi committed
171
172
173
174
        )
        scrollView.contentY = Math.max(newScroll, 0)
    }

175
    function continuousScrolling(x, y) {
176
        // This provides continuous scrolling at the left/right edges.
177
        if (x > scrollView.contentX + scrollView.width - root.baseUnit * 3) {
178
            scrollTimer.horizontal = root.baseUnit
179
180
            scrollTimer.start()
        } else if (x < 50) {
181
            scrollView.contentX = 0;
182
            scrollTimer.horizontal = 0
183
            scrollTimer.stop()
184
        } else if (x < scrollView.contentX + root.baseUnit * 3) {
185
            scrollTimer.horizontal = -root.baseUnit
186
187
            scrollTimer.start()
        } else {
188
189
            if (y > scrollView.contentY + scrollView.height + ruler.height - root.baseUnit) {
                scrollTimer.vertical = root.baseUnit
190
                scrollTimer.horizontal = 0
191
                scrollTimer.start()
192
193
            } else if (scrollView.contentY > 0 && (y - (scrollView.contentY + ruler.height ) < root.baseUnit)) {
                scrollTimer.vertical = -root.baseUnit
194
                scrollTimer.horizontal = 0
195
196
                scrollTimer.start()
            } else {
197
                console.log('TRIGGERING VERTICAL STOP: ', y, ', SCROLL: ',scrollView.contentY, 'SCH:', scrollView.height)
198
199
200
201
                scrollTimer.vertical = 0
                scrollTimer.horizontal = 0
                scrollTimer.stop()
            }
202
203
        }
    }
204

205
    function getMousePos() {
206
207
208
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return (dragProxy.masterObject.x + dragProxy.masterObject.mouseXPos) / timeline.scaleFactor
        }
209
210
211
212
213
        if (tracksArea.containsMouse) {
            return (scrollView.contentX + tracksArea.mouseX) / timeline.scaleFactor
        } else {
            return -1;
        }
214
    }
215
216
217
218
219
220
221
222
223
224
    function getMouseX() {
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return (dragProxy.masterObject.x + dragProxy.masterObject.mouseXPos) - scrollView.contentX
        }
        if (tracksArea.containsMouse) {
            return tracksArea.mouseX
        } else {
            return -1;
        }
    }
225

226
    function getScrollPos() {
227
        return scrollView.contentX
228
229
230
    }

    function setScrollPos(pos) {
231
        return scrollView.contentX = pos
232
233
    }

234
235
236
237
    function getCopiedItemId() {
        return copiedClip
    }

238
    function getMouseTrack() {
239
240
241
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return dragProxy.masterObject.trackId
        }
242
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.contentY - subtitleTrack.height)
243
244
    }

245
246
247
    function getTrackColor(audio, header) {
        var col = activePalette.alternateBase
        if (audio) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
248
            col = Qt.tint(col, "#06FF00CC")
249
250
        }
        if (header) {
251
            col = Qt.darker(col, 1.05)
252
        }
253
        return col
254
    }
Pistos Pi's avatar
Pistos Pi committed
255

256
257
258
    function centerViewOnCursor() {
        scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
    }
259

260
261
262
263
264
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
265
        scrollTimer.stop()
266
    }
267

268
    function isDragging() {
269
        return dragInProgress
270
    }
271

272
    function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
273
        dragProxy.x = itemObject.modelStart * timeScale
274
        dragProxy.y = itemCoord.y
275
        dragProxy.width = itemObject.clipDuration * timeScale
276
277
278
279
280
281
        dragProxy.height = itemCoord.height
        dragProxy.masterObject = itemObject
        dragProxy.draggedItem = itemId
        dragProxy.sourceTrack = itemTrack
        dragProxy.sourceFrame = itemPos
        dragProxy.isComposition = isComposition
282
        dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
283
    }
284

285
    function endDrag() {
286
        console.log('ENDING DRAG!!!!!!!!!!!!!!!!!!!!!!\n')
287
        dragProxy.draggedItem = -1
288
289
        dragProxy.x = 0
        dragProxy.y = 0
290
291
        dragProxy.width = 0
        dragProxy.height = 0
292
        dragProxy.verticalOffset = 0
293
    }
294
    
295
296
297
    function regainFocus(mousePos) {
        var currentMouseTrack = Logic.getTrackIdFromPos(mousePos.y - ruler.height - subtitleTrack.height + scrollView.contentY)
        // Try to find correct item
298
299
        //console.log('checking item on TK: ', currentMouseTrack, ' AT: XPOS', (mousePos.x - trackHeaders.width), ', SCOLL:', scrollView.contentX, ', RES: ', ((mousePos.x - trackHeaders.width + scrollView.contentX) / timeline.scaleFactor), ' SCROLL POS: ', (mousePos.y - ruler.height - subtitleTrack.height + scrollView.contentY))
        var tentativeClip = getItemAtPos(currentMouseTrack, mousePos.x - trackHeaders.width + scrollView.contentX, dragProxy.isComposition)
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
        if (tentativeClip && tentativeClip.clipId) {
            dragProxy.draggedItem = tentativeClip.clipId
            var tk = controller.getItemTrackId(tentativeClip.clipId)
            dragProxy.x = tentativeClip.x
            dragProxy.y = tentativeClip.y + Logic.getTrackYFromId(tk)
            dragProxy.width = tentativeClip.width
            dragProxy.height = tentativeClip.height
            dragProxy.masterObject = tentativeClip
            dragProxy.sourceTrack = tk
            console.log('missing item', tentativeClip.clipId, ', COORDS: ', tentativeClip.x, 'x', tentativeClip.y, ', TK id: ', tk, ', TKY: ', Logic.getTrackYFromId(tk))
            dragProxy.sourceFrame = tentativeClip.modelStart
            dragProxy.isComposition = tentativeClip.isComposition
        } else {
            console.log('item not found')
        }
315
    }
316

317
318
319
320
321
322
323
324
325
326
    function getAudioTracksCount(){
        var audioCount = 0;
        for (var i = 0; i < trackHeaderRepeater.count; i++) {
            if(trackHeaderRepeater.itemAt(i).isAudio) {
                audioCount++;
            }
        }
        return audioCount;
    }

327
328
    function getItemAtPos(tk, posx, isComposition) {
        var track = Logic.getTrackById(tk)
329
330
331
        if (track == undefined || track.children == undefined) {
            return undefined
        }
332
333
334
        var container = track.children[0]
        var tentativeClip = undefined
        for (var i = 0 ; i < container.children.length; i++) {
Vincent Pinon's avatar
Vincent Pinon committed
335
            if (container.children[i].children.length === 0 || container.children[i].children[0].children.length === 0) {
336
337
338
                continue
            }
            tentativeClip = container.children[i].children[0].childAt(posx, 1)
Vincent Pinon's avatar
Vincent Pinon committed
339
            if (tentativeClip && tentativeClip.clipId && (tentativeClip.isComposition === isComposition)) {
340
341
342
343
344
                break
            }
        }
        return tentativeClip
    }
345
346
347
348
349
350
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
351

352
    property int activeTool: 0
353
    property real baseUnit: Math.max(12, fontMetrics.font.pixelSize)
354
    property real fontUnit: fontMetrics.font.pointSize
355
356
357
    property int collapsedHeight: Math.max(28, baseUnit * 1.8)
    property int minHeaderWidth: 6 * collapsedHeight
    property int headerWidth: Math.max(minHeaderWidth, timeline.headerWidth())
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
358
    property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
359
    property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.5)
360
    property bool autoScrolling: timeline.autoScroll
361
    property int duration: timeline.duration
362
363
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
364
    property color titleColor: timeline.titleColor
Sashmita Raghav's avatar
Sashmita Raghav committed
365
    property color imageColor: timeline.imageColor
366
    property color slideshowColor: timeline.slideshowColor
367
    property color lockedColor: timeline.lockedColor
Vincent Pinon's avatar
Vincent Pinon committed
368
    property color selectionColor: timeline.selectionColor
369
    property color groupColor: timeline.groupColor
370
371
    property color thumbColor1: timeline.thumbColor1
    property color thumbColor2: timeline.thumbColor2
372
    property int mainItemId: -1
373
    property int mainFrame: -1
Nicolas Carion's avatar
Nicolas Carion committed
374
    property int clipBeingDroppedId: -1
375
    property string clipBeingDroppedData
376
377
    property int droppedPosition: -1
    property int droppedTrack: -1
378
    property int clipBeingMovedId: -1
379
    property int consumerPosition: proxy.position
380
    property int spacerGroup: -1
381
    property int spacerTrack: -1
382
    property int spacerFrame: -1
383
    property int finalSpacerFrame: -1
384
    property int spacerClickFrame: -1
385
    property bool spacerGuides: false
386
    property real timeScale: timeline.scaleFactor
387
    property int snapping: (timeline.snap && (timeline.scaleFactor < 2 * baseUnit)) ? Math.floor(baseUnit / (timeline.scaleFactor > 3 ? timeline.scaleFactor / 2 : timeline.scaleFactor)) : -1
388
    property var timelineSelection: timeline.selection
389
    property int selectedMix: timeline.selectedMix
390
    property int trackHeight
391
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
392
    property int zoomOnMouse: -1
393
394
    // The first visible frame in case of scaling triggered by zoombar
    property int zoomOnBar: -1
395
    property int viewActiveTrack: timeline.activeTrack
396
397
    property int wheelAccumulatedDelta: 0
    readonly property int defaultDeltasPerStep: 120
398
    property bool seekingFinished : proxy.seekFinished
399
    property int scrollMin: scrollView.contentX / timeline.scaleFactor
400
    property int scrollMax: scrollMin + scrollView.contentItem.width / timeline.scaleFactor
401
    property double dar: 16/9
402
    property bool paletteUnchanged: true
403
    property int maxLabelWidth: 20 * root.baseUnit * Math.sqrt(root.timeScale)
404
    property bool showSubtitles: false
405
406
    property bool subtitlesLocked: timeline.subtitlesLocked
    property bool subtitlesDisabled: timeline.subtitlesDisabled
407
    property int trackTagWidth: fontMetrics.boundingRect("M").width * ((getAudioTracksCount() > 9) || (trackHeaderRepeater.count - getAudioTracksCount() > 9)  ? 3 : 2)
Pistos Pi's avatar
Pistos Pi committed
408
    property bool scrollVertically: timeline.scrollVertically
409
410
411
412

    onSeekingFinishedChanged : {
        playhead.opacity = seekingFinished ? 1 : 0.5
    }
Pistos Pi's avatar
Pistos Pi committed
413

414
415
416
    onShowSubtitlesChanged: {
        subtitleTrack.height = showSubtitles? root.baseUnit * 5 : 0
    }
417

418
    //onCurrentTrackChanged: timeline.selection = []
419

420
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
421
        if (root.zoomOnMouse >= 0) {
422
            scrollView.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - getMouseX())
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
423
            root.zoomOnMouse = -1
424
425
426
        } else if (root.zoomOnBar >= 0) {
            scrollView.contentX = Math.max(0, root.zoomOnBar * timeline.scaleFactor )
            root.zoomOnBar = -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
427
        } else {
428
            scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
429
        }
430
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
431
        ruler.adjustStepSize()
432
433
434
435
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
436
    }
437

438
    onConsumerPositionChanged: {
439
        if (root.autoScrolling) Logic.scrollIfNeeded()
440
441
    }

442
    onViewActiveTrackChanged: {
Vincent Pinon's avatar
Vincent Pinon committed
443
        if (timeline.activeTrack === -2) {
444
445
446
447
            // subtitle track
            scrollView.contentY = 0
            return
        }
448
        var tk = Logic.getTrackById(timeline.activeTrack)
449
450
451
        if (tk.y + subtitleTrack.height < scrollView.contentY) {
            scrollView.contentY = Math.max(0, tk.y + subtitleTrack.height)
        } else if (tk.y + tk.height + subtitleTrack.height > scrollView.contentY + scrollView.height) {
452
            var newY = Math.min(trackHeaders.height + subtitleTrack.height - scrollView.height, tk.y + tk.height - scrollView.height + subtitleTrack.height)
453
454
455
            if (newY >= 0) {
                scrollView.contentY = newY
            }
456
457
458
        }
    }

459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
    onActiveToolChanged: {
        if (root.activeTool == 2) {
            // Spacer activated
            endDrag()
        } else if (root.activeTool == 0) {
            var tk = getMouseTrack()
            if (tk < 0) {
                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()
            }
        }
    }

484
485
486
487
488
489
490
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
491
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
492
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
493
                var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
494
                droppedPosition = frame
495
                if (track >= 0 && !controller.isAudioTrack(track)) {
496
497
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
498
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
499
500
501
502
503
504
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
505
506
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
507
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
Vincent Pinon's avatar
Vincent Pinon committed
508
                if (track !== -1) {
509
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
510
                    if (clipBeingDroppedId >= 0){
511
512
513
514
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
515
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, root.snapping)
516
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
517
                    } else if (!controller.isAudioTrack(track)) {
518
                        frame = controller.suggestSnapPoint(frame, root.snapping)
519
520
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
521
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
522
                    }
523
524
525
                }
            }
        }
526
        onExited:{
527
528
            if (clipBeingDroppedId != -1) {
                // If we exit, remove composition
529
                controller.requestItemDeletion(clipBeingDroppedId, false)
530
                clearDropData()
531
532
            }
        }
533
534
535
536
537
        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
538
                controller.requestItemDeletion(clipBeingDroppedId, false)
539
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
540
            }
541
            clearDropData()
542
        }
543
    }
544
545
546
    DropArea {
        //Drop area for bin/clips
        id: clipDropArea
547
548
549
        /** @brief local helper function to handle the insertion of multiple dragged items */
        function insertAndMaybeGroup(track, frame, droppedData) {
            var binIds = droppedData.split(";")
Vincent Pinon's avatar
Vincent Pinon committed
550
            if (binIds.length === 0) {
551
552
553
554
                return -1
            }

            var id = -1
Vincent Pinon's avatar
Vincent Pinon committed
555
            if (binIds.length === 1) {
556
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
557
            } else {
558
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
559
560
561

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
562
                    timeline.selectItems(ids)
563
564
565
566
567
568
                    id = ids[0]
                }
            }
            return id
        }

569
570
        property int fakeFrame: -1
        property int fakeTrack: -1
571
572
573
574
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
575
        keys: 'kdenlive/producerslist'
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
        function processDrop()
        {
            // Process the drop event, useful if drop event happens outside of drop area
            if (clipBeingDroppedId != -1) {
                var frame = controller.getClipPosition(clipBeingDroppedId)
                var track = controller.getClipTrackId(clipBeingDroppedId)
                if (!controller.normalEdit()) {
                    frame = fakeFrame
                    track = fakeTrack
                }
                /* 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
                 */
                controller.requestItemDeletion(clipBeingDroppedId, false)

                var binIds = clipBeingDroppedData.split(";")
                if (binIds.length == 1) {
                    if (controller.normalEdit()) {
                        timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
                    } 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')
                    }
                }
                fakeTrack = -1
                fakeFrame = -1
            }
            clearDropData()
        }
612
        onEntered: {
613
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
614
                //var track = Logic.getTrackIdFromPos(drag.y)
615
616
617
618
619
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
620
                if (track >= 0  && track < tracksRepeater.count) {
621
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
622
                    droppedPosition = frame
623
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
624
                    //drag.acceptProposedAction()
625
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
626
627
628
629
                    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
630
631
                        frame = controller.adjustFrame(frame, timeline.activeTrack)
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
632
                        if (clipBeingDroppedId > -1) {
633
634
635
                            var moveData = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
                            fakeFrame = moveData[0]
                            fakeTrack = moveData[1]
636
637
638
                        } else {
                            drag.accepted = false
                        }
639
                    }
640
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
641
642
643
644
                } else {
                    drag.accepted = false
                }
            }
645
        }
Nicolas Carion's avatar
Nicolas Carion committed
646
        onExited:{
647
648
            if (clipBeingDroppedId != -1 && drag.y < drag.x) {
                // If we exit on top, remove clip
649
                controller.requestItemDeletion(clipBeingDroppedId, false)
650
651
652
                clearDropData()
            } else {
                // Clip is dropped
Nicolas Carion's avatar
Nicolas Carion committed
653
654
            }
        }
655
        onPositionChanged: {
656
            if (clipBeingMovedId == -1) {
657
658
659
660
661
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
662
                if (track >= 0  && track < tracksRepeater.count) {
663
664
                    //timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
                    var targetTrack = tracksRepeater.itemAt(track).trackInternalId
665
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
666
667
668
669
670
                    if (clipBeingDroppedId > -1) {
                        var moveData = controller.suggestClipMove(clipBeingDroppedId, targetTrack, frame, root.consumerPosition, root.snapping)
                        fakeFrame = moveData[0]
                        fakeTrack = moveData[1]
                        timeline.activeTrack = fakeTrack
671
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
672
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
673
                    } else {
674
                        frame = controller.suggestSnapPoint(frame, root.snapping)
675
                        if (controller.normalEdit()) {
676
677
                            timeline.activeTrack = targetTrack
                            clipBeingDroppedId = insertAndMaybeGroup(targetTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
678
679
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
680
681
682
683
684
685
686
                            clipBeingDroppedId = insertAndMaybeGroup(targetTrack, timeline.fullDuration, clipBeingDroppedData)
                            if (clipBeingDroppedId > -1) {
                                var moveData = controller.suggestClipMove(clipBeingDroppedId, targetTrack, frame, root.consumerPosition, root.snapping)
                                fakeFrame = moveData[0]
                                fakeTrack = moveData[1]
                                timeline.activeTrack = fakeTrack
                            }
687
                        }
688
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
689
                    }
690
                }
Nicolas Carion's avatar
Nicolas Carion committed
691
            }
692
693
        }
        onDropped: {
694
            processDrop()
695
696
        }
    }
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
    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) {
723
724
725
726
727
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
728
729
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
730
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
731
732
                    if (clipBeingDroppedId >= 0) {
                        //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
733
                        //fakeTrack = timeline.activeTrack
734
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
735
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
736
                    } else {
737
                        frame = controller.suggestSnapPoint(frame, root.snapping)
738
739
740
741
742
743
744
745
                        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
                        }
746
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
747
748
749
750
751
                    }
                }
            }
        }
        onDropped: {
752
            var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
            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()
        }
    }
772

773
774
    Row {
        Column {
775
            id: headerContainer
776
            width: headerWidth
777
            z: 1
778
            Item {
779
                // Padding between toolbar and track headers.
780
                width: parent.width
781
                height: ruler.height
782
                Button {
783
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : i18nc("Initial for Master", "M")
784
785
                    font: miniFont
                    flat: true
786
787
788
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
789
790
791
792
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
793
794
795
796
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
797
798
799
                    onClicked: {
                        timeline.showMasterEffects()
                    }
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
                    DropArea { //Drop area for tracks
                        anchors.fill: parent
                        keys: 'kdenlive/effect'
                        property string dropData
                        property string dropSource
                        property int dropRow: -1
                        onEntered: {
                            dropData = drag.getDataAsString('kdenlive/effect')
                            dropSource = drag.getDataAsString('kdenlive/effectsource')
                        }
                        onDropped: {
                            console.log("Add effect: ", dropData)
                            if (dropSource == '') {
                                // drop from effects list
                                controller.addTrackEffect(-1, dropData);
                            } else {
                                controller.copyTrackEffect(-1, dropSource);
                            }
                            dropSource = ''
                            dropRow = -1
                            drag.acceptProposedAction
                        }
                    }
823
                }
824
825
826
            }
            Flickable {
                // Non-slider scroll area for the track headers.
827
                id: headerFlick
828
                contentY: scrollView.contentY
829
830
831
                width: parent.width
                y: ruler.height
                height: root.height - ruler.height
832
                interactive: false
833
                clip: true
834

835
836
                MouseArea {
                    width: trackHeaders.width
837
                    height: trackHeaders.height + subtitleTrackHeader.height
838
839
                    acceptedButtons: Qt.NoButton
                    onWheel: {
840
                        zoomByWheel(wheel)
841
842
                    }
                }
843
                Rectangle {
844
                    id: subtitleTrackHeader
845
                    width: trackHeaders.width
846
847
                    height: subtitleTrack.height
                    property bool collapsed: subtitleTrack.height == root.collapsedHeight
848
                    visible: height > 0
Vincent Pinon's avatar
Vincent Pinon committed
849
                    color: (timeline.activeTrack === -2) ? Qt.tint(getTrackColor(false, false), selectedTrackColor) : getTrackColor(false, false)
850
851
852
853
854
855
                    MouseArea {
                        anchors.fill: parent
                        onClicked: {
                            timeline.activeTrack = -2
                        }
                    }
856
857
858
859
860
                    ToolButton {
                        id: expandSubButton
                        focusPolicy: Qt.NoFocus
                        property var modifier: 0
                        anchors.left: parent.left
861
                        anchors.leftMargin: 1.5 * root.baseUnit
862
                        width: root.collapsedHeight
Pistos Pi's avatar
Pistos Pi committed
863
                        height: root.collapsedHeight
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
                        contentItem: Item {
                            Image {
                                source: subtitleTrackHeader.collapsed ? "image://icon/go-next" : "image://icon/go-down"
                                anchors.centerIn: parent
                                width: root.collapsedHeight - 4
                                height: root.collapsedHeight - 4
                                cache: root.paletteUnchanged
                            }
                        }
                        onClicked: {
                            if (subtitleTrack.height > root.collapsedHeight) {
                                subtitleTrack.height = root.collapsedHeight
                            } else {
                                subtitleTrack.height = 5 * root.baseUnit
                            }
                        }
                    }
                    Label {
882
                        id: subLabel
883
884
885
886
                        anchors.left: expandSubButton.left
                        anchors.top: expandSubButton.bottom
                        font: miniFont
                        text: i18n("Subtitles")
887
                        visible: (subtitleTrackHeader.height > root.collapsedHeight + subLabel.height)
888
                    }
Pistos Pi's avatar
Pistos Pi committed
889

890
891
892
893
894
                    Row {
                        id: subButtonsRow
                        width: childrenRect.width
                        x: Math.max(2 * root.collapsedHeight + 2, parent.width - width - 4)
                        spacing: 0
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
                        ToolButton {
                            id: analyseButton
                            focusPolicy: Qt.NoFocus
                            contentItem: Item {
                                Image {
                                    source: "image://icon/autocorrection"
                                    anchors.centerIn: parent
                                    width: root.collapsedHeight - 4
                                    height: root.collapsedHeight - 4
                                    cache: root.paletteUnchanged
                                }
                            }
                            width: root.collapsedHeight
                            height: root.collapsedHeight
                            onClicked: timeline.triggerAction('audio_recognition')
                            ToolTip {
911
                                visible: analyseButton.hovered
912
913
914
915
916
917
918
919
920
921
922
923
924
                                font: miniFont
                                delay: 1500
                                timeout: 5000
                                background: Rectangle {