1//=============================================================================
2//  MuseScore
3//  Music Composition & Notation
4//
5//  Copyright (C) 2019 Werner Schweer and others
6//
7//  This program is free software; you can redistribute it and/or modify
8//  it under the terms of the GNU General Public License version 2.
9//
10//  This program is distributed in the hope that it will be useful,
11//  but WITHOUT ANY WARRANTY; without even the implied warranty of
12//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13//  GNU General Public License for more details.
14//
15//  You should have received a copy of the GNU General Public License
16//  along with this program; if not, write to the Free Software
17//  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18//=============================================================================
19
20import QtQuick 2.8
21import QtQuick.Controls 2.1
22import QtQml.Models 2.2
23import MuseScore.Palette 3.3
24import MuseScore.Views 3.3
25import MuseScore.Utils 3.3
26
27import "utils.js" as Utils
28
29GridView {
30    id: paletteView
31    clip: true
32
33    interactive: height < contentHeight // TODO: check if it helps on Mac
34    boundsBehavior: Flickable.StopAtBounds
35
36    property size cellSize
37    property bool drawGrid: false
38    property bool showMoreButton: false
39
40    property var paletteModel
41    property var paletteRootIndex
42    property PaletteController paletteController
43    property var selectionModel: null
44
45    property int ncells: paletteCellDelegateModel.count
46    property bool empty: !ncells
47
48    property bool externalDropBlocked: false
49
50    property bool enableAnimations: true
51
52    states: [
53        State {
54            name: "default"
55        },
56        State {
57            name: "drag"
58            PropertyChanges {
59                target: paletteView
60                // unbind width stretching from the delegate model's state while dragging
61                explicit: true
62                stretchWidth: paletteCellDelegateModel.stretchWidth
63                restoreEntryValues: true
64            }
65        }
66    ]
67
68    state: "default"
69
70    // internal property: whether the palette fits to one row
71    readonly property bool oneRow: cellDefaultWidth * ncells + moreButton.implicitWidth < width
72
73    implicitHeight: {
74        if (!ncells)
75            return cellHeight;
76        if (!showMoreButton || oneRow)
77            return contentHeight;
78
79        const moreButtonCells = Math.ceil(moreButton.implicitWidth / cellWidth);
80        const rowCells = Math.floor(width / cellWidth);
81        const lastRowCells = ncells % rowCells;
82        const freeCells = lastRowCells ? rowCells - lastRowCells : 0;
83
84        if (freeCells >= moreButtonCells)
85            return contentHeight;
86        return contentHeight + cellHeight;
87    }
88
89    readonly property int cellDefaultWidth: cellSize.width
90    property bool stretchWidth: !(oneRow && showMoreButton)
91    cellWidth: stretchWidth ? Math.floor(Utils.stretched(cellDefaultWidth, width)) : cellDefaultWidth
92    cellHeight: cellSize.height
93
94    readonly property real ncolumns: Math.floor(width / cellWidth);
95    readonly property real lastColumnCellWidth : cellWidth + (width % cellWidth) // width of last cell in a row: might be stretched to avoid a gap at row end
96
97    signal moreButtonClicked()
98
99    MouseArea {
100        // Dummy MouseArea to prevent propagation of clicks on empty place to palette's parent
101        z: -1000
102        anchors.fill: parent
103    }
104
105    StyledButton {
106        id: moreButton
107        visible: showMoreButton
108        activeFocusOnTab: this === paletteTree.currentTreeItem
109
110        highlighted: visualFocus || hovered
111
112        background: Rectangle {
113            color: mscore.paletteBackground
114            Rectangle {
115                anchors.fill: parent
116                color: globalStyle.voice1Color
117                opacity: moreButton.down ? 0.4 : (moreButton.highlighted ? 0.2 : 0.0)
118            }
119            border.color: moreButton.activeFocus ? "lightblue" : "transparent" // show current item
120            border.width: 2
121        }
122
123        onActiveFocusChanged: {
124            if (activeFocus) {
125                paletteTree.currentTreeItem = this;
126
127                if (mscore.keyboardModifiers() === Qt.NoModifier)
128                    paletteView.selectionModel.clearSelection();
129            }
130        }
131
132        anchors.bottom: parent.bottom
133        anchors.right: parent.right
134        width: {
135            if (paletteView.empty)
136                return implicitWidth;
137
138            // align to the left border of some palette cell
139            var addition = (parent.width - implicitWidth) % cellWidth - 1; // -1 allows to fit into a cell if palette grid is visible
140            if (addition < 0)
141                addition += cellWidth;
142
143            return implicitWidth + addition;
144        }
145        height: cellHeight - (paletteView.oneRow ? 0 : 1)
146
147        text: qsTr("More")
148        textColor: down ? globalStyle.buttonText : "black"// palette background has white or light color
149        visualFocusTextColor: "darkblue"
150
151        onClicked: paletteView.moreButtonClicked()
152
153        Keys.onShortcutOverride: {
154            // Intercept all keys that we want to use with Keys.onPressed
155            // in case they are assigned as shortcuts in Preferences.
156            event.accepted = true; // intercept everything
157            switch (event.key) {
158                case Qt.Key_Up:
159                case Qt.Key_Down:
160                    return;
161            }
162            event.accepted = false; // allow key to function as shortcut (don't intercept)
163        }
164
165        Keys.onPressed: {
166            // NOTE: All keys must be intercepted with Keys.onShortcutOverride.
167            switch (event.key) {
168                case Qt.Key_Up:
169                    focusPreviousItem();
170                    break;
171                case Qt.Key_Down:
172                    paletteTree.focusNextItem(false);
173                    break;
174                default:
175                    return; // don't accept event
176            }
177            event.accepted = true;
178        }
179
180        function focusPreviousItem() {
181            if (paletteView.count == 0) {
182                paletteTree.currentItem.forceActiveFocus();
183            } else {
184                paletteView.currentIndex = paletteView.count - 1
185                paletteView.currentItem.forceActiveFocus();
186            }
187        }
188    }
189
190    PlaceholderManager {
191        id: placeholder
192        delegateModel: paletteCellDelegateModel
193    }
194
195    Timer {
196        id: dragDropReorderTimer
197        interval: 400
198    }
199
200    PaletteBackground {
201        z: -1
202        anchors.fill: parent
203        drawGrid: parent.drawGrid && !parent.empty
204        offsetX: parent.contentX
205        offsetY: parent.contentY
206        cellWidth: parent.cellWidth
207        cellHeight: parent.cellHeight
208
209        DropArea {
210            id: paletteDropArea
211            anchors { fill: parent/*; margins: 10*/ }
212
213//             keys: [ "application/musescore/symbol", "application/musescore/palette/cell" ]
214
215            property var action
216            property var proposedAction: Qt.IgnoreAction
217            property bool internal: false
218
219            function onDrag(drag) {
220                if (drag.proposedAction != proposedAction) {
221                    onEntered(drag);
222                    return;
223                }
224
225                if (drag.source.dragged) {
226                    drag.source.internalDrag = internal;
227                    drag.source.dragCopy = action == Qt.CopyAction;
228                    paletteView.state = "drag";
229                    drag.source.paletteDrag = true;
230                } else if (typeof drag.source.paletteDrag !== "undefined") // if this is a palette and not, e.g., scoreview
231                    return;
232
233                drag.accept(action); // confirm we accept the action we determined inside onEntered
234
235                var idx = paletteView.indexAt(drag.x, drag.y);
236                if (idx == -1)
237                    idx = paletteView.paletteModel.rowCount(paletteView.paletteRootIndex) - (internal ? 1 : 0);
238
239                if (placeholder.active && placeholder.index == idx)
240                    return;
241                placeholder.makePlaceholder(idx, { decoration: "#eeeeee", toolTip: "placeholder", accessibleText: "", cellActive: false, mimeData: {} });
242            }
243
244            onEntered: {
245                onDragOverPaletteFinished();
246
247                // first check if controller allows dropping this item here
248                const mimeData = Utils.dropEventMimeData(drag);
249                internal = (drag.source.parentModelIndex == paletteView.paletteRootIndex);
250                action = paletteView.paletteController.dropAction(mimeData, drag.proposedAction, paletteView.paletteRootIndex, internal);
251                proposedAction = drag.proposedAction;
252
253                if (action != Qt.MoveAction)
254                    internal = false;
255
256                const accept = (action & drag.supportedActions) && (internal || !externalDropBlocked);
257
258                if (accept)
259                    drag.accept(action);
260                else
261                    drag.accepted = false;
262
263                // If event is accepted, process the drag in a usual way
264                if (drag.accepted)
265                    onDrag(drag);
266            }
267
268            onPositionChanged: onDrag(drag)
269
270            function onDragOverPaletteFinished() {
271                if (placeholder.active) {
272                    placeholder.removePlaceholder();
273                    paletteView.state = "default";
274                }
275                if (drag.source && drag.source.parentModelIndex == paletteView.paletteRootIndex)
276                    drag.source.internalDrag = false;
277            }
278
279            onExited: onDragOverPaletteFinished();
280
281            onDropped: {
282                if (!action) {
283                    onDragOverPaletteFinished();
284                    return;
285                    }
286
287                const destIndex = placeholder.active ? placeholder.index : paletteView.paletteModel.rowCount(paletteView.paletteRootIndex);
288                onDragOverPaletteFinished();
289
290                // Moving cells here causes Drag.onDragFinished be not called properly.
291                // Therefore record the necessary information to move cells later.
292                const data = {
293                    action: action,
294                    srcParentModelIndex: drag.source.parentModelIndex,
295                    srcRowIndex: drag.source.rowIndex,
296                    paletteView: paletteView,
297                    destIndex: destIndex,
298                    mimeData: Utils.dropEventMimeData(drop)
299                };
300
301                if (typeof data.srcParentModelIndex !== "undefined")
302                    drag.source.dropData = data;
303                else
304                    data.paletteView.insertCell(data.destIndex, data.mimeData, data.action);
305
306                drop.accept(action);
307            }
308        }
309    }
310
311    Text {
312        anchors {
313            top: parent.top
314            bottom: parent.bottom
315            left: parent.left; leftMargin: 8
316            right: moreButton.left
317        }
318        visible: parent.empty
319        font: globalStyle.font
320        text: paletteController && paletteController.canDropElements
321            ? qsTr("Drag and drop any element here\n(Use %1+Shift to add custom element from the score)").arg(Qt.platform.os === "osx" ? "Cmd" : "Ctrl")
322            : qsTr("No elements")
323        verticalAlignment: Text.AlignVCenter
324        color: "grey"
325        wrapMode: Text.WordWrap
326        elide: Text.ElideRight
327    }
328
329    add: Transition {
330        id: addTransition
331        enabled: paletteView.enableAnimations
332        readonly property bool unresolvedItem: ViewTransition.item && ViewTransition.item.DelegateModel.isUnresolved;
333        ParallelAnimation {
334            NumberAnimation { property: "scale"; from: 0; to: 1; duration: addTransition.unresolvedItem ? 0 : 150; easing.type: Easing.InOutQuad }
335            NumberAnimation { property: "opacity"; from: 0; to: 1; duration: addTransition.unresolvedItem ? 0 : 250 }
336        }
337    }
338
339    remove: Transition {
340        id: removeTransition
341        enabled: paletteView.enableAnimations && !paletteDropArea.containsDrag
342        readonly property bool unresolvedItem: ViewTransition.item && ViewTransition.item.DelegateModel.isUnresolved;
343        ParallelAnimation {
344            NumberAnimation { property: "scale"; to: 0; duration: removeTransition.unresolvedItem ? 0 : 150; easing.type: Easing.InOutQuad }
345            NumberAnimation { property: "opacity"; to: 0; duration: removeTransition.unresolvedItem ? 0 : 250 }
346        }
347    }
348
349    displaced: Transition {
350        enabled: paletteView.enableAnimations
351        NumberAnimation { properties: "x,y"; duration: 150 }
352    }
353
354    function isSelected(modelIndex) {
355        if (!selectionModel)
356            return false;
357        return selectionModel.isSelected(modelIndex);
358    }
359
360    function moveCell(srcRow, destRow) {
361        return paletteController.move(
362            paletteRootIndex, srcRow,
363            paletteRootIndex, destRow
364        );
365    }
366
367    function insertCell(row, mimeData, action) {
368        return paletteController.insert(paletteRootIndex, row, mimeData, action);
369    }
370
371    function drop(row, mimeData, supportedActions) {
372        // TODO
373    }
374
375    function removeCell(row) {
376        return paletteController.remove(model.modelIndex(row));
377    }
378
379    function removeSelectedCells() {
380        Utils.removeSelectedItems(paletteController, selectionModel, paletteRootIndex);
381    }
382
383    function focusNextItem(flags) {
384        if (flags === undefined)
385            flags = ItemSelectionModel.ClearAndSelect;
386
387        if (currentIndex == count - 1) {
388            if (moreButton.visible)
389                moreButton.forceActiveFocus();
390            else
391                paletteTree.focusNextItem(false);
392        } else {
393            currentIndex++; // next grid item
394        }
395    }
396
397    function focusPreviousItem(flags) {
398        if (flags === undefined)
399            flags = ItemSelectionModel.ClearAndSelect;
400
401        if (currentIndex == 0)
402            paletteTree.currentItem.forceActiveFocus();
403        else
404            currentIndex--; // previous grid item
405    }
406
407    function focusFirstItem() {
408        if (count == 0 && moreButton.visible) {
409            moreButton.forceActiveFocus();
410        } else {
411            currentIndex = 0;
412            currentItem.forceActiveFocus();
413        }
414    }
415
416    function focusLastItem() {
417        if (moreButton.visible) {
418            moreButton.forceActiveFocus();
419        } else {
420            currentIndex = count - 1;
421            currentItem.forceActiveFocus();
422        }
423    }
424
425    function focusNextMatchingItem(str, startIndex) {
426        const modelIndex = paletteModel.index(startIndex, 0, paletteRootIndex);
427        const matchedIndexList = paletteModel.match(modelIndex, Qt.ToolTipRole, str);
428        if (matchedIndexList.length) {
429            currentIndex = matchedIndexList[0].row;
430            currentItem.forceActiveFocus();
431            return true;
432        }
433        return false;
434    }
435
436    function typeAheadFind(chr) {
437        if (paletteTree.typeAheadStr.length) {
438            // continue search on current item
439            const sameChr = chr === paletteTree.typeAheadStr;
440            paletteTree.typeAheadStr += chr;
441            const found = focusNextMatchingItem(paletteTree.typeAheadStr, currentIndex);
442            if (found || !sameChr)
443                return;
444        }
445        // start new search on next item
446        paletteTree.typeAheadStr = chr;
447        const nextIndex = (currentIndex === count - 1) ? 0 : currentIndex + 1;
448        focusNextMatchingItem(chr, nextIndex);
449    }
450
451    function updateSelection(itemPressed) {
452        if (itemPressed === undefined)
453            itemPressed = false; // reason function was called
454
455        const modifiers = mscore.keyboardModifiers();
456        const shiftHeld = modifiers & Qt.ShiftModifier;
457        const ctrlHeld = modifiers & Qt.ControlModifier;
458        const herePreviously = selectionModel.currentIndex.parent === paletteRootIndex;
459
460        if (!ctrlHeld || !herePreviously)
461            selectionModel.clearSelection();
462
463        if (shiftHeld && herePreviously)
464            selectionModel.selectRange(currentItem.modelIndex);
465        else if (ctrlHeld)
466            selectionModel.setCurrentIndex(currentItem.modelIndex, itemPressed ? ItemSelectionModel.Toggle : ItemSelectionModel.NoUpdate);
467        else
468            selectionModel.setCurrentIndex(currentItem.modelIndex, ItemSelectionModel.Select);
469    }
470
471    Keys.onShortcutOverride: {
472        // Intercept all keys that we want to use with Keys.onPressed
473        // in case they are assigned as shortcuts in Preferences.
474        event.accepted = true; // intercept everything
475        switch (event.key) {
476            case Qt.Key_Up:
477            case Qt.Key_Down:
478            case Qt.Key_Left:
479            case Qt.Key_Right:
480            case Qt.Key_Backspace:
481            case Qt.Key_Delete:
482                return;
483        }
484        event.accepted = false; // allow key to function as shortcut (don't intercept)
485    }
486
487    Keys.onPressed: {
488        // NOTE: All keys must be intercepted with Keys.onShortcutOverride.
489        switch (event.key) {
490            case Qt.Key_Up:
491                focusPreviousItem();
492                break;
493            case Qt.Key_Down:
494                focusNextItem();
495                break;
496            case Qt.Key_Left:
497                paletteTree.currentItem.forceActiveFocus();
498                break;
499            case Qt.Key_Right:
500                if (moreButton.visible)
501                    moreButton.forceActiveFocus();
502                break;
503            case Qt.Key_Backspace:
504            case Qt.Key_Delete:
505                removeSelectedCells();
506                break;
507            default:
508                return; // don't accept event
509        }
510        event.accepted = true;
511    }
512
513    model: DelegateModel {
514        id: paletteCellDelegateModel
515//         model: paletteView.visible ? paletteView.paletteModel : null // TODO: use this optimization? TODO: apply it manually where appropriate (Custom palette breaks)
516        model: paletteView.paletteModel
517        rootIndex: paletteView.paletteRootIndex
518
519        delegate: ItemDelegate {
520            id: paletteCell
521            property int rowIndex: index
522            property var modelIndex: paletteView.model.modelIndex(index)
523            property var parentModelIndex: paletteView.paletteRootIndex
524
525            onActiveFocusChanged: {
526                if (activeFocus) {
527                    paletteTree.currentTreeItem = this;
528                    paletteView.updateSelection(false);
529                }
530            }
531
532            opacity: enabled ? 1.0 : 0.3
533
534            readonly property bool dragged: Drag.active && !dragDropReorderTimer.running
535            property bool paletteDrag: false
536            property bool internalDrag: false
537            property bool dragCopy: false
538
539            property bool selected: (paletteView.selectionModel && paletteView.selectionModel.hasSelection) ? paletteView.isSelected(modelIndex) : false // hasSelection is to trigger property bindings if selection changes, see https://doc.qt.io/qt-5/qml-qtqml-models-itemselectionmodel.html#hasSelection-prop
540
541            highlighted: visualFocus || hovered || !!model.cellActive
542
543            width: paletteView.cellWidth
544            height: paletteView.cellHeight
545
546            activeFocusOnTab: this === paletteTree.currentTreeItem
547
548            contentItem: QmlIconView {
549                id: icon
550                visible: !parent.paletteDrag || parent.dragCopy
551                anchors.fill: parent
552                icon: model.decoration
553                selected: false // TODO: remove properties?
554                active: false // TODO: remove properties?
555            }
556
557            background: Rectangle {
558                color: "transparent"
559                border.color: paletteCell.activeFocus ? "lightblue" : "transparent" // show current item
560                border.width: 2
561                width: ((paletteCell.rowIndex + 1) % paletteView.ncolumns) ? paletteView.cellWidth : paletteView.lastColumnCellWidth
562
563                Rectangle {
564                    id: cellBackground
565                    anchors.fill: parent
566                    color: globalStyle.voice1Color
567                    opacity: 0.0
568                }
569            }
570
571            onStateChanged: {
572                console.debug("STATE CHANGED " + state)
573            }
574
575            states: [
576                // Note: if "when" is true for multiple states then
577                // the first state listed here takes precendence.
578
579                State {
580                    name: "PRESSED"
581                    when: leftClickArea.pressed
582
583                    PropertyChanges { target: cellBackground; opacity: 0.75 }
584                },
585
586                State {
587                    name: "SELECTED"
588                    when: selected
589
590                    PropertyChanges { target: cellBackground; opacity: 0.5 }
591                },
592
593                State {
594                    name: "HOVERED"
595                    when: highlighted
596
597                    PropertyChanges { target: cellBackground; opacity: 0.2 }
598                }
599            ]
600
601            readonly property var toolTip: model.toolTip
602
603            onHoveredChanged: {
604                if (hovered) {
605                    mscore.tooltip.item = paletteCell;
606                    mscore.tooltip.text = paletteCell.toolTip ? paletteCell.toolTip : "";
607                } else if (mscore.tooltip.item == paletteCell)
608                    mscore.tooltip.item = null;
609            }
610
611            text: model.accessibleText; // Accessible.name is ignored for some reason
612            Accessible.selectable: true;
613            Accessible.selected: selected;
614
615            Keys.onShortcutOverride: {
616                // Intercept all keys that we want to use with Keys.onPressed
617                // in case they are assigned as shortcuts in Preferences.
618                event.accepted = true; // intercept everything
619                switch (event.key) {
620                    case Qt.Key_Space:
621                    case Qt.Key_Enter:
622                    case Qt.Key_Return:
623                    case Qt.Key_Menu:
624                    case Qt.Key_Asterisk:
625                        return;
626                }
627                if (event.key === Qt.Key_F10 && event.modifiers & Qt.ShiftModifier)
628                    return;
629                if (event.text.match(/[^\x00-\x20\x7F]+$/) !== null)
630                    return;
631                event.accepted = false; // allow key to function as shortcut (don't intercept)
632            }
633
634            Keys.onPressed: {
635                // NOTE: All keys must be intercepted with Keys.onShortcutOverride.
636                const shiftHeld = event.modifiers & Qt.ShiftModifier;
637                const ctrlHeld = event.modifiers & Qt.ControlModifier;
638                switch (event.key) {
639                    case Qt.Key_Space:
640                        if (paletteTree.typeAheadStr.length)
641                            paletteView.typeAheadFind(' ');
642                        else
643                            paletteView.updateSelection(true);
644                        break;
645                    case Qt.Key_Enter:
646                    case Qt.Key_Return:
647                        paletteView.selectionModel.setCurrentIndex(modelIndex, ItemSelectionModel.ClearAndSelect);
648                        paletteView.paletteController.applyPaletteElement(modelIndex, mscore.keyboardModifiers());
649                        break;
650                    case Qt.Key_F10:
651                        if (!shiftHeld)
652                            return;
653                        // fallthrough
654                    case Qt.Key_Menu:
655                        showCellMenu();
656                        break;
657                    case Qt.Key_Asterisk:
658                        if (paletteTree.typeAheadStr.length)
659                            paletteView.typeAheadFind('*');
660                        else if (!paletteTree.expandCollapseAll(null))
661                            paletteTree.currentItem.forceActiveFocus();
662                        break;
663                    default:
664                        if (event.text.match(/[^\x00-\x20\x7F]+$/) !== null) {
665                            // Pressed non-control character(s) (e.g. "D") so go
666                            // to matching item (e.g. "D Major" in keysig palette)
667                            paletteView.typeAheadFind(event.text);
668                        }
669                        else {
670                            return; // don't accept event
671                        }
672                }
673                event.accepted = true;
674            }
675
676            MouseArea {
677                id: leftClickArea
678                anchors.fill: parent
679                drag.target: this
680
681                onPressed: {
682                    paletteView.currentIndex = paletteCell.rowIndex;
683                    paletteCell.forceActiveFocus();
684                    paletteView.updateSelection(true);
685                    paletteCell.beginDrag();
686                }
687
688                onClicked: {
689                    if (paletteView.paletteController.applyPaletteElement(paletteCell.modelIndex, mscore.keyboardModifiers()))
690                        paletteView.selectionModel.setCurrentIndex(paletteCell.modelIndex, ItemSelectionModel.Current);
691                }
692
693                onDoubleClicked: {
694                    const index = paletteCell.modelIndex;
695                    paletteView.selectionModel.setCurrentIndex(index, ItemSelectionModel.Current);
696                    paletteView.paletteController.applyPaletteElement(index, mouse.modifiers);
697                }
698            }
699
700            MouseArea {
701                id: rightClickArea
702                anchors.fill: parent
703                acceptedButtons: Qt.RightButton
704
705                onClicked: showCellMenu(true)
706            }
707
708            Drag.active: leftClickArea.drag.active
709            Drag.dragType: Drag.Automatic
710            Drag.supportedActions: Qt.CopyAction | (model.editable ? Qt.MoveAction : 0)
711            Drag.mimeData: Drag.active ? mimeData : {}
712
713            onInternalDragChanged: {
714                if (internalDrag && dragDropReorderTimer.running)
715                    return;
716                DelegateModel.inItems = !internalDrag;
717            }
718            onDraggedChanged: DelegateModel.inItems = !internalDrag;
719
720            property var dropData: null
721
722            Drag.onDragStarted: {
723                paletteView.state = "drag";
724                DelegateModel.inPersistedItems = true;
725            }
726
727            Drag.onDragFinished: {
728                paletteView.state = "default";
729                paletteDrag = false;
730                internalDrag = false;
731                DelegateModel.inPersistedItems = false;
732
733                if (dropData) {
734                    var data = dropData;
735                    if (data.action == Qt.MoveAction && data.srcParentModelIndex == data.paletteView.paletteRootIndex)
736                        data.paletteView.moveCell(data.srcRowIndex, data.destIndex);
737                    else
738                        data.paletteView.insertCell(data.destIndex, data.mimeData, data.action);
739
740                    dropData = null;
741                }
742            }
743//                             Drag.hotSpot: Qt.point(64, 0) // TODO
744
745            function beginDrag() {
746                icon.grabToImage(function(result) {
747                        Drag.imageSource = result.url
748                        dragDropReorderTimer.restart();
749                    })
750            }
751
752            function showCellMenu(useCursorPos) {
753                if (useCursorPos === undefined)
754                    useCursorPos = false;
755                contextMenu.modelIndex = modelIndex;
756                contextMenu.canEdit = paletteView.paletteController.canEdit(paletteView.paletteRootIndex);
757                if (useCursorPos)
758                    contextMenu.popup();
759                else {
760                    contextMenu.x = x + width;
761                    contextMenu.y = y;
762                    contextMenu.open();
763                }
764            }
765
766            Connections {
767                // force not hiding palette cell if it is being dragged to a score
768                enabled: paletteCell.paletteDrag
769                target: mscore
770                onElementDraggedToScoreView: paletteCell.paletteDrag = false
771            }
772        } // end ItemDelegate
773    } // end DelegateModel
774
775    Menu {
776        id: contextMenu
777        property var modelIndex: null
778        property bool canEdit: true
779
780        MenuItem {
781            enabled: contextMenu.canEdit
782            text: qsTr("Delete")
783            onTriggered: paletteView.paletteController.remove(contextMenu.modelIndex)
784        }
785        MenuItem {
786            enabled: contextMenu.canEdit
787            text: qsTr("Properties…")
788            onTriggered: paletteView.paletteController.editCellProperties(contextMenu.modelIndex)
789        }
790    }
791}
792