diff --git a/Persephone.xcodeproj/project.pbxproj b/Persephone.xcodeproj/project.pbxproj index 22c0f07..8ac1d32 100644 --- a/Persephone.xcodeproj/project.pbxproj +++ b/Persephone.xcodeproj/project.pbxproj @@ -112,6 +112,7 @@ E4B11BC22275EE410075461B /* AlbumListActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B11BC12275EE410075461B /* AlbumListActions.swift */; }; E4C8B53C22342005009A20F3 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */; }; E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4C8B53D22349002009A20F3 /* MPDIdle.swift */; }; + E4D3BFA622B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */; }; E4E7A6AD22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */; }; E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC8F2204EC7F0024217A /* Delegate.swift */; }; E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; }; @@ -317,6 +318,7 @@ E4B11BC12275EE410075461B /* AlbumListActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumListActions.swift; sourceTree = ""; }; E4C8B53B22342005009A20F3 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; E4C8B53D22349002009A20F3 /* MPDIdle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDIdle.swift; sourceTree = ""; }; + E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueueViewController+NSOutlineViewDelegate.swift"; sourceTree = ""; }; E4E7A6AC22AAAF98006D566C /* AlbumDetailView+NSTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumDetailView+NSTableViewDelegate.swift"; sourceTree = ""; }; E4E8CC8F2204EC7F0024217A /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = ""; }; @@ -694,6 +696,7 @@ E47E2FD022205C4600F747E6 /* MainSplitViewController.swift */, E4405191227644340090CD6F /* MPDServerController.swift */, E4E8CC912204F4B80024217A /* QueueViewController.swift */, + E4D3BFA522B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift */, E4B11BB52275374B0075461B /* UserNotificationsController.swift */, E465049921E94DF500A70F4C /* WindowController.swift */, ); @@ -921,6 +924,7 @@ E4B11BB62275374B0075461B /* UserNotificationsController.swift in Sources */, E4B11B68226A4FA00075461B /* QueueState.swift in Sources */, E4928E0B2218D62A001D4BEA /* CGColor.swift in Sources */, + E4D3BFA622B419C000C56F48 /* QueueViewController+NSOutlineViewDelegate.swift in Sources */, E4405196227879960090CD6F /* MPDActions.swift in Sources */, E4405192227644340090CD6F /* MPDServerController.swift in Sources */, E4C8B53E22349002009A20F3 /* MPDIdle.swift in Sources */, diff --git a/Persephone/Controllers/QueueViewController+NSOutlineViewDelegate.swift b/Persephone/Controllers/QueueViewController+NSOutlineViewDelegate.swift new file mode 100644 index 0000000..b56e387 --- /dev/null +++ b/Persephone/Controllers/QueueViewController+NSOutlineViewDelegate.swift @@ -0,0 +1,87 @@ +// +// QueueViewController+NSOutlineViewDelegate.swift +// Persephone +// +// Created by Daniel Barber on 2019/6/14. +// Copyright © 2019 Dan Barber. All rights reserved. +// + +import AppKit + +extension QueueViewController: NSOutlineViewDelegate { + func outlineView( + _ outlineView: NSOutlineView, + selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet + ) -> IndexSet { + if proposedSelectionIndexes.contains(0) { + return IndexSet() + } else { + return proposedSelectionIndexes + } + } + + func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { + if let queueItem = item as? QueueItem { + switch tableColumn?.identifier.rawValue { + case "songTitleColumn": + return cellForSongTitle(outlineView, with: queueItem) + case "songArtistColumn": + return cellForSongArtist(outlineView, with: queueItem) + default: + return nil + } + } else if tableColumn?.identifier.rawValue == "songTitleColumn" { + return cellForQueueHeading(outlineView) + } else { + return nil + } + } + + func outlineViewSelectionDidChange(_ notification: Notification) { + if queueView.selectedRow >= 1 { + let queueItem = dataSource.queue[queueView.selectedRow - 1] + + App.store.dispatch(SetSelectedQueueItem(selectedQueueItem: queueItem)) + } else { + App.store.dispatch(SetSelectedQueueItem(selectedQueueItem: nil)) + } + } + + func cellForSongTitle(_ outlineView: NSOutlineView, with queueItem: QueueItem) -> NSView { + let cellView = outlineView.makeView( + withIdentifier: .queueSongTitle, + owner: self + ) as! QueueSongTitleView + + cellView.setQueueSong(queueItem, queueIcon: dataSource.queueIcon) + + return cellView + } + + func cellForSongArtist(_ outlineView: NSOutlineView, with queueItem: QueueItem) -> NSView { + let cellView = outlineView.makeView( + withIdentifier: .queueSongArtist, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = queueItem.song.artist + if queueItem.isPlaying { + cellView.textField?.font = .systemFontBold + } else { + cellView.textField?.font = .systemFontRegular + } + + return cellView + } + + func cellForQueueHeading(_ outlineView: NSOutlineView) -> NSView { + let cellView = outlineView.makeView( + withIdentifier: .queueHeading, + owner: self + ) as! NSTableCellView + + cellView.textField?.stringValue = "QUEUE" + + return cellView + } +} diff --git a/Persephone/Controllers/QueueViewController.swift b/Persephone/Controllers/QueueViewController.swift index bf41fd5..518109f 100644 --- a/Persephone/Controllers/QueueViewController.swift +++ b/Persephone/Controllers/QueueViewController.swift @@ -9,8 +9,7 @@ import AppKit import ReSwift -class QueueViewController: NSViewController, - NSOutlineViewDelegate { +class QueueViewController: NSViewController { var dataSource = QueueDataSource() @IBOutlet var queueView: NSOutlineView! @@ -23,8 +22,10 @@ class QueueViewController: NSViewController, $0.select { $0.queueState } } - queueView.dataSource = dataSource +// queueView.dataSource = dataSource queueView.columnAutoresizingStyle = .sequentialColumnAutoresizingStyle + queueView.registerForDraggedTypes([REORDER_PASTEBOARD_TYPE]) + queueView.draggingDestinationFeedbackStyle = .regular } override func keyDown(with event: NSEvent) { @@ -64,81 +65,31 @@ class QueueViewController: NSViewController, App.store.dispatch(MPDRemoveTrack(queuePos: queuePos)) } } +} - func outlineView( - _ outlineView: NSOutlineView, - selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet - ) -> IndexSet { - if proposedSelectionIndexes.contains(0) { - return IndexSet() - } else { - return proposedSelectionIndexes - } +extension QueueViewController: NSOutlineViewDataSource { + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + return dataSource.outlineView(outlineView, numberOfChildrenOfItem: item) } - func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { - if let queueItem = item as? QueueItem { - switch tableColumn?.identifier.rawValue { - case "songTitleColumn": - return cellForSongTitle(outlineView, with: queueItem) - case "songArtistColumn": - return cellForSongArtist(outlineView, with: queueItem) - default: - return nil - } - } else if tableColumn?.identifier.rawValue == "songTitleColumn" { - return cellForQueueHeading(outlineView) - } else { - return nil - } + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { + return dataSource.outlineView(outlineView, isItemExpandable: item) } - func outlineViewSelectionDidChange(_ notification: Notification) { - if queueView.selectedRow >= 1 { - let queueItem = dataSource.queue[queueView.selectedRow - 1] - - App.store.dispatch(SetSelectedQueueItem(selectedQueueItem: queueItem)) - } else { - App.store.dispatch(SetSelectedQueueItem(selectedQueueItem: nil)) - } + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + return dataSource.outlineView(outlineView, child: index, ofItem: item) } - func cellForSongTitle(_ outlineView: NSOutlineView, with queueItem: QueueItem) -> NSView { - let cellView = outlineView.makeView( - withIdentifier: .queueSongTitle, - owner: self - ) as! QueueSongTitleView - - cellView.setQueueSong(queueItem, queueIcon: dataSource.queueIcon) - - return cellView + func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { + return dataSource.outlineView(outlineView, pasteboardWriterForItem: item) } - func cellForSongArtist(_ outlineView: NSOutlineView, with queueItem: QueueItem) -> NSView { - let cellView = outlineView.makeView( - withIdentifier: .queueSongArtist, - owner: self - ) as! NSTableCellView - - cellView.textField?.stringValue = queueItem.song.artist - if queueItem.isPlaying { - cellView.textField?.font = .systemFontBold - } else { - cellView.textField?.font = .systemFontRegular - } - - return cellView + func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { + return dataSource.outlineView(outlineView, validateDrop: info, proposedItem: item, proposedChildIndex: index) } - func cellForQueueHeading(_ outlineView: NSOutlineView) -> NSView { - let cellView = outlineView.makeView( - withIdentifier: .queueHeading, - owner: self - ) as! NSTableCellView - - cellView.textField?.stringValue = "QUEUE" - - return cellView + func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { + return dataSource.outlineView(outlineView, acceptDrop: info, item: item, childIndex: index) } } diff --git a/Persephone/DataSources/QueueDataSource.swift b/Persephone/DataSources/QueueDataSource.swift index de5b0f1..2298e1f 100644 --- a/Persephone/DataSources/QueueDataSource.swift +++ b/Persephone/DataSources/QueueDataSource.swift @@ -8,6 +8,8 @@ import AppKit +let REORDER_PASTEBOARD_TYPE = NSPasteboard.PasteboardType("me.danbarber.persephone") + class QueueDataSource: NSObject, NSOutlineViewDataSource { var queue: [QueueItem] = [] var queueIcon: NSImage? = nil @@ -35,7 +37,43 @@ class QueueDataSource: NSObject, NSOutlineViewDataSource { if index > 0 { return queue[index - 1] } else { - return false + return "" } } + + func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? { + guard let queueItem = item as? QueueItem + else { return nil } + + let pbItem = NSPasteboardItem() + + pbItem.setPropertyList(["queuePos": queueItem.queuePos], forType: REORDER_PASTEBOARD_TYPE) + + return pbItem + } + + func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { + guard let draggingTypes = info.draggingPasteboard.types, + draggingTypes.contains(REORDER_PASTEBOARD_TYPE), + index >= 0 + else { return [] } + + return .move + } + + func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { + var newQueuePos = index - 1 + + guard let payload = info.draggingPasteboard.propertyList(forType: REORDER_PASTEBOARD_TYPE) as? [String: Int], + let queuePos = payload["queuePos"] + else { return false } + + if newQueuePos > queuePos { newQueuePos -= 1 } + + guard queuePos != newQueuePos else { return false } + + App.store.dispatch(MPDMoveSongInQueue(oldQueuePos: queuePos, newQueuePos: newQueuePos)) + + return true + } } diff --git a/Persephone/MPDClient/Extensions/MPDClient+Command.swift b/Persephone/MPDClient/Extensions/MPDClient+Command.swift index 2b3868a..41b776a 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Command.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Command.swift @@ -69,6 +69,12 @@ extension MPDClient { else { return } sendRemoveSong(at: queuePos) + case .moveSongInQueue: + guard let oldQueuePos = userData["oldQueuePos"] as? Int, + let newQueuePos = userData["newQueuePos"] as? Int + else { return } + sendMoveSongInQueue(at: oldQueuePos, to: newQueuePos) + // Album commands case .fetchAllAlbums: allAlbums() diff --git a/Persephone/MPDClient/Extensions/MPDClient+Queue.swift b/Persephone/MPDClient/Extensions/MPDClient+Queue.swift index ef46c47..ee0ab5a 100644 --- a/Persephone/MPDClient/Extensions/MPDClient+Queue.swift +++ b/Persephone/MPDClient/Extensions/MPDClient+Queue.swift @@ -30,6 +30,10 @@ extension MPDClient { enqueueCommand(command: .removeSong, userData: ["queuePos": queuePos]) } + func moveSongInQueue(at queuePos: Int, to newQueuePos: Int) { + enqueueCommand(command: .moveSongInQueue, userData: ["oldQueuePos": queuePos, "newQueuePos": newQueuePos]) + } + func sendPlayTrack(at queuePos: Int) { mpd_run_play_pos(self.connection, UInt32(queuePos)) } @@ -64,4 +68,8 @@ extension MPDClient { func sendRemoveSong(at queuePos: Int) { mpd_run_delete(self.connection, UInt32(queuePos)) } + + func sendMoveSongInQueue(at oldQueuePos: Int, to newQueuePos: Int) { + mpd_run_move(self.connection, UInt32(oldQueuePos), UInt32(newQueuePos)) + } } diff --git a/Persephone/MPDClient/Models/MPDCommand.swift b/Persephone/MPDClient/Models/MPDCommand.swift index 2ca6ddd..bec3845 100644 --- a/Persephone/MPDClient/Models/MPDCommand.swift +++ b/Persephone/MPDClient/Models/MPDCommand.swift @@ -33,6 +33,7 @@ extension MPDClient { case replaceQueue case appendSong case removeSong + case moveSongInQueue // Album commands case fetchAllAlbums diff --git a/Persephone/Models/QueueItem.swift b/Persephone/Models/QueueItem.swift index c087674..c048510 100644 --- a/Persephone/Models/QueueItem.swift +++ b/Persephone/Models/QueueItem.swift @@ -6,7 +6,7 @@ // Copyright © 2019 Dan Barber. All rights reserved. // -import Foundation +import AppKit struct QueueItem: Equatable { var song: Song diff --git a/Persephone/Resources/Base.lproj/Main.storyboard b/Persephone/Resources/Base.lproj/Main.storyboard index e4ec9ab..a2c4322 100644 --- a/Persephone/Resources/Base.lproj/Main.storyboard +++ b/Persephone/Resources/Base.lproj/Main.storyboard @@ -574,7 +574,7 @@ - + @@ -713,6 +713,7 @@ + diff --git a/Persephone/State/Actions/MPDActions.swift b/Persephone/State/Actions/MPDActions.swift index fc107f0..bc95a68 100644 --- a/Persephone/State/Actions/MPDActions.swift +++ b/Persephone/State/Actions/MPDActions.swift @@ -18,6 +18,11 @@ struct MPDPrevTrackAction: Action {} struct MPDClearQueue: Action {} +struct MPDMoveSongInQueue: Action { + let oldQueuePos: Int + let newQueuePos: Int +} + struct MPDAppendTrack: Action { let song: MPDClient.MPDSong } diff --git a/Persephone/State/Reducers/MPDReducer.swift b/Persephone/State/Reducers/MPDReducer.swift index 49a10a4..c25386d 100644 --- a/Persephone/State/Reducers/MPDReducer.swift +++ b/Persephone/State/Reducers/MPDReducer.swift @@ -33,6 +33,9 @@ func mpdReducer(action: Action, state: MPDState?) -> MPDState { case is MPDClearQueue: App.mpdClient.clearQueue() + case let action as MPDMoveSongInQueue: + App.mpdClient.moveSongInQueue(at: action.oldQueuePos, to: action.newQueuePos) + case let action as MPDAppendTrack: App.mpdClient.appendSong(action.song)