2016-10-03 16:03:18 +03:00
|
|
|
/**
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
* See LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
import RxSwift
|
|
|
|
|
|
|
|
enum FileOutlineViewAction {
|
|
|
|
|
2016-11-12 18:42:10 +03:00
|
|
|
case open(fileItem: FileItem)
|
|
|
|
case openFileInNewTab(fileItem: FileItem)
|
|
|
|
case openFileInCurrentTab(fileItem: FileItem)
|
|
|
|
case openFileInHorizontalSplit(fileItem: FileItem)
|
|
|
|
case openFileInVerticalSplit(fileItem: FileItem)
|
|
|
|
case setAsWorkingDirectory(fileItem: FileItem)
|
|
|
|
case setParentAsWorkingDirectory(fileItem: FileItem)
|
2016-10-03 16:03:18 +03:00
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
class FileOutlineView: NSOutlineView, Flow, NSOutlineViewDataSource, NSOutlineViewDelegate {
|
2016-10-03 16:03:18 +03:00
|
|
|
|
|
|
|
fileprivate let flow: EmbeddableComponent
|
2016-10-05 22:45:52 +03:00
|
|
|
fileprivate let fileItemService: FileItemService
|
|
|
|
|
2016-10-07 22:14:43 +03:00
|
|
|
fileprivate var fileItems = Set<FileItem>()
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
fileprivate var expandedItems = Set<FileItem>()
|
2016-10-03 16:03:18 +03:00
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
// MARK: - API
|
2016-10-03 16:03:18 +03:00
|
|
|
var sink: Observable<Any> {
|
|
|
|
return self.flow.sink
|
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
var cwd: URL = FileUtils.userHomeUrl
|
|
|
|
|
|
|
|
init(source: Observable<Any>, fileItemService: FileItemService) {
|
2016-10-03 16:03:18 +03:00
|
|
|
self.flow = EmbeddableComponent(source: source)
|
2016-10-05 22:45:52 +03:00
|
|
|
self.fileItemService = fileItemService
|
2016-10-03 16:03:18 +03:00
|
|
|
|
|
|
|
super.init(frame: CGRect.zero)
|
2016-10-05 22:45:52 +03:00
|
|
|
|
|
|
|
self.dataSource = self
|
|
|
|
self.delegate = self
|
2016-11-12 18:42:10 +03:00
|
|
|
|
|
|
|
guard Bundle.main.loadNibNamed("FileBrowserMenu", owner: self, topLevelObjects: nil) else {
|
|
|
|
NSLog("WARN: FileBrowserMenu.xib could not be loaded")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.doubleAction = #selector(FileOutlineView.doubleClickAction)
|
2016-10-05 22:45:52 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - NSOutlineView
|
|
|
|
extension FileOutlineView {
|
|
|
|
|
|
|
|
override func reloadItem(_ item: Any?, reloadChildren: Bool) {
|
2016-10-07 02:12:07 +03:00
|
|
|
// NSLog("\(#function): \(item)")
|
2016-10-05 22:45:52 +03:00
|
|
|
let selectedItem = self.selectedItem
|
|
|
|
let visibleRect = self.enclosingScrollView?.contentView.visibleRect
|
|
|
|
|
2016-10-07 01:24:15 +03:00
|
|
|
let expandedItems = self.expandedItems
|
2016-10-07 22:14:43 +03:00
|
|
|
|
|
|
|
if item == nil {
|
|
|
|
self.fileItems.removeAll()
|
|
|
|
} else {
|
|
|
|
guard let fileItem = item as? FileItem else {
|
|
|
|
preconditionFailure("Should not happen")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.fileItems.remove(fileItem)
|
2016-11-12 18:42:10 +03:00
|
|
|
if fileItem.isDir {
|
2016-10-07 22:14:43 +03:00
|
|
|
self.fileItems
|
2016-10-08 20:58:21 +03:00
|
|
|
.filter { fileItem.url.isParent(of: $0.url) }
|
2016-10-07 22:14:43 +03:00
|
|
|
.forEach { self.fileItems.remove($0) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
super.reloadItem(item, reloadChildren: reloadChildren)
|
|
|
|
|
2016-10-07 01:24:15 +03:00
|
|
|
self.restore(expandedItems: expandedItems)
|
2016-10-07 02:12:07 +03:00
|
|
|
self.adjustFileViewWidth()
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
self.scrollToVisible(visibleRect!)
|
|
|
|
|
|
|
|
guard let selectedFileItem = selectedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for idx in 0..<self.numberOfRows {
|
|
|
|
guard let itemAtRow = self.item(atRow: idx) as? FileItem else {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if itemAtRow == selectedFileItem {
|
|
|
|
self.selectRowIndexes(IndexSet([idx]), byExtendingSelection: false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func restoreExpandedState(for item: FileItem, states: Set<FileItem>) {
|
2016-11-12 18:42:10 +03:00
|
|
|
guard item.isDir && states.contains(item) else {
|
2016-10-05 22:45:52 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.expandItem(item)
|
2016-10-07 02:12:07 +03:00
|
|
|
self.expandedItems.insert(item)
|
2016-10-05 22:45:52 +03:00
|
|
|
|
|
|
|
item.children.forEach { [unowned self] child in
|
|
|
|
self.restoreExpandedState(for: child, states: states)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-07 01:24:15 +03:00
|
|
|
fileprivate func restore(expandedItems: Set<FileItem>) {
|
2016-10-07 02:12:07 +03:00
|
|
|
// NSLog("\(#function): \(expandedItems)")
|
2016-10-07 01:24:15 +03:00
|
|
|
if expandedItems.isEmpty {
|
2016-10-05 22:45:52 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let root = self.fileItemService.fileItemWithChildren(for: self.cwd) else {
|
|
|
|
self.expandedItems.removeAll()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.expandedItems.removeAll()
|
2016-10-07 01:24:15 +03:00
|
|
|
root.children.forEach { self.restoreExpandedState(for: $0, states: expandedItems) }
|
2016-10-05 22:45:52 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - NSOutlineViewDataSource
|
|
|
|
extension FileOutlineView {
|
|
|
|
|
|
|
|
func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
|
|
|
|
if item == nil {
|
|
|
|
return self.fileItemService.fileItemWithChildren(for: self.cwd)?.children
|
2016-11-12 18:42:10 +03:00
|
|
|
.filter { !$0.isHidden }
|
2016-10-05 22:45:52 +03:00
|
|
|
.count ?? 0
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let fileItem = item as? FileItem else {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:42:10 +03:00
|
|
|
if fileItem.isDir {
|
2016-10-05 22:45:52 +03:00
|
|
|
return self.fileItemService.fileItemWithChildren(for: fileItem.url)?.children
|
2016-11-12 18:42:10 +03:00
|
|
|
.filter { !$0.isHidden }
|
2016-10-05 22:45:52 +03:00
|
|
|
.count ?? 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineView(_: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
|
|
|
|
if item == nil {
|
2016-11-12 18:42:10 +03:00
|
|
|
let result = self.fileItemService.fileItemWithChildren(for: self.cwd)!.children.filter { !$0.isHidden }[index]
|
2016-10-07 22:14:43 +03:00
|
|
|
self.fileItems.insert(result)
|
|
|
|
|
|
|
|
return result
|
2016-10-05 22:45:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
guard let fileItem = item as? FileItem else {
|
|
|
|
preconditionFailure("Should not happen")
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:42:10 +03:00
|
|
|
let result = self.fileItemService.fileItemWithChildren(for: fileItem.url)!.children.filter { !$0.isHidden }[index]
|
2016-10-07 22:14:43 +03:00
|
|
|
self.fileItems.insert(result)
|
|
|
|
|
|
|
|
return result
|
2016-10-05 22:45:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool {
|
|
|
|
guard let fileItem = item as? FileItem else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:42:10 +03:00
|
|
|
return fileItem.isDir
|
2016-10-05 22:45:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc(outlineView:objectValueForTableColumn:byItem:)
|
|
|
|
func outlineView(_: NSOutlineView, objectValueFor: NSTableColumn?, byItem item: Any?) -> Any? {
|
|
|
|
guard let fileItem = item as? FileItem else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return fileItem
|
2016-10-03 16:03:18 +03:00
|
|
|
}
|
2016-10-05 22:45:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - NSOutlineViewDelegate
|
|
|
|
extension FileOutlineView {
|
|
|
|
|
|
|
|
@objc(outlineView:viewForTableColumn:item:)
|
|
|
|
func outlineView(_: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
|
|
|
|
guard let fileItem = item as? FileItem else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
let cachedCell = self.make(withIdentifier: "file-view-row", owner: self)
|
|
|
|
let cell = cachedCell as? ImageAndTextTableCell ?? ImageAndTextTableCell(withIdentifier: "file-view-row")
|
|
|
|
|
|
|
|
cell.text = fileItem.url.lastPathComponent
|
|
|
|
cell.image = self.fileItemService.icon(forUrl: fileItem.url)
|
|
|
|
|
|
|
|
return cell
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineView(_: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
|
|
|
|
return 20
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineViewItemDidExpand(_ notification: Notification) {
|
2016-10-07 02:12:07 +03:00
|
|
|
if let fileItem = notification.userInfo?["NSObject"] as? FileItem {
|
|
|
|
self.expandedItems.insert(fileItem)
|
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
self.adjustFileViewWidth()
|
|
|
|
}
|
|
|
|
|
|
|
|
func outlineViewItemDidCollapse(_ notification: Notification) {
|
2016-10-07 02:12:07 +03:00
|
|
|
if let fileItem = notification.userInfo?["NSObject"] as? FileItem {
|
|
|
|
self.expandedItems.remove(fileItem)
|
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
self.adjustFileViewWidth()
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func adjustFileViewWidth() {
|
|
|
|
let indentationPerLevel = self.indentationPerLevel
|
|
|
|
let attrs = [NSFontAttributeName: ImageAndTextTableCell.font]
|
|
|
|
|
|
|
|
let maxWidth = (0..<self.numberOfRows).reduce(CGFloat(0)) { (curMaxWidth, idx) in
|
|
|
|
guard let item = self.item(atRow: idx) as? FileItem else {
|
|
|
|
return curMaxWidth
|
|
|
|
}
|
|
|
|
|
|
|
|
let level = CGFloat(self.level(forRow: idx) + 1)
|
|
|
|
let indentation = level * indentationPerLevel
|
|
|
|
let width = (item.url.lastPathComponent as NSString).size(withAttributes: attrs).width + indentation
|
|
|
|
|
|
|
|
return max(curMaxWidth, width)
|
|
|
|
}
|
|
|
|
|
|
|
|
let column = self.outlineTableColumn!
|
|
|
|
column.minWidth = maxWidth + ImageAndTextTableCell.widthWithoutText
|
|
|
|
column.maxWidth = column.minWidth
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:42:10 +03:00
|
|
|
// MARK: - Actions
|
|
|
|
extension FileOutlineView {
|
|
|
|
|
|
|
|
@IBAction func doubleClickAction(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if item.isDir {
|
|
|
|
self.toggle(item: item)
|
|
|
|
} else {
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.open(fileItem: item))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func openInNewTab(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.openFileInNewTab(fileItem: item))
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func openInCurrentTab(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.openFileInCurrentTab(fileItem: item))
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func openInHorizontalSplit(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.openFileInHorizontalSplit(fileItem: item))
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func openInVerticalSplit(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.openFileInVerticalSplit(fileItem: item))
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func setAsWorkingDirectory(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard item.isDir else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.setAsWorkingDirectory(fileItem: item))
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func setParentAsWorkingDirectory(_: Any?) {
|
|
|
|
guard let item = self.clickedItem as? FileItem else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard item.isDir else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard item.url.path != "/" else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.flow.publish(event: FileOutlineViewAction.setParentAsWorkingDirectory(fileItem: item))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - NSUserInterfaceValidations
|
|
|
|
extension FileOutlineView {
|
|
|
|
|
|
|
|
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
guard let clickedItem = self.clickedItem as? FileItem else {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if item.action == #selector(setAsWorkingDirectory(_:))
|
|
|
|
|| item.action == #selector(setParentAsWorkingDirectory(_:))
|
|
|
|
{
|
|
|
|
return clickedItem.isDir
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-05 22:45:52 +03:00
|
|
|
// MARK: - NSView
|
|
|
|
extension FileOutlineView {
|
2016-10-03 16:03:18 +03:00
|
|
|
|
|
|
|
override func keyDown(with event: NSEvent) {
|
|
|
|
guard let char = event.charactersIgnoringModifiers?.characters.first else {
|
|
|
|
super.keyDown(with: event)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let item = self.selectedItem as? FileItem else {
|
|
|
|
super.keyDown(with: event)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch char {
|
|
|
|
case " ", "\r": // Why "\r" and not "\n"?
|
2016-11-12 18:42:10 +03:00
|
|
|
if item.isDir || item.isPackage {
|
2016-10-03 16:03:18 +03:00
|
|
|
self.toggle(item: item)
|
|
|
|
} else {
|
2016-11-12 18:42:10 +03:00
|
|
|
self.flow.publish(event: FileOutlineViewAction.openFileInNewTab(fileItem: item))
|
2016-10-03 16:03:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
super.keyDown(with: event)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|