mirror of
https://github.com/qvacua/vimr.git
synced 2024-12-25 14:52:19 +03:00
React to file additions/removals
This commit is contained in:
parent
ad4326dffb
commit
55bb4762fe
@ -13,6 +13,7 @@
|
||||
1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9AF20D7BD6E5C975128 /* FoundationCommons.swift */; };
|
||||
1929B0F599D1F62C7BE53D2C /* HttpServerMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */; };
|
||||
1929B1837C750CADB3A5BCB9 /* OpenQuicklyFileViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1558455B3A74D93EF2A /* OpenQuicklyFileViewRow.swift */; };
|
||||
1929B1A39CC79D00DC4E04B6 /* FileOutlineViewOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B28110A57C0C1089B782 /* FileOutlineViewOld.swift */; };
|
||||
1929B20CE35B43BB1CE023BA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC2F05E9A5C0DB039739 /* Theme.swift */; };
|
||||
1929B29B95AD176D57942E08 /* UiRootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B457B9D0FA4D21F3751E /* UiRootReducer.swift */; };
|
||||
1929B3217A7A3D79E28C80DB /* PrefWindowReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B49E6924847AD085C8C9 /* PrefWindowReducer.swift */; };
|
||||
@ -296,6 +297,7 @@
|
||||
1929B14A5949FB64C4B2646F /* KeysPref.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysPref.swift; sourceTree = "<group>"; };
|
||||
1929B1558455B3A74D93EF2A /* OpenQuicklyFileViewRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyFileViewRow.swift; sourceTree = "<group>"; };
|
||||
1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServerMiddleware.swift; sourceTree = "<group>"; };
|
||||
1929B28110A57C0C1089B782 /* FileOutlineViewOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileOutlineViewOld.swift; sourceTree = "<group>"; };
|
||||
1929B34FC23D805A8B29E8F7 /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
|
||||
1929B364460D86F17E80943C /* PrefMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefMiddleware.swift; sourceTree = "<group>"; };
|
||||
1929B365A6434354B568B04F /* FileMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMonitor.swift; sourceTree = "<group>"; };
|
||||
@ -513,6 +515,7 @@
|
||||
1929BCE3E156C06EDF1F2806 /* FileOutlineView.swift */,
|
||||
1929BA5C7099CDEB04B76BA4 /* FileBrowser.swift */,
|
||||
1929BD2CA8DD198A6BCDBCB7 /* ThemedTableSubviews.swift */,
|
||||
1929B28110A57C0C1089B782 /* FileOutlineViewOld.swift */,
|
||||
);
|
||||
name = "File Browser";
|
||||
sourceTree = "<group>";
|
||||
@ -1147,6 +1150,7 @@
|
||||
1929B8F498D1E7C53F572CE2 /* KeysPref.swift in Sources */,
|
||||
1929B5A2EE366F79ED32744C /* KeysPrefReducer.swift in Sources */,
|
||||
1929BB67CAAD4F6CBD38DF0A /* RxRedux.swift in Sources */,
|
||||
1929B1A39CC79D00DC4E04B6 /* FileOutlineViewOld.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -92,11 +92,33 @@ class FileOutlineView: NSOutlineView,
|
||||
})
|
||||
.disposed(by: self.disposeBag)
|
||||
|
||||
source
|
||||
.filter { !self.shouldReloadData(for: $0) }
|
||||
.filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
|
||||
.map { $0.lastFileSystemUpdate.payload }
|
||||
.throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
|
||||
latest: true,
|
||||
scheduler: SerialDispatchQueueScheduler(qos: .background))
|
||||
.map { ($0, Set(self.childUrls(for: $0))) }
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { (url, newChildUrls) in
|
||||
guard let changeTreeNode = self.changeRootTreeNode(for: url) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.handleRemoval(changeTreeNode: changeTreeNode,
|
||||
newChildUrls: newChildUrls)
|
||||
self.handleAddition(changeTreeNode: changeTreeNode,
|
||||
newChildUrls: newChildUrls)
|
||||
})
|
||||
.disposed(by: self.disposeBag)
|
||||
|
||||
self.treeController.childrenKeyPath = "children"
|
||||
self.treeController.leafKeyPath = "isLeaf"
|
||||
self.treeController.countKeyPath = "childrenCount"
|
||||
self.treeController.objectClass = Node.self
|
||||
self.treeController.avoidsEmptySelection = false
|
||||
self.treeController.preservesSelection = true
|
||||
self.treeController.sortDescriptors = [
|
||||
NSSortDescriptor(key: "isLeaf", ascending: true), // Folders first,
|
||||
NSSortDescriptor(key: "displayName", ascending: true) // then, name
|
||||
@ -110,6 +132,64 @@ class FileOutlineView: NSOutlineView,
|
||||
self.reloadRoot()
|
||||
}
|
||||
|
||||
private func changeRootTreeNode(`for` url: URL) -> NSTreeNode? {
|
||||
if url == self.cwd {
|
||||
return self.treeController.arrangedObjects
|
||||
}
|
||||
|
||||
let comps = url.pathComponents.suffix(
|
||||
from: self.cwd.pathComponents.count
|
||||
)
|
||||
|
||||
let rootTreeNode = self.treeController.arrangedObjects
|
||||
let changeTreeNode = comps.reduce(rootTreeNode) { (prev, comp) in
|
||||
return prev.children?.first { child in
|
||||
return child.node?.displayName == comp
|
||||
} ?? prev
|
||||
}
|
||||
|
||||
guard let changeNode = changeTreeNode.node else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard changeNode.url == url && changeNode.children != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return changeTreeNode
|
||||
}
|
||||
|
||||
private func handleAddition(
|
||||
changeTreeNode: NSTreeNode, newChildUrls: Set<URL>
|
||||
) {
|
||||
let existingUrls = changeTreeNode.children?
|
||||
.compactMap { $0.node?.url } ?? []
|
||||
let newNodes = newChildUrls
|
||||
.subtracting(existingUrls)
|
||||
.map { Node(url: $0) }
|
||||
let newIndexPaths = (0..<newNodes.count)
|
||||
.map { i in changeTreeNode.indexPath.appending(i) }
|
||||
|
||||
self.treeController.insert(newNodes,
|
||||
atArrangedObjectIndexPaths: newIndexPaths)
|
||||
}
|
||||
|
||||
private func handleRemoval(
|
||||
changeTreeNode: NSTreeNode, newChildUrls: Set<URL>
|
||||
) {
|
||||
let indexPathsToRemove =
|
||||
changeTreeNode.children?
|
||||
.filter { child in
|
||||
guard let url = child.node?.url else { return true }
|
||||
return newChildUrls.contains(url) == false
|
||||
}
|
||||
.map { $0.indexPath } ?? []
|
||||
|
||||
self.treeController.removeObjects(
|
||||
atArrangedObjectIndexPaths: indexPathsToRemove
|
||||
)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
@ -152,6 +232,18 @@ class FileOutlineView: NSOutlineView,
|
||||
private var triangleClosed: NSImage
|
||||
private var triangleOpen: NSImage
|
||||
|
||||
private func childUrls(for url: URL) -> [URL] {
|
||||
let urls = FileUtils.directDescendants(of: url).sorted { lhs, rhs in
|
||||
return lhs.lastPathComponent < rhs.lastPathComponent
|
||||
}
|
||||
|
||||
if self.isShowHidden {
|
||||
return urls
|
||||
}
|
||||
|
||||
return urls.filter { !$0.isHidden }
|
||||
}
|
||||
|
||||
private func childNodes(for node: Node) -> [Node] {
|
||||
if node.isChildrenScanned {
|
||||
return node.children ?? []
|
||||
@ -217,7 +309,7 @@ class FileOutlineView: NSOutlineView,
|
||||
}
|
||||
|
||||
private func node(from item: Any?) -> Node? {
|
||||
return (item as? NSTreeNode)?.representedObject as? Node
|
||||
return (item as? NSTreeNode)?.node
|
||||
}
|
||||
}
|
||||
|
||||
@ -440,7 +532,15 @@ extension FileOutlineView {
|
||||
}
|
||||
}
|
||||
|
||||
class Node: NSObject {
|
||||
class Node: NSObject, Comparable {
|
||||
|
||||
// static func ==(left: Node, right: Node) -> Bool {
|
||||
// return left.url == right.url
|
||||
// }
|
||||
|
||||
static func <(lhs: Node, rhs: Node) -> Bool {
|
||||
return lhs.displayName < rhs.displayName
|
||||
}
|
||||
|
||||
@objc dynamic var url: URL
|
||||
@objc dynamic var isLeaf: Bool
|
||||
@ -459,6 +559,10 @@ class Node: NSObject {
|
||||
}
|
||||
var isChildrenScanned = false
|
||||
|
||||
override var description: String {
|
||||
return "<Node: \(self.url): \(self.childrenCount) children>"
|
||||
}
|
||||
|
||||
override var hash: Int {
|
||||
return self.url.hashValue
|
||||
}
|
||||
@ -470,447 +574,10 @@ class Node: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OLD
|
||||
class FileOutlineViewOld: NSOutlineView,
|
||||
UiComponent,
|
||||
NSOutlineViewDataSource,
|
||||
NSOutlineViewDelegate,
|
||||
ThemedView {
|
||||
fileprivate extension NSTreeNode {
|
||||
|
||||
typealias StateType = MainWindow.State
|
||||
|
||||
private(set) var theme = Theme.default
|
||||
|
||||
required init(
|
||||
source: Observable<StateType>,
|
||||
emitter: ActionEmitter,
|
||||
state: StateType) {
|
||||
|
||||
self.emit = emitter.typedEmit()
|
||||
self.uuid = state.uuid
|
||||
|
||||
self.root = FileBrowserItem(state.cwd)
|
||||
self.isShowHidden = state.fileBrowserShowHidden
|
||||
|
||||
self.usesTheme = state.appearance.usesTheme
|
||||
self.showsFileIcon = state.appearance.showsFileIcon
|
||||
|
||||
super.init(frame: .zero)
|
||||
NSOutlineView.configure(toStandard: self)
|
||||
|
||||
self.dataSource = self
|
||||
self.delegate = self
|
||||
self.allowsEmptySelection = true
|
||||
|
||||
guard Bundle.main.loadNibNamed(
|
||||
NSNib.Name("FileBrowserMenu"),
|
||||
owner: self,
|
||||
topLevelObjects: nil
|
||||
) else {
|
||||
NSLog("WARN: FileBrowserMenu.xib could not be loaded")
|
||||
return
|
||||
}
|
||||
|
||||
// If the target of the menu items is set to the first responder,
|
||||
// the actions are not invoked at all when the file monitor fires
|
||||
// in the background... Dunno why it worked before the redesign... -_-
|
||||
self.menu?.items.forEach { $0.target = self }
|
||||
|
||||
// self.doubleAction = #selector(FileOutlineViewOld.doubleClickAction)
|
||||
|
||||
source
|
||||
.filter { !self.shouldReloadData(for: $0) }
|
||||
.filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
|
||||
.throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
|
||||
latest: true,
|
||||
scheduler: SerialDispatchQueueScheduler(qos: .background))
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { state in
|
||||
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
|
||||
guard let fileBrowserItem = self.fileBrowserItem(
|
||||
with: state.lastFileSystemUpdate.payload
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.update(fileBrowserItem)
|
||||
})
|
||||
.disposed(by: self.disposeBag)
|
||||
|
||||
source
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { state in
|
||||
if state.viewToBeFocused != nil,
|
||||
case .fileBrowser = state.viewToBeFocused! {
|
||||
self.beFirstResponder()
|
||||
}
|
||||
|
||||
let themeChanged = changeTheme(
|
||||
themePrefChanged: state.appearance.usesTheme != self.usesTheme,
|
||||
themeChanged: state.appearance.theme.mark != self.lastThemeMark,
|
||||
usesTheme: state.appearance.usesTheme,
|
||||
forTheme: { self.updateTheme(state.appearance.theme) },
|
||||
forDefaultTheme: { self.updateTheme(Marked(Theme.default)) })
|
||||
|
||||
self.usesTheme = state.appearance.usesTheme
|
||||
|
||||
guard self.shouldReloadData(
|
||||
for: state, themeChanged: themeChanged
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.showsFileIcon = state.appearance.showsFileIcon
|
||||
self.isShowHidden = state.fileBrowserShowHidden
|
||||
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
|
||||
self.root = FileBrowserItem(state.cwd)
|
||||
self.reloadData()
|
||||
})
|
||||
.disposed(by: self.disposeBag)
|
||||
}
|
||||
|
||||
override func reloadData() {
|
||||
self.cells.removeAll()
|
||||
self.widths.removeAll()
|
||||
super.reloadData()
|
||||
}
|
||||
|
||||
func select(_ url: URL) {
|
||||
var stack = [self.root]
|
||||
|
||||
while let item = stack.popLast() {
|
||||
self.expandItem(item)
|
||||
|
||||
if item.url.isDirectParent(of: url) {
|
||||
if let targetItem = item.children.first(where: { $0.url == url }) {
|
||||
let targetRow = self.row(forItem: targetItem)
|
||||
self.selectRowIndexes(
|
||||
IndexSet(integer: targetRow), byExtendingSelection: false
|
||||
)
|
||||
self.scrollRowToVisible(targetRow)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
stack.append(contentsOf: item.children.filter {
|
||||
$0.url.isParent(of: url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private let emit: (UuidAction<FileBrowser.Action>) -> Void
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
private let uuid: String
|
||||
private var lastFileSystemUpdateMark = Token()
|
||||
private var usesTheme: Bool
|
||||
private(set) var lastThemeMark = Token()
|
||||
private var showsFileIcon: Bool
|
||||
|
||||
private var cwd: URL {
|
||||
return self.root.url
|
||||
}
|
||||
private var isShowHidden: Bool
|
||||
|
||||
private var root: FileBrowserItem
|
||||
|
||||
private var widths = [String: CGFloat]()
|
||||
private var cells = [String: ThemedTableCell]()
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateTheme(_ theme: Marked<Theme>) {
|
||||
self.theme = theme.payload
|
||||
self.enclosingScrollView?.backgroundColor = self.theme.background
|
||||
self.backgroundColor = self.theme.background
|
||||
self.lastThemeMark = theme.mark
|
||||
}
|
||||
|
||||
private func shouldReloadData(
|
||||
for state: StateType, themeChanged: Bool = false
|
||||
) -> Bool {
|
||||
if self.isShowHidden != state.fileBrowserShowHidden {
|
||||
return true
|
||||
}
|
||||
|
||||
if themeChanged {
|
||||
return true
|
||||
}
|
||||
|
||||
if self.showsFileIcon != state.appearance.showsFileIcon {
|
||||
return true
|
||||
}
|
||||
|
||||
if state.cwd != self.cwd {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func handleRemovals(for fileBrowserItem: FileBrowserItem,
|
||||
new newChildren: [FileBrowserItem]) {
|
||||
let curChildren = fileBrowserItem.children
|
||||
|
||||
let curPreparedChildren = self.prepare(curChildren)
|
||||
let newPreparedChildren = self.prepare(newChildren)
|
||||
|
||||
let indicesToRemove = curPreparedChildren
|
||||
.enumerated()
|
||||
.filter { (_, fileBrowserItem) in
|
||||
newPreparedChildren.contains(fileBrowserItem) == false
|
||||
}
|
||||
.map { (idx, _) in idx }
|
||||
|
||||
indicesToRemove.forEach { index in
|
||||
let path = curPreparedChildren[index].url.path
|
||||
|
||||
self.cells.removeValue(forKey: path)
|
||||
self.widths.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
fileBrowserItem.children = curChildren.filter { newChildren.contains($0) }
|
||||
|
||||
let parent = fileBrowserItem == self.root ? nil : fileBrowserItem
|
||||
self.removeItems(at: IndexSet(indicesToRemove), inParent: parent)
|
||||
}
|
||||
|
||||
private func handleAdditions(for fileBrowserItem: FileBrowserItem,
|
||||
new newChildren: [FileBrowserItem]) {
|
||||
let curChildren = fileBrowserItem.children
|
||||
|
||||
let curPreparedChildren = self.prepare(curChildren)
|
||||
let newPreparedChildren = self.prepare(newChildren)
|
||||
|
||||
let indicesToInsert = newPreparedChildren
|
||||
.enumerated()
|
||||
.filter { (_, fileBrowserItem) in
|
||||
curPreparedChildren.contains(fileBrowserItem) == false
|
||||
}
|
||||
.map { (idx, _) in idx }
|
||||
|
||||
// We don't just take newChildren because NSOutlineView look
|
||||
// at the pointer equality for preserving the expanded states...
|
||||
fileBrowserItem.children = newChildren.substituting(elements: curChildren)
|
||||
|
||||
let parent = fileBrowserItem == self.root ? nil : fileBrowserItem
|
||||
self.insertItems(at: IndexSet(indicesToInsert), inParent: parent)
|
||||
}
|
||||
|
||||
private func sortedChildren(of url: URL) -> [FileBrowserItem] {
|
||||
return FileUtils.directDescendants(of: url)
|
||||
.map(FileBrowserItem.init).sorted {
|
||||
if ($0.isDir == $1.isDir) {
|
||||
return $0.url.absoluteString < $1.url.absoluteString
|
||||
}
|
||||
|
||||
return $0.isDir
|
||||
}
|
||||
}
|
||||
|
||||
private func update(_ fileBrowserItem: FileBrowserItem) {
|
||||
let url = fileBrowserItem.url
|
||||
|
||||
// Sort the array to keep the order.
|
||||
let newChildren = self.sortedChildren(of: url)
|
||||
|
||||
self.beginUpdates()
|
||||
self.handleRemovals(for: fileBrowserItem, new: newChildren)
|
||||
self.endUpdates()
|
||||
|
||||
self.beginUpdates()
|
||||
self.handleAdditions(for: fileBrowserItem, new: newChildren)
|
||||
self.endUpdates()
|
||||
|
||||
fileBrowserItem.isChildrenScanned = true
|
||||
|
||||
fileBrowserItem.children
|
||||
.filter { self.isItemExpanded($0) }
|
||||
.forEach(self.update)
|
||||
}
|
||||
|
||||
private func fileBrowserItem(with url: URL) -> FileBrowserItem? {
|
||||
if self.cwd == url {
|
||||
return self.root
|
||||
}
|
||||
|
||||
guard self.cwd.isParent(of: url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rootPathComps = self.cwd.pathComponents
|
||||
let pathComps = url.pathComponents
|
||||
let childPart = pathComps[rootPathComps.count..<pathComps.count]
|
||||
|
||||
return childPart
|
||||
.reduce(self.root) { (resultItem, childName) -> FileBrowserItem? in
|
||||
guard let parent = resultItem else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return parent.child(with: parent.url.appendingPathComponent(childName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSOutlineViewDataSource
|
||||
extension FileOutlineViewOld {
|
||||
|
||||
private func scanChildrenIfNecessary(_ fileBrowserItem: FileBrowserItem) {
|
||||
guard fileBrowserItem.isChildrenScanned == false else {
|
||||
return
|
||||
}
|
||||
|
||||
fileBrowserItem.children = self.sortedChildren(of: fileBrowserItem.url)
|
||||
fileBrowserItem.isChildrenScanned = true
|
||||
}
|
||||
|
||||
private func prepare(_ children: [FileBrowserItem]) -> [FileBrowserItem] {
|
||||
return self.isShowHidden ? children : children.filter { !$0.isHidden }
|
||||
}
|
||||
|
||||
func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
|
||||
if item == nil {
|
||||
self.scanChildrenIfNecessary(self.root)
|
||||
|
||||
return self.prepare(self.root.children).count
|
||||
}
|
||||
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
return 0
|
||||
}
|
||||
|
||||
if fileBrowserItem.url.isDir {
|
||||
self.scanChildrenIfNecessary(fileBrowserItem)
|
||||
return self.prepare(fileBrowserItem.children).count
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func outlineView(
|
||||
_: NSOutlineView, child index: Int, ofItem item: Any?
|
||||
) -> Any {
|
||||
let level = self.level(forItem: item) + 2
|
||||
defer { self.adjustColumnWidths() }
|
||||
|
||||
if item == nil {
|
||||
let child = self.prepare(self.root.children)[index]
|
||||
|
||||
let cell = self.cell(forItem: child)
|
||||
self.cells[child.url.path] = cell
|
||||
self.widths[child.url.path] = self.cellWidth(for: cell, level: level)
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
preconditionFailure("Should not happen")
|
||||
}
|
||||
|
||||
let child = self.prepare(fileBrowserItem.children)[index]
|
||||
|
||||
let cell = self.cell(forItem: child)
|
||||
self.cells[child.url.path] = cell
|
||||
self.widths[child.url.path] = self.cellWidth(for: cell, level: level)
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
private func cell(forItem item: FileBrowserItem) -> ThemedTableCell {
|
||||
if let existingCell = self.cells[item.url.path] {
|
||||
return existingCell
|
||||
}
|
||||
|
||||
let cell = ThemedTableCell(withIdentifier: "file-cell-view")
|
||||
|
||||
cell.isDir = item.isDir
|
||||
cell.text = item.url.lastPathComponent
|
||||
|
||||
if self.showsFileIcon {
|
||||
let icon = FileUtils.icon(forUrl: item.url)
|
||||
cell.image = cell.isHidden
|
||||
? icon?.tinting(with: NSColor.white.withAlphaComponent(0.4))
|
||||
: icon
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool {
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
return false
|
||||
}
|
||||
return fileBrowserItem.url.isDir
|
||||
}
|
||||
|
||||
@objc(outlineView:objectValueForTableColumn:byItem:)
|
||||
func outlineView(_: NSOutlineView,
|
||||
objectValueFor: NSTableColumn?,
|
||||
byItem item: Any?) -> Any? {
|
||||
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileBrowserItem
|
||||
}
|
||||
|
||||
private func cellWidth(for cell: NSView?, level: Int) -> CGFloat {
|
||||
let cellWidth = cell?.intrinsicContentSize.width ?? 0
|
||||
let indentation = CGFloat(level + 1) * self.indentationPerLevel + 4
|
||||
return cellWidth + indentation
|
||||
}
|
||||
|
||||
private func adjustColumnWidths() {
|
||||
guard let column = self.outlineTableColumn else {
|
||||
return
|
||||
}
|
||||
|
||||
column.minWidth = self.widths.values.max() ?? 100
|
||||
column.maxWidth = self.widths.values.max() ?? 100
|
||||
}
|
||||
}
|
||||
|
||||
private class FileBrowserItem: Hashable, Comparable, CustomStringConvertible {
|
||||
|
||||
static func ==(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
|
||||
return left.url == right.url
|
||||
}
|
||||
|
||||
static func <(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
|
||||
return left.url.lastPathComponent < right.url.lastPathComponent
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
return self.url.hashValue
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return self.url.path
|
||||
}
|
||||
|
||||
let url: URL
|
||||
let isDir: Bool
|
||||
let isHidden: Bool
|
||||
var children: [FileBrowserItem] = []
|
||||
var isChildrenScanned = false
|
||||
|
||||
init(_ url: URL) {
|
||||
self.url = url
|
||||
|
||||
// We cache the value here since we often get the value when the file is
|
||||
// not there, eg when updating because the file gets deleted
|
||||
// (in self.prepare() function)
|
||||
self.isHidden = url.isHidden
|
||||
self.isDir = url.isDir
|
||||
}
|
||||
|
||||
func child(with url: URL) -> FileBrowserItem? {
|
||||
return self.children.first { $0.url == url }
|
||||
var node: Node? {
|
||||
return self.representedObject as? Node
|
||||
}
|
||||
}
|
||||
|
||||
|
452
VimR/VimR/FileOutlineViewOld.swift
Normal file
452
VimR/VimR/FileOutlineViewOld.swift
Normal file
@ -0,0 +1,452 @@
|
||||
//
|
||||
// Created by Tae Won Ha on 2019-02-25.
|
||||
// Copyright (c) 2019 Tae Won Ha. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import NvimView
|
||||
import PureLayout
|
||||
import RxSwift
|
||||
|
||||
class FileOutlineViewOld: NSOutlineView,
|
||||
UiComponent,
|
||||
NSOutlineViewDataSource,
|
||||
NSOutlineViewDelegate,
|
||||
ThemedView {
|
||||
|
||||
typealias StateType = MainWindow.State
|
||||
|
||||
private(set) var theme = Theme.default
|
||||
|
||||
required init(
|
||||
source: Observable<StateType>,
|
||||
emitter: ActionEmitter,
|
||||
state: StateType) {
|
||||
|
||||
self.emit = emitter.typedEmit()
|
||||
self.uuid = state.uuid
|
||||
|
||||
self.root = FileBrowserItem(state.cwd)
|
||||
self.isShowHidden = state.fileBrowserShowHidden
|
||||
|
||||
self.usesTheme = state.appearance.usesTheme
|
||||
self.showsFileIcon = state.appearance.showsFileIcon
|
||||
|
||||
super.init(frame: .zero)
|
||||
NSOutlineView.configure(toStandard: self)
|
||||
|
||||
self.dataSource = self
|
||||
self.delegate = self
|
||||
self.allowsEmptySelection = true
|
||||
|
||||
guard Bundle.main.loadNibNamed(
|
||||
NSNib.Name("FileBrowserMenu"),
|
||||
owner: self,
|
||||
topLevelObjects: nil
|
||||
) else {
|
||||
NSLog("WARN: FileBrowserMenu.xib could not be loaded")
|
||||
return
|
||||
}
|
||||
|
||||
// If the target of the menu items is set to the first responder,
|
||||
// the actions are not invoked at all when the file monitor fires
|
||||
// in the background... Dunno why it worked before the redesign... -_-
|
||||
self.menu?.items.forEach { $0.target = self }
|
||||
|
||||
// self.doubleAction = #selector(FileOutlineViewOld.doubleClickAction)
|
||||
|
||||
source
|
||||
.filter { !self.shouldReloadData(for: $0) }
|
||||
.filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
|
||||
.throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
|
||||
latest: true,
|
||||
scheduler: SerialDispatchQueueScheduler(qos: .background))
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { state in
|
||||
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
|
||||
guard let fileBrowserItem = self.fileBrowserItem(
|
||||
with: state.lastFileSystemUpdate.payload
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.update(fileBrowserItem)
|
||||
})
|
||||
.disposed(by: self.disposeBag)
|
||||
|
||||
source
|
||||
.observeOn(MainScheduler.instance)
|
||||
.subscribe(onNext: { state in
|
||||
if state.viewToBeFocused != nil,
|
||||
case .fileBrowser = state.viewToBeFocused! {
|
||||
self.beFirstResponder()
|
||||
}
|
||||
|
||||
let themeChanged = changeTheme(
|
||||
themePrefChanged: state.appearance.usesTheme != self.usesTheme,
|
||||
themeChanged: state.appearance.theme.mark != self.lastThemeMark,
|
||||
usesTheme: state.appearance.usesTheme,
|
||||
forTheme: { self.updateTheme(state.appearance.theme) },
|
||||
forDefaultTheme: { self.updateTheme(Marked(Theme.default)) })
|
||||
|
||||
self.usesTheme = state.appearance.usesTheme
|
||||
|
||||
guard self.shouldReloadData(
|
||||
for: state, themeChanged: themeChanged
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.showsFileIcon = state.appearance.showsFileIcon
|
||||
self.isShowHidden = state.fileBrowserShowHidden
|
||||
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
|
||||
self.root = FileBrowserItem(state.cwd)
|
||||
self.reloadData()
|
||||
})
|
||||
.disposed(by: self.disposeBag)
|
||||
}
|
||||
|
||||
override func reloadData() {
|
||||
self.cells.removeAll()
|
||||
self.widths.removeAll()
|
||||
super.reloadData()
|
||||
}
|
||||
|
||||
func select(_ url: URL) {
|
||||
var stack = [self.root]
|
||||
|
||||
while let item = stack.popLast() {
|
||||
self.expandItem(item)
|
||||
|
||||
if item.url.isDirectParent(of: url) {
|
||||
if let targetItem = item.children.first(where: { $0.url == url }) {
|
||||
let targetRow = self.row(forItem: targetItem)
|
||||
self.selectRowIndexes(
|
||||
IndexSet(integer: targetRow), byExtendingSelection: false
|
||||
)
|
||||
self.scrollRowToVisible(targetRow)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
stack.append(contentsOf: item.children.filter {
|
||||
$0.url.isParent(of: url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private let emit: (UuidAction<FileBrowser.Action>) -> Void
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
private let uuid: String
|
||||
private var lastFileSystemUpdateMark = Token()
|
||||
private var usesTheme: Bool
|
||||
private(set) var lastThemeMark = Token()
|
||||
private var showsFileIcon: Bool
|
||||
|
||||
private var cwd: URL {
|
||||
return self.root.url
|
||||
}
|
||||
private var isShowHidden: Bool
|
||||
|
||||
private var root: FileBrowserItem
|
||||
|
||||
private var widths = [String: CGFloat]()
|
||||
private var cells = [String: ThemedTableCell]()
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateTheme(_ theme: Marked<Theme>) {
|
||||
self.theme = theme.payload
|
||||
self.enclosingScrollView?.backgroundColor = self.theme.background
|
||||
self.backgroundColor = self.theme.background
|
||||
self.lastThemeMark = theme.mark
|
||||
}
|
||||
|
||||
private func shouldReloadData(
|
||||
for state: StateType, themeChanged: Bool = false
|
||||
) -> Bool {
|
||||
if self.isShowHidden != state.fileBrowserShowHidden {
|
||||
return true
|
||||
}
|
||||
|
||||
if themeChanged {
|
||||
return true
|
||||
}
|
||||
|
||||
if self.showsFileIcon != state.appearance.showsFileIcon {
|
||||
return true
|
||||
}
|
||||
|
||||
if state.cwd != self.cwd {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func handleRemovals(for fileBrowserItem: FileBrowserItem,
|
||||
new newChildren: [FileBrowserItem]) {
|
||||
let curChildren = fileBrowserItem.children
|
||||
|
||||
let curPreparedChildren = self.prepare(curChildren)
|
||||
let newPreparedChildren = self.prepare(newChildren)
|
||||
|
||||
let indicesToRemove = curPreparedChildren
|
||||
.enumerated()
|
||||
.filter { (_, fileBrowserItem) in
|
||||
newPreparedChildren.contains(fileBrowserItem) == false
|
||||
}
|
||||
.map { (idx, _) in idx }
|
||||
|
||||
indicesToRemove.forEach { index in
|
||||
let path = curPreparedChildren[index].url.path
|
||||
|
||||
self.cells.removeValue(forKey: path)
|
||||
self.widths.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
fileBrowserItem.children = curChildren.filter { newChildren.contains($0) }
|
||||
|
||||
let parent = fileBrowserItem == self.root ? nil : fileBrowserItem
|
||||
self.removeItems(at: IndexSet(indicesToRemove), inParent: parent)
|
||||
}
|
||||
|
||||
private func handleAdditions(for fileBrowserItem: FileBrowserItem,
|
||||
new newChildren: [FileBrowserItem]) {
|
||||
let curChildren = fileBrowserItem.children
|
||||
|
||||
let curPreparedChildren = self.prepare(curChildren)
|
||||
let newPreparedChildren = self.prepare(newChildren)
|
||||
|
||||
let indicesToInsert = newPreparedChildren
|
||||
.enumerated()
|
||||
.filter { (_, fileBrowserItem) in
|
||||
curPreparedChildren.contains(fileBrowserItem) == false
|
||||
}
|
||||
.map { (idx, _) in idx }
|
||||
|
||||
// We don't just take newChildren because NSOutlineView look
|
||||
// at the pointer equality for preserving the expanded states...
|
||||
fileBrowserItem.children = newChildren.substituting(elements: curChildren)
|
||||
|
||||
let parent = fileBrowserItem == self.root ? nil : fileBrowserItem
|
||||
self.insertItems(at: IndexSet(indicesToInsert), inParent: parent)
|
||||
}
|
||||
|
||||
private func sortedChildren(of url: URL) -> [FileBrowserItem] {
|
||||
return FileUtils.directDescendants(of: url)
|
||||
.map(FileBrowserItem.init).sorted {
|
||||
if ($0.isDir == $1.isDir) {
|
||||
return $0.url.absoluteString < $1.url.absoluteString
|
||||
}
|
||||
|
||||
return $0.isDir
|
||||
}
|
||||
}
|
||||
|
||||
private func update(_ fileBrowserItem: FileBrowserItem) {
|
||||
let url = fileBrowserItem.url
|
||||
|
||||
// Sort the array to keep the order.
|
||||
let newChildren = self.sortedChildren(of: url)
|
||||
|
||||
self.beginUpdates()
|
||||
self.handleRemovals(for: fileBrowserItem, new: newChildren)
|
||||
self.endUpdates()
|
||||
|
||||
self.beginUpdates()
|
||||
self.handleAdditions(for: fileBrowserItem, new: newChildren)
|
||||
self.endUpdates()
|
||||
|
||||
fileBrowserItem.isChildrenScanned = true
|
||||
|
||||
fileBrowserItem.children
|
||||
.filter { self.isItemExpanded($0) }
|
||||
.forEach(self.update)
|
||||
}
|
||||
|
||||
private func fileBrowserItem(with url: URL) -> FileBrowserItem? {
|
||||
if self.cwd == url {
|
||||
return self.root
|
||||
}
|
||||
|
||||
guard self.cwd.isParent(of: url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rootPathComps = self.cwd.pathComponents
|
||||
let pathComps = url.pathComponents
|
||||
let childPart = pathComps[rootPathComps.count..<pathComps.count]
|
||||
|
||||
return childPart
|
||||
.reduce(self.root) { (resultItem, childName) -> FileBrowserItem? in
|
||||
guard let parent = resultItem else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return parent.child(with: parent.url.appendingPathComponent(childName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSOutlineViewDataSource
|
||||
extension FileOutlineViewOld {
|
||||
|
||||
private func scanChildrenIfNecessary(_ fileBrowserItem: FileBrowserItem) {
|
||||
guard fileBrowserItem.isChildrenScanned == false else {
|
||||
return
|
||||
}
|
||||
|
||||
fileBrowserItem.children = self.sortedChildren(of: fileBrowserItem.url)
|
||||
fileBrowserItem.isChildrenScanned = true
|
||||
}
|
||||
|
||||
private func prepare(_ children: [FileBrowserItem]) -> [FileBrowserItem] {
|
||||
return self.isShowHidden ? children : children.filter { !$0.isHidden }
|
||||
}
|
||||
|
||||
func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
|
||||
if item == nil {
|
||||
self.scanChildrenIfNecessary(self.root)
|
||||
|
||||
return self.prepare(self.root.children).count
|
||||
}
|
||||
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
return 0
|
||||
}
|
||||
|
||||
if fileBrowserItem.url.isDir {
|
||||
self.scanChildrenIfNecessary(fileBrowserItem)
|
||||
return self.prepare(fileBrowserItem.children).count
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func outlineView(
|
||||
_: NSOutlineView, child index: Int, ofItem item: Any?
|
||||
) -> Any {
|
||||
let level = self.level(forItem: item) + 2
|
||||
defer { self.adjustColumnWidths() }
|
||||
|
||||
if item == nil {
|
||||
let child = self.prepare(self.root.children)[index]
|
||||
|
||||
let cell = self.cell(forItem: child)
|
||||
self.cells[child.url.path] = cell
|
||||
self.widths[child.url.path] = self.cellWidth(for: cell, level: level)
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
preconditionFailure("Should not happen")
|
||||
}
|
||||
|
||||
let child = self.prepare(fileBrowserItem.children)[index]
|
||||
|
||||
let cell = self.cell(forItem: child)
|
||||
self.cells[child.url.path] = cell
|
||||
self.widths[child.url.path] = self.cellWidth(for: cell, level: level)
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
private func cell(forItem item: FileBrowserItem) -> ThemedTableCell {
|
||||
if let existingCell = self.cells[item.url.path] {
|
||||
return existingCell
|
||||
}
|
||||
|
||||
let cell = ThemedTableCell(withIdentifier: "file-cell-view")
|
||||
|
||||
cell.isDir = item.isDir
|
||||
cell.text = item.url.lastPathComponent
|
||||
|
||||
if self.showsFileIcon {
|
||||
let icon = FileUtils.icon(forUrl: item.url)
|
||||
cell.image = cell.isHidden
|
||||
? icon?.tinting(with: NSColor.white.withAlphaComponent(0.4))
|
||||
: icon
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool {
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
return false
|
||||
}
|
||||
return fileBrowserItem.url.isDir
|
||||
}
|
||||
|
||||
@objc(outlineView:objectValueForTableColumn:byItem:)
|
||||
func outlineView(_: NSOutlineView,
|
||||
objectValueFor: NSTableColumn?,
|
||||
byItem item: Any?) -> Any? {
|
||||
|
||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileBrowserItem
|
||||
}
|
||||
|
||||
private func cellWidth(for cell: NSView?, level: Int) -> CGFloat {
|
||||
let cellWidth = cell?.intrinsicContentSize.width ?? 0
|
||||
let indentation = CGFloat(level + 1) * self.indentationPerLevel + 4
|
||||
return cellWidth + indentation
|
||||
}
|
||||
|
||||
private func adjustColumnWidths() {
|
||||
guard let column = self.outlineTableColumn else {
|
||||
return
|
||||
}
|
||||
|
||||
column.minWidth = self.widths.values.max() ?? 100
|
||||
column.maxWidth = self.widths.values.max() ?? 100
|
||||
}
|
||||
}
|
||||
|
||||
private class FileBrowserItem: Hashable, Comparable, CustomStringConvertible {
|
||||
|
||||
static func ==(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
|
||||
return left.url == right.url
|
||||
}
|
||||
|
||||
static func <(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
|
||||
return left.url.lastPathComponent < right.url.lastPathComponent
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
return self.url.hashValue
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return self.url.path
|
||||
}
|
||||
|
||||
let url: URL
|
||||
let isDir: Bool
|
||||
let isHidden: Bool
|
||||
var children: [FileBrowserItem] = []
|
||||
var isChildrenScanned = false
|
||||
|
||||
init(_ url: URL) {
|
||||
self.url = url
|
||||
|
||||
// We cache the value here since we often get the value when the file is
|
||||
// not there, eg when updating because the file gets deleted
|
||||
// (in self.prepare() function)
|
||||
self.isHidden = url.isHidden
|
||||
self.isDir = url.isDir
|
||||
}
|
||||
|
||||
func child(with url: URL) -> FileBrowserItem? {
|
||||
return self.children.first { $0.url == url }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user