timeline.qml 71.7 KB
Newer Older
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
1
import QtQuick 2.6
2
import QtQml.Models 2.2
3
4
import QtQuick.Controls 1.5 as OLD
import QtQuick.Controls.Styles 1.4
5
import QtQuick.Controls 2.2
6
import QtQuick.Layouts 1.3
7
import QtQuick.Dialogs 1.2
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
23
    signal zoomIn(bool onMouse)
    signal zoomOut(bool onMouse)
24
    signal processingDrag(bool dragging)
25

26
27
28
29
    FontMetrics {
        id: fontMetrics
        font.family: "Arial"
    }
30
    ClipMenu {
31
32
        id: clipMenu
    }
33
    CompositionMenu {
34
35
        id: compositionMenu
    }
36

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

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

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

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

53
54
    function updatePalette() {
        root.color = activePalette.window
55
        root.textColor = activePalette.text
56
57
58
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
    }
59

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

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

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    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()
        }
    }
120
121
122
    function getTrackYFromId(a_track) {
        return Logic.getTrackYFromId(a_track)
    }
123

124
125
126
127
    function getTrackYFromMltIndex(a_track) {
        return Logic.getTrackYFromMltIndex(a_track)
    }

128
129
130
131
    function getTracksCount() {
        return Logic.getTracksList()
    }

132
    function getMousePos() {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
133
        return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
134
135
    }

136
137
138
139
140
141
142
143
    function getScrollPos() {
        return scrollView.flickableItem.contentX
    }

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

144
145
146
147
    function getCopiedItemId() {
        return copiedClip
    }

148
    function getMouseTrack() {
149
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
150
151
    }

152
153
154
    function getTrackColor(audio, header) {
        var col = activePalette.alternateBase
        if (audio) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
155
            col = Qt.tint(col, "#06FF00CC")
156
157
        }
        if (header) {
158
            col = Qt.darker(col, 1.05)
159
        }
160
        return col
161
162
    }

163
164
165
166
167
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
168
        scrollTimer.stop()
169
    }
170

171
    function isDragging() {
172
        return dragInProgress
173
    }
174

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

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
    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
    }
213
214
215
216
217
218
    Keys.onDownPressed: {
        root.moveSelectedTrack(1)
    }
    Keys.onUpPressed: {
        root.moveSelectedTrack(-1)
    }
219

220
    property int headerWidth: timeline.headerWidth()
221
    property int activeTool: 0
222
    property real baseUnit: fontMetrics.font.pointSize
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
223
224
    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)
225
    property bool autoScrolling: timeline.autoScroll
226
    property int duration: timeline.duration
227
228
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
229
    property color lockedColor: timeline.lockedColor
Vincent Pinon's avatar
Vincent Pinon committed
230
    property color selectionColor: timeline.selectionColor
231
    property color groupColor: timeline.groupColor
Nicolas Carion's avatar
Nicolas Carion committed
232
    property int clipBeingDroppedId: -1
233
    property string clipBeingDroppedData
234
235
    property int droppedPosition: -1
    property int droppedTrack: -1
236
    property int clipBeingMovedId: -1
237
    property int consumerPosition: proxy.position
238
    property int spacerGroup: -1
239
240
    property int spacerFrame: -1
    property int spacerClickFrame: -1
241
    property real timeScale: timeline.scaleFactor
242
    property real snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? 10 / Math.sqrt(timeScale) - 0.5 : -1
243
    property var timelineSelection: timeline.selection
244
    property int trackHeight
245
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
246
    property int zoomOnMouse: -1
247
    property int viewActiveTrack: timeline.activeTrack
248
249
    property int wheelAccumulatedDelta: 0
    readonly property int defaultDeltasPerStep: 120
250

251
    //onCurrentTrackChanged: timeline.selection = []
252
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
253
        if (root.zoomOnMouse >= 0) {
254
            scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
255
256
            root.zoomOnMouse = -1
        } else {
257
            scrollView.flickableItem.contentX = Math.max(0, root.consumerPosition * timeline.scaleFactor - (scrollView.width / 2))
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
258
        }
259
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
260
        ruler.adjustStepSize()
261
262
263
264
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
265
    }
266

267
268
269
270
    onConsumerPositionChanged: {
        if (autoScrolling) Logic.scrollIfNeeded()
    }

271
272
273
274
275
    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) {
276
            scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
277
278
279
        }
    }

280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    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()
            }
        }
    }

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

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
385
                    timeline.selectItems(ids)
386
387
388
389
390
391
                    id = ids[0]
                }
            }
            return id
        }

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

                var binIds = clipBeingDroppedData.split(";")
                if (binIds.length == 1) {
476
477
478
479
480
                    if (controller.normalEdit()) {
                        timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
                    } else {
                        timeline.insertClipZone(clipBeingDroppedData, track, frame)
                    }
481
                } else {
482
483
484
485
486
487
                    if (controller.normalEdit()) {
                        timeline.insertClips(track, frame, binIds, true, true)
                    } else {
                        // TODO
                        console.log('multiple clips insert/overwrite not supported yet')
                    }
488
                }
489
490
                fakeTrack = -1
                fakeFrame = -1
491
            }
492
            clearDropData()
493
494
        }
    }
495
    OLD.Menu {
496
        id: menu
497
498
        property int clickedX
        property int clickedY
499
500
        onAboutToHide: {
            timeline.ungrabHack()
501
            editGuideMenu.visible = false
502
        }
503
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
504
            text: i18n("Paste")
505
            iconName: 'edit-paste'
506
507
            visible: copiedClip != -1
            onTriggered: {
508
509
510
                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)
511
512
            }
        }
513
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
514
            text: i18n("Insert Space")
515
            onTriggered: {
516
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
517
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
518
519
520
                timeline.insertSpace(track, frame);
            }
        }
521
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
522
            text: i18n("Remove Space On Active Track")
523
            onTriggered: {
524
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
525
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
526
527
528
                timeline.removeSpace(track, frame);
            }
        }
529
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
530
            text: i18n("Remove Space")
531
            onTriggered: {
532
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
533
                var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
534
535
536
                timeline.removeSpace(track, frame, true);
            }
        }
537
        OLD.MenuItem {
538
            id: addGuideMenu
Yuri Chornoivan's avatar
Yuri Chornoivan committed
539
            text: i18n("Add Guide")
540
            onTriggered: {
541
                timeline.switchGuide(root.consumerPosition);
542
543
            }
        }
544
        GuidesMenu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
545
            title: i18n("Go to guide...")
546
547
548
            menuModel: guidesModel
            enabled: guidesDelegateModel.count > 0
            onGuideSelected: {
549
                proxy.position = assetFrame
550
551
            }
        }
552
        OLD.MenuItem {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
553
            id: editGuideMenu
Yuri Chornoivan's avatar
Yuri Chornoivan committed
554
            text: i18n("Edit Guide")
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
555
556
            visible: false
            onTriggered: {
557
                timeline.editGuide(root.consumerPosition);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
558
559
            }
        }
560
        AssetMenu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
561
            title: i18n("Insert a composition...")
562
            menuModel: transitionModel
563
            isTransition: true
564
            onAssetSelected: {
565
                var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
566
567
568
569
570
571
572
                var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                var id = timeline.insertComposition(track, frame, assetId, true)
                if (id == -1) {
                    compositionFail.open()
                }
            }
        }
573
        onAboutToShow: {
574
            if (guidesModel.hasMarker(root.consumerPosition)) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
575
                // marker at timeline position
Yuri Chornoivan's avatar
Yuri Chornoivan committed
576
                addGuideMenu.text = i18n("Remove Guide")
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
577
578
                editGuideMenu.visible = true
            } else {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
579
                addGuideMenu.text = i18n("Add Guide")
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
580
            }
581
582
            console.log("pop menu")
        }
583
    }
584
585
586
587
    OLD.Menu {
        id: rulermenu
        property int clickedX
        property int clickedY
588
589
590
591
        onAboutToHide: {
            timeline.ungrabHack()
            editGuideMenu2.visible = false
        }
592
593
        OLD.MenuItem {
            id: addGuideMenu2
Yuri Chornoivan's avatar
Yuri Chornoivan committed
594
            text: i18n("Add Guide")
595
            onTriggered: {
596
                timeline.switchGuide(root.consumerPosition);
597
598
            }
        }
599
        GuidesMenu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
600
            title: i18n("Go to guide...")
601
602
603
            menuModel: guidesModel
            enabled: guidesDelegateModel.count > 0
            onGuideSelected: {
604
                proxy.position = assetFrame
605
606
            }
        }
607
608
        OLD.MenuItem {
            id: editGuideMenu2
Yuri Chornoivan's avatar
Yuri Chornoivan committed
609
            text: i18n("Edit Guide")
610
611
            visible: false
            onTriggered: {
612
                timeline.editGuide(root.consumerPosition);
613
614
            }
        }
615
616
        OLD.MenuItem {
            id: addProjectNote
Yuri Chornoivan's avatar
Yuri Chornoivan committed
617
            text: i18n("Add Project Note")
618
619
620
621
            onTriggered: {
                timeline.triggerAction('add_project_note')
            }
        }
622
        onAboutToShow: {
623
            if (guidesModel.hasMarker(root.consumerPosition)) {
624
                // marker at timeline position
Yuri Chornoivan's avatar
Yuri Chornoivan committed
625
                addGuideMenu2.text = i18n("Remove Guide")
626
627
                editGuideMenu2.visible = true
            } else {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
628
                addGuideMenu2.text = i18n("Add Guide")
629
630
631
632
            }
            console.log("pop menu")
        }
    }
633
634
635
636
637
638
    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
639
    }
640
    OLD.Menu {
641
        id: headerMenu
642
643
644
        property int trackId: -1
        property int thumbsFormat: 0
        property bool audioTrack: false
645
        property bool recEnabled: false
646
647
648
        onAboutToHide: {
            timeline.ungrabHack()
        }
649
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
650
            text: i18n("Add Track")
651
652
653
            onTriggered: {
                timeline.addTrack(timeline.activeTrack)
            }
654
        }
655
        OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
656
            text: i18n("Delete Track")
657
658
659
            onTriggered: {
                timeline.deleteTrack(timeline.activeTrack)
            }
660
        }
661
662
663
        OLD.MenuItem {
            visible: headerMenu.audioTrack
            id: showRec
Yuri Chornoivan's avatar
Yuri Chornoivan committed
664
            text: i18n("Show Record Controls")
665
666
667
668
669
670
            onTriggered: {
                controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0')
            }
            checkable: true
            checked: headerMenu.recEnabled
        }
671
672
673
        OLD.MenuItem {
            visible: headerMenu.audioTrack
            id: configRec
Yuri Chornoivan's avatar
Yuri Chornoivan committed
674
            text: i18n("Configure Recording")
675
676
677
678
            onTriggered: {
                timeline.showConfig(4,2)
            }
        }
679
        OLD.Menu {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
680
            title: i18n("Track thumbnails")
681
682
683
            visible: !headerMenu.audioTrack
                    OLD.ExclusiveGroup { id: thumbStyle }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
684
                        text: i18n("In frame")
685
686
687
688
689
690
                        id: inFrame
                        onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2)
                        checkable: true
                        exclusiveGroup: thumbStyle
                    }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
691
                        text: i18n("In / out frames")
692
693
694
695
696
697
698
                        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
699
                        text: i18n("All frames")
700
701
702
703
704
705
                        id: allFrame
                        onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1)
                        checkable: true
                        exclusiveGroup: thumbStyle
                    }
                    OLD.MenuItem {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
706
                        text: i18n("No thumbnails")
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
                        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
                        }
                }
        }
729
    }
730

731
732
    Row {
        Column {
733
            id: headerContainer
734
735
736
737
738
739
740
            z: 1
            Rectangle {
                id: cornerstone
                property bool selected: false
                // Padding between toolbar and track headers.
                width: headerWidth
                height: ruler.height
741
                color: 'transparent' //selected? shotcutBlue : activePalette.window
742
743
744
                border.color: selected? 'red' : 'transparent'
                border.width: selected? 1 : 0
                z: 1
745
746
747
748
749
                Button {
                    text: parent.width > metrics.boundingRect.width * 1.4 ? metrics.text : "M"
                    anchors.fill: parent
                    anchors.leftMargin: 2
                    anchors.rightMargin: 2
750
751
752
753
                    ToolTip.delay: 1000
                    ToolTip.timeout: 5000
                    ToolTip.visible: hovered
                    ToolTip.text: i18n("Show master effects")
754
755
756
757
                    TextMetrics {
                        id: metrics
                        text: i18n("Master")
                    }
758
759
760
761
                    onClicked: {
                        timeline.showMasterEffects()
                    }
                }
762
763
764
            }
            Flickable {
                // Non-slider scroll area for the track headers.
765
                id: headerFlick
766
                contentY: scrollView.flickableItem.contentY
767
                width: headerWidth
768
                height: 100
769
770
                interactive: false

771
772
773
774
775
                MouseArea {
                    width: trackHeaders.width
                    height: trackHeaders.height
                    acceptedButtons: Qt.NoButton
                    onWheel: {
776
                        var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
777
778
779
                        scrollView.flickableItem.contentY = Math.max(newScroll, 0)
                    }
                }
780
781
                Column {
                    id: trackHeaders
782
                    spacing: 0
783
784
785
786
787
                    Repeater {
                        id: trackHeaderRepeater
                        model: multitrack
                        TrackHead {
                            trackName: model.name
788
                            thumbsFormat: model.thumbsFormat
789
                            trackTag: model.trackTag
790
                            isDisabled: model.disabled
791
792
                            isComposite: model.composite
                            isLocked: model.locked
793
                            isActive: model.trackActive
794
                            isAudio: model.audio
795
                            showAudioRecord: model.audioRecord
796
797
                            effectNames: model.effectNames
                            isStackEnabled: model.isStackEnabled
798
                            width: headerWidth
799
                            current: item === timeline.activeTrack
800
                            trackId: item
801
                            height: model.trackHeight
802
                            onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
803
                            collapsed: height <= collapsedHeight
804
                            onMyTrackHeightChanged: {
805
                                collapsed = myTrackHeight <= collapsedHeight
806
807
                                if (!collapsed) {
                                    controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
808
809
810
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
                                } else {
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
811
                                }
812
813
                                // hack: change property to trigger transition adjustment
                                root.trackHeight = root.trackHeight === 1 ? 0 : 1
814
                            }
815
                            onClicked: {
816
817
                                timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId
                                console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId)
818
                            }
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
                        }
                    }
                }
                Column {
                    id: trackHeadersResizer
                    spacing: 0
                    width: 5
                    Rectangle {
                        id: resizer
                        height: trackHeaders.height
                        width: 3
                        x: root.headerWidth - 2
                        color: 'red'
                        opacity: 0
                        Drag.active: headerMouseArea.drag.active
                        Drag.proposedAction: Qt.MoveAction

                        MouseArea {
                            id: headerMouseArea
                            anchors.fill: parent
                            hoverEnabled: true
                            cursorShape: Qt.SizeHorCursor
                            drag.target: parent
                            drag.axis: Drag.XAxis
843
                            drag.minimumX: 2 * baseUnit
844
845
846
847
848
                            property double startX
                            property double originalX
                            drag.smoothed: false

                            onPressed: {
849
                                root.autoScrolling = false
850
851
                            }
                            onReleased: {
852
                                root.autoScrolling = timeline.autoScroll
853
854
855
856
857
858
859
860
861
862
863
864
                                parent.opacity = 0
                            }
                            onEntered: parent.opacity = 0.5
                            onExited: parent.opacity = 0
                            onPositionChanged: {
                                if (mouse.buttons === Qt.LeftButton) {
                                    parent.opacity = 0.5
                                    headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
                                    timeline.setHeaderWidth(headerWidth)
                                }
                            }
                        }
865
866
867
868
869
870
                    }
                }
            }
        }
        MouseArea {
            id: tracksArea
871
872
            property real clickX
            property real clickY
873
874
875
876
            width: root.width - headerWidth
            height: root.height
            // This provides continuous scrubbing and scimming at the left/right edges.
            hoverEnabled: true
877
            acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
878
            cursorShape: root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
879
            onWheel: {
880
881
882
883
884
885
886
887
888
                if (wheel.modifiers & Qt.AltModifier) {
                    // Alt + wheel = seek to next snap point
                    if (wheel.angleDelta.x > 0) {
                        timeline.triggerAction('monitor_seek_snap_backward')
                    } else {
                        timeline.triggerAction('monitor_seek_snap_forward')