timeline.qml 92.9 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
16
    property var groupTrimData
    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
    FontMetrics {
        id: fontMetrics
33
        font: miniFont
34
    }
35

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

40
41
42
43
    function endBinDrag() {
        clipDropArea.processDrop()
    }

44
45
46
47
48
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

    function scrollPos() {
49
        return scrollView.contentX
50
51
52
    }

    function goToStart(pos) {
53
        scrollView.contentX = pos
54
55
    }

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

78
    function getActiveTrackStreamPos() {
79
80
        // Return the relative y click position, to display the context menu
        return Logic.getTrackYFromId(timeline.activeTrack) + rulercontainer.height - scrollView.contentY
81
    }
82

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

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

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

Pistos Pi's avatar
Pistos Pi committed
155
156
157
158
159
160
161
162
163
164
165
    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,
166
            trackHeaders.height + subtitleTrackHeader.height - tracksArea.height + horScroll.height + ruler.height
Pistos Pi's avatar
Pistos Pi committed
167
168
169
170
        )
        scrollView.contentY = Math.max(newScroll, 0)
    }

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

201
    function getMousePos() {
202
203
204
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return (dragProxy.masterObject.x + dragProxy.masterObject.mouseXPos) / timeline.scaleFactor
        }
205
206
207
208
209
        if (tracksArea.containsMouse) {
            return (scrollView.contentX + tracksArea.mouseX) / timeline.scaleFactor
        } else {
            return -1;
        }
210
    }
211
212
213
214
215
216
217
218
219
220
    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;
        }
    }
221

222
    function getScrollPos() {
223
        return scrollView.contentX
224
225
226
    }

    function setScrollPos(pos) {
227
        return scrollView.contentX = pos
228
229
    }

230
231
232
233
    function getCopiedItemId() {
        return copiedClip
    }

234
    function getMouseTrack() {
235
236
237
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            return dragProxy.masterObject.trackId
        }
238
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.contentY - subtitleTrack.height)
239
240
    }

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

252
253
254
    function centerViewOnCursor() {
        scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
    }
255

256
257
258
259
260
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
261
        scrollTimer.stop()
262
    }
263

264
    function isDragging() {
265
        return dragInProgress
266
    }
267

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

281
    function endDrag() {
282
        console.log('ENDING DRAG!!!!!!!!!!!!!!!!!!!!!!\n')
283
        dragProxy.draggedItem = -1
284
285
        dragProxy.x = 0
        dragProxy.y = 0
286
287
        dragProxy.width = 0
        dragProxy.height = 0
288
        dragProxy.verticalOffset = 0
289
    }
290
    
291
292
293
    function regainFocus(mousePos) {
        var currentMouseTrack = Logic.getTrackIdFromPos(mousePos.y - ruler.height - subtitleTrack.height + scrollView.contentY)
        // Try to find correct item
294
295
        //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)
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
        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')
        }
311
    }
312

313
314
315
316
317
318
319
320
321
322
    function getAudioTracksCount(){
        var audioCount = 0;
        for (var i = 0; i < trackHeaderRepeater.count; i++) {
            if(trackHeaderRepeater.itemAt(i).isAudio) {
                audioCount++;
            }
        }
        return audioCount;
    }

323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
    function getItemAtPos(tk, posx, isComposition) {
        var track = Logic.getTrackById(tk)
        var container = track.children[0]
        var tentativeClip = undefined
        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)) {
                break
            }
        }
        return tentativeClip
    }
338
339
340
341
342
343
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
344

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

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

405
406
407
    onShowSubtitlesChanged: {
        subtitleTrack.height = showSubtitles? root.baseUnit * 5 : 0
    }
408

409
    //onCurrentTrackChanged: timeline.selection = []
410
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
411
        if (root.zoomOnMouse >= 0) {
412
            scrollView.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - getMouseX())
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
413
414
            root.zoomOnMouse = -1
        } else {
415
            scrollView.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
416
        }
417
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
418
        ruler.adjustStepSize()
419
420
421
422
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
423
    }
424

425
    onConsumerPositionChanged: {
426
        if (root.autoScrolling) Logic.scrollIfNeeded()
427
428
    }

429
430
    onViewActiveTrackChanged: {
        var tk = Logic.getTrackById(timeline.activeTrack)
431
432
433
434
        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) {
            var newY = Math.min(trackHeaders.height + subtitleTrack.height - scrollView.height + scrollView.ScrollBar.horizontal.height, tk.y + tk.height - scrollView.height + subtitleTrack.height)
435
436
437
            if (newY >= 0) {
                scrollView.contentY = newY
            }
438
439
440
        }
    }

441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
    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()
            }
        }
    }

466
467
468
469
470
471
472
    DropArea { //Drop area for compositions
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
        keys: 'kdenlive/composition'
        onEntered: {
473
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
474
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
475
                var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
476
                droppedPosition = frame
477
                if (track >= 0 && !controller.isAudioTrack(track)) {
478
479
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                    clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
480
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
481
482
483
484
485
486
                    drag.acceptProposedAction()
                } else {
                    drag.accepted = false
                }
            }
        }
487
488
        onPositionChanged: {
            if (clipBeingMovedId == -1) {
489
                var track = Logic.getTrackIdFromPos(drag.y + scrollView.contentY - subtitleTrack.height)
490
                if (track !=-1) {
491
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
492
                    if (clipBeingDroppedId >= 0){
493
494
495
496
                        if (controller.isAudioTrack(track)) {
                            // Don't allow moving composition to an audio track
                            track = controller.getCompositionTrackId(clipBeingDroppedId)
                        }
497
                        controller.suggestCompositionMove(clipBeingDroppedId, track, frame, root.consumerPosition, root.snapping)
498
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
499
                    } else if (!controller.isAudioTrack(track)) {
500
                        frame = controller.suggestSnapPoint(frame, root.snapping)
501
502
                        clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
                        clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
503
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
504
                    }
505
506
507
                }
            }
        }
508
        onExited:{
509
510
            if (clipBeingDroppedId != -1) {
                // If we exit, remove composition
511
                controller.requestItemDeletion(clipBeingDroppedId, false)
512
                clearDropData()
513
514
            }
        }
515
516
517
518
519
        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
520
                controller.requestItemDeletion(clipBeingDroppedId, false)
521
                timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
522
            }
523
            clearDropData()
524
        }
525
    }
526
527
528
    DropArea {
        //Drop area for bin/clips
        id: clipDropArea
529
530
531
532
533
534
535
536
537
        /** @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) {
538
                id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
539
            } else {
540
                var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
541
542
543

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
544
                    timeline.selectItems(ids)
545
546
547
548
549
550
                    id = ids[0]
                }
            }
            return id
        }

551
552
        property int fakeFrame: -1
        property int fakeTrack: -1
553
554
555
556
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
557
        keys: 'kdenlive/producerslist'
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
        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()
        }
594
        onEntered: {
595
            if (clipBeingMovedId == -1 && clipBeingDroppedId == -1) {
596
                //var track = Logic.getTrackIdFromPos(drag.y)
597
598
599
600
601
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
602
                if (track >= 0  && track < tracksRepeater.count) {
603
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
604
                    droppedPosition = frame
605
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
Nicolas Carion's avatar
Nicolas Carion committed
606
                    //drag.acceptProposedAction()
607
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
608
609
610
611
                    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
612
613
                        frame = controller.adjustFrame(frame, timeline.activeTrack)
                        clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
614
                        if (clipBeingDroppedId > -1) {
615
616
617
                            var moveData = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, root.snapping)
                            fakeFrame = moveData[0]
                            fakeTrack = moveData[1]
618
619
620
                        } else {
                            drag.accepted = false
                        }
621
                    }
622
                    continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
623
624
625
626
                } else {
                    drag.accepted = false
                }
            }
627
        }
Nicolas Carion's avatar
Nicolas Carion committed
628
        onExited:{
629
630
            if (clipBeingDroppedId != -1 && drag.y < drag.x) {
                // If we exit on top, remove clip
631
                controller.requestItemDeletion(clipBeingDroppedId, false)
632
633
634
                clearDropData()
            } else {
                // Clip is dropped
Nicolas Carion's avatar
Nicolas Carion committed
635
636
            }
        }
637
        onPositionChanged: {
638
            if (clipBeingMovedId == -1) {
639
640
641
642
643
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
644
                if (track >= 0  && track < tracksRepeater.count) {
645
646
                    //timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
                    var targetTrack = tracksRepeater.itemAt(track).trackInternalId
647
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
648
649
650
651
652
                    if (clipBeingDroppedId > -1) {
                        var moveData = controller.suggestClipMove(clipBeingDroppedId, targetTrack, frame, root.consumerPosition, root.snapping)
                        fakeFrame = moveData[0]
                        fakeTrack = moveData[1]
                        timeline.activeTrack = fakeTrack
653
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
654
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
655
                    } else {
656
                        frame = controller.suggestSnapPoint(frame, root.snapping)
657
                        if (controller.normalEdit()) {
658
659
                            timeline.activeTrack = targetTrack
                            clipBeingDroppedId = insertAndMaybeGroup(targetTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
660
661
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
662
663
664
665
666
667
668
                            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
                            }
669
                        }
670
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
671
                    }
672
                }
Nicolas Carion's avatar
Nicolas Carion committed
673
            }
674
675
        }
        onDropped: {
676
            processDrop()
677
678
        }
    }
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
    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) {
705
706
707
708
709
                var yOffset = 0
                if (root.showSubtitles) {
                    yOffset = subtitleTrack.height
                }
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.contentY - yOffset)
710
711
                if (track >= 0  && track < tracksRepeater.count) {
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
712
                    var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
713
714
                    if (clipBeingDroppedId >= 0) {
                        //fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, root.consumerPosition, Math.floor(root.snapping))
715
                        //fakeTrack = timeline.activeTrack
716
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
717
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
718
                    } else {
719
                        frame = controller.suggestSnapPoint(frame, root.snapping)
720
721
722
723
724
725
726
727
                        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
                        }
728
                        continuousScrolling(drag.x + scrollView.contentX, drag.y + scrollView.contentY)
729
730
731
732
733
                    }
                }
            }
        }
        onDropped: {
734
            var frame = Math.round((drag.x + scrollView.contentX) / timeline.scaleFactor)
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
            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()
        }
    }
754

755
756
    Row {
        Column {
757
            id: headerContainer
758
            width: headerWidth
759
            z: 1
760
            Item {
761
                // Padding between toolbar and track headers.
762
                width: parent.width
763
                height: ruler.height
764
                Button {
765
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : i18nc("Initial for Master", "M")
766
767
                    font: miniFont
                    flat: true
768
769
770
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
771
772
773
774
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
775
776
777
778
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
779
780
781
                    onClicked: {
                        timeline.showMasterEffects()
                    }
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
                    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
                        }
                    }
805
                }
806
807
808
            }
            Flickable {
                // Non-slider scroll area for the track headers.
809
                id: headerFlick
810
                contentY: scrollView.contentY
811
812
813
                width: parent.width
                y: ruler.height
                height: root.height - ruler.height
814
                interactive: false
815
                clip: true
816

817
818
                MouseArea {
                    width: trackHeaders.width
819
                    height: trackHeaders.height + subtitleTrackHeader.height
820
821
                    acceptedButtons: Qt.NoButton
                    onWheel: {
822
                        zoomByWheel(wheel)
823
824
                    }
                }
825
                Rectangle {
826
                    id: subtitleTrackHeader
827
                    width: trackHeaders.width
828
829
                    height: subtitleTrack.height
                    property bool collapsed: subtitleTrack.height == root.collapsedHeight
830
                    visible: height > 0
831
832
833
834
835
836
837
                    color: (timeline.activeTrack == -2) ? Qt.tint(getTrackColor(false, false), selectedTrackColor) : getTrackColor(false, false)
                    MouseArea {
                        anchors.fill: parent
                        onClicked: {
                            timeline.activeTrack = -2
                        }
                    }
838
839
840
841
842
                    ToolButton {
                        id: expandSubButton
                        focusPolicy: Qt.NoFocus
                        property var modifier: 0
                        anchors.left: parent.left
843
                        anchors.leftMargin: 1.5 * root.baseUnit
844
                        width: root.collapsedHeight
Pistos Pi's avatar
Pistos Pi committed
845
                        height: root.collapsedHeight
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
                        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 {
864
                        id: subLabel
865
866
867
868
                        anchors.left: expandSubButton.left
                        anchors.top: expandSubButton.bottom
                        font: miniFont
                        text: i18n("Subtitles")
869
                        visible: (subtitleTrackHeader.height > root.collapsedHeight + subLabel.height)
870
                    }
Pistos Pi's avatar
Pistos Pi committed
871

872
873
874
875
876
                    Row {
                        id: subButtonsRow
                        width: childrenRect.width
                        x: Math.max(2 * root.collapsedHeight + 2, parent.width - width - 4)
                        spacing: 0
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
                        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 {
893
                                visible: analyseButton.hovered
894
895
896
897
898
899
900
901
902
903
904
905
906
                                font: miniFont
                                delay: 1500
                                timeout: 5000
                                background: Rectangle {
                                    color: activePalette.alternateBase
                                    border.color: activePalette.light
                                }
                                contentItem: Label {
                                    color: activePalette.text
                                    text: i18n("Speech recognition")
                                }
                            }
                        }
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
                        ToolButton {
                            id: muteButton
                            focusPolicy: Qt.NoFocus
                            contentItem: Item {
                                Image {
                                    source: root.subtitlesDisabled ? "image://icon/view-hidden" : "image://icon/view-visible"
                                    anchors.centerIn: parent
                                    width: root.collapsedHeight - 4
                                    height: root.collapsedHeight - 4
                                    cache: root.paletteUnchanged
                                }
                            }
                            width: root.collapsedHeight
                            height: root.collapsedHeight
                            onClicked: timeline.triggerAction('disable_subtitle')
                            ToolTip {
                                visible: muteButton.hovered
                                font: miniFont
                                delay: 1500
                                timeout: 5000
                                background: Rectangle {
                                    color: activePalette.alternateBase
                                    border.color: activePalette.light
                                }
                                contentItem: Label {
                                    color: activePalette.text
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
933