From 426c3e54dcc8c9bd89145d97b7e65e5eeedbdc5b Mon Sep 17 00:00:00 2001 From: "Antoine C." Date: Mon, 26 May 2025 16:06:06 +0000 Subject: [PATCH] feat: add Library cappability on QML --- CMakeLists.txt | 14 + res/qml/ActionButton.qml | 47 +++ res/qml/ActionPopup.qml | 69 ++++ res/qml/DeckInfoBar.qml | 13 +- res/qml/InputField.qml | 48 +++ res/qml/Library.qml | 205 ++++++------ res/qml/Library/Browser.qml | 223 +++++++++++++ res/qml/Library/Cell.qml | 104 ++++++ .../Control.qml} | 34 +- .../ControlLoadSelectedTrackHandler.qml} | 0 res/qml/Library/SourceTree.qml | 194 ++++++++++++ res/qml/Library/Track.qml | 131 ++++++++ res/qml/Library/TrackList.qml | 295 ++++++++++++++++++ res/qml/PreviewDeck.qml | 42 +++ res/qml/Sampler.qml | 9 +- res/qml/Theme/Theme.qml | 17 +- res/qml/images/library_computer.png | Bin 0 -> 2442 bytes res/qml/images/library_crates.png | Bin 0 -> 1651 bytes res/qml/images/library_playlist.png | Bin 0 -> 1610 bytes src/library/browse/browsetablemodel.cpp | 4 +- src/library/sidebarmodel.h | 4 +- src/library/treeitemmodel.h | 2 +- src/main.cpp | 5 + src/qml/qmlconfigproxy.h | 4 + src/qml/qmllibraryproxy.cpp | 28 +- src/qml/qmllibraryproxy.h | 23 +- src/qml/qmllibrarysource.cpp | 61 ++++ src/qml/qmllibrarysource.h | 116 +++++++ src/qml/qmllibrarysourcetree.cpp | 80 +++++ src/qml/qmllibrarysourcetree.h | 65 ++++ src/qml/qmllibrarytracklistcolumn.cpp | 25 ++ src/qml/qmllibrarytracklistcolumn.h | 86 +++++ src/qml/qmllibrarytracklistmodel.cpp | 221 ++++++++++--- src/qml/qmllibrarytracklistmodel.h | 92 +++++- src/qml/qmlsidebarmodelproxy.cpp | 88 ++++++ src/qml/qmlsidebarmodelproxy.h | 55 ++++ src/qml/qmlwaveformoverview.h | 2 +- 37 files changed, 2181 insertions(+), 225 deletions(-) create mode 100644 res/qml/ActionButton.qml create mode 100644 res/qml/ActionPopup.qml create mode 100644 res/qml/InputField.qml create mode 100644 res/qml/Library/Browser.qml create mode 100644 res/qml/Library/Cell.qml rename res/qml/{LibraryControl.qml => Library/Control.qml} (68%) rename res/qml/{LibraryControlLoadSelectedTrackHandler.qml => Library/ControlLoadSelectedTrackHandler.qml} (100%) create mode 100644 res/qml/Library/SourceTree.qml create mode 100644 res/qml/Library/Track.qml create mode 100644 res/qml/Library/TrackList.qml create mode 100644 res/qml/PreviewDeck.qml create mode 100644 res/qml/images/library_computer.png create mode 100644 res/qml/images/library_crates.png create mode 100644 res/qml/images/library_playlist.png create mode 100644 src/qml/qmllibrarysource.cpp create mode 100644 src/qml/qmllibrarysource.h create mode 100644 src/qml/qmllibrarysourcetree.cpp create mode 100644 src/qml/qmllibrarysourcetree.h create mode 100644 src/qml/qmllibrarytracklistcolumn.cpp create mode 100644 src/qml/qmllibrarytracklistcolumn.h create mode 100644 src/qml/qmlsidebarmodelproxy.cpp create mode 100644 src/qml/qmlsidebarmodelproxy.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 25874a9aa390..c346035ba195 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3474,6 +3474,15 @@ if(QML) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml) qt_add_library(mixxx-qml-lib STATIC) + + if(WIN32) + target_compile_definitions(mixxx-qml-lib PUBLIC __WINDOWS__) + endif() + + if(ENGINEPRIME) + target_compile_definitions(mixxx-qml-lib PUBLIC __ENGINEPRIME__) + endif() + foreach(component ${QT_COMPONENTS}) target_link_libraries( mixxx-qml-lib @@ -3555,10 +3564,15 @@ if(QML) src/qml/qmleffectslotproxy.cpp src/qml/qmleffectsmanagerproxy.cpp src/qml/qmllibraryproxy.cpp + src/qml/qmllibrarysource.cpp + src/qml/qmllibrarysourcetree.cpp src/qml/qmllibrarytracklistmodel.cpp src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlplayermanagerproxy.cpp src/qml/qmlplayerproxy.cpp + src/qml/qmlsidebarmodelproxy.cpp + src/qml/qmllibrarytracklistcolumn.cpp + src/qml/qmltrackproxy.cpp src/qml/qmlvisibleeffectsmodel.cpp src/qml/qmlwaveformdisplay.cpp src/qml/qmlwaveformoverview.cpp diff --git a/res/qml/ActionButton.qml b/res/qml/ActionButton.qml new file mode 100644 index 000000000000..77e57dca5dd1 --- /dev/null +++ b/res/qml/ActionButton.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Controls 2.12 +import Qt5Compat.GraphicalEffects +import "Theme" + +AbstractButton { + id: root + enum Category { + None, + Danger, + Action + } + + property var category: ActionButton.Category.None + property alias label: labelField + + implicitHeight: 24 + background: Item { + Rectangle { + id: content + anchors.fill: parent + color: root.category == ActionButton.Category.Action ? '#2D4EA1' : root.category == ActionButton.Category.Danger ? '#7D3B3B' : '#3F3F3F' + radius: 4 + } + DropShadow { + anchors.fill: parent + source: content + horizontalOffset: 0 + verticalOffset: 0 + radius: 8.0 + color: "#80000000" + } + } + contentItem: Item { + Label { + id: labelField + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family: Theme.fontFamily + font.capitalization: Font.AllUppercase + font.bold: true + font.pixelSize: Theme.buttonFontPixelSize + color: Theme.white + } + } +} diff --git a/res/qml/ActionPopup.qml b/res/qml/ActionPopup.qml new file mode 100644 index 000000000000..b8b30ea392e8 --- /dev/null +++ b/res/qml/ActionPopup.qml @@ -0,0 +1,69 @@ +import QtQml +import QtQuick +import QtQml.Models +import QtQuick.Layouts +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.12 +import Qt5Compat.GraphicalEffects +import "Theme" + +Popup { + id: root + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + width: 200 + + padding: 0 + margins: 0 + leftInset: 0 + + default property alias children: content.children + + contentItem: Item { + ColumnLayout { + spacing: 2 + anchors.fill: parent + anchors.leftMargin: 20 + id: content + } + } + + background: Item { + Item { + id: content3 + anchors.fill: parent + Shape { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + implicitHeight: 20 + ShapePath { + strokeWidth: 0 + strokeColor: 'transparent' + fillColor: Theme.backgroundColor + fillRule: ShapePath.OddEvenFill + + startX: 0 + startY: 10 + PathLine { x: 20; y: 0 } + PathLine { x: 20; y: 20 } + PathLine { x: 0; y: 10 } + } + } + Rectangle { + anchors.fill: parent + anchors.right: parent.right + anchors.leftMargin: 20 + border.width: 0 + radius: 8 + color: Theme.backgroundColor + } + } + DropShadow { + anchors.fill: parent + source: content3 + horizontalOffset: 0 + verticalOffset: 0 + radius: 8.0 + color: "#80000000" + } + } +} diff --git a/res/qml/DeckInfoBar.qml b/res/qml/DeckInfoBar.qml index 8dc7326dc1e4..cedb6e2ba965 100644 --- a/res/qml/DeckInfoBar.qml +++ b/res/qml/DeckInfoBar.qml @@ -11,6 +11,7 @@ Rectangle { required property string group required property int rightColumnWidth property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) + property var currentTrack: deckPlayer.currentTrack property color lineColor: Theme.deckLineColor border.width: 2 @@ -26,7 +27,7 @@ Rectangle { anchors.bottom: parent.bottom anchors.margins: 5 width: height - source: root.deckPlayer.coverArtUrl + source: root.currentTrack.coverArtUrl visible: false asynchronous: true } @@ -95,7 +96,7 @@ Rectangle { Skin.EmbeddedText { id: infoBarTitle - text: root.deckPlayer.title + text: root.currentTrack.title anchors.top: infoBarHSeparator1.top anchors.left: infoBarVSeparator.left anchors.right: infoBarHSeparator1.left @@ -119,7 +120,7 @@ Rectangle { Skin.EmbeddedText { id: infoBarArtist - text: root.deckPlayer.artist + text: root.currentTrack.artist anchors.top: infoBarVSeparator.bottom anchors.left: infoBarVSeparator.left anchors.right: infoBarHSeparator1.left @@ -144,7 +145,7 @@ Rectangle { Skin.EmbeddedText { id: infoBarKey - text: root.deckPlayer.keyText + text: root.currentTrack.keyText anchors.top: infoBarHSeparator1.top anchors.bottom: infoBarVSeparator.top anchors.right: infoBarHSeparator2.left @@ -206,11 +207,11 @@ Rectangle { GradientStop { position: 0 color: { - const trackColor = root.deckPlayer.color; + const trackColor = root.currentTrack.color; if (!trackColor.valid) return Theme.deckBackgroundColor; - return Qt.darker(root.deckPlayer.color, 2); + return Qt.darker(root.currentTrack.color, 2); } } diff --git a/res/qml/InputField.qml b/res/qml/InputField.qml new file mode 100644 index 000000000000..43dc1cc2ffe4 --- /dev/null +++ b/res/qml/InputField.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls 2.15 +import Qt5Compat.GraphicalEffects +import "Theme" + +FocusScope { + id: root + + property alias input: inputField + + Rectangle { + id: backgroundInput + radius: 4 + color: '#232323' + anchors.fill: parent + } + DropShadow { + id: dropSetting + anchors.fill: parent + horizontalOffset: 0 + verticalOffset: 0 + radius: 4.0 + color: "#000000" + source: backgroundInput + } + InnerShadow { + id: effect2 + anchors.fill: parent + source: dropSetting + spread: 0.2 + radius: 12 + samples: 24 + horizontalOffset: 0 + verticalOffset: 0 + color: "#353535" + } + TextInput { + id: inputField + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: 7 + focus: true + clip: true + color: acceptableInput ? "#FFFFFF" : "#7D3B3B" + horizontalAlignment: TextInput.AlignLeft + } +} diff --git a/res/qml/Library.qml b/res/qml/Library.qml index 3f94320aac26..7be4f4ade0a5 100644 --- a/res/qml/Library.qml +++ b/res/qml/Library.qml @@ -1,140 +1,115 @@ +import "." as Skin import Mixxx 1.0 as Mixxx -import QtQuick 2.12 +import Qt.labs.qmlmodels +import QtQml +import QtQuick +import QtQml.Models +import QtQuick.Layouts +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.6 import "Theme" +import "Library" as LibraryComponent Item { - Rectangle { - color: Theme.deckBackgroundColor - anchors.fill: parent - - LibraryControl { - id: libraryControl - - onMoveVertical: (offset) => { - listView.moveSelectionVertical(offset); - } - onLoadSelectedTrack: (group, play) => { - listView.loadSelectedTrack(group, play); - } - onLoadSelectedTrackIntoNextAvailableDeck: (play) => { - listView.loadSelectedTrackIntoNextAvailableDeck(play); - } - onFocusWidgetChanged: { - switch (focusWidget) { - case FocusedWidgetControl.WidgetKind.LibraryView: - listView.forceActiveFocus(); - break; - } - } - } + id: root - ListView { - id: listView + property var sidebar: librarySources.sidebar() - function moveSelectionVertical(value) { - if (value == 0) - return ; - - const rowCount = model.rowCount(); - if (rowCount == 0) - return ; - - currentIndex = Mixxx.MathUtils.positiveModulo(currentIndex + value, rowCount); - } - - function loadSelectedTrackIntoNextAvailableDeck(play) { - const url = model.get(currentIndex).fileUrl; - if (!url) - return ; - - Mixxx.PlayerManager.loadLocationUrlIntoNextAvailableDeck(url, play); - } - - function loadSelectedTrack(group, play) { - const url = model.get(currentIndex).fileUrl; - if (!url) - return ; - - const player = Mixxx.PlayerManager.getPlayer(group); - if (!player) - return ; + LibraryComponent.SourceTree { + id: librarySources + } - player.loadTrackFromLocationUrl(url, play); - } + SplitView { + id: librarySplitView + orientation: Qt.Horizontal + anchors.fill: parent - anchors.fill: parent - anchors.margins: 10 + handle: Rectangle { + id: handleDelegate + implicitWidth: 8 + implicitHeight: 8 + color: Theme.panelSplitterBackground clip: true - keyNavigationWraps: true - highlightMoveDuration: 250 - highlightResizeDuration: 50 - model: Mixxx.Library.model - Keys.onPressed: (event) => { - switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - listView.loadSelectedTrackIntoNextAvailableDeck(false); - break; + property color handleColor: SplitHandle.pressed || SplitHandle.hovered ? Theme.panelSplitterHandleActive : Theme.panelSplitterHandle + property int handleSize: SplitHandle.pressed || SplitHandle.hovered ? 6 : 5 + + ColumnLayout { + anchors.centerIn: parent + Repeater { + model: 3 + Rectangle { + width: handleSize + height: handleSize + radius: handleSize + color: handleColor + } } } - delegate: Item { - id: itemDlgt - - required property int index - required property url fileUrl - required property string artist - required property string title - - implicitWidth: listView.width - implicitHeight: 30 - - Text { - anchors.verticalCenter: parent.verticalCenter - text: itemDlgt.artist + " - " + itemDlgt.title - color: (listView.currentIndex == itemDlgt.index && listView.activeFocus) ? Theme.blue : Theme.deckTextColor + containmentMask: Item { + x: (handleDelegate.width - width) / 2 + width: 8 + height: librarySplitView.height + } + } - Behavior on color { - ColorAnimation { - duration: listView.highlightMoveDuration + SplitView { + id: sideBarSplitView + SplitView.minimumWidth: 100 + SplitView.preferredWidth: 415 + SplitView.maximumWidth: 600 + + orientation: Qt.Vertical + + handle: Rectangle { + id: handleDelegate + implicitWidth: 8 + implicitHeight: 8 + color: Theme.panelSplitterBackground + clip: true + property color handleColor: SplitHandle.pressed || SplitHandle.hovered ? Theme.panelSplitterHandleActive : Theme.panelSplitterHandle + property int handleSize: SplitHandle.pressed || SplitHandle.hovered ? 6 : 5 + + RowLayout { + anchors.centerIn: parent + Repeater { + model: 3 + Rectangle { + width: handleSize + height: handleSize + radius: handleSize + color: handleColor } } } - Image { - id: dragItem - - Drag.active: dragArea.drag.active - Drag.dragType: Drag.Automatic - Drag.supportedActions: Qt.CopyAction - Drag.mimeData: { - "text/uri-list": itemDlgt.fileUrl, - "text/plain": itemDlgt.fileUrl - } - anchors.fill: parent + containmentMask: Item { + x: (handleDelegate.width - width) / 2 + height: 8 + width: sideBarSplitView.width } + } + LibraryComponent.Browser { + SplitView.minimumHeight: 200 + SplitView.preferredHeight: 500 + SplitView.fillHeight: true - MouseArea { - id: dragArea - - anchors.fill: parent - drag.target: dragItem - onPressed: { - listView.forceActiveFocus(); - listView.currentIndex = itemDlgt.index; - parent.grabToImage((result) => { - dragItem.Drag.imageSource = result.url; - }); - } - onDoubleClicked: listView.loadSelectedTrackIntoNextAvailableDeck(false) - } + model: root.sidebar } - highlight: Rectangle { - border.color: listView.activeFocus ? Theme.blue : Theme.deckTextColor - border.width: 1 - color: "transparent" + Skin.PreviewDeck { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 100 + SplitView.maximumHeight: 200 } } + LibraryComponent.TrackList { + SplitView.fillHeight: true + + // FIXME: this is necessary to prevent the header label to render outside of the table when horizontally scrolling: https://github.com/mixxxdj/mixxx/pull/14514#issuecomment-3311914346 + clip: true + + model: root.sidebar.tracklist + } } } diff --git a/res/qml/Library/Browser.qml b/res/qml/Library/Browser.qml new file mode 100644 index 000000000000..3e28934e6b8a --- /dev/null +++ b/res/qml/Library/Browser.qml @@ -0,0 +1,223 @@ +import ".." as Skin +import Mixxx 1.0 as Mixxx +import Qt.labs.qmlmodels +import QtQml +import QtQuick +import QtQml.Models +import QtQuick.Layouts +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.12 +import Qt5Compat.GraphicalEffects +import "../Theme" + +Rectangle { + id: root + + required property var model + readonly property var featureSelection: ItemSelectionModel {} + + color: Theme.backgroundColor + + Component.onCompleted: { + root.model.activate(root.model.index(0, 0)) + } + + Rectangle { + anchors.fill: parent + anchors.topMargin: 7 + anchors.leftMargin: 7 + anchors.rightMargin: 25 + anchors.bottomMargin: 40 + color: Theme.sunkenBackgroundColor + + ColumnLayout { + anchors.fill: parent + spacing: 0 + ScrollView { + Layout.fillHeight: true + Layout.fillWidth: true + + TreeView { + id: featureView + Layout.fillWidth: true + + clip: true + + model: root.model + + selectionModel: featureSelection + + delegate: FocusScope { + required property string label + required property var icon + + readonly property real indentation: 40 + readonly property real padding: 5 + + // Assigned to by TreeView: + required property TreeView treeView + required property bool isTreeNode + required property bool expanded + required property int hasChildren + required property int depth + required property int row + required property int column + required property bool current + // FIXME The signature for that function has changed after Qt 6.4.2 (currently shipped on Ubuntu 24.04) + // See https://github.com/mixxxdj/mixxx/pull/14514#issuecomment-2770811094 for further details + readonly property var index: treeView.modelIndex(column, row) + + implicitWidth: treeView.width + implicitHeight: depth == 0 ? 42 : 35 + + // Rotate indicator when expanded by the user + // (requires TreeView to have a selectionModel) + property Animation indicatorAnimation: NumberAnimation { + target: indicator + property: "rotation" + from: expanded ? 0 : 90 + to: expanded ? 90 : 0 + duration: 100 + easing.type: Easing.OutQuart + } + TableView.onPooled: indicatorAnimation.complete() + TableView.onReused: if (current) indicatorAnimation.start() + onExpandedChanged: indicator.rotation = expanded ? 90 : 0 + + Rectangle { + id: background + anchors.fill: parent + color: depth == 0 ? Theme.darkGray3 : 'transparent' + + MouseArea { + id: rowMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (event) => { + treeView.selectionModel.select(treeView.selectionModel.model.index(row, 0), ItemSelectionModel.Rows | ItemSelectionModel.Select | ItemSelectionModel.Clear | ItemSelectionModel.Current); + treeView.model.activate(index) + if (isTreeNode && hasChildren) { + treeView.toggleExpanded(row) + } + event.accepted = true + } + } + + Rectangle { + width: 25 + anchors.left: parent.left + anchors.leftMargin: 10 + 15 * depth + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + color: current ? Theme.midGray : 'transparent' + + Repeater { + id: lineIcon + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + model: !!icon ? 1 : 0 + Image { + visible: depth == 0 && icon + source: icon + height: 25 + width: 25 + } + } + + Label { + id: indicator + Layout.preferredWidth: indicator.implicitWidth + visible: isTreeNode && hasChildren + color: Theme.textColor + text: "▶" + + anchors { + left: parent.left + verticalCenter: lineIcon.bottom + } + } + + Label { + id: labelItem + anchors.left: parent.left + anchors.leftMargin: depth == 0 && row == 0 ? 10 : 34 + anchors.verticalCenter: parent.verticalCenter + clip: true + font.weight: depth == 0 ? Font.Bold : Font.Medium + font.pixelSize: 14 + text: label + color: Theme.textColor + } + Item { + visible: rowMouseArea.containsMouse && isTreeNode && hasChildren + id: newItem + height: parent.height + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: 10 + } + Rectangle { + width: 30 + height: parent.height + anchors.centerIn: parent + gradient: Gradient { + orientation: Gradient.Horizontal + + GradientStop { + position: 1 + color: Theme.sunkenBackgroundColor + } + + GradientStop { + position: 0 + color: 'transparent' + } + } + } + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 5 + width: 20 + height: 20 + border.width: 2 + border.color: Theme.white + radius: 20 + color: 'transparent' + Shape { + anchors.fill: parent + anchors.margins: 4 + ShapePath { + strokeWidth: 2 + fillColor: Theme.white + capStyle: ShapePath.RoundCap + + startX: 6 + startY: 0 + PathLine { x: 6; y: 12 } + PathLine { x: 6; y: 8 } + } + ShapePath { + strokeWidth: 2 + fillColor: Theme.white + capStyle: ShapePath.RoundCap + + startX: 0 + startY: 6 + PathLine { y: 6; x: 12 } + PathLine { y: 6; x: 8 } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/res/qml/Library/Cell.qml b/res/qml/Library/Cell.qml new file mode 100644 index 000000000000..d93e57a0e765 --- /dev/null +++ b/res/qml/Library/Cell.qml @@ -0,0 +1,104 @@ +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Layouts +import "../Theme" + +Rectangle { + id: root + + readonly property alias dragImage: dragImageEffect + + anchors.fill: parent + + color: selected ? Theme.accent : (row % 2 == 0 ? Theme.sunkenBackgroundColor : Theme.backgroundColor) + + Drag.dragType: Drag.Automatic + Drag.supportedActions: Qt.CopyAction + Drag.mimeData: { + "text/uri-list": file_url.toString(), + "text/plain": file_url.toString(), + } + Item { + id: dragImageSource + width: 190 + height: 85 + visible: false + Rectangle { + color: Theme.sunkenBackgroundColor + anchors { + left: parent.left + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 5 + } + radius: 12 + RowLayout { + anchors.fill: parent + Image { + id: cover + Layout.fillHeight: true + Layout.preferredWidth: cover_art ? 75 : 0 + fillMode: Image.PreserveAspectFit + source: cover_art + clip: true + asynchronous: true + } + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Text { + text: track ? track.title : 'Unknown title' + color: Theme.textColor + } + Text { + text: track ? track.artist : 'Unknown artist' + color: Theme.midGray + } + } + } + Rectangle { + width: 20 + anchors { + top: parent.top + right: parent.right + bottom:parent.bottom + } + gradient: Gradient { + orientation: Gradient.Horizontal + + GradientStop { + position: 1 + color: Theme.darkGray + } + + GradientStop { + position: 0 + color: 'transparent' + } + } + } + } + } + DropShadow { + id: dragImageEffect + visible: false + anchors.fill: dragImageSource + source: dragImageSource + horizontalOffset: 0 + verticalOffset: 0 + radius: 10.0 + color: "#80000000" + } + + Rectangle { + id: border + color: Theme.darkGray2 + width: 1 + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + } + } +} diff --git a/res/qml/LibraryControl.qml b/res/qml/Library/Control.qml similarity index 68% rename from res/qml/LibraryControl.qml rename to res/qml/Library/Control.qml index 353e4b8bb21e..a5da4c133810 100644 --- a/res/qml/LibraryControl.qml +++ b/res/qml/Library/Control.qml @@ -1,3 +1,5 @@ +import ".." as Skin +import "." as LibraryComponent import Mixxx 1.0 as Mixxx import QtQuick 2.12 @@ -10,17 +12,17 @@ Item { signal loadSelectedTrack(string group, bool play) signal loadSelectedTrackIntoNextAvailableDeck(bool play) - FocusedWidgetControl { + Skin.FocusedWidgetControl { id: focusedWidgetControl - Component.onCompleted: this.value = FocusedWidgetControl.WidgetKind.LibraryView + Component.onCompleted: this.value = Skin.FocusedWidgetControl.WidgetKind.LibraryView } Mixxx.ControlProxy { group: "[Library]" key: "GoToItem" onValueChanged: (value) => { - if (value != 0 && root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView) + if (value != 0 && root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView) root.loadSelectedTrackIntoNextAvailableDeck(false); } } @@ -29,7 +31,7 @@ Item { group: "[Playlist]" key: "LoadSelectedIntoFirstStopped" onValueChanged: (value) => { - if (value != 0 && root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView) + if (value != 0 && root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView) root.loadSelectedTrackIntoNextAvailableDeck(false); } } @@ -39,7 +41,7 @@ Item { key: "SelectTrackKnob" onValueChanged: (value) => { if (value != 0) { - root.focusWidget = FocusedWidgetControl.WidgetKind.LibraryView; + root.focusWidget = Skin.FocusedWidgetControl.WidgetKind.LibraryView; root.moveVertical(value); } } @@ -50,7 +52,7 @@ Item { key: "SelectPrevTrack" onValueChanged: (value) => { if (value != 0) { - root.focusWidget = FocusedWidgetControl.WidgetKind.LibraryView; + root.focusWidget = Skin.FocusedWidgetControl.WidgetKind.LibraryView; root.moveVertical(-1); } } @@ -61,7 +63,7 @@ Item { key: "SelectNextTrack" onValueChanged: (value) => { if (value != 0) { - root.focusWidget = FocusedWidgetControl.WidgetKind.LibraryView; + root.focusWidget = Skin.FocusedWidgetControl.WidgetKind.LibraryView; root.moveVertical(1); } } @@ -71,7 +73,7 @@ Item { group: "[Library]" key: "MoveVertical" onValueChanged: (value) => { - if (value != 0 && root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView) + if (value != 0 && root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView) root.moveVertical(value); } } @@ -80,7 +82,7 @@ Item { group: "[Library]" key: "MoveUp" onValueChanged: (value) => { - if (value != 0 && root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView) + if (value != 0 && root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView) root.moveVertical(-1); } } @@ -89,7 +91,7 @@ Item { group: "[Library]" key: "MoveDown" onValueChanged: (value) => { - if (value != 0 && root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView) + if (value != 0 && root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView) root.moveVertical(1); } } @@ -104,11 +106,11 @@ Item { Instantiator { model: numDecksControl.value - delegate: LibraryControlLoadSelectedTrackHandler { + delegate: LibraryComponent.ControlLoadSelectedTrackHandler { required property int index group: "[Channel" + (index + 1) + "]" - enabled: root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView + enabled: root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView onLoadTrackRequested: (play) => { root.loadSelectedTrack(this.group, play); } @@ -125,11 +127,11 @@ Item { Instantiator { model: numPreviewDecksControl.value - delegate: LibraryControlLoadSelectedTrackHandler { + delegate: LibraryComponent.ControlLoadSelectedTrackHandler { required property int index group: "[PreviewDeck" + (index + 1) + "]" - enabled: root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView + enabled: root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView onLoadTrackRequested: (play) => { root.loadSelectedTrack(this.group, play); } @@ -146,11 +148,11 @@ Item { Instantiator { model: numSamplersControl.value - delegate: LibraryControlLoadSelectedTrackHandler { + delegate: LibraryComponent.ControlLoadSelectedTrackHandler { required property int index group: "[Sampler" + (index + 1) + "]" - enabled: root.focusWidget == FocusedWidgetControl.WidgetKind.LibraryView + enabled: root.focusWidget == Skin.FocusedWidgetControl.WidgetKind.LibraryView onLoadTrackRequested: (play) => { root.loadSelectedTrack(this.group, play); } diff --git a/res/qml/LibraryControlLoadSelectedTrackHandler.qml b/res/qml/Library/ControlLoadSelectedTrackHandler.qml similarity index 100% rename from res/qml/LibraryControlLoadSelectedTrackHandler.qml rename to res/qml/Library/ControlLoadSelectedTrackHandler.qml diff --git a/res/qml/Library/SourceTree.qml b/res/qml/Library/SourceTree.qml new file mode 100644 index 000000000000..470a03da7101 --- /dev/null +++ b/res/qml/Library/SourceTree.qml @@ -0,0 +1,194 @@ +import QtQuick +import Mixxx 1.0 as Mixxx +import "." as LibraryComponent +import "../Theme" + +Mixxx.LibrarySourceTree { + id: root + + component DefaultDelegate: LibraryComponent.Cell { + id: cell + readonly property var caps: capabilities + // FIXME: https://bugreports.qt.io/browse/QTBUG-111789 + Binding on Drag.active { + value: dragArea.drag.active + // This delays the update until the even queue is cleared + // preventing any potential oscillations causing a loop + delayed: true + } + + LibraryComponent.Track { + id: dragArea + anchors.fill: parent + capabilities: cell.caps + + onPressed: { + if (pressedButtons == Qt.LeftButton) { + tableView.selectionModel.selectRow(row); + parent.dragImage.grabToImage((result) => { + parent.Drag.imageSource = result.url; + }, Qt.size(parent.dragImage.width, parent.dragImage.height)); + } + } + onDoubleClicked: { + tableView.selectionModel.selectRow(row); + tableView.loadSelectedTrackIntoNextAvailableDeck(false); + } + } + + Text { + id: value + anchors.fill: parent + anchors.leftMargin: 15 + font.pixelSize: 14 + text: display ?? "" + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: Theme.textColor + } + } + + defaultColumns: [ + Mixxx.TrackListColumn { + preferredWidth: 110 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Album + + delegate: Rectangle { + color: decoration + implicitHeight: 30 + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: cover_art + clip: true + asynchronous: true + } + } + }, + // FIXME: WaveformOverview is currently disabled due to performance limitation. Like for the legacy UI, a cache likely needs to be implemented to help + // Mixxx.TrackListColumn { + // label: qsTr("Preview") + // fillSpan: 3 + // preferredWidth: 300 + // columnIdx: Mixxx.TrackListColumn.SQLColumns.Title + + // delegate: LibraryCell { + // // implicitHeight: 30 + // anchors.fill: parent + + // readonly property var trackProxy: track + + // Drag.active: dragArea.drag.active + // Drag.dragType: Drag.Automatic + // Drag.supportedActions: Qt.CopyAction + // Drag.mimeData: { + // "text/uri-list": file_url, + // "text/plain": file_url + // } + + // LibraryComponent.Track { + // id: dragArea + // anchors.fill: parent + // capabilities: parent.capabilities + + // onPressed: { + // if (pressedButtons == Qt.LeftButton) { + // tableView.selectionModel.selectRow(row); + // parent.dragImage.grabToImage((result) => { + // parent.Drag.imageSource = result.url; + // }); + // } else { + // } + // } + // onDoubleClicked: { + // tableView.selectionModel.selectRow(row); + // tableView.loadSelectedTrackIntoNextAvailableDeck(false); + // } + // } + + // Mixxx.WaveformOverview { + // anchors.fill: parent + // channels: Mixxx.WaveformOverview.Channels.LeftChannel + // renderer: Mixxx.WaveformOverview.Renderer.Filtered + // colorHigh: Theme.white + // colorMid: Theme.blue + // colorLow: Theme.green + // track: trackProxy + // } + // Rectangle { + // id: border + // color: Theme.darkGray2 + // width: 1 + // anchors { + // top: parent.top + // bottom: parent.bottom + // right: parent.right + // } + // } + // } + + // }, + Mixxx.TrackListColumn { + label: qsTr("Title") + fillSpan: 3 + columnIdx: Mixxx.TrackListColumn.SQLColumns.Title + + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("Artist") + fillSpan: 2 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Artist + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("Album") + fillSpan: 1 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Album + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("Year") + preferredWidth: 80 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Year + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("Bpm") + preferredWidth: 60 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Bpm + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("Key") + preferredWidth: 70 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Key + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("File Type") + preferredWidth: 70 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.FileType + delegate: DefaultDelegate { } + }, + Mixxx.TrackListColumn { + label: qsTr("Bitrate") + preferredWidth: 70 + + columnIdx: Mixxx.TrackListColumn.SQLColumns.Bitrate + delegate: DefaultDelegate { } + } + ] + Mixxx.LibraryAllTrackSource { + label: qsTr("All...") + columns: root.defaultColumns + } +} diff --git a/res/qml/Library/Track.qml b/res/qml/Library/Track.qml new file mode 100644 index 000000000000..253c4a188b9e --- /dev/null +++ b/res/qml/Library/Track.qml @@ -0,0 +1,131 @@ +import Mixxx 1.0 as Mixxx +import QtQuick +import QtQuick.Controls 2.15 +import "../Theme" + +MouseArea { + id: dragArea + + required property var capabilities + + readonly property var library: Mixxx.Library + + drag.target: value + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) + contextMenu.popup() + } + onPressAndHold: (mouse) => { + if (mouse.source === Qt.MouseEventNotSynthesized) + contextMenu.popup() + } + + function hasCapabilities(caps) { + return (dragArea.capabilities & caps) == caps; + } + + Menu { + id: contextMenu + title: qsTr("File") + + Menu { + title: qsTr("Load to") + enabled: { + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.LoadToDeck) || + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.LoadToSampler) || + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.LoadToPreviewDeck) + } + + Menu { + id: loadToDeckMenu + title: qsTr("Deck") + enabled: hasCapabilities(Mixxx.LibraryTrackListModel.Capability.LoadToDeck) + Instantiator { + model: 4 + delegate: MenuItem { + text: qsTr("Deck %1").arg(modelData+1) + onTriggered: Mixxx.PlayerManager.getPlayer(`[Channel${modelData+1}]`).loadTrack(track) + } + + onObjectAdded: (index, object) => loadToDeckMenu.insertItem(index, object) + onObjectRemoved: (index, object) => loadToDeckMenu.removeItem(object) + } + } + + Menu { + title: qsTr("Sampler") + enabled: hasCapabilities(Mixxx.LibraryTrackListModel.Capability.LoadToSampler) + } + + // Instantiator { + // id: recentFilesInstantiator + // model: settings.recentFiles + // delegate: MenuItem { + // text: settings.displayableFilePath(modelData) + // onTriggered: loadFile(modelData) + // } + + // onObjectAdded: (index, object) => recentFilesMenu.insertItem(index, object) + // onObjectRemoved: (index, object) => recentFilesMenu.removeItem(object) + // } + } + + Menu { + id: addToPlaylistMenu + title: qsTr("Add to playlists") + enabled: { + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.AddToTrackSet) + } + + MenuSeparator {} + + MenuItem { + enabled: false // TODO implement + text: qsTr("Create New Playlist") + } + } + + Menu { + id: addToCrateMenu + title: qsTr("Crates") + enabled: { + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.AddToTrackSet) + } + + MenuSeparator {} + + MenuItem { + enabled: false // TODO implement + text: qsTr("Create New Crate") + } + } + + Menu { + id: analyzeMenu + title: qsTr("Analyze") + enabled: { + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.EditMetadata)|| + hasCapabilities(Mixxx.LibraryTrackListModel.Capability.Analyze) + } + MenuItem { + text: qsTr("Analyze") + onTriggered: { + library.analyze(track) + } + } + MenuItem { + enabled: false // TODO implement + text: qsTr("Reanalyze") + } + MenuItem { + enabled: false // TODO implement + text: qsTr("Reanalyze (constant BPM)") + } + MenuItem { + enabled: false // TODO implement + text: qsTr("Reanalyze (variable BPM)") + } + } + } +} diff --git a/res/qml/Library/TrackList.qml b/res/qml/Library/TrackList.qml new file mode 100644 index 000000000000..715b7a6323e4 --- /dev/null +++ b/res/qml/Library/TrackList.qml @@ -0,0 +1,295 @@ +import ".." as Skin +import "." as LibraryComponent +import Mixxx 1.0 as Mixxx +import Qt.labs.qmlmodels +import QtQml +import QtQuick +import QtQml.Models +import QtQuick.Layouts +import QtQuick.Controls 2.15 +import "../Theme" + +Rectangle { + id: root + + color: Theme.darkGray + + required property var model + + LibraryComponent.Control { + id: libraryControl + + onMoveVertical: (offset) => { + view.selectionModel.moveSelectionVertical(offset); + } + onLoadSelectedTrack: (group, play) => { + view.loadSelectedTrack(group, play); + } + onLoadSelectedTrackIntoNextAvailableDeck: (play) => { + view.loadSelectedTrackIntoNextAvailableDeck(play); + } + onFocusWidgetChanged: { + switch (focusWidget) { + case Skin.FocusedWidgetControl.WidgetKind.LibraryView: + view.forceActiveFocus(); + break; + } + } + } + + HorizontalHeaderView { + id: horizontalHeader + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 5 + syncView: view + + property int sortingColumn: -1 + property var sortingOrder: Qt.Descending + + delegate: Item { + id: column + required property string display + required property int index + + implicitHeight: columnName.contentHeight + 5 + implicitWidth: columnName.contentWidth + 5 + + MouseArea { + id: columnMouseHandler + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + if (horizontalHeader.sortingColumn == index) { + horizontalHeader.sortingOrder = horizontalHeader.sortingOrder == Qt.DescendingOrder ? Qt.AscendingOrder : Qt.DescendingOrder + } else { + horizontalHeader.sortingColumn = index + horizontalHeader.sortingOrder = Qt.AscendingOrder + } + view.model.sort(horizontalHeader.sortingColumn, horizontalHeader.sortingOrder); + } + } + + Text { + id: columnName + + text: display + anchors.fill: parent + anchors.leftMargin: 15 + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + font.family: Theme.fontFamily + font.capitalization: Font.Capitalize + font.pixelSize: 12 + font.weight: Font.Medium + color: Theme.textColor + } + + Item { + anchors { + left: parent.left + leftMargin: 5 + top: parent.top + bottom: parent.bottom + } + Label { + id: sortIndicator + + visible: horizontalHeader.sortingColumn == index + + text: "▶" + rotation: horizontalHeader.sortingOrder == Qt.AscendingOrder ? 90 : -90 + + anchors.centerIn: parent + elide: Text.ElideRight + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.family: Theme.fontFamily + font.capitalization: Font.AllUppercase + font.bold: true + font.pixelSize: Theme.buttonFontPixelSize + color: "red" + } + } + Rectangle { + id: columnResizer + color: Theme.darkGray2 + width: 1 + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + } + MouseArea { + id: columnResizeHandler + + property int sizeOffset: 0 + + anchors.fill: parent + preventStealing: true + drag { + target: parent + axis: Drag.XAxis + threshold: 2 + onActiveChanged: { + if (!drag.active && columnResizeHandler.sizeOffset !== 0) { + view.model.columns[index].preferredWidth = column.width + columnResizeHandler.sizeOffset = 0 + view.updateColumnSize() + view.forceLayout() + } + } + } + cursorShape: Qt.SizeHorCursor + onMouseXChanged: { + if (drag.active) { + column.width += mouseX + sizeOffset += mouseX + } + } + } + } + } + } + + TableView { + id: view + + function loadSelectedTrackIntoNextAvailableDeck(play) { + const urls = this.selectionModel.selectedTrackUrls(); + if (urls.length == 0) + return ; + + Mixxx.PlayerManager.loadLocationUrlIntoNextAvailableDeck(urls[0], play); + } + + function loadSelectedTrack(group, play) { + const urls = this.selectionModel.selectedTrackUrls(); + if (urls.length == 0) + return ; + + player.loadTrackFromLocationUrl(urls[0], play); + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + } + + anchors.top: horizontalHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 5 + clip: true + reuseItems: true + Keys.onUpPressed: this.selectionModel.moveSelectionVertical(-1) + Keys.onDownPressed: this.selectionModel.moveSelectionVertical(1) + Keys.onEnterPressed: this.loadSelectedTrackIntoNextAvailableDeck(false) + Keys.onReturnPressed: this.loadSelectedTrackIntoNextAvailableDeck(false) + model: root.model + function updateColumnSize() { + usedWidth = 0; + dynamicColumnCount = 0; + if (model == null) { + return; + } + for (let c = 0; c < model.columns.length; c++) { + if (model.columns[c].hidden) { + continue + } else if (model.columns[c].preferredWidth > 0) { + usedWidth += model.columns[c].preferredWidth; + } else { + dynamicColumnCount += model.columns[c].fillSpan || 1 + } + } + } + Component.onCompleted: this.updateColumnSize() + onModelChanged: this.updateColumnSize() + + property int usedWidth: 0 + property int dynamicColumnCount: 0 + + columnWidthProvider: function(column) { + const columnDef = view.model.columns[column] + if (columnDef.hidden) { + return 0; + } + if (columnDef.preferredWidth >= 0) { + return columnDef.preferredWidth; + } + const span = columnDef.fillSpan || 1; + return span * (view.width - view.usedWidth) / view.dynamicColumnCount; + } + + selectionModel: ItemSelectionModel { + function selectRow(row) { + const rowCount = this.model.rowCount(); + if (rowCount == 0) { + this.clear(); + return ; + } + const newRow = Mixxx.MathUtils.positiveModulo(row, rowCount); + this.select(this.model.index(newRow, 0), ItemSelectionModel.Rows | ItemSelectionModel.Select | ItemSelectionModel.Clear | ItemSelectionModel.Current); + } + + function moveSelectionVertical(value) { + if (value == 0) + return ; + + const selected = this.selectedIndexes; + const oldRow = (selected.length == 0) ? 0 : selected[0].row; + this.selectRow(oldRow + value); + } + + function selectedTrackUrls() { + return this.selectedIndexes.map((index) => { + return this.model.getUrl(index.row); + }); + } + model: view.model + } + + delegate: Item { + id: item + required property bool selected + required property color decoration + required property var display + required property var track + required property string file_url + required property url cover_art + required property int row + + implicitHeight: 30 + + Loader { + id: loader + anchors.fill: parent + property bool selected: item.selected + property color decoration: item.decoration + property var display: item.display + property var track: item.track + property url file_url: item.file_url + property url cover_art: item.cover_art + property int row: item.row + property var tableView: view + property var capabilities: root.model ? root.model.getCapabilities() : Mixxx.LibraryTrackListModel.Capability.None + sourceComponent: delegate + focus: true + + onLoaded: { + // Workaround needed for WaveformOverview column to load the data + // if (track) + // Mixxx.Library.analyze(track) + } + } + // Workaround needed for WaveformOverview column to load the data + // TableView.onReused: { + // if (track) + // Mixxx.Library.analyze(track) + // } + } + } +} diff --git a/res/qml/PreviewDeck.qml b/res/qml/PreviewDeck.qml new file mode 100644 index 000000000000..d7e22d7cd8cd --- /dev/null +++ b/res/qml/PreviewDeck.qml @@ -0,0 +1,42 @@ +import "." as Skin +import Mixxx 1.0 as Mixxx +import Qt.labs.qmlmodels +import QtQml +import QtQuick +import QtQml.Models +import QtQuick.Layouts +import QtQuick.Controls 2.15 +import QtQuick.Shapes 1.6 +import "Theme" + +Rectangle { + id: root + + color: 'transparent' + + Shape { + anchors.fill: parent + ShapePath { + strokeColor: Theme.midGray + strokeWidth: 1 + fillColor: "transparent" + capStyle: ShapePath.RoundCap + + startX: 0 + startY: 0 + PathLine { x: width; y: 0 } + PathLine { x: width; y: height } + PathLine { x: 0; y: height } + PathLine { x: 0; y: 0 } + PathLine { x: width; y: height } + PathLine { x: 0; y: height } + PathLine { x: width; y: 0 } + } + } + + Text { + anchors.centerIn: parent + color: 'white' + text: "PreviewDeck placeholder" + } +} diff --git a/res/qml/Sampler.qml b/res/qml/Sampler.qml index df39c786ed06..936d683990c7 100644 --- a/res/qml/Sampler.qml +++ b/res/qml/Sampler.qml @@ -9,13 +9,14 @@ Rectangle { required property string group property bool minimized: false property var deckPlayer: Mixxx.PlayerManager.getPlayer(group) + property var currentTrack: deckPlayer.currentTrack color: { - const trackColor = root.deckPlayer.color; + const trackColor = root.currentTrack.color; if (!trackColor.valid) return Theme.backgroundColor; - return Qt.darker(root.deckPlayer.color, 2); + return Qt.darker(root.currentTrack.color, 2); } implicitHeight: gainKnob.height + 10 Drag.active: dragArea.drag.active @@ -25,7 +26,7 @@ Rectangle { let data = { "mixxx/player": group }; - const trackLocationUrl = deckPlayer.trackLocationUrl; + const trackLocationUrl = root.currentTrack.trackLocationUrl; if (trackLocationUrl) data["text/uri-list"] = trackLocationUrl; @@ -83,7 +84,7 @@ Rectangle { Text { id: label - text: root.deckPlayer.title + text: root.currentTrack.title anchors.top: embedded.top anchors.left: playButton.right anchors.right: embedded.right diff --git a/res/qml/Theme/Theme.qml b/res/qml/Theme/Theme.qml index 375911fcd0e8..31154df62ce0 100644 --- a/res/qml/Theme/Theme.qml +++ b/res/qml/Theme/Theme.qml @@ -2,20 +2,21 @@ import QtQuick 2.12 pragma Singleton QtObject { + property color accent: "#2D4EA1" property color accentColor: "#3a60be" - property color backgroundColor: "#1e1e20" + property color backgroundColor: "#2E2E2E" property color blue: "#01dcfc" property color bpmSliderBarColor: green property color buttonNormalColor: midGray property color crossfaderBarColor: red property color crossfaderOrientationColor: lightGray property color darkGray: "#0f0f0f" - property color darkGray2: "#2e2e2e" - property color darkGray3: "#3F3F3F" + property color darkGray2: "#242424" + property color darkGray3: "#202020" property color deckActiveColor: green property color deckBackgroundColor: darkGray property color deckLineColor: darkGray2 - property color deckTextColor: lightGray2 + property color deckTextColor: white property color effectColor: yellow property color effectUnitColor: red property color embeddedBackgroundColor: "#a0000000" @@ -26,13 +27,17 @@ QtObject { property color gainKnobColor: blue property color green: "#85c85b" property color knobBackgroundColor: "#262626" - property color lightGray: "#747474" property color lightGray2: "#b0b0b0" + property color lightGray: "#747474" property color midGray: "#696969" + property color panelSplitterBackground: backgroundColor + property color panelSplitterHandleActive: lightGray2 + property color panelSplitterHandle: midGray property color pflActiveButtonColor: blue property color red: "#ea2a4e" property color samplerColor: blue - property color textColor: lightGray2 + property color sunkenBackgroundColor: "#0C0C0C" + property color textColor: white property color toolbarActiveColor: white property color toolbarBackgroundColor: darkGray2 property color volumeSliderBarColor: blue diff --git a/res/qml/images/library_computer.png b/res/qml/images/library_computer.png new file mode 100644 index 0000000000000000000000000000000000000000..c8fd2fd4d368df4b6e91cab96dc915d95e1fcb2b GIT binary patch literal 2442 zcmV;533c{~P)EX>4Tx04R}tkv&MmP!xqvQ$>+V5j%)DWT*~e7Zq`=RVYG*P%E_RVDi#GXws0R zxHt-~1qXi?s}3&Cx;nTDg5VE`yWphgA|>9J6k5c1;qgAsyXWxUeSpxYFwN?U1DbA| z>10C8=2pd?R|GMD0KyoTnPtpLQVPEHbx)mCcQKyj-}h(rt9gq70g*V)4AUmwAfDN@ z4bJ<-5mu5_;&b8&lP*a7$aTfzH_kz@3Dp}fAb%yn8LNMaF7kRU=q4P{hdBSyPUiiI?tCw%<_Qpd2CnqBzuEw1KS{5* zwdfHL-UcqN+nTZmTrh~5IIJlTCM;902y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00&}8L_t(&-tAdyY@1gZe$M$Wwm;vUG&kqkHP?2O!9<&l zPFg2G6r^rtEt@n66`}n@sQj3w4W@`Os!1DY|JV?kkf>G)3`J|h5w_`d+z_=WTI8Fx$* z9~T8;H*AelT^0DhwP%m_+*Kl$SKdCBimGQg=jLLJN|M;^XfAtd?!?2(TSY`%ed9X) zR%0Zty}>y*%d!Z&!(!Ya99Ue4KvlJ)3qx~H1NiBd6G=iiz;#w8iUJ;bqyvg#D#+l~ zkH&FxVjh&SN7?It{CQEUljj*uo=g}bqN%zCAdqD-LxOXT;KDK(<4|m7D2klZpw*V1 z%NP#PKr~w1K3jmUv*3)s!%gH_JpV*rP9hr-jV5vF;t-njo^ zUn45M%3k>GGx*W*SMw$^Ix>TSYm?xdgAf8xK-U;Pxq1_-%FxnQof)fX41@rJK+_K; z>H5*?b>ll<-dhv_opJaVm+`xv5%?FPlv4c__QPLa!i!J6pOZ)|mPCK=&2;s#h^Xpt zLNrm#42BU~OkuEp3II^wSdJ?nj$&~o^vWyj`0VF*ud`KNK*89EN|%C;x-z`_hbtHk zgg^x8MGhR^IF4pBV6QZz_H!PXY$61SqI#bTR!=(q2m7b+&%g9xel7yWH~?UNHiDiD zgNR2{B@Zo=B;d&XjjQLyL7X~g$%{z&ZaXU5oDjsdV}=ld%613LP6+^@su~Cds`oij z)!_sc31ZPC#>ZywNS^F+uCAS1pa>N?LrXFXaMdZFYK)2$-u-U)t_G1@>c*%6cfLSNyHNmwcx?_n!LuRQX2YuC!wkuVzDHK2Lt%)pL;PfG>v#X z|5Cm_ex5rv#7@8UFinzi0QlD7c2v0)LxrHO+z!Q*e+@|_))wpZDu~R)p{8^Ud@_lN zv02nNmZPPu#vqXmXs&U=TV~rbO|@%}9iB!jLX%M}O~(<7CNbFW1JgO$_Ie8^vWoeo z1m64jM&=YZf>y5!Uudbyd%Pf0lr`C5ambh*UV@fn2nLsn^T;aR?it1h*QX2G<%|FE z8zQ2{HH3U|EcxT$JiT$%UQcO=s4Je3O(NVbYhDXVrZj|tQKVEo@7wn5P#BS!B>2kn z%oYi4duvhOP+l4$86{ffysiFc4ad19^jx@}`|4zoAxZ)?RmV~wefLom87=KKsB0+C z>uf1VqLdnvB%tHoTHF|$0|?O2RDs(1GKhjP(XpjOWGQ{fEiSB2-o~a1G&WV_Wg`|% zWzH8yXs93}0bdZa(;pX1fqv*aoR*?PKk@{VZ zb)O$Scn=QmuY-Rf3K*~#Tu$PR_tH%i9kf$_{=|4eInr9|E?KTPEHWImvY|P?Tk}fP zxuImgIN*mvk+B^RiK&HGt#_~7!2b$1CF1tjG9~kFL)CRG&My~6%-+iv?Lxx$66gH8+aa_lVXtZ2^#Zz%EETpfUB9oDDfyxX zbq{EEkw-MGiGzF_09re;Hw!rD7#^Hn>0{xSToAH%ZMe<_l}xGbST|0c3>7EiyS_1% z0?y{#u^*rNJ~ReTnGLP&n>Pw({9%N{F>uO<*$$%3i4gfY=?mvXx zE1en6g+nC^y;2z>Ksx|b)wuGyG6Mi%yc3+0?L*`z=Q;si1pqEj?mpO>THqeMbhdNb z5TP1>WHqeJW+~@*vqgr@o(@6E@IZ+oqKL^|zxjZGA1q0Zfn4HU-|W0A9wC%9l`O{y zYrbPLJ{OpTgMub@fH49P5mYy>`58?DfNr&1#twF{gFA-*0u8d*dQ32SSO5S307*qo IM6N<$f)Mg)i2wiq literal 0 HcmV?d00001 diff --git a/res/qml/images/library_crates.png b/res/qml/images/library_crates.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6874104abe046cacc28d00bf3654fda59eff86 GIT binary patch literal 1651 zcmV-(28{WMP)EX>4Tx04R}tkv&MmP!xqvQ$>+V5j%)DWT*~e7Zq`=RVYG*P%E_RVDi#GXws0R zxHt-~1qXi?s}3&Cx;nTDg5VE`yWphgA|>9J6k5c1;qgAsyXWxUeSpxYFwN?U1DbA| z>10C8=2pd?R|GMD0KyoTnPtpLQVPEHbx)mCcQKyj-}h(rt9gq70g*V)4AUmwAfDN@ z4bJ<-5mu5_;&b8&lP*a7$aTfzH_kz@3Dp}fAb%yn8LNMaF7kRU=q4P{hdBSyPUiiI?tCw%<_Qpd2CnqBzuEw1KS{5* zwdfHL-UcqN+nTZmTrh~5jw_=I#mDw02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00c`(L_t(&-tC!NY!p=($A4#Lc4ygbX-f;`Qfm`T2{w}8 z4I*en9;%|zq-o288k$HViNS~Rf{G#Wfj*FGO?>dh8j%NzV7VDE#Da;+ML~sNND$lF z2D+8fZTC8}XO0hTMazP{?RIMR|1_DIGc&*WzVkodHv?H^mDSxOM(^0r+~(VNt#`Rs zFhAu8O(~yYT<@;0I^|lBIXheKk43DuSW?dWuYq2Ve%kLZsO{gfuEPx=+Q`Q9huVtz z24Y9X0s&yzaz!NE_jz^8ceC68VvKBjd9G&_iZV?InqI4!qs=z#FcKh)l!^0E+VbcZC3Wa#S1Xs(9t_dh8xjLDzF*(|S(}u}=u|$SN7wpoQ#P@+u03raxqg#3YghBm zt~P?9=v<_%N#W>|k}VZlLSOIQxo>5BOI1f&KmdyK1HAk48qQz6PEUUXrPP!`NJnwv zLOX*|Yrcxb8-c2{fdI_Q4^a8w;D)X%JbV@b6=|95nbOK7izr`INaaI|Wm@J) zA}L8&$&A3fRl6D~9sR)8mbsKi2C1uyPa32*6y<}1XZZDxUZkTk8Jr~;1i7lgwoi_t zt4(abS7q>S}#L5xLxTgJ6FYG(p~TE6{Sxj3d~= z5aHuLffA2Uv2HDU_kJ~QkiNm#5Pjinj0FYL5(_iePxSO;l*^*Og=4nr@-Tqq; z0417Wo6p0^9Fxa%ZDxQ3grG!A-7(&y33dm(yg$oBi4g7v@r~h}7d$%0b4=<@V^U4B zD_qMI@VaSmG+>fH!RWZke#>=1jYpgLAOK7C{|U%%jx+N?x*SETHG@Htz#dz&E@II& z!S7;eUD@Rr&N(j?JK{EHr8B8}r4^)KDSDKmIEAH#lwx~4$pL$6N|m&y*5;^9_#f$# z1U|MTkB2Q(bFFpdJ)UYrU#-UNch`J z!(kD-*!IfrBgg)=QWZ2ED!tj)cM^v(fZ~E2a(!MpdqY^mZ$X$IjKbUu=MuwhMztg;%n{sQ5!l~}8jm*fBd002ovPDHLkV1h@n1}Fdk literal 0 HcmV?d00001 diff --git a/res/qml/images/library_playlist.png b/res/qml/images/library_playlist.png new file mode 100644 index 0000000000000000000000000000000000000000..c6f88e1ce98cbca2991b58f3d6115431b4360c58 GIT binary patch literal 1610 zcmV-Q2DSN#P)EX>4Tx04R}tkv&MmP!xqvQ$>+V5j%)DWT*~e7Zq`=RVYG*P%E_RVDi#GXws0R zxHt-~1qXi?s}3&Cx;nTDg5VE`yWphgA|>9J6k5c1;qgAsyXWxUeSpxYFwN?U1DbA| z>10C8=2pd?R|GMD0KyoTnPtpLQVPEHbx)mCcQKyj-}h(rt9gq70g*V)4AUmwAfDN@ z4bJ<-5mu5_;&b8&lP*a7$aTfzH_kz@3Dp}fAb%yn8LNMaF7kRU=q4P{hdBSyPUiiI?tCw%<_Qpd2CnqBzuEw1KS{5* zwdfHL-UcqN+nTZmTrh~5gz)E-h==E02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00bgQL_t(&-tC!RY*SSn$G`X7+k1Q4u77}b8)XC5xxp9< zWKl7o#t_61V~j6K6vLkf9()iDI;X}*jW5W9FPiAcOicU}AM^VKKFNi|IP(0S+ZnlIf<)iR^JC62=+tZ zHMgOwQArwf?jyW=c;;@W3KiW@l#c=IAQ&DV^$c@V6u(-PxQluwrm0K%O+b{!wLAawwENuRTGny2pj zhDS)hv)jlkpsWQd*H|ZvG1&MwEN2(r`)VW2WOr#ATLDe0`$A>AGfk;>dj73ve!U}* z#=(<;WF$F2^YeSm_~dy(_(W=I>B}8{D0W*PnTkxFqItU6tXoK>bl+_3?@j>sFK&_7 z2ls<#1BD+62gxa$0$?taD~&-8@xXll-VS0I?4pV?AH|B8h zi|-3ST8=&f;6&Lx!Wi-0y7mSArhxze$!Ui#=r5L*K&t`OZ~etUIsMh|U26PF1hLCu z*xgR7-n1T)>!#l+nH&-$QDi1k2z0a{*xgon6;lh4Yv+E4a$^=4OCUBJE<0vsJgEzW z)W%U~m#%~yc71haz4tm11o+#U(e^<1@&XBM?}4XrMIjQ|WC?;~(_iV=8bp*Oc!R5q zx2)k|k!nyfIWVf$w)ZmZ)L=A@$WMP_CYAuDH>+iDqYup+)-Mwf#xQ*P0%F6Fg>dl{ zrsJvF1P#4^Fmm>{vJf?I>-9{Yx*kRNm%j?1y)HW*zqb>eYgS=0ox^8mhA^JWnjd8R zaxqK$Jx;u{e-~D-EFK5%+!8|nz_++GI$`duHF3Szr0wp#rvw531fF2u&MoHm5seiO zySiHaWyf^221Dnnd5nC(GYR60vSak9iYf*JIn@@ZA2rk6;t9^~nceui_biZV<^OW7~7Pv36_6 zf`#&gAh50(_dWY48miyPBQ*P-CQ5qpI5+03-#T{Kz|nx9QQR&%lx;OaD*(1F9fJvR>P1+5i9m07*qo IM6N<$g5DGBfB*mh literal 0 HcmV?d00001 diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index dfd259448205..06b98574e78d 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -219,7 +219,9 @@ TrackPointer BrowseTableModel::getTrack(const QModelIndex& index) const { } TrackPointer BrowseTableModel::getTrackByRef(const TrackRef& trackRef) const { - if (m_pRecordingManager->getRecordingLocation() == trackRef.getLocation()) { + if (m_pRecordingManager && + m_pRecordingManager->getRecordingLocation() == + trackRef.getLocation()) { QMessageBox::critical(nullptr, tr("Mixxx Library"), tr("Could not load the following file because it is in use by " diff --git a/src/library/sidebarmodel.h b/src/library/sidebarmodel.h index 742bb0c28417..f5a36d2c50ae 100644 --- a/src/library/sidebarmodel.h +++ b/src/library/sidebarmodel.h @@ -86,11 +86,13 @@ class SidebarModel : public QAbstractItemModel { private slots: void slotPressedUntilClickedTimeout(); + protected: + QList m_sFeatures; + private: QModelIndex translateSourceIndex(const QModelIndex& parent); QModelIndex translateIndex(const QModelIndex& index, const QAbstractItemModel* model); void featureRenamed(LibraryFeature*); - QList m_sFeatures; unsigned int m_iDefaultSelectedIndex; /** Index of the item in the sidebar model to select at startup. */ QTimer* const m_pressedUntilClickedTimer; diff --git a/src/library/treeitemmodel.h b/src/library/treeitemmodel.h index 3972e3889cf4..142d635e2822 100644 --- a/src/library/treeitemmodel.h +++ b/src/library/treeitemmodel.h @@ -13,7 +13,7 @@ class TreeItemModel : public QAbstractItemModel { static const int kDataRole = Qt::UserRole; static const int kBoldRole = Qt::UserRole + 1; - explicit TreeItemModel(QObject *parent = 0); + explicit TreeItemModel(QObject* parent = nullptr); ~TreeItemModel() override; QVariant data(const QModelIndex &index, int role) const override; diff --git a/src/main.cpp b/src/main.cpp index 3f4c0b1f6d6a..f7741c98882f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -62,6 +62,11 @@ int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { int exitCode; #ifdef MIXXX_USE_QML if (args.isQml()) { + // This is a workaround to support Qt 6.4.2, currently shipped on + // Ubuntu 24.04 See + // https://github.com/mixxxdj/mixxx/pull/14514#issuecomment-2770811094 + // for further details + qputenv("QT_QUICK_TABLEVIEW_COMPAT_VERSION", "6.4"); mixxx::qml::QmlApplication qmlApplication(pApp, args); exitCode = pApp->exec(); } else diff --git a/src/qml/qmlconfigproxy.h b/src/qml/qmlconfigproxy.h index d5a03bc7040e..3b2e534fb34d 100644 --- a/src/qml/qmlconfigproxy.h +++ b/src/qml/qmlconfigproxy.h @@ -34,6 +34,10 @@ class QmlConfigProxy : public QObject { s_pUserSettings = std::move(pConfig); } + static UserSettingsPointer get() { + return s_pUserSettings; + } + private: static inline UserSettingsPointer s_pUserSettings = nullptr; diff --git a/src/qml/qmllibraryproxy.cpp b/src/qml/qmllibraryproxy.cpp index fec3d21120c4..5856438354ed 100644 --- a/src/qml/qmllibraryproxy.cpp +++ b/src/qml/qmllibraryproxy.cpp @@ -1,17 +1,35 @@ #include "qml/qmllibraryproxy.h" #include +#include #include "library/library.h" +#include "library/librarytablemodel.h" #include "moc_qmllibraryproxy.cpp" +#include "qml/qmllibraryproxy.h" +#include "qml/qmllibrarytracklistmodel.h" +#include "qmltrackproxy.h" +#include "track/track.h" +#include "util/assert.h" namespace mixxx { namespace qml { -QmlLibraryProxy::QmlLibraryProxy(std::shared_ptr pLibrary, QObject* parent) - : QObject(parent), - m_pLibrary(pLibrary), - m_pModelProperty(new QmlLibraryTrackListModel(m_pLibrary->trackTableModel(), this)) { +QmlLibraryProxy::QmlLibraryProxy(QObject* parent) + : QObject(parent) { +} + +QmlLibraryTrackListModel* QmlLibraryProxy::model() const { + return make_qml_owned( + QList{}, s_pLibrary->trackTableModel()) + .get(); +} + +void QmlLibraryProxy::analyze(const QmlTrackProxy* track) const { + VERIFY_OR_DEBUG_ASSERT(track && track->internal()) { + return; + } + emit s_pLibrary->analyzeTracks({track->internal()->getId()}); } // static @@ -26,7 +44,7 @@ QmlLibraryProxy* QmlLibraryProxy::create(QQmlEngine* pQmlEngine, QJSEngine* pJsE qWarning() << "Library hasn't been registered yet"; return nullptr; } - return new QmlLibraryProxy(s_pLibrary, pQmlEngine); + return new QmlLibraryProxy(pQmlEngine); } } // namespace qml diff --git a/src/qml/qmllibraryproxy.h b/src/qml/qmllibraryproxy.h index 3b5d80967b9b..07f46106ac4d 100644 --- a/src/qml/qmllibraryproxy.h +++ b/src/qml/qmllibraryproxy.h @@ -1,39 +1,42 @@ #pragma once #include #include +#include #include -#include "qml/qmllibrarytracklistmodel.h" -#include "util/parented_ptr.h" +#include "qml_owned_ptr.h" +#include "qmllibrarytracklistmodel.h" class Library; namespace mixxx { namespace qml { -class QmlLibraryTrackListModel; +class QmlTrackProxy; class QmlLibraryProxy : public QObject { Q_OBJECT - Q_PROPERTY(mixxx::qml::QmlLibraryTrackListModel* model MEMBER m_pModelProperty CONSTANT) + Q_PROPERTY(mixxx::qml::QmlLibraryTrackListModel* model READ model CONSTANT) QML_NAMED_ELEMENT(Library) QML_SINGLETON public: - explicit QmlLibraryProxy(std::shared_ptr pLibrary, QObject* parent = nullptr); + explicit QmlLibraryProxy(QObject* parent = nullptr); static QmlLibraryProxy* create(QQmlEngine* pQmlEngine, QJSEngine* pJsEngine); static void registerLibrary(std::shared_ptr pLibrary) { s_pLibrary = std::move(pLibrary); } - private: - static inline std::shared_ptr s_pLibrary; + static Library* get() { + return s_pLibrary.get(); + } - std::shared_ptr m_pLibrary; + QmlLibraryTrackListModel* model() const; + Q_INVOKABLE void analyze(const mixxx::qml::QmlTrackProxy* track) const; - /// This needs to be a plain pointer because it's used as a `Q_PROPERTY` member variable. - QmlLibraryTrackListModel* m_pModelProperty; + private: + static inline std::shared_ptr s_pLibrary; }; } // namespace qml diff --git a/src/qml/qmllibrarysource.cpp b/src/qml/qmllibrarysource.cpp new file mode 100644 index 000000000000..cc11d6f16606 --- /dev/null +++ b/src/qml/qmllibrarysource.cpp @@ -0,0 +1,61 @@ +#include "qml/qmllibrarysource.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "library/browse/browsefeature.h" +#include "library/library.h" +#include "library/librarytablemodel.h" +#include "library/trackcollection.h" +#include "library/trackcollectionmanager.h" +#include "library/trackset/crate/cratefeature.h" +#include "library/trackset/crate/cratesummary.h" +#include "library/trackset/playlistfeature.h" +#include "library/treeitemmodel.h" +#include "moc_qmllibrarysource.cpp" +#include "qmllibraryproxy.h" +#include "track/track.h" + +AllTrackLibraryFeature::AllTrackLibraryFeature(Library* pLibrary, UserSettingsPointer pConfig) + : LibraryFeature(pLibrary, pConfig, QStringLiteral("")), + m_pSidebarModel(make_parented(this)), + m_pLibraryTableModel(pLibrary->trackTableModel()) { + m_pSidebarModel->setRootItem(TreeItem::newRoot(this)); +} + +void AllTrackLibraryFeature::activate() { + emit showTrackModel(m_pLibraryTableModel); +} + +namespace mixxx { +namespace qml { + +QmlLibrarySource::QmlLibrarySource( + QObject* parent, const QList& columns) + : QObject(parent), + m_columns(columns) { +} + +void QmlLibrarySource::slotShowTrackModel(QAbstractItemModel* pModel) { + emit requestTrackModel(std::make_shared(columns(), pModel)); +} + +QmlLibraryAllTrackSource::QmlLibraryAllTrackSource( + QObject* parent, const QList& columns) + : QmlLibrarySource(parent, columns), + m_pLibraryFeature(std::make_unique( + QmlLibraryProxy::get(), QmlConfigProxy::get())) { + connect(m_pLibraryFeature.get(), + &LibraryFeature::showTrackModel, + this, + &QmlLibrarySource::slotShowTrackModel); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarysource.h b/src/qml/qmllibrarysource.h new file mode 100644 index 000000000000..88e4d675d96a --- /dev/null +++ b/src/qml/qmllibrarysource.h @@ -0,0 +1,116 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "library/browse/browsefeature.h" +#include "library/libraryfeature.h" +#include "library/sidebarmodel.h" +#include "library/trackset/crate/cratefeature.h" +#include "library/trackset/playlistfeature.h" +#include "library/treeitem.h" +#include "qmlconfigproxy.h" +#include "qmllibrarytracklistmodel.h" +#include "util/parented_ptr.h" + +class LibraryTableModel; +class TreeItemModel; +class AllTrackLibraryFeature final : public LibraryFeature { + Q_OBJECT + public: + AllTrackLibraryFeature(Library* pLibrary, + UserSettingsPointer pConfig); + ~AllTrackLibraryFeature() override = default; + + QVariant title() override { + return tr("All..."); + } + TreeItemModel* sidebarModel() const override { + return m_pSidebarModel; + } + + bool hasTrackTable() override { + return true; + } + + LibraryTableModel* trackTableModel() const { + return m_pLibraryTableModel; + } + + void searchAndActivate(const QString& query); + + public slots: + void activate() override; + + private: + LibraryTableModel* m_pLibraryTableModel; + + parented_ptr m_pSidebarModel; +}; + +namespace mixxx { +namespace qml { + +class QmlLibraryTrackListColumn; + +class QmlLibrarySource : public QObject { + Q_OBJECT + Q_PROPERTY(QString label MEMBER m_label) + Q_PROPERTY(QString icon MEMBER m_icon) + Q_PROPERTY(QQmlListProperty columns READ columnsQml) + Q_CLASSINFO("DefaultProperty", "columns") + QML_NAMED_ELEMENT(LibrarySource) + QML_UNCREATABLE("Only accessible via its specialization") + public: + explicit QmlLibrarySource(QObject* parent = nullptr, + const QList& columns = {}); + + QQmlListProperty columnsQml() { + return {this, &m_columns}; + } + + const QList& columns() const { + return m_columns; + } + virtual LibraryFeature* internal() = 0; + public slots: + void slotShowTrackModel(QAbstractItemModel* pModel); + + signals: +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + void requestTrackModel(std::shared_ptr pModel); +#else + void requestTrackModel(std::shared_ptr pModel); +#endif + + protected: + QString m_label; + QString m_icon; + QList m_columns; +}; + +class QmlLibraryAllTrackSource : public QmlLibrarySource { + Q_OBJECT + QML_NAMED_ELEMENT(LibraryAllTrackSource) + public: + explicit QmlLibraryAllTrackSource(QObject* parent = nullptr, + const QList& columns = {}); + + LibraryFeature* internal() override { + return m_pLibraryFeature.get(); + } + + private: + std::unique_ptr m_pLibraryFeature; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarysourcetree.cpp b/src/qml/qmllibrarysourcetree.cpp new file mode 100644 index 000000000000..2854c2b5063d --- /dev/null +++ b/src/qml/qmllibrarysourcetree.cpp @@ -0,0 +1,80 @@ +#include "qml/qmllibrarysourcetree.h" + +#include +#include + +#include +#include + +#include "library/library.h" +#include "library/librarytablemodel.h" +#include "moc_qmllibrarysourcetree.cpp" +#include "qml_owned_ptr.h" +#include "qmllibraryproxy.h" +#include "qmllibrarytracklistmodel.h" +#include "qmlsidebarmodelproxy.h" + +namespace mixxx { +namespace qml { + +QmlLibrarySourceTree::QmlLibrarySourceTree(QQuickItem* parent) + : QQuickItem(parent), + m_model(new QmlSidebarModelProxy(this)) { +} +QmlLibrarySourceTree::~QmlLibrarySourceTree() = default; + +Q_INVOKABLE QmlLibraryTrackListModel* QmlLibrarySourceTree::allTracks() const { + return make_qml_owned( + m_defaultColumns, QmlLibraryProxy::get()->trackTableModel()); +}; + +void QmlLibrarySourceTree::append_source( + QQmlListProperty* list, QmlLibrarySource* source) { + reinterpret_cast*>(list->data)->append(source); + QmlLibrarySourceTree* librarySourceTree = qobject_cast(list->object); + if (librarySourceTree && librarySourceTree->isComponentComplete()) { + librarySourceTree->m_model->update(librarySourceTree->m_sources); + } +} + +void QmlLibrarySourceTree::clear_source(QQmlListProperty* p) { + reinterpret_cast*>(p->data)->clear(); + QmlLibrarySourceTree* librarySourceTree = qobject_cast(p->object); + if (librarySourceTree) { + librarySourceTree->m_model->update(librarySourceTree->m_sources); + } +} +void QmlLibrarySourceTree::replace_source(QQmlListProperty* p, + qsizetype idx, + QmlLibrarySource* v) { + return reinterpret_cast*>(p->data)->replace(idx, v); + QmlLibrarySourceTree* librarySourceTree = qobject_cast(p->object); + if (librarySourceTree && librarySourceTree->isComponentComplete()) { + librarySourceTree->m_model->update(librarySourceTree->m_sources); + } +} +void QmlLibrarySourceTree::removeLast_source(QQmlListProperty* p) { + return reinterpret_cast*>(p->data)->removeLast(); + QmlLibrarySourceTree* librarySourceTree = qobject_cast(p->object); + if (librarySourceTree && librarySourceTree->isComponentComplete()) { + librarySourceTree->m_model->update(librarySourceTree->m_sources); + } +} + +QQmlListProperty QmlLibrarySourceTree::sources() { + return QQmlListProperty(this, + &m_sources, + &QmlLibrarySourceTree::append_source, + &QmlLibrarySourceTree::count_source, + &QmlLibrarySourceTree::at_source, + &QmlLibrarySourceTree::clear_source, + &QmlLibrarySourceTree::replace_source, + &QmlLibrarySourceTree::removeLast_source); +} + +void QmlLibrarySourceTree::componentComplete() { + m_model->update(m_sources); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarysourcetree.h b/src/qml/qmllibrarysourcetree.h new file mode 100644 index 000000000000..907f890f2aa5 --- /dev/null +++ b/src/qml/qmllibrarysourcetree.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "qmllibrarysource.h" +#include "qmllibrarytracklistcolumn.h" +#include "qmlsidebarmodelproxy.h" +#include "util/parented_ptr.h" + +namespace mixxx { +namespace qml { + +class QmlLibrarySourceTree : public QQuickItem { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(QQmlListProperty sources READ sources) + Q_PROPERTY(QQmlListProperty defaultColumns READ + defaultColumns CONSTANT) + Q_CLASSINFO("DefaultProperty", "sources") + QML_NAMED_ELEMENT(LibrarySourceTree) + + public: + Q_DISABLE_COPY_MOVE(QmlLibrarySourceTree) + explicit QmlLibrarySourceTree(QQuickItem* parent = nullptr); + ~QmlLibrarySourceTree() override; + + void componentComplete() override; + + QQmlListProperty defaultColumns() { + return {this, &m_defaultColumns}; + } + + QQmlListProperty sources(); + Q_INVOKABLE mixxx::qml::QmlSidebarModelProxy* sidebar() const { + return m_model.get(); + }; + Q_INVOKABLE mixxx::qml::QmlLibraryTrackListModel* allTracks() const; + + private: + static void append_source(QQmlListProperty* list, QmlLibrarySource* slice); + static qsizetype count_source(QQmlListProperty* p) { + return reinterpret_cast*>(p->data)->size(); + } + static QmlLibrarySource* at_source(QQmlListProperty* p, qsizetype idx) { + return reinterpret_cast*>(p->data)->at(idx); + } + static void clear_source(QQmlListProperty* p); + static void replace_source(QQmlListProperty* p, + qsizetype idx, + QmlLibrarySource* v); + static void removeLast_source(QQmlListProperty* p); + + QList m_sources; + parented_ptr m_model; + QList m_defaultColumns; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarytracklistcolumn.cpp b/src/qml/qmllibrarytracklistcolumn.cpp new file mode 100644 index 000000000000..5e64555c6f7d --- /dev/null +++ b/src/qml/qmllibrarytracklistcolumn.cpp @@ -0,0 +1,25 @@ +#include "qml/qmllibrarytracklistcolumn.h" + +#include "moc_qmllibrarytracklistcolumn.cpp" + +namespace mixxx { +namespace qml { + +QmlLibraryTrackListColumn::QmlLibraryTrackListColumn(QObject* parent, + const QString& label, + int fillSpan, + int columnIdx, + double preferredWidth, + QQmlComponent* pDelegate, + Role role) + : QObject(parent), + m_label(label), + m_role(role), + m_fillSpan(fillSpan), + m_columnIdx(columnIdx), + m_preferredWidth(preferredWidth), + m_pDelegate(pDelegate) { +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarytracklistcolumn.h b/src/qml/qmllibrarytracklistcolumn.h new file mode 100644 index 000000000000..3d55002ba9a6 --- /dev/null +++ b/src/qml/qmllibrarytracklistcolumn.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "library/columncache.h" +#include "qml/qml_owned_ptr.h" + +namespace mixxx { +namespace qml { + +class QmlLibraryTrackListColumn : public QObject { + Q_OBJECT + Q_PROPERTY(QString label MEMBER m_label FINAL) + Q_PROPERTY(int fillSpan MEMBER m_fillSpan FINAL) + Q_PROPERTY(int columnIdx MEMBER m_columnIdx FINAL) + Q_PROPERTY(double preferredWidth MEMBER m_preferredWidth FINAL) + Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate FINAL) + Q_PROPERTY(Role role MEMBER m_role FINAL) + QML_NAMED_ELEMENT(TrackListColumn) + public: + enum class SQLColumns { + Album = ColumnCache::COLUMN_LIBRARYTABLE_ALBUM, + Artist = ColumnCache::COLUMN_LIBRARYTABLE_ARTIST, + Title = ColumnCache::COLUMN_LIBRARYTABLE_TITLE, + Year = ColumnCache::COLUMN_LIBRARYTABLE_YEAR, + Bpm = ColumnCache::COLUMN_LIBRARYTABLE_BPM, + Key = ColumnCache::COLUMN_LIBRARYTABLE_KEY, + FileType = ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE, + Bitrate = ColumnCache::COLUMN_LIBRARYTABLE_BITRATE, + }; + Q_ENUM(SQLColumns) + enum class Role { + Location, + Artist, + Title, + Cover, + }; + Q_ENUM(Role) + explicit QmlLibraryTrackListColumn(QObject* parent = nullptr) + : QObject(parent) { + } + explicit QmlLibraryTrackListColumn(QObject* parent, + const QString& label, + int fillSpan, + int columnIdx, + double preferredWidth, + QQmlComponent* delegate, + Role role); + const QString& label() const { + return m_label; + } + Role role() const { + return m_role; + } + int fillSpan() const { + return m_fillSpan; + } + int columnIdx() const { + return m_columnIdx; + } + double preferredWidth() const { + return m_preferredWidth; + } + QQmlComponent* delegate() const { + return m_pDelegate; + } + void setDelegate(QQmlComponent* delegate) { + m_pDelegate = qml_owned_ptr(delegate); + } + + private: + QString m_label; + Role m_role; + int m_fillSpan{0}; + int m_columnIdx{-1}; + double m_preferredWidth{-1}; + qml_owned_ptr m_pDelegate; +}; +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarytracklistmodel.cpp b/src/qml/qmllibrarytracklistmodel.cpp index 1d8d77c27b16..2c14a5ccffc9 100644 --- a/src/qml/qmllibrarytracklistmodel.cpp +++ b/src/qml/qmllibrarytracklistmodel.cpp @@ -1,23 +1,68 @@ #include "qml/qmllibrarytracklistmodel.h" -#include "library/librarytablemodel.h" +#include + +#include +#include +#include +#include +#include + +#include "library/basetracktablemodel.h" +#include "library/columncache.h" #include "moc_qmllibrarytracklistmodel.cpp" +#include "qml/asyncimageprovider.h" +#include "qml/qmllibrarytracklistcolumn.h" +#include "qml_owned_ptr.h" +#include "qmltrackproxy.h" +#include "track/track.h" +#include "util/assert.h" +#include "util/parented_ptr.h" namespace mixxx { namespace qml { namespace { const QHash kRoleNames = { - {QmlLibraryTrackListModel::TitleRole, "title"}, - {QmlLibraryTrackListModel::ArtistRole, "artist"}, - {QmlLibraryTrackListModel::AlbumRole, "album"}, - {QmlLibraryTrackListModel::AlbumArtistRole, "albumArtist"}, - {QmlLibraryTrackListModel::FileUrlRole, "fileUrl"}, + {Qt::DisplayRole, "display"}, + {Qt::DecorationRole, "decoration"}, + {QmlLibraryTrackListModel::Delegate, "delegate"}, + {QmlLibraryTrackListModel::Track, "track"}, + {QmlLibraryTrackListModel::FileURL, "file_url"}, + {QmlLibraryTrackListModel::CoverArt, "cover_art"}, }; + +QColor colorFromRgbCode(double colorValue) { + if (colorValue < 0 || colorValue > 0xFFFFFF) { + return {}; + } + + QRgb rgbValue = static_cast(colorValue) | 0xFF000000; + return QColor(rgbValue); } +} // namespace -QmlLibraryTrackListModel::QmlLibraryTrackListModel(LibraryTableModel* pModel, QObject* pParent) - : QIdentityProxyModel(pParent) { - pModel->select(); +QmlLibraryTrackListModel::QmlLibraryTrackListModel( + const QList& librarySource, + QAbstractItemModel* pModel, + QObject* pParent) + : QIdentityProxyModel(pParent), + m_columns() { + m_columns.reserve(librarySource.size()); + for (const auto* pColumn : std::as_const(librarySource)) { + m_columns.emplace_back(make_parented(this, + pColumn->label(), + pColumn->fillSpan(), + pColumn->columnIdx(), + pColumn->preferredWidth(), + pColumn->delegate(), + pColumn->role())); + } + + auto* pTrackModel = dynamic_cast(pModel); + VERIFY_OR_DEBUG_ASSERT(pTrackModel) { + return; + } + pTrackModel->select(); setSourceModel(pModel); } @@ -29,72 +74,148 @@ QVariant QmlLibraryTrackListModel::data(const QModelIndex& proxyIndex, int role) VERIFY_OR_DEBUG_ASSERT(checkIndex(proxyIndex)) { return {}; } - - const auto pSourceModel = static_cast(sourceModel()); - VERIFY_OR_DEBUG_ASSERT(pSourceModel) { + auto columnIdx = proxyIndex.column(); + VERIFY_OR_DEBUG_ASSERT(columnIdx >= 0 || columnIdx < m_columns.size()) { return {}; } - if (proxyIndex.column() > 0) { - return {}; - } + auto* const pTrackTableModel = qobject_cast(sourceModel()); + auto* const pTrackModel = dynamic_cast(sourceModel()); + + const auto& pColumn = m_columns[columnIdx]; - int column = -1; switch (role) { - case TitleRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); - break; - case ArtistRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); - break; - case AlbumRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); - break; - case AlbumArtistRole: - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); - break; - case FileUrlRole: { - column = pSourceModel->fieldIndex(ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION); - const QString location = QIdentityProxyModel::data( - proxyIndex.siblingAtColumn(column), Qt::DisplayRole) - .toString(); + case Track: { + if (pTrackModel == nullptr) { + return {}; + } + auto pTrack = make_qml_owned(pTrackModel->getTrack( + QIdentityProxyModel::mapToSource(proxyIndex))); + return QVariant::fromValue(pTrack.get()); + } + case Qt::DecorationRole: { + if (pTrackTableModel == nullptr) { + return {}; + }; + return colorFromRgbCode(QIdentityProxyModel::data( + proxyIndex.siblingAtColumn(pTrackTableModel->fieldIndex( + ColumnCache::COLUMN_LIBRARYTABLE_COLOR)), + Qt::DisplayRole) + .toDouble()); + } + case CoverArt: { + QString location; + if (pTrackTableModel != nullptr) { + location = QIdentityProxyModel::data( + proxyIndex.siblingAtColumn(pTrackTableModel->fieldIndex( + ColumnCache::COLUMN_TRACKLOCATIONSTABLE_LOCATION)), + Qt::DisplayRole) + .toString(); + } else if (pTrackModel != nullptr) { + auto pTrack = pTrackModel->getTrack( + QIdentityProxyModel::mapToSource(proxyIndex)); + location = pTrack->getCoverInfo().coverLocation; + } if (location.isEmpty()) { return {}; } - return QUrl::fromLocalFile(location); + + return AsyncImageProvider::trackLocationToCoverArtUrl(location); + } + case FileURL: { + if (pTrackModel == nullptr) { + return {}; + } + return pTrackModel->getTrackUrl(QIdentityProxyModel::mapToSource(proxyIndex)); } - default: + case Delegate: + return QVariant::fromValue(pColumn->delegate()); break; } - - if (column < 0) { - return {}; + if (pColumn->columnIdx() < 0) { + // Use proxyIndex.column() + return QIdentityProxyModel::data(proxyIndex, role); } - - return QIdentityProxyModel::data(proxyIndex.siblingAtColumn(column), Qt::DisplayRole); + return QIdentityProxyModel::data( + proxyIndex.siblingAtColumn(pTrackTableModel != nullptr + ? pTrackTableModel->fieldIndex( + static_cast( + pColumn->columnIdx())) + : pColumn->columnIdx()), + role); } int QmlLibraryTrackListModel::columnCount(const QModelIndex& parent) const { - // This is a list model, i.e. no entries have a parent. - VERIFY_OR_DEBUG_ASSERT(!parent.isValid()) { + VERIFY_OR_DEBUG_ASSERT(static_cast( + parent.internalPointer()) != this) { return 0; } + return m_columns.size(); +} - // There is exactly one column. All data is exposed as roles. - return 1; +QVariant QmlLibraryTrackListModel::headerData( + int section, Qt::Orientation orientation, int role) const { + VERIFY_OR_DEBUG_ASSERT(section >= 0 || section < m_columns.size()) { + return {}; + } + // TODO role + return m_columns[section]->label(); } QHash QmlLibraryTrackListModel::roleNames() const { return kRoleNames; } -QVariant QmlLibraryTrackListModel::get(int row) const { - QModelIndex idx = index(row, 0); - QVariantMap dataMap; - for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { - dataMap.insert(it.value(), data(idx, it.key())); +QUrl QmlLibraryTrackListModel::getUrl(int row) const { + auto* const pTrackModel = dynamic_cast(sourceModel()); + + if (pTrackModel == nullptr) { + // TODO search for column with role + return {}; + } + return pTrackModel->getTrackUrl(sourceModel()->index(row, 0)); +} + +QmlTrackProxy* QmlLibraryTrackListModel::getTrack(int row) const { + auto* const pTrackModel = dynamic_cast(sourceModel()); + + if (pTrackModel == nullptr) { + // TODO search for column with role + return {}; + } + return make_qml_owned(pTrackModel->getTrack(sourceModel()->index(row, 0))); +} + +TrackModel::Capabilities QmlLibraryTrackListModel::getCapabilities() const { + auto* const pTrackModel = dynamic_cast(sourceModel()); + + if (pTrackModel != nullptr) { + return pTrackModel->getCapabilities(); + } + return TrackModel::Capability::None; +} +bool QmlLibraryTrackListModel::hasCapabilities(TrackModel::Capabilities caps) const { + return (getCapabilities() & caps) == caps; +} +void QmlLibraryTrackListModel::sort(int column, Qt::SortOrder order) { + VERIFY_OR_DEBUG_ASSERT(column >= 0 || column < m_columns.size()) { + return; + } + const auto& pColumn = m_columns[column]; + emit layoutAboutToBeChanged(QList(), + QAbstractItemModel::VerticalSortHint); + if (pColumn->columnIdx() < 0) { + // Use proxyIndex.column() + return sourceModel()->sort(column, order); } - return dataMap; + auto* const pTrackTableModel = qobject_cast(sourceModel()); + sourceModel()->sort(pTrackTableModel != nullptr + ? pTrackTableModel->fieldIndex( + static_cast( + pColumn->columnIdx())) + : pColumn->columnIdx(), + order); + emit layoutChanged(QList(), QAbstractItemModel::VerticalSortHint); } } // namespace qml diff --git a/src/qml/qmllibrarytracklistmodel.h b/src/qml/qmllibrarytracklistmodel.h index 10d8e9d5353f..6ee7203b8ae9 100644 --- a/src/qml/qmllibrarytracklistmodel.h +++ b/src/qml/qmllibrarytracklistmodel.h @@ -2,7 +2,9 @@ #include #include -class LibraryTableModel; +#include "library/trackmodel.h" +#include "qml/qmllibrarytracklistcolumn.h" +#include "qml/qmltrackproxy.h" namespace mixxx { namespace qml { @@ -10,25 +12,97 @@ namespace qml { class QmlLibraryTrackListModel : public QIdentityProxyModel { Q_OBJECT QML_NAMED_ELEMENT(LibraryTrackListModel) - QML_UNCREATABLE("Only accessible via Mixxx.Library.model") + Q_PROPERTY(QQmlListProperty columns READ columns FINAL) + QML_UNCREATABLE("Only accessible via Mixxx.Library") public: enum Roles { - TitleRole = Qt::UserRole, - ArtistRole, - AlbumRole, - AlbumArtistRole, - FileUrlRole, + Track = Qt::UserRole, + FileURL, + CoverArt, + Delegate }; Q_ENUM(Roles); - QmlLibraryTrackListModel(LibraryTableModel* pModel, QObject* pParent = nullptr); + // FIXME Remove the enum duplication with the `Capability` in `trackmodel.h` + enum class Capability { + None = 0u, + Reorder = 1u << 0u, + ReceiveDrops = 1u << 1u, + AddToTrackSet = 1u << 2u, + AddToAutoDJ = 1u << 3u, + Locked = 1u << 4u, + EditMetadata = 1u << 5u, + LoadToDeck = 1u << 6u, + LoadToSampler = 1u << 7u, + LoadToPreviewDeck = 1u << 8u, + Remove = 1u << 9u, + ResetPlayed = 1u << 10u, + Hide = 1u << 11u, + Unhide = 1u << 12u, + Purge = 1u << 13u, + RemovePlaylist = 1u << 14u, + RemoveCrate = 1u << 15u, + RemoveFromDisk = 1u << 16u, + Analyze = 1u << 17u, + Properties = 1u << 18u, + Sorting = 1u << 19u, + }; + Q_ENUM(Capability) + + QmlLibraryTrackListModel(const QList& librarySource, + QAbstractItemModel* pModel, + QObject* pParent = nullptr); ~QmlLibraryTrackListModel() = default; + QQmlListProperty columns() { + return {this, + &m_columns, + parent_qlist_append, + parent_qlist_count, + parent_qlist_at, + parent_qlist_clear}; + } + QVariant data(const QModelIndex& index, int role) const override; int columnCount(const QModelIndex& index = QModelIndex()) const override; + Q_INVOKABLE QUrl getUrl(int row) const; + Q_INVOKABLE mixxx::qml::QmlTrackProxy* getTrack(int row) const; + Q_INVOKABLE TrackModel::Capabilities getCapabilities() const; + Q_INVOKABLE bool hasCapabilities(TrackModel::Capabilities caps) const; QHash roleNames() const override; - Q_INVOKABLE QVariant get(int row) const; + Q_INVOKABLE QVariant headerData(int section, + Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + Q_INVOKABLE void sort(int column, Qt::SortOrder order) override; + + private: + std::vector> m_columns; + + static void parent_qlist_append( + QQmlListProperty* p, + QmlLibraryTrackListColumn* v) { + reinterpret_cast>*>( + p->data) + ->emplace_back(v); + } + static qsizetype parent_qlist_count(QQmlListProperty* p) { + return reinterpret_cast< + std::vector>*>(p->data) + ->size(); + } + static QmlLibraryTrackListColumn* parent_qlist_at( + QQmlListProperty* p, qsizetype idx) { + return reinterpret_cast< + std::vector>*>(p->data) + ->at(idx) + .get(); + } + static void parent_qlist_clear(QQmlListProperty* p) { + return reinterpret_cast< + std::vector>*>(p->data) + ->clear(); + } }; } // namespace qml diff --git a/src/qml/qmlsidebarmodelproxy.cpp b/src/qml/qmlsidebarmodelproxy.cpp new file mode 100644 index 000000000000..4483a732ccab --- /dev/null +++ b/src/qml/qmlsidebarmodelproxy.cpp @@ -0,0 +1,88 @@ +#include "qml/qmlsidebarmodelproxy.h" + +#include + +#include +#include +#include + +#include "library/treeitem.h" +#include "moc_qmlsidebarmodelproxy.cpp" +#include "qml/qmllibrarysource.h" +#include "util/assert.h" +#include "util/parented_ptr.h" + +namespace mixxx { +namespace qml { + +namespace { +const QHash kRoleNames = { + {Qt::DisplayRole, "label"}, + {QmlSidebarModelProxy::IconRole, "icon"}, +}; +} // namespace + +QHash QmlSidebarModelProxy::roleNames() const { + return kRoleNames; +} + +QVariant QmlSidebarModelProxy::get(int row) const { + QModelIndex idx = index(row, 0); + QVariantMap dataMap; + for (auto it = kRoleNames.constBegin(); it != kRoleNames.constEnd(); it++) { + dataMap.insert(it.value(), data(idx, it.key())); + } + return dataMap; +} + +void QmlSidebarModelProxy::activate(const QModelIndex& index) { + VERIFY_OR_DEBUG_ASSERT(index.isValid()) { + return; + } + if (index.internalPointer() == this) { + VERIFY_OR_DEBUG_ASSERT(index.row() >= 0 && index.row() < m_sFeatures.length()) { + return; + } + m_sFeatures[index.row()]->activate(); + } else { + TreeItem* pTreeItem = static_cast(index.internalPointer()); + VERIFY_OR_DEBUG_ASSERT(pTreeItem != nullptr) { + return; + } + LibraryFeature* pFeature = pTreeItem->feature(); + DEBUG_ASSERT(pFeature); + pFeature->activateChild(index); + pFeature->onLazyChildExpandation(index); + } +} + +QmlSidebarModelProxy::QmlSidebarModelProxy(QObject* parent) + : SidebarModel(parent), + m_tracklist(nullptr) { +} +QmlSidebarModelProxy::~QmlSidebarModelProxy() = default; + +void QmlSidebarModelProxy::update(const QList& sources) { + beginResetModel(); + qDeleteAll(m_sFeatures); + for (const auto& librarySource : sources) { + VERIFY_OR_DEBUG_ASSERT(librarySource) { + continue; + } + connect(librarySource, + &QmlLibrarySource::requestTrackModel, + this, + &QmlSidebarModelProxy::slotShowTrackModel); + auto* pLibrarySource = librarySource->internal(); + addLibraryFeature(pLibrarySource); + } + endResetModel(); +} + +void QmlSidebarModelProxy::slotShowTrackModel(std::shared_ptr pModel) { + m_tracklist = pModel; + emit tracklistChanged(); +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlsidebarmodelproxy.h b/src/qml/qmlsidebarmodelproxy.h new file mode 100644 index 000000000000..8607da4c2562 --- /dev/null +++ b/src/qml/qmlsidebarmodelproxy.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "library/libraryfeature.h" +#include "library/sidebarmodel.h" +#include "qmllibrarytracklistmodel.h" +#include "util/parented_ptr.h" + +namespace mixxx { +namespace qml { + +class QmlLibrarySource; + +class QmlSidebarModelProxy : public SidebarModel { + Q_OBJECT + Q_PROPERTY(QmlLibraryTrackListModel* tracklist READ tracklist NOTIFY tracklistChanged) + QML_ANONYMOUS + public: + enum Roles { + LabelRole = Qt::UserRole, + IconRole, + }; + Q_ENUM(Roles); + Q_DISABLE_COPY_MOVE(QmlSidebarModelProxy) + explicit QmlSidebarModelProxy(QObject* parent = nullptr); + ~QmlSidebarModelProxy() override; + + QmlLibraryTrackListModel* tracklist() const { + return m_tracklist.get(); + } + + void update(const QList& sources); + QHash roleNames() const override; + Q_INVOKABLE QVariant get(int row) const; + Q_INVOKABLE void activate(const QModelIndex& index); + signals: + void tracklistChanged(); + + protected slots: + void slotShowTrackModel(std::shared_ptr pModel); + + private: + std::shared_ptr m_tracklist; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlwaveformoverview.h b/src/qml/qmlwaveformoverview.h index 1b3ebe745095..431ab48aa034 100644 --- a/src/qml/qmlwaveformoverview.h +++ b/src/qml/qmlwaveformoverview.h @@ -6,7 +6,7 @@ #include #include -#include "qmlplayerproxy.h" +#include "qmltrackproxy.h" #include "waveform/waveform.h" namespace mixxx {