Archived
1
This repository has been archived on 2020-06-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tagtunes/TagTunes/MainViewController.swift
2015-09-03 00:22:33 +02:00

755 lines
32 KiB
Swift

//
// ViewController.swift
// TagTunes
//
// Created by Kim Wittenburg on 28.08.15.
// Copyright © 2015 Kim Wittenburg. All rights reserved.
//
import Cocoa
import AppKitPlus
internal class MainViewController: NSViewController {
// MARK: Types
private struct OutlineViewConstants {
struct ViewIdentifiers {
static let simpleTableCellViewIdentifier = "SimpleTableCellViewIdentifier"
static let centeredTableCellViewIdentifier = "CenteredTableCellViewIdentifier"
static let albumTableCellViewIdentifier = "AlbumTableCellViewIdentifier"
static let trackTableCellViewIdentifier = "TrackTableCellViewIdentifier"
}
struct Items {
static let loadingItem: AnyObject = "LoadingItem"
static let noResultsItem: AnyObject = "NoResultsItem"
static let searchResultsHeaderItem: AnyObject = MainViewController.Section.SearchResults.rawValue
static let albumsHeaderItem: AnyObject = MainViewController.Section.Albums.rawValue
static let unsortedTracksHeaderItem: AnyObject = MainViewController.Section.UnsortedTracks.rawValue
}
static let pasteboardType = "public.item.tagtunes"
}
internal enum Section: String {
case SearchResults = "SearchResults"
case Albums = "Albums"
case UnsortedTracks = "UnsortedTracks"
static func isHeaderItem(item: AnyObject) -> Bool {
if let itemAsString = item as? String {
return Section(rawValue: itemAsString) != nil
} else {
return false
}
}
}
// MARK: IBOutlets
@IBOutlet private weak var outlineView: NSOutlineView!
// MARK: Properties
/// Used for searching and loading search results
private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
/// The URL task currently loading the search results
private var searchTask: NSURLSessionDataTask?
/// If `true` the search section is displayed at the top of the
/// `outlineView`.
internal var showsSearch: Bool = false
/// `true` if there is currently a search in progress.
internal var searching: Bool {
return searchTask != nil
}
/// The error that occured during searching, if any.
internal private(set) var searchError: NSError?
/// The URL tasks currently loading the tracks for the respective albums.
private var trackTasks = [Album: NSURLSessionDataTask]()
/// Errors that occured during loading the tracks for the respective album.
private var trackErrors = [Album: NSError]()
// MARK: Overrides
override internal func viewDidLoad() {
super.viewDidLoad()
outlineView.setDraggingSourceOperationMask(.Move, forLocal: true)
outlineView.registerForDraggedTypes([OutlineViewConstants.pasteboardType])
}
// MARK: Outline View Content
internal private(set) var searchResults = [SearchResult]()
internal private(set) var albums = [Album]()
internal private(set) var unsortedTracks = [iTunesTrack]()
/// Returns all `iTunesTrack` objects that are somewhere down the outline
/// view.
private var allITunesTracks: Set<iTunesTrack> {
return Set(unsortedTracks).union(albums.flatMap({ $0.tracks.flatMap { $0.associatedTracks } }))
}
/// Returns all contents of the outline view.
///
/// This property is regenerated every time it is queried. If you need to
/// access it a lot of times it is recommended to chache it into a local
/// variable.
private var outlineViewContents: [AnyObject] {
var contents = [AnyObject]()
if showsSearch {
contents.append(OutlineViewConstants.Items.searchResultsHeaderItem)
if !searchResults.isEmpty {
contents.extend(searchResults as [AnyObject])
} else if searching {
contents.append(OutlineViewConstants.Items.loadingItem)
} else if let error = searchError {
contents.append(error)
} else {
contents.append(OutlineViewConstants.Items.noResultsItem)
}
if !albums.isEmpty {
contents.append(OutlineViewConstants.Items.albumsHeaderItem)
}
}
contents.extend(albums as [AnyObject])
if !unsortedTracks.isEmpty {
contents.append(OutlineViewConstants.Items.unsortedTracksHeaderItem)
contents.extend(unsortedTracks as [AnyObject])
}
return contents
}
/// Returns the section of the specified row or `nil` if the row is not a
/// valid row.
internal func sectionOfRow(row: Int) -> Section? {
if row < 0 {
return nil
}
var relativeRow = row
if showsSearch {
let searchRelatedItemCount = 1 + (searchResults.isEmpty ? 1 : searchResults.count)
if relativeRow < searchRelatedItemCount {
return .SearchResults
} else {
relativeRow -= searchRelatedItemCount
}
}
var maxRow = outlineView.numberOfRows
if !unsortedTracks.isEmpty {
maxRow -= unsortedTracks.count + 1
}
if relativeRow < maxRow {
return .Albums
} else {
relativeRow -= maxRow
}
if relativeRow < unsortedTracks.count + 1 {
return .UnsortedTracks
}
return nil
}
/// Returns the section the specified item resides in or `nil` if the item is
/// not part of the outline view's contents.
internal func sectionOfItem(item: AnyObject) -> Section? {
if let album = item as? Album where albums.contains(album) {
return .Albums
} else if let track = item as? Track where albums.contains(track.album) {
return .Albums
} else if let track = item as? iTunesTrack {
if let parentTrack = outlineView.parentForItem(track) as? Track where albums.contains(parentTrack.album) {
return .Albums
} else {
return unsortedTracks.contains(track) ? .UnsortedTracks : nil
}
} else if item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError {
return .SearchResults
} else if let string = item as? String {
return Section(rawValue: string)
} else {
return nil
}
}
/// Returns `true` if the specified `album` is currently loading its tracks.
internal func isAlbumLoading(album: Album) -> Bool {
return trackTasks[album] != nil
}
// MARK: Searching
/// Starts a search for the specified search term. Calling this method
internal func beginSearchForTerm(term: String) {
searchTask?.cancel()
searchResults.removeAll()
if let url = iTunesAPI.createAlbumSearchURLForTerm(term) {
showsSearch = true
searchTask = urlSession.dataTaskWithURL(url, completionHandler: processSearchResults)
searchTask?.resume()
} else {
showsSearch = false
}
outlineView.reloadData()
}
/// Processes the data returned from a network request into the
/// `searchResults`.
private func processSearchResults(data: NSData?, response: NSURLResponse?, error: NSError?) {
searchTask = nil
if let theError = error {
searchError = theError
} else if let theData = data {
do {
let searchResults = try iTunesAPI.parseAPIData(theData).map { SearchResult(representedAlbum: $0) }
self.searchResults = searchResults
} catch let error as NSError {
searchError = error
}
}
showsSearch = true
outlineView.reloadData()
}
/// Adds the search result at the specified `row` to the albums section and
/// begins loading its tracks.
internal func selectSearchResultAtRow(row: Int) {
guard sectionOfRow(row) == .SearchResults else {
return
}
let searchResult = outlineView.itemAtRow(row) as! SearchResult
if !Preferences.sharedPreferences.keepSearchResults {
searchResults.removeAll()
showsSearch = false
}
var albumAlreadyPresent = false
for album in albums {
if album == searchResult {
albumAlreadyPresent = true
}
}
if !albumAlreadyPresent {
albums.append(beginLoadingTracksForSearchResult(searchResult))
}
outlineView.reloadData()
}
// MARK: Albums
private func beginLoadingTracksForSearchResult(searchResult: SearchResult) -> Album {
let album = Album(searchResult: searchResult)
let url = iTunesAPI.createAlbumLookupURLForId(album.id)
let task = urlSession.dataTaskWithURL(url) { (data, response, var error) -> Void in
self.trackTasks[album] = nil
do {
if let theData = data {
let newAlbum = try iTunesAPI.parseAPIData(theData)[0]
let index = self.albums.indexOf(album)!
self.albums.removeAtIndex(index)
self.albums.insert(newAlbum, atIndex: index)
}
} catch let theError as NSError {
error = theError
} catch _ {
// Will never happen
}
self.trackErrors[album] = error
self.outlineView.reloadData()
}
trackTasks[album] = task
task.resume()
return album
}
func cancelLoadingTracksForAlbum(album: Album) {
trackTasks[album]?.cancel()
trackTasks[album] = nil
}
private func saveTracks(tracks: [Track: [iTunesTrack]]) {
let numberOfTracks = tracks.reduce(0) { (count: Int, element: (key: Track, value: [iTunesTrack])) -> Int in
return count + element.value.count
}
let progress = NSProgress(totalUnitCount: Int64(numberOfTracks))
NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving tracks…", comment: "Alert message indicating that the selected tracks are currently being saved")
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
for (parentTrack, targetTracks) in tracks {
for track in targetTracks {
parentTrack.saveToTrack(track)
++progress.completedUnitCount
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
}
}
}
private func saveArtworks(tracks: [Track: [iTunesTrack]]) {
var albums = Set<Album>()
for (track, _) in tracks {
albums.insert(track.album)
}
let progress = NSProgress(totalUnitCount: Int64(albums.count))
var errorCount = 0
NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving artworks…", comment: "Alert message indicating that the artworks for the selected tracks are currently being saved")
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
for album in albums {
do {
try album.saveArtwork()
} catch _ {
++errorCount
}
++progress.completedUnitCount
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
}
if errorCount > 0 {
dispatch_sync(dispatch_get_main_queue()) {
let alert = NSAlert()
if errorCount == 1 {
alert.messageText = NSLocalizedString("1 artwork could not be saved.", comment: "Error message indicating that one of the artworks could not be saved.")
} else {
alert.messageText = String(format: NSLocalizedString("%d artworks could not be saved.", comment: "Error message indicating that n artworks could not be saved."), errorCount)
}
alert.informativeText = NSLocalizedString("Please check your privileges for the folder you set in the preferences and try again.", comment: "Informative text for 'artwork(s) could not be saved' errors")
alert.alertStyle = .WarningAlertStyle
alert.addButtonWithTitle("OK")
alert.beginSheetModalForWindow(self.view.window!, completionHandler: nil)
}
}
}
// MARK: Actions
/// Adds the current iTunes selection
@IBAction internal func addITunesSelection(sender: AnyObject?) {
if !iTunes.running {
let alert = NSAlert()
alert.messageText = NSLocalizedString("iTunes is not running", comment: "Error message informing the user that iTunes is not currently running.")
alert.informativeText = NSLocalizedString("Please launch iTunes and try again.", comment: "Informative text for the 'iTunes is not running' error")
alert.addButtonWithTitle(NSLocalizedString("OK", comment: "Button title"))
alert.beginSheetModalForWindow(view.window!, completionHandler: nil)
} else if let selection = iTunes.selection.get() as? [iTunesTrack] {
let newTracks = Set(selection).subtract(allITunesTracks)
unsortedTracks.extend(newTracks)
outlineView.reloadData()
}
}
/// Begins to search for the `sender`'s `stringValue`.
@IBAction internal func performSearch(sender: AnyObject?) {
if let searchTerm = sender?.stringValue {
beginSearchForTerm(searchTerm)
}
}
/// Selects the search result associated with the `sender`'s row (as
/// determined by `NSOutlineView.rowForView`) and adds it to the list of
/// albums.
@IBAction private func selectSearchResult(sender: AnyObject?) {
if let view = sender as? NSView {
let row = outlineView.rowForView(view)
selectSearchResultAtRow(row)
}
}
/// Saves the selected items to iTunes. The saving process will be reported
/// to the user in a progress sheet.
@IBAction internal func performSave(sender: AnyObject?) {
var itemsToBeSaved = [Track: [iTunesTrack]]()
for row in outlineView.selectedRowIndexes where sectionOfRow(row) == .Albums {
let item = outlineView.itemAtRow(row)
if let album = item as? Album {
for track in album.tracks where !track.associatedTracks.isEmpty {
itemsToBeSaved[track] = track.associatedTracks
}
} else if let track = item as? Track {
itemsToBeSaved[track] = track.associatedTracks
} else if let track = item as? iTunesTrack {
if let parentTrack = outlineView.parentForItem(track) as? Track {
if itemsToBeSaved[parentTrack] != nil {
itemsToBeSaved[parentTrack]?.append(track)
} else {
itemsToBeSaved[parentTrack] = [track]
}
}
}
}
guard !itemsToBeSaved.isEmpty else {
return
}
let progress = NSProgress(totalUnitCount: 100)
progress.beginProgressSheetModalForWindow(self.view.window!) {
reponse in
self.outlineView.reloadData()
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
if Preferences.sharedPreferences.saveArtwork {
progress.becomeCurrentWithPendingUnitCount(90)
} else {
progress.becomeCurrentWithPendingUnitCount(100)
}
self.saveTracks(itemsToBeSaved)
progress.resignCurrent()
if Preferences.sharedPreferences.saveArtwork {
progress.becomeCurrentWithPendingUnitCount(10)
self.saveArtworks(itemsToBeSaved)
progress.resignCurrent()
}
}
}
/// Removes the selected items from the outline view.
@IBAction internal func delete(sender: AnyObject?) {
let items = outlineView.selectedRowIndexes.map { ($0, outlineView.itemAtRow($0)) }
for (row, item) in items {
if sectionOfRow(row)! != .SearchResults {
if let album = item as? Album {
cancelLoadingTracksForAlbum(album)
albums.removeElement(album)
} else if let track = item as? Track {
track.associatedTracks = []
} else if let track = item as? iTunesTrack {
if let parentTrack = outlineView.parentForItem(track) as? Track {
parentTrack.associatedTracks.removeElement(track)
} else {
unsortedTracks.removeElement(track)
}
}
}
}
outlineView.reloadData()
}
/// Action that should be triggered from a view inside the outline view. If
/// `sender` is not an `NSError` instance the item at the row associated with
/// the `sender` (as determined by `NSOutlineView.rowForView`) should be a
/// `NSError` or `Album` instance for this method to work correctly.
@IBAction private func showErrorDetails(sender: AnyObject?) {
var error: NSError
if let theError = sender as? NSError {
error = theError
} else if let view = sender as? NSView {
let row = outlineView.rowForView(view)
let item = outlineView.itemAtRow(row)
if let theError = item as? NSError {
error = theError
} else if let album = item as? Album {
if let theError = trackErrors[album] {
error = theError
} else {
return
}
} else {
return
}
} else {
return
}
presentError(error, modalForWindow: view.window!, delegate: nil, didPresentSelector: nil, contextInfo: nil)
}
}
// MARK: - User Interface Validations
extension MainViewController: NSUserInterfaceValidations {
func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool {
if anItem.action() == "performSave:" {
for row in outlineView.selectedRowIndexes {
return sectionOfRow(row) == .Albums
}
} else if anItem.action() == "addITunesSelection:" {
guard iTunes.running else {
return false
}
return !(iTunes.selection.get() as! [AnyObject]).isEmpty
} else if anItem.action() == "delete:" {
for row in outlineView.selectedRowIndexes {
if sectionOfRow(row) != .SearchResults {
return true
}
}
}
return false
}
}
// MARK: - Outline View
extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if item == nil {
return outlineViewContents.count
} else if let album = item as? Album {
return album.tracks.count
} else if let track = item as? Track {
return track.associatedTracks.count
} else {
return 0
}
}
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
if item == nil {
return outlineViewContents[index]
} else if let album = item as? Album {
return album.tracks[index]
} else if let track = item as? Track {
return track.associatedTracks[index]
} else {
return ""
}
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
return self.outlineView(outlineView, numberOfChildrenOfItem: item) > 0
}
func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
return Section.isHeaderItem(item)
}
func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool {
return !(self.outlineView(outlineView, isGroupItem: item) || item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError)
}
func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
if item is Album || item is SearchResult {
return 39
} else if let track = item as? Track {
return track.album.hasSameArtistNameAsTracks ? 24 : 31
} else if item === OutlineViewConstants.Items.loadingItem {
return 39
} else if item === OutlineViewConstants.Items.noResultsItem || item is NSError {
return 32
} else if Section.isHeaderItem(item) {
return 24
} else {
return 17
}
}
func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
if item === OutlineViewConstants.Items.searchResultsHeaderItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
if view == nil {
view = AdvancedTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
}
view?.style = .Simple
view?.textField?.font = NSFont.boldSystemFontOfSize(0)
view?.textField?.textColor = NSColor.disabledControlTextColor()
view?.textField?.stringValue = NSLocalizedString("Search Results", comment: "Header name for the seach results section")
return view
}
if item === OutlineViewConstants.Items.albumsHeaderItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
if view == nil {
view = AdvancedTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
}
view?.style = .Simple
view?.textField?.font = NSFont.boldSystemFontOfSize(0)
view?.textField?.textColor = NSColor.disabledControlTextColor()
view?.textField?.stringValue = NSLocalizedString("Albums", comment: "Header name for the albums section")
return view
}
if item === OutlineViewConstants.Items.unsortedTracksHeaderItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
if view == nil {
view = AdvancedTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
}
view?.style = .Simple
view?.textField?.font = NSFont.boldSystemFontOfSize(0)
view?.textField?.textColor = NSColor.disabledControlTextColor()
view?.textField?.stringValue = NSLocalizedString("Unsorted Tracks", comment: "Header name for the unsorted tracks section")
return view
}
if item === OutlineViewConstants.Items.loadingItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView
if view == nil {
view = CenteredTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier
}
view?.setupForLoading()
return view
}
if item === OutlineViewConstants.Items.noResultsItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView
if view == nil {
view = CenteredTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier
}
view?.setupForMessage(NSLocalizedString("No Results", comment: "Message informing the user that the search didn't return any results"))
return view
}
if let error = item as? NSError {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView
if view == nil {
view = CenteredTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier
}
view?.button?.target = self
view?.button?.action = "showErrorDetails:"
view?.setupForError(error, errorMessage: NSLocalizedString("Failed to load results", comment: "Error message informing the user that an error occured during searching."))
return view
}
if let album = item as? Album {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
if view == nil {
view = AlbumTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
}
view?.setupForAlbum(album, loading: isAlbumLoading(album), error: trackErrors[album])
view?.errorButton?.target = self
view?.errorButton?.action = "showErrorDetails:"
return view
}
if let searchResult = item as? SearchResult {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
if view == nil {
view = AlbumTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
}
view?.button.target = self
view?.button.action = "selectSearchResult:"
let selectable = albums.filter { $0.id == searchResult.id }.isEmpty
view?.setupForSearchResult(searchResult, selectable: selectable)
return view
}
if let track = item as? Track {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier, owner: nil) as? TrackTableCellView
if view == nil {
view = TrackTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier
}
view?.setupForTrack(track)
return view
}
if let track = item as? iTunesTrack {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
if view == nil {
view = AdvancedTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
}
view?.style = .Simple
view?.textField?.font = NSFont.systemFontOfSize(0)
view?.textField?.textColor = NSColor.textColor()
view?.textField?.stringValue = track.name
return view
}
return nil
}
func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
var rows = [Int]()
var containsValidItems = false
for item in items {
let row = outlineView.rowForItem(item)
rows.append(row)
if sectionOfRow(row) != .SearchResults {
containsValidItems = true
}
}
if !containsValidItems {
return false
}
let data = NSKeyedArchiver.archivedDataWithRootObject(rows)
pasteboard.declareTypes([OutlineViewConstants.pasteboardType], owner: nil)
pasteboard.setData(data, forType: OutlineViewConstants.pasteboardType)
return true
}
func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
let firstUnsortedRow = outlineViewContents.count - (unsortedTracks.isEmpty ? 0 : unsortedTracks.count+1)
// Drop in the 'unsorted' section
if item == nil && index >= firstUnsortedRow || item === OutlineViewConstants.Items.unsortedTracksHeaderItem {
outlineView.setDropItem(nil, dropChildIndex: outlineViewContents.count)
return .Every
}
// Drop on iTunesTrack item or between items
if index != NSOutlineViewDropOnItemIndex || item is iTunesTrack {
return .None
}
// Drop on header row
if item != nil && self.outlineView(outlineView, isGroupItem: item!) {
return .None
}
// Drop in 'search results' section
let row = outlineView.rowForItem(item)
if sectionOfRow(row) == .SearchResults {
return .None
}
if let album = item as? Album where isAlbumLoading(album) || trackErrors[album] != nil {
return .None
}
return .Every
}
func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
guard let data = info.draggingPasteboard().dataForType(OutlineViewConstants.pasteboardType), draggedRows = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [Int] else {
return false
}
// Get the dragged tracks and remove them from their previous location
var draggedTracks = Set<iTunesTrack>()
for row in draggedRows {
if sectionOfRow(row) != .SearchResults {
let item = outlineView.itemAtRow(row)
if let album = item as? Album {
for track in album.tracks {
draggedTracks.unionInPlace(track.associatedTracks)
track.associatedTracks.removeAll()
}
} else if let track = item as? Track {
draggedTracks.unionInPlace(track.associatedTracks)
track.associatedTracks.removeAll()
} else if let track = item as? iTunesTrack {
draggedTracks.insert(track)
if let parentTrack = outlineView.parentForItem(track) as? Track {
parentTrack.associatedTracks.removeElement(track)
} else {
unsortedTracks.removeElement(track)
}
}
}
}
// Add the dragged tracks to the new target
if let targetTrack = item as? Track {
targetTrack.associatedTracks.extend(draggedTracks)
} else if let targetAlbum = item as? Album {
for draggedTrack in draggedTracks {
var inserted = false
for track in targetAlbum.tracks {
if (draggedTrack.discNumber == track.discNumber || draggedTrack.discNumber == 0) && draggedTrack.trackNumber == track.trackNumber {
track.associatedTracks.append(draggedTrack)
inserted = true
break
}
}
if !inserted {
unsortedTracks.append(draggedTrack)
}
}
} else {
unsortedTracks.extend(draggedTracks)
}
outlineView.reloadData()
return true
}
}