timeline.qml 69.6 KB
Newer Older
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
1
import QtQuick 2.6
2
import QtQml.Models 2.2
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
3
import QtQuick.Controls 1.4 as OLD
4
import QtQuick.Controls 2.2
5
import QtQuick.Layouts 1.3
6
import QtQuick.Dialogs 1.2
7
import Kdenlive.Controls 1.0
8 9 10 11 12 13
import QtQuick.Window 2.2
import 'Timeline.js' as Logic

Rectangle {
    id: root
    objectName: "timelineview"
14

15 16
    SystemPalette { id: activePalette }
    color: activePalette.window
17
    property bool validMenu: false
18
    property color textColor: activePalette.text
19

20
    signal clipClicked()
21
    signal mousePosChanged(int position)
22 23
    signal zoomIn(bool onMouse)
    signal zoomOut(bool onMouse)
24

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

36 37 38 39 40 41 42 43 44 45 46 47
    function fitZoom() {
        return scrollView.width / (timeline.duration * 1.1)
    }

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

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

48 49
    function updatePalette() {
        root.color = activePalette.window
50
        root.textColor = activePalette.text
51 52 53 54
        playhead.fillColor = activePalette.windowText
        ruler.repaintRuler()
    }

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

68
    function zoomByWheel(wheel) {
69
        if (wheel.modifiers & Qt.AltModifier) {
70
            // Seek to next snap
71 72 73 74 75 76
            if (wheel.angleDelta.x > 0) {
                timeline.triggerAction('monitor_seek_snap_backward')
            } else {
                timeline.triggerAction('monitor_seek_snap_forward')
            }
        } else if (wheel.modifiers & Qt.ControlModifier) {
77
            // Zoom
78 79 80 81 82
            if (wheel.angleDelta.y > 0) {
                root.zoomIn(true);
            } else {
                root.zoomOut(true);
            }
83 84 85 86
        } 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)
87
        } else {
88
            // Horizontal scroll
89
            var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
90
            scrollView.flickableItem.contentX = Math.max(newScroll, 0)
91
        }
92
        wheel.accepted = true
93 94
    }

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
    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()
        }

    }
113 114 115
    function getTrackYFromId(a_track) {
        return Logic.getTrackYFromId(a_track)
    }
116

117 118 119 120
    function getTrackYFromMltIndex(a_track) {
        return Logic.getTrackYFromMltIndex(a_track)
    }

121 122 123 124
    function getTracksCount() {
        return Logic.getTracksList()
    }

125
    function getMousePos() {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
126
        return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
127 128
    }

129 130 131 132 133 134 135 136
    function getScrollPos() {
        return scrollView.flickableItem.contentX
    }

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

137 138 139 140
    function getCopiedItemId() {
        return copiedClip
    }

141
    function getMouseTrack() {
142
        return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
143 144
    }

145 146 147
    function getTrackColor(audio, header) {
        var col = activePalette.alternateBase
        if (audio) {
148
            col = Qt.tint(col, "#06FF00CC")
149 150
        }
        if (header) {
151
            col = Qt.darker(col, 1.05)
152
        }
153
        return col
154 155
    }

156 157 158 159 160
    function clearDropData() {
        clipBeingDroppedId = -1
        droppedPosition = -1
        droppedTrack = -1
        scrollTimer.running = false
161
        scrollTimer.stop()
162
    }
163

164 165 166
    function isDragging() {
        return dragProxy.draggedItem > -1 && dragProxyArea.pressed
    }
167

168
    function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
169
        dragProxy.x = itemObject.modelStart * timeScale
170
        dragProxy.y = itemCoord.y
171
        dragProxy.width = itemObject.clipDuration * timeScale
172 173 174 175 176 177
        dragProxy.height = itemCoord.height
        dragProxy.masterObject = itemObject
        dragProxy.draggedItem = itemId
        dragProxy.sourceTrack = itemTrack
        dragProxy.sourceFrame = itemPos
        dragProxy.isComposition = isComposition
178
        dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
179 180 181 182 183 184 185
    }
    function endDrag() {
        dragProxy.draggedItem = -1
        dragProxy.x = 0
        dragProxy.y = 0
        dragProxy.width = 0
        dragProxy.height = 0
186
        dragProxy.verticalOffset = 0
187 188
    }

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    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
    }

207
    property int headerWidth: timeline.headerWidth()
208
    property int activeTool: 0
209
    property real baseUnit: fontMetrics.font.pointSize
210 211
    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)
212
    property bool stopScrolling: false
213
    property int duration: timeline.duration
214 215
    property color audioColor: timeline.audioColor
    property color videoColor: timeline.videoColor
216
    property color lockedColor: timeline.lockedColor
217
    property color selectionColor: timeline.selectionColor
218
    property color groupColor: timeline.groupColor
219
    property int clipBeingDroppedId: -1
220
    property string clipBeingDroppedData
221 222
    property int droppedPosition: -1
    property int droppedTrack: -1
223
    property int clipBeingMovedId: -1
224
    property int spacerGroup: -1
225 226
    property int spacerFrame: -1
    property int spacerClickFrame: -1
227
    property real timeScale: timeline.scaleFactor
228
    property real snapping: timeline.snap ? 10 / Math.sqrt(timeScale) - 0.5 : -1
229
    property var timelineSelection: timeline.selection
230
    property int trackHeight
231
    property int copiedClip: -1
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
232
    property int zoomOnMouse: -1
233
    property int viewActiveTrack: timeline.activeTrack
234

235
    //onCurrentTrackChanged: timeline.selection = []
236
    onTimeScaleChanged: {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
237
        if (root.zoomOnMouse >= 0) {
238
            scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
239 240 241 242
            root.zoomOnMouse = -1
        } else {
            scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2))
        }
243
        //root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
244
        ruler.adjustStepSize()
245 246 247 248
        if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
            // update dragged item pos
            dragProxy.masterObject.updateDrag()
        }
249
    }
250

251 252 253 254 255
    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) {
256
            scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
257 258 259
        }
    }

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
    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()
            }
        }
    }

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

                // if the clip insertion succeeded, request the clips to be grouped
                if (ids.length > 0) {
365
                    timeline.selectItems(ids)
366 367 368 369 370 371
                    id = ids[0]
                }
            }
            return id
        }

372 373
        property int fakeFrame: -1
        property int fakeTrack: -1
374 375 376 377
        width: root.width - headerWidth
        height: root.height - ruler.height
        y: ruler.height
        x: headerWidth
378
        keys: 'kdenlive/producerslist'
379
        onEntered: {
380
            if (clipBeingMovedId == -1) {
381
                //var track = Logic.getTrackIdFromPos(drag.y)
382
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
383
                if (track >= 0  && track < tracksRepeater.count) {
384 385
                    var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                    droppedPosition = frame
386
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
387
                    //drag.acceptProposedAction()
388
                    clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
389
                    console.log('dropped data: ', clipBeingDroppedData)
390 391 392 393 394 395 396 397
                    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)
                        fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
                        fakeTrack = timeline.activeTrack
                    }
398
                    continuousScrolling(drag.x + scrollView.flickableItem.contentX)
399 400 401 402
                } else {
                    drag.accepted = false
                }
            }
403
        }
404 405
        onExited:{
            if (clipBeingDroppedId != -1) {
406
                controller.requestItemDeletion(clipBeingDroppedId, false)
407
            }
408
            clearDropData()
409
        }
410
        onPositionChanged: {
411
            if (clipBeingMovedId == -1) {
412
                var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
413
                if (track >= 0  && track < tracksRepeater.count) {
414
                    timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
415 416
                    var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
                    if (clipBeingDroppedId >= 0){
417 418 419
                        fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
                        fakeTrack = timeline.activeTrack
                        //controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
420 421
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    } else {
422 423 424 425 426 427 428 429 430
                        frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
                        if (controller.normalEdit()) {
                            clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
                        } else {
                            // we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
                            clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
                            fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
                            fakeTrack = timeline.activeTrack
                        }
431 432
                        continuousScrolling(drag.x + scrollView.flickableItem.contentX)
                    }
433
                }
434
            }
435 436
        }
        onDropped: {
437 438 439
            if (clipBeingDroppedId != -1) {
                var frame = controller.getClipPosition(clipBeingDroppedId)
                var track = controller.getClipTrackId(clipBeingDroppedId)
440 441 442 443
                if (!controller.normalEdit()) {
                    frame = fakeFrame
                    track = fakeTrack
                }
444 445 446 447
                /* 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
448
                controller.requestItemDeletion(clipBeingDroppedId, false)
449 450 451

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

686 687
    Row {
        Column {
688
            id: headerContainer
689 690 691 692 693 694 695
            z: 1
            Rectangle {
                id: cornerstone
                property bool selected: false
                // Padding between toolbar and track headers.
                width: headerWidth
                height: ruler.height
696
                color: 'transparent' //selected? shotcutBlue : activePalette.window
697 698 699 700 701 702
                border.color: selected? 'red' : 'transparent'
                border.width: selected? 1 : 0
                z: 1
            }
            Flickable {
                // Non-slider scroll area for the track headers.
703
                id: headerFlick
704
                contentY: scrollView.flickableItem.contentY
705
                width: headerWidth
706
                height: 100
707 708
                interactive: false

709 710 711 712 713
                MouseArea {
                    width: trackHeaders.width
                    height: trackHeaders.height
                    acceptedButtons: Qt.NoButton
                    onWheel: {
714
                        var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
715 716 717
                        scrollView.flickableItem.contentY = Math.max(newScroll, 0)
                    }
                }
718 719
                Column {
                    id: trackHeaders
720
                    spacing: 0
721 722 723 724 725
                    Repeater {
                        id: trackHeaderRepeater
                        model: multitrack
                        TrackHead {
                            trackName: model.name
726
                            thumbsFormat: model.thumbsFormat
727
                            trackTag: model.trackTag
728
                            isDisabled: model.disabled
729 730
                            isComposite: model.composite
                            isLocked: model.locked
731
                            isActive: model.trackActive
732
                            isAudio: model.audio
733
                            showAudioRecord: model.audioRecord
734 735
                            effectNames: model.effectNames
                            isStackEnabled: model.isStackEnabled
736
                            width: headerWidth
737
                            current: item === timeline.activeTrack
738
                            trackId: item
739
                            height: model.trackHeight
740
                            onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
741
                            collapsed: height <= collapsedHeight
742
                            onMyTrackHeightChanged: {
743
                                collapsed = myTrackHeight <= collapsedHeight
744 745
                                if (!collapsed) {
                                    controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
746 747 748
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
                                } else {
                                    controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
749
                                }
750 751
                                // hack: change property to trigger transition adjustment
                                root.trackHeight = root.trackHeight === 1 ? 0 : 1
752
                            }
753
                            onClicked: {
754 755
                                timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId
                                console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId)
756
                                //timeline.selectTrackHead(currentTrack)
757
                            }
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
                        }
                    }
                }
                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
782
                            drag.minimumX: 2 * baseUnit
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803
                            property double startX
                            property double originalX
                            drag.smoothed: false

                            onPressed: {
                                root.stopScrolling = true
                            }
                            onReleased: {
                                root.stopScrolling = false
                                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)
                                }
                            }
                        }
804 805 806 807 808 809
                    }
                }
            }
        }
        MouseArea {
            id: tracksArea
810 811
            property real clickX
            property real clickY
812 813
            width: root.width - headerWidth
            height: root.height
814 815 816 817 818 819
            Keys.onDownPressed: {
                root.moveSelectedTrack(1)
            }
            Keys.onUpPressed: {
                root.moveSelectedTrack(-1)
            }
820 821
            // This provides continuous scrubbing and scimming at the left/right edges.
            hoverEnabled: true
822
            acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
823
            cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
824
            onWheel: {
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840
                if (wheel.modifiers & Qt.AltModifier) {
                    // Alt + wheel = seek to next snap point
                    if (wheel.angleDelta.x > 0) {
                        timeline.triggerAction('monitor_seek_snap_backward')
                    } else {
                        timeline.triggerAction('monitor_seek_snap_forward')
                    }
                } else {
                    var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
                    if (timeline.seekPosition > -1) {
                        timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
                    } else {
                        timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
                    }
                    timeline.position = timeline.seekPosition
                }
841
            }
842
            onPressed: {
843
                focus = true
844
                if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier)) {
845 846 847 848
                    clickX = mouseX
                    clickY = mouseY
                    return
                }
849
                if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) {
850 851 852 853 854 855 856 857
                        // rubber selection
                        rubberSelect.x = mouse.x + tracksArea.x
                        rubberSelect.y = mouse.y
                        rubberSelect.originX = mouse.x
                        rubberSelect.originY = rubberSelect.y
                        rubberSelect.width = 0
                        rubberSelect.height = 0
                } else if (mouse.button & Qt.LeftButton) {
858 859 860 861 862
                    if (root.activeTool === 1) {
                        // razor tool
                        var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
                        timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId)
                    }
863 864 865 866
                    if (dragProxy.draggedItem > -1) {
                        mouse.accepted = false
                        return
                    }
867
                    if (root.activeTool === 2 && mouse.y > ruler.height) {
868
                        // spacer tool
869
                        var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
870
                        var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor
871
                        var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1
872 873 874 875 876 877
                        spacerGroup = timeline.requestSpacerStartOperation(track, frame)
                        if (spacerGroup > -1) {
                            drag.axis = Drag.XAxis
                            Drag.active = true
                            Drag.proposedAction = Qt.MoveAction
                            spacerClickFrame = frame
878
                            spacerFrame = controller.getItemPosition(spacerGroup)
879
                        }
880
                    } else if (root.activeTool === 0 || mouse.y <= ruler.height) {
881
                        if (mouse.y > ruler.height) {
882
                            controller.requestClearSelection();
883
                        }
884
                        timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
885
                        timeline.position = timeline.seekPosition
886
                    }
887
                } else if (mouse.button & Qt.RightButton) {
888 889
                    menu.clickedX = mouse.x
                    menu.clickedY = mouse.y
890
                    if (mouse.y > ruler.height) {
891
                        timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId
892 893 894 895 896
                        menu.popup()
                    } else {
                        // ruler menu
                        rulermenu.popup()
                    }
897
                }
898
            }
899
            property bool scim: false
900 901 902
            onExited: {
                scim = false
            }
903
            onPositionChanged: {
904
                if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier))) {
905
                    var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
906
                    var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height)
907 908 909 910 911 912
                    scrollView.flickableItem.contentX = Math.max(newScroll, 0)
                    scrollView.flickableItem.contentY = Math.max(vertScroll, 0)
                    clickX = mouseX
                    clickY = mouseY
                    return
                }
913 914 915 916 917 918
                if (!pressed && !rubberSelect.visible && root.activeTool === 1) {
                    cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX
                    if (mouse.modifiers & Qt.ShiftModifier) {
                        timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor)
                    }
                }
919 920 921 922
                if (dragProxy.draggedItem > -1) {
                    mouse.accepted = false
                    return
                }
923 924
                var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor))
                root.mousePosChanged(mousePos)
925
                ruler.showZoneLabels = mouse.y < ruler.height
926 927 928
                if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) {
                    // rubber selection
                    rubberSelect.visible = true
929
                }
930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947
                if (rubberSelect.visible) {
                    var newX = mouse.x
                    var newY = mouse.y
                    if (newX < rubberSelect.originX) {
                        rubberSelect.x = newX + tracksArea.x
                        rubberSelect.width = rubberSelect.originX - newX
                    } else {
                        rubberSelect.x = rubberSelect.originX + tracksArea.x
                        rubberSelect.width = newX - rubberSelect.originX
                    }
                    if (newY < rubberSelect.originY) {
                        rubberSelect.y = newY
                        rubberSelect.height = rubberSelect.originY - newY
                    } else {
                        rubberSelect.y = rubberSelect.originY
                        rubberSelect.height= newY - rubberSelect.originY
                    }
                } else if (mouse.buttons === Qt.LeftButton) {
948
                    if (root.activeTool === 0 || mouse.y < ruler.height) {
949
                        timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1))
950
                        timeline.position = timeline.seekPosition
951 952
                    } else if (root.activeTool === 2 && spacerGroup > -1) {
                        // Move group
953
                        var track = controller.getItemTrackId(spacerGroup)
954
                        var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame
955
                        frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping))
956
                        continuousScrolling(mouse.x + scrollView.flickableItem.contentX)
957
                    }
958
                    scim = true
959
                } else {
960
                    scim = false
961
                }
962
            }
963
            onReleased: {
964 965
                if (rubberSelect.visible) {
                    rubberSelect.visible = false
966 967 968
                    var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY
                    var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y))
                    var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height)
969 970 971
                    if (bottomTrack >= topTrack) {
                        var t = []
                        for (var i = topTrack; i <= bottomTrack; i++) {
972
                            t.push(tracksRepeater.itemAt(i).trackInternalId)
973 974 975
                        }
                        var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor
                        var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor
976
                        timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier);
977
                    }
978
                    rubberSelect.y = -1
979 980
                } else if (mouse.modifiers & Qt.ShiftModifier) {
                    // Shift click, process seek
981
                    timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
982
                    timeline.position = timeline.seekPosition
983
                }
984
                if (spacerGroup > -1) {
985
                    var frame = controller.getItemPosition(spacerGroup)
986 987 988 989 990 991 992
                    timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame);
                    spacerClickFrame = -1
                    spacerFrame = -1
                    spacerGroup = -1
                }
                scim = false
            }
993 994 995 996 997 998
            Timer {
                id: scrubTimer
                interval: 25
                repeat: true
                running: parent.scim && parent.containsMouse
                         && (parent.mouseX < 50 || parent.mouseX > parent.width - 50)
999
                         && (timeline.position * timeline.scaleFactor >= 50)
1000 1001
                onTriggered: {
                    if (parent.mouseX < 50)
1002
                        timeline.seekPosition = Math.max(0, timeline.position - 10)
1003
                    else
1004
                        timeline.seekPosition = Math.min(timeline.position + 10, timeline.fullDuration - 1)
1005 1006 1007 1008 1009 1010
                }
            }

            Column {
                Flickable {
                    // Non-slider scroll area for the Ruler.
1011
                    id: rulercontainer
1012
                    width: root.width - headerWidth