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/OutlineContentViewController.swift
Kim Wittenburg 0a485ff42a Stuff…
2019-02-01 22:59:01 +01:00

421 lines
17 KiB
Swift
Executable File

//
// OutlineContentViewController.swift
// TagTunes
//
// Created by Kim Wittenburg on 21.01.16.
// Copyright © 2016 Kim Wittenburg. All rights reserved.
//
import Foundation
import AppKitPlus
import SearchAPI
class OutlineContentViewController: ContentViewController {
// MARK: Types
private struct OutlineViewConstants {
struct ViewIdentifiers {
/// The identifier used for the outline view rows representing
/// `TagTunesTrack`s.
static let SimpleTableCellViewIdentifier = "SimpleTableCellViewIdentifier"
/// The identifier used for the outline view rows representing
/// `AlbumItem`s.
static let AlbumTableCellViewIdentifier = "AlbumTableCellViewIdentifier"
/// The identifier used for the outline view rows representing
/// `SongItem`s.
static let TrackTableCellViewIdentifier = "TrackTableCellViewIdentifier"
}
}
// MARK: IBOutlets
@IBOutlet private weak var outlineView: NSOutlineView!
// MARK: Outline View Content
/// All items in the outline view.
internal private(set) var items = [TagTunesItem]()
override func viewDidLoad() {
super.viewDidLoad()
outlineView.registerForDraggedTypes([TrackPboardType])
}
override var selectedItems: [AnyObject] {
var selectedGroups = Set<TagTunesGroupItem>()
var selectedEntities = Set<TagTunesEntityItem>()
var selectedTracks = Set<TagTunesTrack>()
for row in outlineView.selectedRowIndexes {
let item = outlineView.itemAtRow(row)
if let album = item as? TagTunesGroupItem {
selectedGroups.insert(album)
} else if let track = item as? TagTunesEntityItem {
selectedEntities.insert(track)
} else if let track = item as? TagTunesTrack {
selectedTracks.insert(track)
}
}
for group in selectedGroups {
for entity in group.children {
for track in entity.associatedTracks {
selectedTracks.remove(track)
}
selectedEntities.remove(entity)
}
}
for entity in selectedEntities {
for track in entity.associatedTracks {
selectedTracks.remove(track)
}
}
var selectedItems = [AnyObject]()
selectedItems.appendContentsOf(Array(selectedGroups) as [AnyObject])
selectedItems.appendContentsOf(Array(selectedEntities) as [AnyObject])
selectedItems.appendContentsOf(Array(selectedTracks) as [AnyObject])
return selectedItems
}
override var clickedItems: [AnyObject] {
if outlineView.clickedRow < 0 {
return []
} else if outlineView.selectedRowIndexes.contains(outlineView.clickedRow) {
return selectedItems
} else {
return [outlineView.itemAtRow(outlineView.clickedRow)!]
}
}
override func addItem(item: TagTunesItem) {
if itemForEntity(item.entity) == nil {
items.append(item)
outlineView.insertItemsAtIndexes(NSIndexSet(index: items.count-1), inParent: nil, withAnimation: NSTableViewAnimationOptions.EffectNone)
}
}
override func itemForEntity(entity: SearchAPIEntity) -> TagTunesItem? {
for item in items {
if item.entity == entity {
return item
}
if let group = item as? TagTunesGroupItem {
for child in group.children where child.entity == entity {
return child
}
}
}
return nil
}
override func updateItem(item: TagTunesItem) {
outlineView.reloadDataForRowIndexes(NSIndexSet(index: outlineView.rowForItem(item)), columnIndexes: NSIndexSet(index: 0))
outlineView.reloadItem(item, reloadChildren: true)
}
override func updateAllItems() {
outlineView.reloadData()
}
override func removeItems(objects: [AnyObject]) {
outlineView.beginUpdates()
for object in objects {
if let item = object as? TagTunesItem {
item.clearAssociatedTracks()
if let index = items.indexOf({ $0 == item }) {
items.removeAtIndex(index)
outlineView.removeItemsAtIndexes(NSIndexSet(index: index), inParent: nil, withAnimation: .EffectNone)
}
} else if let track = object as? TagTunesTrack {
if let entity = track.entity {
entity.associatedTracks.remove(track)
let row = outlineView.rowForItem(entity)
outlineView.reloadDataForRowIndexes(NSIndexSet(index: row), columnIndexes: NSIndexSet(index: 0))
outlineView.reloadItem(entity, reloadChildren: true)
}
}
}
outlineView.endUpdates()
}
}
// MARK: - Outline View Data Source & Delegate
extension OutlineContentViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if item == nil {
return items.count
} else if let group = item as? TagTunesGroupItem {
if case .Normal = group.loadingState {
return group.children.count
} else {
return 0
}
} else if let entity = item as? TagTunesEntityItem {
return entity.associatedTracks.count
} else {
return 0
}
}
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
if item == nil {
return items[index]
} else if let group = item as? TagTunesGroupItem {
return group.children[index]
} else if let entity = item as? TagTunesEntityItem {
return Array(entity.associatedTracks)[index]
} else {
return ""
}
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
return self.outlineView(outlineView, numberOfChildrenOfItem: item) > 0
}
func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
if item is AlbumItem {
return 39
} else if let entity = item as? TagTunesEntityItem {
return (entity.parentItem != nil && entity.parentItem!.hasCommonArtist) ? 24 : 31
} else {
return 17
}
}
func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
if let albumItem = item as? AlbumItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.AlbumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
if view == nil {
view = AlbumTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.AlbumTableCellViewIdentifier
}
let height = self.outlineView(outlineView, heightOfRowByItem: item)
view?.setupForAlbumItem(albumItem, height: height)
view?.errorButton?.target = self
view?.errorButton.action = #selector(OutlineContentViewController.showErrorDetails(_:))
return view
}
if let songItem = item as? SongItem {
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.TrackTableCellViewIdentifier, owner: nil) as? SongTableCellView
if view == nil {
view = SongTableCellView()
view?.identifier = OutlineViewConstants.ViewIdentifiers.TrackTableCellViewIdentifier
}
view?.setupForSongItem(songItem)
return view
}
if let track = item as? TagTunesTrack {
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()
let nameFont = NSFont.labelFontOfSize(13)
if let name = track.name {
view?.textField?.stringValue = name
view?.textField?.textColor = NSColor.textColor()
view?.textField?.font = nameFont
} else {
let italicFont = NSFontManager.sharedFontManager().convertFont(nameFont, toHaveTrait: .ItalicFontMask)
view?.textField?.stringValue = NSLocalizedString("Unnamed Track", comment: "Default name for a track that has no name associated with it.")
view?.textField?.textColor = NSColor.disabledControlTextColor()
view?.textField?.font = italicFont
}
return view
}
return nil
}
// MARK: Drag and Drop
func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
if items.count > 1 {
for item in items {
if item is TagTunesGroupItem {
return false
}
}
}
var draggedItems = [NSPasteboardItem]()
let addDraggedTrack: (TagTunesTrack, row: Int) -> Void = { track, row in
let item = NSPasteboardItem()
item.setData(NSKeyedArchiver.archivedDataWithRootObject(track), forType: TrackPboardType)
item.setData(NSKeyedArchiver.archivedDataWithRootObject(row), forType: IndexPboardType)
draggedItems.append(item)
}
for item in items {
if let draggedItem = item as? TagTunesItem {
let row = outlineView.rowForItem(draggedItem)
for track in draggedItem.associatedTracks {
addDraggedTrack(track, row: row)
}
} else if let track = item as? TagTunesTrack, entity = track.entity {
if !items.contains({ $0 === entity }) {
let row = outlineView.rowForItem(entity)
addDraggedTrack(track, row: row)
}
}
}
pasteboard.clearContents()
pasteboard.writeObjects(draggedItems)
return !draggedItems.isEmpty
}
func outlineView(outlineView: NSOutlineView, draggingSession session: NSDraggingSession, endedAtPoint screenPoint: NSPoint, operation: NSDragOperation) {
if let pointInWindow = view.window?.convertRectFromScreen(NSRect(origin: screenPoint, size: NSSize.zero)).origin {
let pointInView = view.window!.contentView!.convertPoint(pointInWindow, fromView: nil)
let targetView = view.window!.contentView!.hitTest(pointInView)
if targetView?.isDescendantOf(outlineView) ?? false {
return
}
}
if let draggedItems = session.draggingPasteboard.pasteboardItems where operation != .None {
outlineView.beginUpdates()
let draggedTracks = draggedItems.map { ($0.dataForType(IndexPboardType).map(NSKeyedUnarchiver.unarchiveObjectWithData) as! Int, $0.dataForType(TrackPboardType).map(NSKeyedUnarchiver.unarchiveObjectWithData) as! TagTunesTrack) }
for (row, track) in draggedTracks {
let item = outlineView.itemAtRow(row) as! TagTunesItem
if item is TagTunesGroupItem {
item.clearAssociatedTracks()
} else if let entity = item as? TagTunesEntityItem {
entity.associatedTracks.remove(track)
}
outlineView.reloadDataForRowIndexes(NSIndexSet(index: row), columnIndexes: NSIndexSet(index: 0))
outlineView.reloadItem(item, reloadChildren: true)
}
outlineView.endUpdates()
}
}
func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
// Validate pasteboard contents
guard info.draggingPasteboard().canReadItemWithDataConformingToTypes([TrackPboardType]) else {
return .None
}
// Drop onto empty outline view
if item == nil && index == NSOutlineViewDropOnItemIndex {
return .None
}
// Drop on TagTunesTrack item or between items
if index != NSOutlineViewDropOnItemIndex || item is TagTunesTrack {
return .None
}
// Drop on loading (or failed) album
if let group = item as? TagTunesGroupItem {
guard case .Normal = group.loadingState else {
return .None
}
}
return .Move
}
func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
guard let draggedItems = info.draggingPasteboard().pasteboardItems, targetItem = item as? TagTunesItem else {
return false
}
outlineView.beginUpdates()
let draggedTracks = draggedItems.map { ($0.dataForType(IndexPboardType).map(NSKeyedUnarchiver.unarchiveObjectWithData) as? Int, $0.dataForType(TrackPboardType).map(NSKeyedUnarchiver.unarchiveObjectWithData) as? TagTunesTrack, $0) }
if info.draggingSource() === outlineView {
for (row, track, _) in draggedTracks {
let item = outlineView.itemAtRow(row!) as! TagTunesItem
if item is TagTunesGroupItem {
item.clearAssociatedTracks()
} else if let entity = item as? TagTunesEntityItem, theTrack = track {
entity.associatedTracks.remove(theTrack)
}
outlineView.reloadDataForRowIndexes(NSIndexSet(index: row!), columnIndexes: NSIndexSet(index: 0))
outlineView.reloadItem(item, reloadChildren: true)
}
}
var remainingItems = [NSPasteboardItem]()
for (_, track, item) in draggedTracks {
if let theTrack = track {
if !targetItem.addAssociatedTracks([theTrack]).isEmpty {
remainingItems.append(item)
}
}
}
NSApp.unsortedTracksController.returnDraggedItems(remainingItems)
outlineView.reloadDataForRowIndexes(NSIndexSet(index: outlineView.rowForItem(targetItem)), columnIndexes: NSIndexSet(index: 0))
outlineView.reloadItem(targetItem, reloadChildren: true)
outlineView.endUpdates()
return true
}
}
// MARK: - Error Handling
extension OutlineContentViewController {
/// 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.
@objc @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 group = item as? TagTunesGroupItem {
if case let .Error(theError) = group.loadingState {
error = theError as NSError
} else {
return
}
} else {
return
}
} else {
return
}
presentError(error, modalForWindow: view.window!, delegate: nil, didPresentSelector: nil, contextInfo: nil)
}
override func willPresentError(error: NSError) -> NSError {
let recoveryOptions = [
NSLocalizedString("OK", comment: "Button title"),
NSLocalizedString("Try Again", comment: "Button title for error alerts offering the user to try again.")
]
return DescriptiveError(underlyingError: error, userInfo: [NSRecoveryAttempterErrorKey: self, NSLocalizedRecoveryOptionsErrorKey: recoveryOptions])
}
override func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int, delegate: AnyObject?, didRecoverSelector: Selector, contextInfo: UnsafeMutablePointer<Void>) {
let didRecover = attemptRecoveryFromError(error, optionIndex: recoveryOptionIndex)
delegate?.performSelector(didRecoverSelector, withObject: didRecover, withObject: contextInfo as! AnyObject)
}
override func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int) -> Bool {
if recoveryOptionIndex == 0 {
return true
}
for item in items where item is TagTunesGroupItem {
let group = item as! TagTunesGroupItem
if case let .Error(loadingError as NSError) = group.loadingState where error == loadingError || error.userInfo[NSUnderlyingErrorKey] === loadingError {
group.beginLoadingChildren()
return true
}
}
return false
}
}