Stuff…
This commit is contained in:
421
TagTunes/OutlineContentViewController.swift
Executable file
421
TagTunes/OutlineContentViewController.swift
Executable file
@@ -0,0 +1,421 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user