mirror of
https://github.com/qvacua/vimr.git
synced 2024-12-26 07:13:24 +03:00
Merge remote-tracking branch 'origin/develop' into update-neovim
This commit is contained in:
commit
70e508816b
@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.25.0</string>
|
<string>SNAPSHOT-298</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>297</string>
|
<string>298</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
@ -783,7 +783,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 297;
|
CURRENT_PROJECT_VERSION = 298;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
@ -845,7 +845,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 297;
|
CURRENT_PROJECT_VERSION = 298;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@ -874,7 +874,7 @@
|
|||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
DYLIB_CURRENT_VERSION = 297;
|
DYLIB_CURRENT_VERSION = 298;
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
|
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
|
||||||
FRAMEWORK_VERSION = A;
|
FRAMEWORK_VERSION = A;
|
||||||
@ -896,7 +896,7 @@
|
|||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
DYLIB_CURRENT_VERSION = 297;
|
DYLIB_CURRENT_VERSION = 298;
|
||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
|
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/Mac";
|
||||||
FRAMEWORK_VERSION = A;
|
FRAMEWORK_VERSION = A;
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>FMWK</string>
|
<string>FMWK</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.25.0</string>
|
<string>SNAPSHOT-298</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>297</string>
|
<string>298</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>Copyright © 2017 Tae Won Ha. All rights reserved.</string>
|
<string>Copyright © 2017 Tae Won Ha. All rights reserved.</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.25.0</string>
|
<string>SNAPSHOT-298</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>297</string>
|
<string>298</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1272,7 +1272,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 297;
|
CURRENT_PROJECT_VERSION = 298;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
@ -1330,7 +1330,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 297;
|
CURRENT_PROJECT_VERSION = 298;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
@ -219,7 +219,7 @@ extension NSOutlineView {
|
|||||||
return self.item(atRow: self.clickedRow)
|
return self.item(atRow: self.clickedRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggle(item: Any) {
|
func toggle(item: Any?) {
|
||||||
if self.isItemExpanded(item) {
|
if self.isItemExpanded(item) {
|
||||||
self.collapseItem(item)
|
self.collapseItem(item)
|
||||||
} else {
|
} else {
|
||||||
|
@ -21,9 +21,14 @@ class BuffersList: NSView,
|
|||||||
case open(NvimView.Buffer)
|
case open(NvimView.Buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private(set) var lastThemeMark = Token()
|
||||||
private(set) var theme = Theme.default
|
private(set) var theme = Theme.default
|
||||||
|
|
||||||
required init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType) {
|
required init(
|
||||||
|
source: Observable<StateType>,
|
||||||
|
emitter: ActionEmitter,
|
||||||
|
state: StateType
|
||||||
|
) {
|
||||||
self.emit = emitter.typedEmit()
|
self.emit = emitter.typedEmit()
|
||||||
self.uuid = state.uuid
|
self.uuid = state.uuid
|
||||||
|
|
||||||
@ -52,14 +57,15 @@ class BuffersList: NSView,
|
|||||||
|
|
||||||
self.usesTheme = state.appearance.usesTheme
|
self.usesTheme = state.appearance.usesTheme
|
||||||
|
|
||||||
if self.buffers == state.buffers && !themeChanged && self.showsFileIcon == state.appearance.showsFileIcon {
|
if self.buffers == state.buffers
|
||||||
|
&& !themeChanged
|
||||||
|
&& self.showsFileIcon == state.appearance.showsFileIcon {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.showsFileIcon = state.appearance.showsFileIcon
|
self.showsFileIcon = state.appearance.showsFileIcon
|
||||||
self.buffers = state.buffers
|
self.buffers = state.buffers
|
||||||
self.bufferList.reloadData()
|
self.bufferList.reloadData()
|
||||||
self.adjustFileViewWidth()
|
|
||||||
})
|
})
|
||||||
.disposed(by: self.disposeBag)
|
.disposed(by: self.disposeBag)
|
||||||
}
|
}
|
||||||
@ -69,7 +75,6 @@ class BuffersList: NSView,
|
|||||||
|
|
||||||
private let uuid: String
|
private let uuid: String
|
||||||
private var usesTheme: Bool
|
private var usesTheme: Bool
|
||||||
private var lastThemeMark = Token()
|
|
||||||
private var showsFileIcon: Bool
|
private var showsFileIcon: Bool
|
||||||
|
|
||||||
private let bufferList = NSTableView.standardTableView()
|
private let bufferList = NSTableView.standardTableView()
|
||||||
@ -95,16 +100,6 @@ class BuffersList: NSView,
|
|||||||
self.addSubview(scrollView)
|
self.addSubview(scrollView)
|
||||||
scrollView.autoPinEdgesToSuperviewEdges()
|
scrollView.autoPinEdgesToSuperviewEdges()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func adjustFileViewWidth() {
|
|
||||||
let maxWidth = self.buffers.reduce(CGFloat(100)) { (curMaxWidth, buffer) in
|
|
||||||
return max(self.text(for: buffer).size().width, curMaxWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
let column = self.bufferList.tableColumns[0]
|
|
||||||
// If we set the minWidth and maxWidth here, the column does not get resized... Dunno why.
|
|
||||||
column.width = maxWidth + ThemedTableCell.widthWithoutText
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
@ -132,14 +127,26 @@ extension BuffersList {
|
|||||||
// MARK: - NSTableViewDelegate
|
// MARK: - NSTableViewDelegate
|
||||||
extension BuffersList {
|
extension BuffersList {
|
||||||
|
|
||||||
public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
public func tableView(
|
||||||
return tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("buffer-row-view"), owner: self)
|
_ tableView: NSTableView,
|
||||||
as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "buffer-row-view", themedView: self)
|
rowViewForRow row: Int
|
||||||
|
) -> NSTableRowView? {
|
||||||
|
return tableView.makeView(
|
||||||
|
withIdentifier: NSUserInterfaceItemIdentifier("buffer-row-view"),
|
||||||
|
owner: self
|
||||||
|
) as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "buffer-row-view",
|
||||||
|
themedView: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
func tableView(
|
||||||
let cachedCell = (tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("buffer-cell-view"), owner: self)
|
_ tableView: NSTableView,
|
||||||
as? ThemedTableCell)?.reset()
|
viewFor tableColumn: NSTableColumn?,
|
||||||
|
row: Int
|
||||||
|
) -> NSView? {
|
||||||
|
let cachedCell = (tableView.makeView(
|
||||||
|
withIdentifier: NSUserInterfaceItemIdentifier("buffer-cell-view"),
|
||||||
|
owner: self
|
||||||
|
) as? ThemedTableCell)?.reset()
|
||||||
|
|
||||||
let cell = cachedCell ?? ThemedTableCell(withIdentifier: "buffer-cell-view")
|
let cell = cachedCell ?? ThemedTableCell(withIdentifier: "buffer-cell-view")
|
||||||
|
|
||||||
@ -155,6 +162,22 @@ extension BuffersList {
|
|||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(
|
||||||
|
_ tableView: NSTableView,
|
||||||
|
didAdd rowView: NSTableRowView,
|
||||||
|
forRow row: Int
|
||||||
|
) {
|
||||||
|
guard let cellWidth = (rowView.view(atColumn: 0) as? NSTableCellView)?
|
||||||
|
.fittingSize.width
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bufferList.tableColumns[0].width = max(
|
||||||
|
self.bufferList.tableColumns[0].width, cellWidth + CGFloat(10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func text(for buffer: NvimView.Buffer) -> NSAttributedString {
|
private func text(for buffer: NvimView.Buffer) -> NSAttributedString {
|
||||||
guard let name = buffer.name else {
|
guard let name = buffer.name else {
|
||||||
return NSAttributedString(string: "No Name")
|
return NSAttributedString(string: "No Name")
|
||||||
@ -164,16 +187,22 @@ extension BuffersList {
|
|||||||
return NSAttributedString(string: name)
|
return NSAttributedString(string: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pathInfo = url.pathComponents.dropFirst().dropLast().reversed().joined(separator: " / ") + " /"
|
let pathInfo = url.pathComponents
|
||||||
|
.dropFirst()
|
||||||
|
.dropLast()
|
||||||
|
.reversed()
|
||||||
|
.joined(separator: " / ") + " /"
|
||||||
let rowText = NSMutableAttributedString(string: "\(name) — \(pathInfo)")
|
let rowText = NSMutableAttributedString(string: "\(name) — \(pathInfo)")
|
||||||
|
|
||||||
rowText.addAttribute(NSAttributedString.Key.foregroundColor,
|
rowText.addAttribute(NSAttributedString.Key.foregroundColor,
|
||||||
value: self.theme.foreground,
|
value: self.theme.foreground,
|
||||||
range: NSRange(location: 0, length: name.count))
|
range: NSRange(location: 0, length: name.count))
|
||||||
|
|
||||||
rowText.addAttribute(NSAttributedString.Key.foregroundColor,
|
rowText.addAttribute(
|
||||||
value: self.theme.foreground.brightening(by: 1.15),
|
NSAttributedString.Key.foregroundColor,
|
||||||
range: NSRange(location: name.count, length: pathInfo.count + 3))
|
value: self.theme.foreground.brightening(by: 1.15),
|
||||||
|
range: NSRange(location: name.count, length: pathInfo.count + 3)
|
||||||
|
)
|
||||||
|
|
||||||
return rowText
|
return rowText
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,9 @@ extension FileBrowser {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if NOPE
|
||||||
self.fileView.select(url)
|
self.fileView.select(url)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func refreshAction(_ sender: Any?) {
|
@objc func refreshAction(_ sender: Any?) {
|
||||||
|
@ -7,67 +7,52 @@ import Cocoa
|
|||||||
import NvimView
|
import NvimView
|
||||||
import PureLayout
|
import PureLayout
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
import CocoaFontAwesome
|
||||||
|
|
||||||
class FileOutlineView: NSOutlineView,
|
class FileOutlineView: NSOutlineView,
|
||||||
UiComponent,
|
UiComponent,
|
||||||
NSOutlineViewDataSource,
|
|
||||||
NSOutlineViewDelegate,
|
NSOutlineViewDelegate,
|
||||||
ThemedView {
|
ThemedView {
|
||||||
|
|
||||||
typealias StateType = MainWindow.State
|
typealias StateType = MainWindow.State
|
||||||
|
|
||||||
|
@objc dynamic var content = [Node]()
|
||||||
|
|
||||||
|
private(set) var lastThemeMark = Token()
|
||||||
private(set) var theme = Theme.default
|
private(set) var theme = Theme.default
|
||||||
|
|
||||||
required init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType) {
|
required init(
|
||||||
|
source: Observable<StateType>,
|
||||||
|
emitter: ActionEmitter,
|
||||||
|
state: StateType
|
||||||
|
) {
|
||||||
self.emit = emitter.typedEmit()
|
self.emit = emitter.typedEmit()
|
||||||
self.uuid = state.uuid
|
self.uuid = state.uuid
|
||||||
|
self.root = Node(url: state.cwd)
|
||||||
self.root = FileBrowserItem(state.cwd)
|
|
||||||
self.isShowHidden = state.fileBrowserShowHidden
|
|
||||||
|
|
||||||
self.usesTheme = state.appearance.usesTheme
|
self.usesTheme = state.appearance.usesTheme
|
||||||
self.showsFileIcon = state.appearance.showsFileIcon
|
self.showsFileIcon = state.appearance.showsFileIcon
|
||||||
|
self.isShowHidden = state.fileBrowserShowHidden
|
||||||
|
self.triangleClosed = NSImage.fontAwesomeIcon(
|
||||||
|
name: .caretRight,
|
||||||
|
textColor: self.theme.directoryForeground,
|
||||||
|
dimension: triangleImageSize
|
||||||
|
)
|
||||||
|
self.triangleOpen = NSImage.fontAwesomeIcon(
|
||||||
|
name: .caretDown,
|
||||||
|
textColor: self.theme.directoryForeground,
|
||||||
|
dimension: triangleImageSize
|
||||||
|
)
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
NSOutlineView.configure(toStandard: self)
|
NSOutlineView.configure(toStandard: self)
|
||||||
|
|
||||||
self.dataSource = self
|
|
||||||
self.delegate = 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(FileOutlineView.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
|
source
|
||||||
.observeOn(MainScheduler.instance)
|
.observeOn(MainScheduler.instance)
|
||||||
.subscribe(onNext: { state in
|
.subscribe(onNext: { state in
|
||||||
if state.viewToBeFocused != nil, case .fileBrowser = state.viewToBeFocused! {
|
if state.viewToBeFocused != nil,
|
||||||
|
case .fileBrowser = state.viewToBeFocused! {
|
||||||
self.beFirstResponder()
|
self.beFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,76 +65,235 @@ class FileOutlineView: NSOutlineView,
|
|||||||
|
|
||||||
self.usesTheme = state.appearance.usesTheme
|
self.usesTheme = state.appearance.usesTheme
|
||||||
|
|
||||||
guard self.shouldReloadData(for: state, themeChanged: themeChanged) else {
|
guard self.shouldReloadData(
|
||||||
|
for: state, themeChanged: themeChanged
|
||||||
|
) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.showsFileIcon = state.appearance.showsFileIcon
|
self.showsFileIcon = state.appearance.showsFileIcon
|
||||||
self.isShowHidden = state.fileBrowserShowHidden
|
self.isShowHidden = state.fileBrowserShowHidden
|
||||||
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
|
self.lastFileSystemUpdateMark = state.lastFileSystemUpdate.mark
|
||||||
self.root = FileBrowserItem(state.cwd)
|
self.root = Node(url: state.cwd)
|
||||||
self.reloadData()
|
self.reloadRoot()
|
||||||
})
|
})
|
||||||
.disposed(by: self.disposeBag)
|
.disposed(by: self.disposeBag)
|
||||||
}
|
|
||||||
|
|
||||||
override func reloadData() {
|
source
|
||||||
self.cells.removeAll()
|
.filter { !self.shouldReloadData(for: $0) }
|
||||||
self.widths.removeAll()
|
.filter { $0.lastFileSystemUpdate.mark != self.lastFileSystemUpdateMark }
|
||||||
super.reloadData()
|
.map { $0.lastFileSystemUpdate.payload }
|
||||||
}
|
.throttle(2 * FileMonitor.fileSystemEventsLatency + 1,
|
||||||
|
latest: true,
|
||||||
func select(_ url: URL) {
|
scheduler: SerialDispatchQueueScheduler(qos: .background))
|
||||||
var stack = [self.root]
|
.map { ($0, Set(self.childUrls(for: $0))) }
|
||||||
|
.observeOn(MainScheduler.instance)
|
||||||
while let item = stack.popLast() {
|
.subscribe(onNext: { (url, newChildUrls) in
|
||||||
self.expandItem(item)
|
guard let changeTreeNode = self.changeRootTreeNode(for: url) else {
|
||||||
|
return
|
||||||
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
|
self.handleRemoval(changeTreeNode: changeTreeNode,
|
||||||
}
|
newChildUrls: newChildUrls)
|
||||||
|
self.handleAddition(changeTreeNode: changeTreeNode,
|
||||||
|
newChildUrls: newChildUrls)
|
||||||
|
})
|
||||||
|
.disposed(by: self.disposeBag)
|
||||||
|
|
||||||
stack.append(contentsOf: item.children.filter { $0.url.isParent(of: url) })
|
self.initContextMenu()
|
||||||
|
self.initBindings()
|
||||||
|
self.reloadRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot use outlineView(_:willDisplayOutlineCell:for:item:) delegate
|
||||||
|
// method to customize the disclosure triangle in a view-based
|
||||||
|
// NSOutlineView.
|
||||||
|
// See https://stackoverflow.com/a/20454413/9850227
|
||||||
|
override func makeView(
|
||||||
|
withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?
|
||||||
|
) -> NSView? {
|
||||||
|
let result = super.makeView(withIdentifier: identifier, owner: owner)
|
||||||
|
|
||||||
|
if identifier == NSOutlineView.disclosureButtonIdentifier {
|
||||||
|
let triangleButton = result as? NSButton
|
||||||
|
triangleButton?.image = self.triangleClosed
|
||||||
|
triangleButton?.alternateImage = self.triangleOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
private let emit: (UuidAction<FileBrowser.Action>) -> Void
|
private let emit: (UuidAction<FileBrowser.Action>) -> Void
|
||||||
private let disposeBag = DisposeBag()
|
private let disposeBag = DisposeBag()
|
||||||
|
|
||||||
private let uuid: String
|
private let uuid: String
|
||||||
private var lastFileSystemUpdateMark = Token()
|
|
||||||
private var usesTheme: Bool
|
|
||||||
private var lastThemeMark = Token()
|
|
||||||
private var showsFileIcon: Bool
|
|
||||||
|
|
||||||
|
private var root: Node
|
||||||
private var cwd: URL {
|
private var cwd: URL {
|
||||||
return self.root.url
|
return self.root.url
|
||||||
}
|
}
|
||||||
|
private let treeController = NSTreeController()
|
||||||
|
|
||||||
|
private var cachedColumnWidth = CGFloat(20)
|
||||||
|
private var usesTheme: Bool
|
||||||
|
private var lastFileSystemUpdateMark = Token()
|
||||||
|
private var showsFileIcon: Bool
|
||||||
private var isShowHidden: Bool
|
private var isShowHidden: Bool
|
||||||
|
|
||||||
private var root: FileBrowserItem
|
private var triangleClosed: NSImage
|
||||||
|
private var triangleOpen: NSImage
|
||||||
|
|
||||||
private var widths = [String: CGFloat]()
|
private func initContextMenu() {
|
||||||
private var cells = [String: ThemedTableCell]()
|
// Loading the nib file will set self.menu.
|
||||||
|
guard Bundle.main.loadNibNamed(
|
||||||
|
NSNib.Name("FileBrowserMenu"),
|
||||||
|
owner: self,
|
||||||
|
topLevelObjects: nil
|
||||||
|
) else {
|
||||||
|
fileLog.error("FileBrowserMenu.xib could not be loaded")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.menu?.items.forEach { $0.target = self }
|
||||||
|
self.doubleAction = #selector(FileOutlineView.doubleClickAction)
|
||||||
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
private func initBindings() {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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
|
||||||
|
]
|
||||||
|
self.treeController.bind(.contentArray, to: self, withKeyPath: "content")
|
||||||
|
self.bind(.content, to: self.treeController, withKeyPath: "arrangedObjects")
|
||||||
|
self.bind(.selectionIndexPaths,
|
||||||
|
to: self.treeController,
|
||||||
|
withKeyPath: "selectionIndexPaths")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func changeRootTreeNode(`for` url: URL) -> NSTreeNode? {
|
||||||
|
if url == self.cwd {
|
||||||
|
return self.treeController.arrangedObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
let cwdCompsCount = self.cwd.pathComponents.count
|
||||||
|
guard cwdCompsCount <= url.pathComponents.count else { return nil }
|
||||||
|
let comps = url.pathComponents.suffix(cwdCompsCount)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodes = FileUtils
|
||||||
|
.directDescendants(of: node.url)
|
||||||
|
.map { Node(url: $0) }
|
||||||
|
|
||||||
|
if self.isShowHidden {
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.filter { !$0.isHidden }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadRoot() {
|
||||||
|
let children = self.childNodes(for: self.root)
|
||||||
|
|
||||||
|
self.root.children = children
|
||||||
|
self.content.removeAll()
|
||||||
|
self.content.append(contentsOf: children)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTheme(_ theme: Marked<Theme>) {
|
private func updateTheme(_ theme: Marked<Theme>) {
|
||||||
self.theme = theme.payload
|
self.theme = theme.payload
|
||||||
self.enclosingScrollView?.backgroundColor = self.theme.background
|
self.enclosingScrollView?.backgroundColor = self.theme.background
|
||||||
self.backgroundColor = self.theme.background
|
self.backgroundColor = self.theme.background
|
||||||
|
self.triangleClosed = NSImage.fontAwesomeIcon(
|
||||||
|
name: .caretRight,
|
||||||
|
textColor: self.theme.directoryForeground,
|
||||||
|
dimension: triangleImageSize
|
||||||
|
)
|
||||||
|
self.triangleOpen = NSImage.fontAwesomeIcon(
|
||||||
|
name: .caretDown,
|
||||||
|
textColor: self.theme.directoryForeground,
|
||||||
|
dimension: triangleImageSize
|
||||||
|
)
|
||||||
|
|
||||||
self.lastThemeMark = theme.mark
|
self.lastThemeMark = theme.mark
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shouldReloadData(for state: StateType, themeChanged: Bool = false) -> Bool {
|
private func shouldReloadData(
|
||||||
|
for state: StateType, themeChanged: Bool = false
|
||||||
|
) -> Bool {
|
||||||
if self.isShowHidden != state.fileBrowserShowHidden {
|
if self.isShowHidden != state.fileBrowserShowHidden {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -169,233 +313,8 @@ class FileOutlineView: NSOutlineView,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleRemovals(for fileBrowserItem: FileBrowserItem,
|
private func node(from item: Any?) -> Node? {
|
||||||
new newChildren: [FileBrowserItem]) {
|
return (item as? NSTreeNode)?.node
|
||||||
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 FileOutlineView {
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - NSOutlineViewDelegate
|
|
||||||
extension FileOutlineView {
|
|
||||||
|
|
||||||
func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? {
|
|
||||||
return self.makeView(withIdentifier: NSUserInterfaceItemIdentifier("file-row-view"), owner: self) as? ThemedTableRow
|
|
||||||
?? ThemedTableRow(withIdentifier: "file-row-view", themedView: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func outlineView(_: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
|
|
||||||
guard let fileBrowserItem = item as? FileBrowserItem else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.cells[fileBrowserItem.url.path]
|
|
||||||
}
|
|
||||||
|
|
||||||
func outlineView(_: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
|
|
||||||
return 20
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,84 +322,184 @@ extension FileOutlineView {
|
|||||||
extension FileOutlineView {
|
extension FileOutlineView {
|
||||||
|
|
||||||
@IBAction func doubleClickAction(_: Any?) {
|
@IBAction func doubleClickAction(_: Any?) {
|
||||||
guard let item = self.clickedItem as? FileBrowserItem else {
|
let clickedTreeNode = self.clickedItem
|
||||||
|
guard let node = self.node(from: clickedTreeNode) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.url.isDir {
|
if node.isDir {
|
||||||
self.toggle(item: item)
|
self.toggle(item: clickedTreeNode)
|
||||||
} else {
|
} else {
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .default))
|
UuidAction(uuid: self.uuid,
|
||||||
|
action: .open(url: node.url, mode: .default))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func openInNewTab(_: Any?) {
|
@IBAction func openInNewTab(_: Any?) {
|
||||||
guard let item = self.clickedItem as? FileBrowserItem else {
|
guard let node = self.node(from: self.clickedItem) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .newTab))
|
UuidAction(uuid: self.uuid, action: .open(url: node.url, mode: .newTab))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func openInCurrentTab(_: Any?) {
|
@IBAction func openInCurrentTab(_: Any?) {
|
||||||
guard let item = self.clickedItem as? FileBrowserItem else {
|
guard let node = self.node(from: self.clickedItem) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .currentTab))
|
UuidAction(uuid: self.uuid,
|
||||||
|
action: .open(url: node.url, mode: .currentTab))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func openInHorizontalSplit(_: Any?) {
|
@IBAction func openInHorizontalSplit(_: Any?) {
|
||||||
guard let item = self.clickedItem as? FileBrowserItem else {
|
guard let node = self.node(from: self.clickedItem) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .horizontalSplit))
|
UuidAction(uuid: self.uuid,
|
||||||
|
action: .open(url: node.url, mode: .horizontalSplit))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func openInVerticalSplit(_: Any?) {
|
@IBAction func openInVerticalSplit(_: Any?) {
|
||||||
guard let item = self.clickedItem as? FileBrowserItem else {
|
guard let node = self.node(from: self.clickedItem) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .verticalSplit))
|
UuidAction(uuid: self.uuid,
|
||||||
|
action: .open(url: node.url, mode: .verticalSplit))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func setAsWorkingDirectory(_: Any?) {
|
@IBAction func setAsWorkingDirectory(_: Any?) {
|
||||||
guard let item = self.clickedItem as? FileBrowserItem else {
|
guard let node = self.node(from: self.clickedItem) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard item.url.isDir else {
|
guard node.url.isDir else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .setAsWorkingDirectory(item.url))
|
UuidAction(uuid: self.uuid, action: .setAsWorkingDirectory(node.url))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - NSOutlineViewDelegate
|
||||||
|
extension FileOutlineView {
|
||||||
|
|
||||||
|
func outlineView(
|
||||||
|
_ outlineView: NSOutlineView,
|
||||||
|
rowViewForItem item: Any
|
||||||
|
) -> NSTableRowView? {
|
||||||
|
let view = self.makeView(
|
||||||
|
withIdentifier: NSUserInterfaceItemIdentifier("file-row-view"),
|
||||||
|
owner: self
|
||||||
|
) as? ThemedTableRow ?? ThemedTableRow(withIdentifier: "file-row-view",
|
||||||
|
themedView: self)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func outlineView(
|
||||||
|
_: NSOutlineView,
|
||||||
|
viewFor tableColumn: NSTableColumn?,
|
||||||
|
item: Any
|
||||||
|
) -> NSView? {
|
||||||
|
guard let node = self.node(from: item) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellView = self.makeView(
|
||||||
|
withIdentifier: NSUserInterfaceItemIdentifier("file-cell-view"),
|
||||||
|
owner: self
|
||||||
|
) as? ThemedTableCell ?? ThemedTableCell(withIdentifier: "file-cell-view")
|
||||||
|
|
||||||
|
cellView.isDir = node.isDir
|
||||||
|
cellView.text = node.displayName
|
||||||
|
|
||||||
|
let icon = FileUtils.icon(forUrl: node.url)
|
||||||
|
cellView.image = node.isHidden
|
||||||
|
? icon?.tinting(with: NSColor.white.withAlphaComponent(0.4))
|
||||||
|
: icon
|
||||||
|
|
||||||
|
return cellView
|
||||||
|
}
|
||||||
|
|
||||||
|
func outlineView(_: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
|
func outlineView(
|
||||||
|
_ outlineView: NSOutlineView,
|
||||||
|
shouldExpandItem item: Any
|
||||||
|
) -> Bool {
|
||||||
|
guard let node = self.node(from: item) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.isChildrenScanned {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
node.children = FileUtils.directDescendants(of: node.url).map { url in
|
||||||
|
return Node(url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func outlineView(
|
||||||
|
_ outlineView: NSOutlineView,
|
||||||
|
didAdd rowView: NSTableRowView,
|
||||||
|
forRow row: Int
|
||||||
|
) {
|
||||||
|
guard let cellWidth = (rowView.view(atColumn: 0) as? NSTableCellView)?
|
||||||
|
.fittingSize.width
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let level = CGFloat(self.level(forRow: row))
|
||||||
|
let width = level * self.indentationPerLevel + cellWidth
|
||||||
|
+ columnWidthRightPadding
|
||||||
|
self.cachedColumnWidth = max(self.cachedColumnWidth, width)
|
||||||
|
self.tableColumns[0].width = cachedColumnWidth
|
||||||
|
|
||||||
|
let rv = rowView as? ThemedTableRow
|
||||||
|
guard rv?.themeToken != self.lastThemeMark else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let triangleView = rv?.triangleView
|
||||||
|
triangleView?.image = self.triangleClosed
|
||||||
|
triangleView?.alternateImage = self.triangleOpen
|
||||||
|
rv?.themeToken = self.lastThemeMark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - NSUserInterfaceValidations
|
// MARK: - NSUserInterfaceValidations
|
||||||
extension FileOutlineView {
|
extension FileOutlineView {
|
||||||
|
|
||||||
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
override func validateUserInterfaceItem(
|
||||||
guard let clickedItem = self.clickedItem as? FileBrowserItem else {
|
_ item: NSValidatedUserInterfaceItem
|
||||||
|
) -> Bool {
|
||||||
|
guard let clickedNode = self.node(from: self.clickedItem) else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.action == #selector(setAsWorkingDirectory(_:)) {
|
if item.action == #selector(setAsWorkingDirectory(_:)) {
|
||||||
return clickedItem.url.isDir
|
return clickedNode.url.isDir
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -496,18 +515,19 @@ extension FileOutlineView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let item = self.selectedItem as? FileBrowserItem else {
|
guard let node = self.node(from: self.selectedItem) else {
|
||||||
super.keyDown(with: event)
|
super.keyDown(with: event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch char {
|
switch char {
|
||||||
case " ", "\r": // Why "\r" and not "\n"?
|
case " ", "\r": // Why "\r" and not "\n"?
|
||||||
if item.url.isDir || item.url.isPackage {
|
if node.url.isDir || node.url.isPackage {
|
||||||
self.toggle(item: item)
|
self.toggle(item: node)
|
||||||
} else {
|
} else {
|
||||||
self.emit(
|
self.emit(
|
||||||
UuidAction(uuid: self.uuid, action: .open(url: item.url, mode: .newTab))
|
UuidAction(uuid: self.uuid,
|
||||||
|
action: .open(url: node.url, mode: .newTab))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -517,40 +537,50 @@ extension FileOutlineView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FileBrowserItem: Hashable, Comparable, CustomStringConvertible {
|
class Node: NSObject, Comparable {
|
||||||
|
|
||||||
static func ==(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
|
static func <(lhs: Node, rhs: Node) -> Bool {
|
||||||
return left.url == right.url
|
return lhs.displayName < rhs.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
static func <(left: FileBrowserItem, right: FileBrowserItem) -> Bool {
|
@objc dynamic var url: URL
|
||||||
return left.url.lastPathComponent < right.url.lastPathComponent
|
@objc dynamic var isLeaf: Bool
|
||||||
|
@objc dynamic var isHidden: Bool
|
||||||
|
@objc dynamic var children: [Node]?
|
||||||
|
|
||||||
|
@objc dynamic var childrenCount: Int {
|
||||||
|
return self.children?.count ?? -1
|
||||||
|
}
|
||||||
|
@objc dynamic var displayName: String {
|
||||||
|
return self.url.lastPathComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashValue: Int {
|
var isDir: Bool {
|
||||||
|
return !self.isLeaf
|
||||||
|
}
|
||||||
|
var isChildrenScanned = false
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
return "<Node: \(self.url): \(self.childrenCount) children>"
|
||||||
|
}
|
||||||
|
|
||||||
|
override var hash: Int {
|
||||||
return self.url.hashValue
|
return self.url.hashValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var description: String {
|
init(url: URL) {
|
||||||
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
|
self.url = url
|
||||||
|
self.isLeaf = !url.isDir
|
||||||
// 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.isHidden = url.isHidden
|
||||||
self.isDir = url.isDir
|
|
||||||
}
|
|
||||||
|
|
||||||
func child(with url: URL) -> FileBrowserItem? {
|
|
||||||
return self.children.first { $0.url == url }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension NSTreeNode {
|
||||||
|
|
||||||
|
var node: Node? {
|
||||||
|
return self.representedObject as? Node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let columnWidthRightPadding = CGFloat(40)
|
||||||
|
private let triangleImageSize = CGFloat(18)
|
||||||
|
@ -1224,7 +1224,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.25.0</string>
|
<string>SNAPSHOT-298</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@ -1241,7 +1241,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>297</string>
|
<string>298</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.productivity</string>
|
<string>public.app-category.productivity</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
@ -10,18 +10,30 @@ import PureLayout
|
|||||||
protocol ThemedView: class {
|
protocol ThemedView: class {
|
||||||
|
|
||||||
var theme: Theme { get }
|
var theme: Theme { get }
|
||||||
|
var lastThemeMark: Token { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThemedTableRow: NSTableRowView {
|
class ThemedTableRow: NSTableRowView {
|
||||||
|
|
||||||
|
weak var triangleView: NSButton?
|
||||||
|
var themeToken: Token
|
||||||
|
|
||||||
init(withIdentifier identifier: String, themedView: ThemedView) {
|
init(withIdentifier identifier: String, themedView: ThemedView) {
|
||||||
self.themedView = themedView
|
self.themedView = themedView
|
||||||
|
self.themeToken = themedView.lastThemeMark
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
self.identifier = NSUserInterfaceItemIdentifier(identifier)
|
self.identifier = NSUserInterfaceItemIdentifier(identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didAddSubview(_ subview: NSView) {
|
||||||
|
super.didAddSubview(subview)
|
||||||
|
if subview.identifier == NSOutlineView.disclosureButtonIdentifier {
|
||||||
|
self.triangleView = subview as? NSButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open override func drawBackground(in dirtyRect: NSRect) {
|
open override func drawBackground(in dirtyRect: NSRect) {
|
||||||
if let cell = self.view(atColumn: 0) as? ThemedTableCell {
|
if let cell = self.view(atColumn: 0) as? ThemedTableCell {
|
||||||
if cell.isDir {
|
if cell.isDir {
|
||||||
@ -57,16 +69,16 @@ class ThemedTableCell: NSTableCellView {
|
|||||||
static let font = NSFont.systemFont(ofSize: 12)
|
static let font = NSFont.systemFont(ofSize: 12)
|
||||||
static let widthWithoutText = CGFloat(2 + 16 + 4 + 2)
|
static let widthWithoutText = CGFloat(2 + 16 + 4 + 2)
|
||||||
|
|
||||||
static func width(with text: String) -> CGFloat {
|
// static func width(with text: String) -> CGFloat {
|
||||||
let attrStr = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: ThemedTableCell.font])
|
// let attrStr = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: ThemedTableCell.font])
|
||||||
|
//
|
||||||
return self.widthWithoutText + attrStr.size().width
|
// return self.widthWithoutText + attrStr.size().width
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override var intrinsicContentSize: CGSize {
|
// override var intrinsicContentSize: CGSize {
|
||||||
return CGSize(width: ThemedTableCell.widthWithoutText + self._textField.intrinsicContentSize.width,
|
// return CGSize(width: ThemedTableCell.widthWithoutText + self._textField.intrinsicContentSize.width,
|
||||||
height: max(self._textField.intrinsicContentSize.height, 16))
|
// height: max(self._textField.intrinsicContentSize.height, 16))
|
||||||
}
|
// }
|
||||||
|
|
||||||
var isDir = false
|
var isDir = false
|
||||||
|
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.25.0</string>
|
<string>SNAPSHOT-298</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>297</string>
|
<string>298</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -7,36 +7,22 @@
|
|||||||
<description>Most recent changes with links to updates for VimR.</description>
|
<description>Most recent changes with links to updates for VimR.</description>
|
||||||
<language>en</language>
|
<language>en</language>
|
||||||
<item>
|
<item>
|
||||||
<title>v0.25.0-297</title>
|
<title>SNAPSHOT-298</title>
|
||||||
<description><![CDATA[
|
<description><![CDATA[
|
||||||
<ul>
|
<ul>
|
||||||
<li>Neovim 0.3.4</li>
|
<li>Improved handling of changes of <code>cwd</code> in the file browser.</li>
|
||||||
<li>GH-625: <code>vimr --cur-env</code> will pass the current environment variables to the new neovim process. This will result in <code>virtualenv</code> support.</li>
|
|
||||||
<li>GH-443: <code>vimr --line ${LINE_NUMBER} ${SOME_FILE}</code> will open the file and go to the given line. If the file is already open in a UI window, then that window will be selected and the cursor will be moved to the given line. This can be used for example to reverse-search LaTeX.</li>
|
|
||||||
<li>GH-603: Bugfix: <code>Cmd-V</code> pastes at the wrong location in the insert mode.</li>
|
|
||||||
<li>GH-659: Bugfix (introduced in a snapshot): Turning off ligatures does not really turn off ligatures.</li>
|
|
||||||
<li>GH-664: Bugfix: VimR crashes for some shell configurations.</li>
|
|
||||||
<li>GH-666: Adapt to the new UI-API of Neovim</li>
|
|
||||||
<li>Dependencies updates:<ul>
|
|
||||||
<li>ReactiveX/RxSwift@4.4.1</li>
|
|
||||||
<li>httpswift/swifter@1.4.5</li>
|
|
||||||
<li>PureLayout/PureLayout@3.1.4</li>
|
|
||||||
<li>sindresorhus/github-markdown-css@3.0.1</li>
|
|
||||||
<li>sparkle-project/Sparkle@1.21.3</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
]]></description>
|
]]></description>
|
||||||
<releaseNotesLink>
|
<releaseNotesLink>
|
||||||
https://github.com/qvacua/vimr/releases/tag/v0.25.0-297
|
https://github.com/qvacua/vimr/releases/tag/snapshot/298
|
||||||
</releaseNotesLink>
|
</releaseNotesLink>
|
||||||
<pubDate>2019-02-23T17:55:14.811734</pubDate>
|
<pubDate>2019-02-26T07:24:18.032526</pubDate>
|
||||||
<minimumSystemVersion>10.10.0</minimumSystemVersion>
|
<minimumSystemVersion>10.10.0</minimumSystemVersion>
|
||||||
<enclosure url="https://github.com/qvacua/vimr/releases/download/v0.25.0-297/VimR-v0.25.0-297.tar.bz2"
|
<enclosure url="https://github.com/qvacua/vimr/releases/download/snapshot/298/VimR-SNAPSHOT-298.tar.bz2"
|
||||||
sparkle:version="297"
|
sparkle:version="298"
|
||||||
sparkle:shortVersionString="0.25.0"
|
sparkle:shortVersionString="SNAPSHOT-298"
|
||||||
sparkle:dsaSignature="MC4CFQC7d60NkbkgZndkDtFrmfJ2Um1yBAIVAKwxaxYy+9FpNNJXOFhAhVUTdxfK"
|
sparkle:dsaSignature="MC0CFQCvFlIJAyiqFN5B1OqsorK6OV1IJwIUJPVNVeWDXCpCSj76EWCYPnNxD4A="
|
||||||
length="14935040"
|
length="14969397"
|
||||||
type="application/octet-stream"/>
|
type="application/octet-stream"/>
|
||||||
</item>
|
</item>
|
||||||
</channel>
|
</channel>
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
# 0.25.0-???
|
# 0.26.0-???
|
||||||
|
|
||||||
|
* Draw the disclosure triangle in appropriate color of the current color scheme (and improve handling of changes of `cwd` in the file browser).
|
||||||
|
* ...
|
||||||
|
|
||||||
|
# 0.25.0-297
|
||||||
|
|
||||||
* Neovim 0.3.4
|
* Neovim 0.3.4
|
||||||
* GH-625: `vimr --cur-env` will pass the current environment variables to the new neovim process. This will result in `virtualenv` support.
|
* GH-625: `vimr --cur-env` will pass the current environment variables to the new neovim process. This will result in `virtualenv` support.
|
||||||
|
Loading…
Reference in New Issue
Block a user