From 109f498291ecd55bd24b2c3b6b85d3e73e4d32d0 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Tue, 27 Sep 2016 00:17:53 +0200 Subject: [PATCH 01/11] GH-286 Rename and use Any? --- SwiftNeoVim/NeoVimView.swift | 4 +-- SwiftNeoVim/NeoVimViewDelegate.swift | 4 +-- VimR/MainWindowComponent.swift | 41 ++++++++++++++++------------ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/SwiftNeoVim/NeoVimView.swift b/SwiftNeoVim/NeoVimView.swift index 05ccaa0e..abcd4d79 100644 --- a/SwiftNeoVim/NeoVimView.swift +++ b/SwiftNeoVim/NeoVimView.swift @@ -1232,7 +1232,7 @@ extension NeoVimView: NeoVimUiBridgeProtocol { public func setTitle(_ title: String) { DispatchUtils.gui { - self.delegate?.setTitle(title) + self.delegate?.set(title: title) } } @@ -1241,7 +1241,7 @@ extension NeoVimView: NeoVimUiBridgeProtocol { public func setDirtyStatus(_ dirty: Bool) { DispatchUtils.gui { - self.delegate?.setDirtyStatus(dirty) + self.delegate?.set(dirtyStatus: dirty) } } diff --git a/SwiftNeoVim/NeoVimViewDelegate.swift b/SwiftNeoVim/NeoVimViewDelegate.swift index 0d2f77af..2e180a47 100644 --- a/SwiftNeoVim/NeoVimViewDelegate.swift +++ b/SwiftNeoVim/NeoVimViewDelegate.swift @@ -8,8 +8,8 @@ import Cocoa // See http://stackoverflow.com/a/24104371 for class public protocol NeoVimViewDelegate: class { - func setTitle(_ title: String) - func setDirtyStatus(_ dirty: Bool) + func set(title: String) + func set(dirtyStatus: Bool) func cwdChanged() func neoVimStopped() } diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index 8264a9d8..cca26e51 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -15,16 +15,24 @@ enum MainWindowAction { class MainWindowComponent: WindowComponent, NSWindowDelegate { - fileprivate let fontManager = NSFontManager.shared() + fileprivate static let nibName = "MainWindow" fileprivate var defaultEditorFont: NSFont fileprivate var usesLigatures: Bool + fileprivate var _cwd: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + + fileprivate let fontManager = NSFontManager.shared() + fileprivate let fileItemService: FileItemService + + fileprivate let workspace: Workspace + fileprivate let neoVimView: NeoVimView + + // MARK: - API var uuid: String { return self.neoVimView.uuid } - fileprivate var _cwd: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) var cwd: URL { get { self._cwd = self.neoVimView.cwd @@ -43,10 +51,6 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { self.fileItemService.monitor(url: newValue) } } - fileprivate let fileItemService: FileItemService - - fileprivate let workspace: Workspace - fileprivate let neoVimView: NeoVimView // TODO: Consider an option object for cwd, urls, etc... init(source: Observable, @@ -66,7 +70,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { self.fileItemService = fileItemService self._cwd = cwd - super.init(source: source, nibName: "MainWindow") + super.init(source: source, nibName: MainWindowComponent.nibName) self.window.delegate = self @@ -99,6 +103,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { self.neoVimView.closeAllWindowsWithoutSaving() } + // MARK: - WindowComponent override func addViews() { self.window.contentView?.addSubview(self.workspace) self.workspace.autoPinEdgesToSuperviewEdges() @@ -122,11 +127,11 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { // MARK: - File Menu Items extension MainWindowComponent { - @IBAction func newTab(_ sender: AnyObject!) { + @IBAction func newTab(_ sender: Any?) { self.neoVimView.newTab() } - @IBAction func openDocument(_ sender: AnyObject!) { + @IBAction func openDocument(_ sender: Any?) { let panel = NSOpenPanel() panel.canChooseDirectories = true panel.beginSheetModal(for: self.window) { result in @@ -139,11 +144,11 @@ extension MainWindowComponent { } } - @IBAction func openQuickly(_ sender: AnyObject!) { + @IBAction func openQuickly(_ sender: Any?) { self.publish(event: MainWindowAction.openQuickly(mainWindow: self)) } - @IBAction func saveDocument(_ sender: AnyObject!) { + @IBAction func saveDocument(_ sender: Any?) { let curBuf = self.neoVimView.currentBuffer() if curBuf.fileName == nil { @@ -154,7 +159,7 @@ extension MainWindowComponent { self.neoVimView.saveCurrentTab() } - @IBAction func saveDocumentAs(_ sender: AnyObject!) { + @IBAction func saveDocumentAs(_ sender: Any?) { self.savePanelSheet { url in self.neoVimView.saveCurrentTab(url: url) @@ -196,18 +201,18 @@ extension MainWindowComponent { // MARK: - Font Menu Items extension MainWindowComponent { - @IBAction func resetFontSize(_ sender: AnyObject!) { + @IBAction func resetFontSize(_ sender: Any?) { self.neoVimView.font = self.defaultEditorFont } - @IBAction func makeFontBigger(_ sender: AnyObject!) { + @IBAction func makeFontBigger(_ sender: Any?) { let curFont = self.neoVimView.font let font = self.fontManager.convert(curFont, toSize: min(curFont.pointSize + 1, PrefStore.maximumEditorFontSize)) self.neoVimView.font = font } - @IBAction func makeFontSmaller(_ sender: AnyObject!) { + @IBAction func makeFontSmaller(_ sender: Any?) { let curFont = self.neoVimView.font let font = self.fontManager.convert(curFont, toSize: max(curFont.pointSize - 1, PrefStore.minimumEditorFontSize)) @@ -218,12 +223,12 @@ extension MainWindowComponent { // MARK: - NeoVimViewDelegate extension MainWindowComponent: NeoVimViewDelegate { - func setTitle(_ title: String) { + func set(title: String) { self.window.title = title } - func setDirtyStatus(_ dirty: Bool) { - self.windowController.setDocumentEdited(dirty) + func set(dirtyStatus: Bool) { + self.windowController.setDocumentEdited(dirtyStatus) } func cwdChanged() { From 46b69882efd9873f97ef6e72b9cd1756e8629b1e Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Tue, 27 Sep 2016 18:02:05 +0200 Subject: [PATCH 02/11] GH-286 Add a stub file browser --- SwiftNeoVim/Grid.swift | 14 +-- SwiftNeoVim/NeoVimView.swift | 28 +++++- VimR.xcodeproj/project.pbxproj | 14 +++ VimR/AppKitCommons.swift | 20 ++++ VimR/Component.swift | 69 ++++++++++++- VimR/FileBrowserComponent.swift | 138 ++++++++++++++++++++++++++ VimR/FileItemService.swift | 62 ++++++++---- VimR/ImageAndTextTableCell.swift | 18 +++- VimR/MainWindowComponent.swift | 35 ++++++- VimR/MainWindowManager.swift | 3 + VimR/OpenQuicklyWindowComponent.swift | 7 +- VimR/PrefPane.swift | 44 +------- VimR/PrefStore.swift | 27 ++--- VimR/Workspace/Workspace.swift | 27 ++++- VimR/Workspace/WorkspaceBar.swift | 54 +++++++--- 15 files changed, 436 insertions(+), 124 deletions(-) create mode 100644 VimR/FileBrowserComponent.swift diff --git a/SwiftNeoVim/Grid.swift b/SwiftNeoVim/Grid.swift index c4e24686..ee732cd1 100644 --- a/SwiftNeoVim/Grid.swift +++ b/SwiftNeoVim/Grid.swift @@ -26,7 +26,7 @@ struct Cell: CustomStringConvertible { } } -extension Position: CustomStringConvertible { +extension Position: CustomStringConvertible, Equatable { static let zero = Position(row: 0, column: 0) static let null = Position(row: -1, column: -1) @@ -36,18 +36,14 @@ extension Position: CustomStringConvertible { } } -func == (left: Position, right: Position) -> Bool { +public func == (left: Position, right: Position) -> Bool { if left.row != right.row { return false } if left.column != right.column { return false } return true } -func != (left: Position, right: Position) -> Bool { - return !(left == right) -} - -struct Size: CustomStringConvertible { +struct Size: CustomStringConvertible, Equatable { static let zero = Size(width: 0, height: 0) @@ -59,6 +55,10 @@ struct Size: CustomStringConvertible { } } +func == (left: Size, right: Size) -> Bool { + return left.width == right.width && left.height == right.height +} + struct Region: CustomStringConvertible { static let zero = Region(top: 0, bottom: 0, left: 0, right: 0) diff --git a/SwiftNeoVim/NeoVimView.swift b/SwiftNeoVim/NeoVimView.swift index abcd4d79..203623f0 100644 --- a/SwiftNeoVim/NeoVimView.swift +++ b/SwiftNeoVim/NeoVimView.swift @@ -79,6 +79,10 @@ open class NeoVimView: NSView, NSUserInterfaceValidations { self.agent.vimCommand("silent cd \(escapedCwd)") } } + + override open var acceptsFirstResponder: Bool { + return true + } fileprivate var _font = NeoVimView.defaultFont @@ -119,12 +123,14 @@ open class NeoVimView: NSView, NSUserInterfaceValidations { fileprivate var pinchTargetScale = CGFloat(1) fileprivate var pinchImage = NSImage() + fileprivate var currentlyResizing = false + public init(frame rect: NSRect, config: Config) { self.drawer = TextDrawer(font: self._font, useLigatures: false) self.agent = NeoVimAgent(uuid: self.uuid) super.init(frame: CGRect.zero) - + self.wantsLayer = true self.cellSize = self.drawer.cellSize self.descent = self.drawer.descent @@ -168,6 +174,17 @@ open class NeoVimView: NSView, NSUserInterfaceValidations { // MARK: - API extension NeoVimView { + + public func enterResizeMode() { + self.currentlyResizing = true + self.needsDisplay = true + } + + public func exitResizeMode() { + self.currentlyResizing = false + self.needsDisplay = true + self.resizeNeoVimUiTo(size: self.bounds.size) + } public func currentBuffer() -> NeoVimBuffer { return self.agent.buffers().filter { $0.current }.first! @@ -261,7 +278,7 @@ extension NeoVimView { return } - if self.inLiveResize { + if self.inLiveResize || self.currentlyResizing { // TODO: Turn off live resizing for now. // self.resizeNeoVimUiTo(size: newSize) return @@ -281,6 +298,10 @@ extension NeoVimView { // NSLog("\(#function): \(size)") let discreteSize = self.discreteSize(size: size) + if discreteSize == self.grid.size { + return + } + self.xOffset = floor((size.width - self.cellSize.width * CGFloat(discreteSize.width)) / 2) self.yOffset = floor((size.height - self.cellSize.height * CGFloat(discreteSize.height)) / 2) @@ -301,7 +322,7 @@ extension NeoVimView { return } - if self.inLiveResize { + if self.inLiveResize || self.currentlyResizing { NSColor.windowBackgroundColor.set() dirtyUnionRect.fill() @@ -863,6 +884,7 @@ extension NeoVimView { extension NeoVimView { override open func mouseDown(with event: NSEvent) { +// self.window?.makeFirstResponder(self) self.mouse(event: event, vimName:"LeftMouse") } diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index 0bf63261..d466584c 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ 1929BEFEABA0448306CDB6D4 /* FileItemIgnorePatternTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BBC84557C8351EC6183E /* FileItemIgnorePatternTest.swift */; }; 1929BF81A40B4154D3EA33CE /* server_ui.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929B93013228985F509C8F6 /* server_ui.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 4B029F1A1D45E349004EE0D3 /* PrefWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B029F1C1D45E349004EE0D3 /* PrefWindow.xib */; }; + 4B0677371D99D9C3001A2588 /* FileBrowserComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */; }; + 4B06773C1D9DCF23001A2588 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B39DA7AC4A9B62D7CD39 /* Component.swift */; }; 4B0BCC941D70320C00D3CE65 /* Logger.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0BCC931D70320C00D3CE65 /* Logger.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4B0C905B1D5DED69007753A3 /* NeoVimBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */; }; 4B0E22581D5DEDC700C072E6 /* NeoVimBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0C90591D5DED69007753A3 /* NeoVimBuffer.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -253,6 +255,7 @@ 1929BEEB33113B0E33C3830F /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; 1929BF00B466B40629C2AABE /* NeoVimView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeoVimView.swift; sourceTree = ""; }; 4B029F1B1D45E349004EE0D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PrefWindow.xib; sourceTree = ""; }; + 4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileBrowserComponent.swift; sourceTree = ""; }; 4B0BCC931D70320C00D3CE65 /* Logger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logger.h; path = VimR/Logger.h; sourceTree = SOURCE_ROOT; }; 4B0C90591D5DED69007753A3 /* NeoVimBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NeoVimBuffer.h; sourceTree = ""; }; 4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NeoVimBuffer.m; sourceTree = ""; }; @@ -413,6 +416,14 @@ path = resources; sourceTree = ""; }; + 4B0677351D99D9A2001A2588 /* File Browser */ = { + isa = PBXGroup; + children = ( + 4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */, + ); + name = "File Browser"; + sourceTree = ""; + }; 4B1AC1AF1D7F395300898F0B /* Open Quickly */ = { isa = PBXGroup; children = ( @@ -569,6 +580,7 @@ 1929B39DA7AC4A9B62D7CD39 /* Component.swift */, 4BD3BF961D32B0DB00082605 /* MainWindowManager.swift */, 4BD3BF921D32A95800082605 /* MainWindowComponent.swift */, + 4B0677351D99D9A2001A2588 /* File Browser */, 4B1AC1AF1D7F395300898F0B /* Open Quickly */, 4B238BED1D3ED55300CBDD98 /* Preferences */, ); @@ -1023,6 +1035,7 @@ 1929B67DA3EB21A631EF1DBB /* FileUtils.swift in Sources */, 1929B3F5743967125F357C9F /* Matcher.swift in Sources */, 1929B53876E6952D378C2B30 /* ScoredFileItem.swift in Sources */, + 4B0677371D99D9C3001A2588 /* FileBrowserComponent.swift in Sources */, 1929BD3F9E609BFADB27584B /* Scorer.swift in Sources */, 4BAD84E81D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift in Sources */, 1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */, @@ -1033,6 +1046,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4B06773C1D9DCF23001A2588 /* Component.swift in Sources */, 4BF8EEDC1D85908400CAC08A /* PrefUtils.swift in Sources */, 1929B43FBA7A31F39D294CD0 /* FileItem.swift in Sources */, 1929B741C4E58EEDAF901353 /* FileItemIgnorePattern.swift in Sources */, diff --git a/VimR/AppKitCommons.swift b/VimR/AppKitCommons.swift index a21ac8a2..dabccb6c 100644 --- a/VimR/AppKitCommons.swift +++ b/VimR/AppKitCommons.swift @@ -100,6 +100,26 @@ extension NSTableView { } } +extension NSOutlineView { + + static func standardOutlineView() -> NSOutlineView { + let outlineView = NSOutlineView(frame: CGRect.zero) + + let column = NSTableColumn(identifier: "name") + column.isEditable = false + + outlineView.addTableColumn(column) + outlineView.outlineTableColumn = column + outlineView.sizeLastColumnToFit() + outlineView.allowsEmptySelection = false + outlineView.allowsMultipleSelection = false + outlineView.headerView = nil + outlineView.focusRingType = .none + + return outlineView + } +} + extension NSScrollView { static func standardScrollView() -> NSScrollView { diff --git a/VimR/Component.swift b/VimR/Component.swift index 508b1fda..a0702dab 100644 --- a/VimR/Component.swift +++ b/VimR/Component.swift @@ -15,18 +15,36 @@ protocol Store: Flow {} protocol Component: Flow {} -class StandardFlow: Flow { +class PublishingFlow: Flow { var sink: Observable { return self.subject.asObservable() } let subject = PublishSubject() + + init() { + + } + + deinit { + self.subject.onCompleted() + } + + func publish(event: Any) { + self.subject.onNext(event) + } +} + +class StandardFlow: PublishingFlow { + let source: Observable let disposeBag = DisposeBag() init(source: Observable) { self.source = source + super.init() + self.subscription(source: source).addDisposableTo(self.disposeBag) } @@ -37,10 +55,6 @@ class StandardFlow: Flow { func subscription(source: Observable) -> Disposable { preconditionFailure("Please override") } - - func publish(event: Any) { - self.subject.onNext(event) - } } class StandardComponent: NSObject, Component { @@ -78,6 +92,51 @@ class StandardComponent: NSObject, Component { } } +class ViewComponent: NSView, Component { + + var view: NSView { + preconditionFailure("Please override") + } + + var sink: Observable { + return self.subject.asObservable() + } + + let subject = PublishSubject() + let source: Observable + let disposeBag = DisposeBag() + + init(source: Observable) { + self.source = source + + super.init(frame: CGRect.zero) + self.translatesAutoresizingMaskIntoConstraints = false + + self.addViews() + self.subscription(source: source).addDisposableTo(self.disposeBag) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.subject.onCompleted() + } + + func addViews() { + preconditionFailure("Please override") + } + + func subscription(source: Observable) -> Disposable { + preconditionFailure("Please override") + } + + func publish(event: Any) { + self.subject.onNext(event) + } +} + class WindowComponent: StandardComponent { let windowController: NSWindowController diff --git a/VimR/FileBrowserComponent.swift b/VimR/FileBrowserComponent.swift new file mode 100644 index 00000000..c0ebe3a0 --- /dev/null +++ b/VimR/FileBrowserComponent.swift @@ -0,0 +1,138 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Cocoa +import RxSwift + +class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineViewDelegate { + + fileprivate static let userHomeUrl = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + + fileprivate var cwd = FileBrowserComponent.userHomeUrl + fileprivate var cwdFileItem = FileItem(FileBrowserComponent.userHomeUrl) + + fileprivate let fileView = NSOutlineView.standardOutlineView() + fileprivate let dumb = [NSAttributedString(string: "A"), NSAttributedString(string: "B")] + + fileprivate let fileItemService: FileItemService + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + init(source: Observable, fileItemService: FileItemService) { + self.fileItemService = fileItemService + super.init(source: source) + } + + override func addViews() { + let fileView = self.fileView + fileView.dataSource = self + fileView.delegate = self + + let scrollView = NSScrollView.standardScrollView() + scrollView.borderType = .noBorder + scrollView.backgroundColor = NSColor.windowBackgroundColor + scrollView.documentView = fileView + + self.addSubview(scrollView) + scrollView.autoPinEdgesToSuperviewEdges() + } + + override func subscription(source: Observable) -> Disposable { + return source + .filter { $0 is MainWindowAction } + .map { $0 as! MainWindowAction } + .subscribe(onNext: { [unowned self] action in + switch action { + case let .changeCwd(mainWindow: mainWindow): + self.cwd = mainWindow.cwd + self.cwdFileItem = self.fileItemService.fileItemWithChildren(for: self.cwd) ?? + self.fileItemService.fileItemWithChildren(for: FileBrowserComponent.userHomeUrl)! + NSLog("cwd changed to \(self.cwd) of \(mainWindow.uuid)") + self.fileView.reloadData() + + default: + break + } + }) + } +} + +// MARK: - NSOutlineViewDataSource +extension FileBrowserComponent { + + func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + if item == nil { + return self.fileItemService.fileItemWithChildren(for: self.cwd)?.children + .filter { !$0.hidden } + .count ?? 0 + } + + guard let fileItem = item as? FileItem else { + return 0 + } + + if fileItem.dir { + return self.fileItemService.fileItemWithChildren(for: fileItem.url)?.children + .filter { !$0.hidden } + .count ?? 0 + } + + return 0 + } + + func outlineView(_: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + if item == nil { + return self.fileItemService.fileItemWithChildren(for: self.cwd)!.children.filter { !$0.hidden }[index] + } + + guard let fileItem = item as? FileItem else { + preconditionFailure("Should not happen") + } + + return self.fileItemService.fileItemWithChildren(for: fileItem.url)!.children.filter { !$0.hidden }[index] + } + + func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool { + guard let fileItem = item as? FileItem else { + return false + } + + return fileItem.dir && !fileItem.package + } + + @objc(outlineView:objectValueForTableColumn:byItem:) + func outlineView(_: NSOutlineView, objectValueFor: NSTableColumn?, byItem item: Any?) -> Any? { + guard let fileItem = item as? FileItem else { + return nil + } + + return fileItem + } +} + +// MARK: - NSOutlineViewDelegate +extension FileBrowserComponent { + + @objc(outlineView:viewForTableColumn:item:) + func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { + guard let fileItem = item as? FileItem else { + return nil + } + + let cachedCell = outlineView.make(withIdentifier: "file-view-row", owner: self) + let cell = cachedCell as? ImageAndTextTableCell ?? ImageAndTextTableCell(withIdentifier: "file-view-row") + + cell.text = fileItem.url.lastPathComponent + cell.image = self.fileItemService.icon(forUrl: fileItem.url) + + return cell + } + + func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { + return 20 + } +} diff --git a/VimR/FileItemService.swift b/VimR/FileItemService.swift index 9be1804a..201fb6d4 100644 --- a/VimR/FileItemService.swift +++ b/VimR/FileItemService.swift @@ -13,13 +13,7 @@ func == (left: Token, right: Token) -> Bool { class Token: Equatable {} -class FileItemService { - - fileprivate(set) var ignorePatterns: Set = [] { - didSet { - self.ignoreToken = Token() - } - } +class FileItemService: PublishingFlow { /// Used to cache fnmatch calls in `FileItem`. fileprivate var ignoreToken = Token() @@ -40,8 +34,15 @@ class FileItemService { fileprivate let iconsCache = NSCache() fileprivate var spinLock = OS_SPINLOCK_INIT - - init() { + + // MARK: - API + fileprivate(set) var ignorePatterns: Set = [] { + didSet { + self.ignoreToken = Token() + } + } + + override init() { self.iconsCache.countLimit = 2000 self.iconsCache.name = "icon-cache" } @@ -74,7 +75,7 @@ class FileItemService { { [unowned self] events in let urls = events.map { URL(fileURLWithPath: $0.path) } let parent = FileUtils.commonParent(ofUrls: urls) - self.fileItem(forUrl: parent)?.needsScanChildren = true + self.fileItem(for: parent)?.needsScanChildren = true } self.monitors[url] = monitor @@ -106,14 +107,10 @@ class FileItemService { return } - self.parentFileItem(ofUrl: url).removeChild(withUrl: url) + self.parentFileItem(of: url).removeChild(withUrl: url) } } - fileprivate func parentFileItem(ofUrl url: URL) -> FileItem { - return self.fileItem(forPathComponents: Array(url.pathComponents.dropLast()))! - } - func flatFileItems(ofUrl url: URL) -> Observable<[FileItem]> { guard url.isFileURL else { return Observable.empty() @@ -130,7 +127,7 @@ class FileItemService { } self.scanDispatchQueue.async { [unowned self] in - guard let targetItem = self.fileItem(forPathComponents: pathComponents) else { + guard let targetItem = self.fileItem(for: pathComponents) else { observer.onCompleted() return } @@ -193,16 +190,41 @@ class FileItemService { } } - fileprivate func fileItem(forUrl url: URL) -> FileItem? { + /// Returns the `FileItem` corresponding to the `url` parameter with children. This is like mkdir -p, i.e. it + /// instantiates the intermediate `FileItem`s. + /// + /// - returns: `FileItem` corresponding to `url` with children. `nil` if the file does not exist. + func fileItemWithChildren(for url: URL) -> FileItem? { + guard let fileItem = self.fileItem(for: url) else { + return nil + } + + if !fileItem.childrenScanned || fileItem.needsScanChildren { + self.scanChildren(fileItem) + } + + return fileItem + } + + // FIXME: what if root? + fileprivate func parentFileItem(of url: URL) -> FileItem { + return self.fileItem(for: Array(url.pathComponents.dropLast()))! + } + + /// Returns the `FileItem` corresponding to the `url` parameter. This is like mkdir -p, i.e. it + /// instantiates the intermediate `FileItem`s. The children of the result may be empty. + /// + /// - returns: `FileItem` corresponding to `pathComponents`. `nil` if the file does not exist. + fileprivate func fileItem(for url: URL) -> FileItem? { let pathComponents = url.pathComponents - return self.fileItem(forPathComponents: pathComponents) + return self.fileItem(for: pathComponents) } /// Returns the `FileItem` corresponding to the `pathComponents` parameter. This is like mkdir -p, i.e. it - /// instantiates the intermediate `FileItem`s. + /// instantiates the intermediate `FileItem`s. The children of the result may be empty. /// /// - returns: `FileItem` corresponding to `pathComponents`. `nil` if the file does not exist. - fileprivate func fileItem(forPathComponents pathComponents: [String]) -> FileItem? { + fileprivate func fileItem(for pathComponents: [String]) -> FileItem? { let result = pathComponents.dropFirst().reduce(self.root) { (resultItem, childName) -> FileItem? in guard let parent = resultItem else { return nil diff --git a/VimR/ImageAndTextTableCell.swift b/VimR/ImageAndTextTableCell.swift index 786f2d38..8b210bc9 100644 --- a/VimR/ImageAndTextTableCell.swift +++ b/VimR/ImageAndTextTableCell.swift @@ -8,15 +8,25 @@ import PureLayout class ImageAndTextTableCell: NSView { - var text: NSAttributedString { + var attributedText: NSAttributedString { get { return self.textField.attributedStringValue } - + set { self.textField.attributedStringValue = newValue } } + + var text: String { + get { + return self.textField.stringValue + } + + set { + self.textField.stringValue = newValue + } + } var image: NSImage? { get { @@ -38,7 +48,11 @@ class ImageAndTextTableCell: NSView { let textField = self.textField textField.isBordered = false + textField.isBezeled = false + textField.allowsDefaultTighteningForTruncation = true + textField.allowsEditingTextAttributes = false textField.isEditable = false + textField.usesSingleLineMode = true textField.lineBreakMode = .byTruncatingTail textField.drawsBackground = false diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index cca26e51..d57e782b 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -10,10 +10,16 @@ import RxSwift enum MainWindowAction { case becomeKey(mainWindow: MainWindowComponent) case openQuickly(mainWindow: MainWindowComponent) + case changeCwd(mainWindow: MainWindowComponent) case close(mainWindow: MainWindowComponent) } -class MainWindowComponent: WindowComponent, NSWindowDelegate { +fileprivate enum Tool { + + case fileBrowser +} + +class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate { fileprivate static let nibName = "MainWindow" @@ -27,6 +33,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { fileprivate let workspace: Workspace fileprivate let neoVimView: NeoVimView + fileprivate var tools = [Tool: WorkspaceTool]() // MARK: - API var uuid: String { @@ -73,8 +80,18 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { super.init(source: source, nibName: MainWindowComponent.nibName) self.window.delegate = self + self.workspace.delegate = self - self.neoVimView.cwd = cwd + let fileBrowser = FileBrowserComponent(source: self.sink, fileItemService: fileItemService) + let fileBrowserTool = WorkspaceTool(title: "Files", view: fileBrowser) + self.tools[.fileBrowser] = fileBrowserTool + self.workspace.append(tool: fileBrowserTool, location: .left) + + // FIXME: temporarily for dev + fileBrowserTool.dimension = 200 + self.workspace.bars[.left]?.toggle(fileBrowserTool) + + self.neoVimView.cwd = cwd // This will publish the MainWindowAction.changeCwd action for the file browser. self.neoVimView.delegate = self self.neoVimView.font = self.defaultEditorFont self.neoVimView.usesLigatures = self.usesLigatures @@ -124,6 +141,18 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate { } } +// MARK: - WorkspaceDelegate +extension MainWindowComponent { + + func resizeWillStart(workspace: Workspace) { + self.neoVimView.enterResizeMode() + } + + func resizeDidEnd(workspace: Workspace) { + self.neoVimView.exitResizeMode() + } +} + // MARK: - File Menu Items extension MainWindowComponent { @@ -236,6 +265,8 @@ extension MainWindowComponent: NeoVimViewDelegate { self._cwd = self.neoVimView.cwd self.fileItemService.unmonitor(url: old) self.fileItemService.monitor(url: self._cwd) + + self.publish(event: MainWindowAction.changeCwd(mainWindow: self)) } func neoVimStopped() { diff --git a/VimR/MainWindowManager.swift b/VimR/MainWindowManager.swift index 4bcc98ec..8382609a 100644 --- a/VimR/MainWindowManager.swift +++ b/VimR/MainWindowManager.swift @@ -46,6 +46,9 @@ class MainWindowManager: StandardFlow { case let .close(mainWindow): self.closeMainWindow(mainWindow) + + case .changeCwd: + break } }) .addDisposableTo(self.disposeBag) diff --git a/VimR/OpenQuicklyWindowComponent.swift b/VimR/OpenQuicklyWindowComponent.swift index 3c11ad60..a7ede019 100644 --- a/VimR/OpenQuicklyWindowComponent.swift +++ b/VimR/OpenQuicklyWindowComponent.swift @@ -216,7 +216,7 @@ extension OpenQuicklyWindowComponent { let cell = cachedCell as? ImageAndTextTableCell ?? ImageAndTextTableCell(withIdentifier: "file-view-row") let url = self.fileViewItems[row].url - cell.text = self.rowText(forUrl: url as URL) + cell.attributedText = self.rowText(forUrl: url as URL) cell.image = self.fileItemService.icon(forUrl: url) return cell @@ -240,9 +240,8 @@ extension OpenQuicklyWindowComponent { rowText = NSMutableAttributedString(string: "\(name) — \(pathInfo)") rowText.addAttribute(NSForegroundColorAttributeName, value: NSColor.lightGray, - range: NSRange(location:name.characters.count, - length: pathInfo.characters.count + 3)) - + range: NSRange(location:name.characters.count, length: pathInfo.characters.count + 3)) + return rowText } } diff --git a/VimR/PrefPane.swift b/VimR/PrefPane.swift index 87f096bd..cbb85330 100644 --- a/VimR/PrefPane.swift +++ b/VimR/PrefPane.swift @@ -6,17 +6,8 @@ import Cocoa import RxSwift -class PrefPane: NSView, Component { +class PrefPane: ViewComponent { - let disposeBag = DisposeBag() - - fileprivate let source: Observable - - fileprivate let subject = PublishSubject() - var sink: Observable { - return self.subject.asObservable() - } - // Return true to place this to the upper left corner when the scroll view is bigger than this view. override var isFlipped: Bool { return true @@ -30,38 +21,8 @@ class PrefPane: NSView, Component { return false } - init(source: Observable) { - self.source = source - - super.init(frame: CGRect.zero) - self.translatesAutoresizingMaskIntoConstraints = false - - self.addViews() - self.subscription(source: self.source).addDisposableTo(self.disposeBag) - } - - deinit { - self.subject.onCompleted() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func addViews() { - preconditionFailure("Please override") - } - - func subscription(source: Observable) -> Disposable { - preconditionFailure("Please override") - } - - func publish(event: Any) { - self.subject.onNext(event) - } - func windowWillClose() { - + // noop, override } } @@ -103,7 +64,6 @@ extension PrefPane { func configureCheckbox(button: NSButton, title: String, action: Selector) { button.title = title button.setButtonType(.switch) -// button.bezelStyle = .ThickSquareBezelStyle button.target = self button.action = action } diff --git a/VimR/PrefStore.swift b/VimR/PrefStore.swift index d6a67919..df631ee2 100644 --- a/VimR/PrefStore.swift +++ b/VimR/PrefStore.swift @@ -19,21 +19,13 @@ private class PrefKeys { static let useInteractiveZsh = "use-interactive-zsh" } -class PrefStore: Store { +class PrefStore: StandardFlow { fileprivate static let compatibleVersion = "38" fileprivate static let defaultEditorFont = NeoVimView.defaultFont static let minimumEditorFontSize = NeoVimView.minFontSize static let maximumEditorFontSize = NeoVimView.maxFontSize - fileprivate let source: Observable - fileprivate let disposeBag = DisposeBag() - - fileprivate let subject = PublishSubject() - var sink: Observable { - return self.subject.asObservable() - } - fileprivate let userDefaults = UserDefaults.standard fileprivate let fontManager = NSFontManager.shared() @@ -45,20 +37,14 @@ class PrefStore: Store { advanced: AdvancedPrefData(useInteractiveZsh: false) ) - init(source: Observable) { - self.source = source + override init(source: Observable) { + super.init(source: source) if let prefs = self.userDefaults.dictionary(forKey: PrefStore.compatibleVersion) { self.data = self.prefDataFromDict(prefs) } else { self.userDefaults.setValue(self.prefsDict(self.data), forKey: PrefStore.compatibleVersion) } - - self.addReactions() - } - - deinit { - self.subject.onCompleted() } fileprivate func prefDataFromDict(_ prefs: [String: Any]) -> PrefData { @@ -125,15 +111,14 @@ class PrefStore: Store { return prefs } - fileprivate func addReactions() { - self.source + override func subscription(source: Observable) -> Disposable { + return source .filter { $0 is PrefData } .map { $0 as! PrefData } .subscribe(onNext: { [unowned self] prefData in self.data = prefData self.userDefaults.setValue(self.prefsDict(prefData), forKey: PrefStore.compatibleVersion) - self.subject.onNext(prefData) + self.publish(event: prefData) }) - .addDisposableTo(self.disposeBag) } } diff --git a/VimR/Workspace/Workspace.swift b/VimR/Workspace/Workspace.swift index 4287c6d5..1602687d 100644 --- a/VimR/Workspace/Workspace.swift +++ b/VimR/Workspace/Workspace.swift @@ -13,7 +13,13 @@ enum WorkspaceBarLocation { case left } -class Workspace: NSView { +protocol WorkspaceDelegate: class { + + func resizeWillStart(workspace: Workspace) + func resizeDidEnd(workspace: Workspace) +} + +class Workspace: NSView, WorkspaceBarDelegate { struct Config { let mainViewMinimumSize: CGSize @@ -25,16 +31,17 @@ class Workspace: NSView { } } - fileprivate let bars: [WorkspaceBarLocation: WorkspaceBar] - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - API let mainView: NSView + let bars: [WorkspaceBarLocation: WorkspaceBar] let config: Config + weak var delegate: WorkspaceDelegate? + init(mainView: NSView, config: Config = Config(mainViewMinimumSize: CGSize(width: 100, height: 100))) { self.config = config self.mainView = mainView @@ -49,6 +56,8 @@ class Workspace: NSView { super.init(frame: CGRect.zero) self.translatesAutoresizingMaskIntoConstraints = false + self.bars.values.forEach { [unowned self] in $0.delegate = self } + self.relayout() } @@ -65,6 +74,18 @@ class Workspace: NSView { } } +// MARK: - WorkspaceBarDelegate +extension Workspace { + + func resizeWillStart(workspaceBar: WorkspaceBar) { + self.delegate?.resizeWillStart(workspace: self) + } + + func resizeDidEnd(workspaceBar: WorkspaceBar) { + self.delegate?.resizeDidEnd(workspace: self) + } +} + // MARK: - Layout extension Workspace { diff --git a/VimR/Workspace/WorkspaceBar.swift b/VimR/Workspace/WorkspaceBar.swift index cde59477..9458c6a5 100644 --- a/VimR/Workspace/WorkspaceBar.swift +++ b/VimR/Workspace/WorkspaceBar.swift @@ -6,6 +6,12 @@ import Cocoa import PureLayout +protocol WorkspaceBarDelegate: class { + + func resizeWillStart(workspaceBar: WorkspaceBar) + func resizeDidEnd(workspaceBar: WorkspaceBar) +} + class WorkspaceBar: NSView, WorkspaceToolDelegate { static fileprivate let separatorColor = NSColor.controlShadowColor @@ -32,6 +38,8 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { } var dimensionConstraint = NSLayoutConstraint() + weak var delegate: WorkspaceBarDelegate? + init(location: WorkspaceBarLocation) { self.location = location @@ -47,7 +55,7 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { self.removeAllSubviews() if self.isEmpty() { - self.set(0) + self.set(dimension: 0) return } @@ -60,9 +68,9 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { self.layout(curTool) let newDimension = self.barDimension(withToolDimension: curTool.dimension) - self.set(newDimension) + self.set(dimension: newDimension) } else { - self.set(self.barDimensionWithButtonsWithoutTool()) + self.set(dimension: self.barDimensionWithButtonsWithoutTool()) } } else { @@ -72,13 +80,11 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { self.layoutWithoutButtons(curTool) let newDimension = self.barDimensionWithoutButtons(withToolDimension: curTool.dimension) - self.set(newDimension) + self.set(dimension: newDimension) } else { - self.set(0) + self.set(dimension: 0) } } - - self.needsDisplay = true } func append(tool: WorkspaceTool) { @@ -110,6 +116,14 @@ extension WorkspaceBar { } } + override func hitTest(_ point: NSPoint) -> NSView? { + if self.resizeRect().contains(point) { + return self + } + + return super.hitTest(point) + } + override func mouseDown(with event: NSEvent) { guard self.isOpen() else { return @@ -128,6 +142,7 @@ extension WorkspaceBar { } self.isMouseDownOngoing = true + self.delegate?.resizeWillStart(workspaceBar: self) self.dimensionConstraint.priority = NSLayoutPriorityDragThatCannotResizeWindow - 1 var dragged = false @@ -138,10 +153,10 @@ extension WorkspaceBar { NSEventMask.leftMouseUp ] while curEvent.type != .leftMouseUp { - let nextEvent = NSApp.nextEvent(matching: NSEventMask(rawValue: UInt64(Int(nextEventMask.rawValue))), - until: Date.distantFuture, - inMode: RunLoopMode.eventTrackingRunLoopMode, - dequeue: true) + let nextEvent = NSApp.nextEvent(matching: nextEventMask, + until: Date.distantFuture, + inMode: RunLoopMode.eventTrackingRunLoopMode, + dequeue: true) guard nextEvent != nil else { break } @@ -162,15 +177,14 @@ extension WorkspaceBar { let locInSuperview = self.superview!.convert(curEvent.locationInWindow, from: nil) let newDimension = self.newDimension(forLocationInSuperview: locInSuperview) - self.set(newDimension) - - self.window?.invalidateCursorRects(for: self) + self.set(dimension: newDimension) dragged = true } self.dimensionConstraint.priority = NSLayoutPriorityDragThatCannotResizeWindow self.isMouseDownOngoing = false + self.delegate?.resizeDidEnd(workspaceBar: self) } override func resetCursorRects() { @@ -284,13 +298,19 @@ extension WorkspaceBar { } } - fileprivate func set(_ dimension: CGFloat) { + fileprivate func set(dimension: CGFloat) { self.dimensionConstraint.constant = dimension let toolDimension = self.toolDimension(fromBarDimension: dimension) if self.isOpen() { self.selectedTool?.dimension = toolDimension } + + // In 10.12 we need the following, otherwise resizing the tools does not work correctly. + self.layoutSubtreeIfNeeded() + + self.window?.invalidateCursorRects(for: self) + self.needsDisplay = true } } @@ -493,6 +513,8 @@ extension WorkspaceBar { extension WorkspaceBar { func toggle(_ tool: WorkspaceTool) { + self.delegate?.resizeWillStart(workspaceBar: self) + if self.isOpen() { let curTool = self.selectedTool! if curTool === tool { @@ -508,5 +530,7 @@ extension WorkspaceBar { } self.relayout() + + self.delegate?.resizeDidEnd(workspaceBar: self) } } From 1105ec0805015d570d436c9a69824c8b8cd38ae3 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sun, 2 Oct 2016 12:25:57 +0200 Subject: [PATCH 03/11] GH-286 Add double click action --- VimR/AppKitCommons.swift | 12 ++++++++++++ VimR/FileBrowserComponent.swift | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/VimR/AppKitCommons.swift b/VimR/AppKitCommons.swift index dabccb6c..46108412 100644 --- a/VimR/AppKitCommons.swift +++ b/VimR/AppKitCommons.swift @@ -118,6 +118,18 @@ extension NSOutlineView { return outlineView } + + /** + The selected item. When the selection is empty, then returns `nil`. When multiple items are selected, then returns + the last selected item. + */ + var selectedItem: Any? { + if self.selectedRow < 0 { + return nil + } + + return self.item(atRow: self.selectedRow) + } } extension NSScrollView { diff --git a/VimR/FileBrowserComponent.swift b/VimR/FileBrowserComponent.swift index c0ebe3a0..2df8b0ab 100644 --- a/VimR/FileBrowserComponent.swift +++ b/VimR/FileBrowserComponent.swift @@ -31,6 +31,7 @@ class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineVie let fileView = self.fileView fileView.dataSource = self fileView.delegate = self + fileView.doubleAction = #selector(FileBrowserComponent.fileViewDoubleAction) let scrollView = NSScrollView.standardScrollView() scrollView.borderType = .noBorder @@ -61,6 +62,22 @@ class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineVie } } +// MARK: - Actions +extension FileBrowserComponent { + + func fileViewDoubleAction() { + guard let item = self.fileView.selectedItem as? FileItem else { + return + } + + if item.dir { + self.fileView.expandItem(item) + } else { + NSLog("open \(item)") + } + } +} + // MARK: - NSOutlineViewDataSource extension FileBrowserComponent { From 786f87564dc8147fea8a3e7a0946fdf26983c40d Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sun, 2 Oct 2016 12:28:47 +0200 Subject: [PATCH 04/11] GH-286 Refactor slightly --- VimR/FileBrowserComponent.swift | 8 +++----- VimR/FileUtils.swift | 2 ++ VimR/MainWindowManager.swift | 6 ++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/VimR/FileBrowserComponent.swift b/VimR/FileBrowserComponent.swift index 2df8b0ab..dd07c024 100644 --- a/VimR/FileBrowserComponent.swift +++ b/VimR/FileBrowserComponent.swift @@ -8,10 +8,8 @@ import RxSwift class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineViewDelegate { - fileprivate static let userHomeUrl = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) - - fileprivate var cwd = FileBrowserComponent.userHomeUrl - fileprivate var cwdFileItem = FileItem(FileBrowserComponent.userHomeUrl) + fileprivate var cwd = FileUtils.userHomeUrl + fileprivate var cwdFileItem = FileItem(FileUtils.userHomeUrl) fileprivate let fileView = NSOutlineView.standardOutlineView() fileprivate let dumb = [NSAttributedString(string: "A"), NSAttributedString(string: "B")] @@ -51,7 +49,7 @@ class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineVie case let .changeCwd(mainWindow: mainWindow): self.cwd = mainWindow.cwd self.cwdFileItem = self.fileItemService.fileItemWithChildren(for: self.cwd) ?? - self.fileItemService.fileItemWithChildren(for: FileBrowserComponent.userHomeUrl)! + self.fileItemService.fileItemWithChildren(for: FileUtils.userHomeUrl)! NSLog("cwd changed to \(self.cwd) of \(mainWindow.uuid)") self.fileView.reloadData() diff --git a/VimR/FileUtils.swift b/VimR/FileUtils.swift index 0565245c..fbd5ccde 100644 --- a/VimR/FileUtils.swift +++ b/VimR/FileUtils.swift @@ -20,6 +20,8 @@ class FileUtils { ] fileprivate static let fileManager = FileManager.default + + static let userHomeUrl = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) static func directDescendants(_ url: URL) -> [URL] { guard let childUrls = try? self.fileManager.contentsOfDirectory( diff --git a/VimR/MainWindowManager.swift b/VimR/MainWindowManager.swift index 8382609a..965d1919 100644 --- a/VimR/MainWindowManager.swift +++ b/VimR/MainWindowManager.swift @@ -11,8 +11,6 @@ enum MainWindowEvent { } class MainWindowManager: StandardFlow { - - static fileprivate let userHomeUrl = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) fileprivate var mainWindowComponents = [String:MainWindowComponent]() fileprivate weak var keyMainWindow: MainWindowComponent? @@ -27,7 +25,7 @@ class MainWindowManager: StandardFlow { super.init(source: source) } - func newMainWindow(urls: [URL] = [], cwd: URL = MainWindowManager.userHomeUrl) -> MainWindowComponent { + func newMainWindow(urls: [URL] = [], cwd: URL = FileUtils.userHomeUrl) -> MainWindowComponent { let mainWindowComponent = MainWindowComponent( source: self.source, fileItemService: self.fileItemService, cwd: cwd, urls: urls, initialData: self.data ) @@ -72,7 +70,7 @@ class MainWindowManager: StandardFlow { return self.mainWindowComponents.values.reduce(false) { $0 ? true : $1.isDirty() } } - func openInKeyMainWindow(urls:[URL] = [], cwd: URL = MainWindowManager.userHomeUrl) { + func openInKeyMainWindow(urls:[URL] = [], cwd: URL = FileUtils.userHomeUrl) { guard !self.mainWindowComponents.isEmpty else { _ = self.newMainWindow(urls: urls, cwd: cwd) return From d0999546666826392b6798d6f0471829ae2d7033 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sun, 2 Oct 2016 14:07:12 +0200 Subject: [PATCH 05/11] GH-286 Add double click action --- VimR/Component.swift | 2 +- VimR/FileBrowserComponent.swift | 10 +++++++--- VimR/MainWindowComponent.swift | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/VimR/Component.swift b/VimR/Component.swift index a0702dab..6dd0dd83 100644 --- a/VimR/Component.swift +++ b/VimR/Component.swift @@ -6,7 +6,7 @@ import Cocoa import RxSwift -protocol Flow { +protocol Flow: class { var sink: Observable { get } } diff --git a/VimR/FileBrowserComponent.swift b/VimR/FileBrowserComponent.swift index dd07c024..8f23c1d4 100644 --- a/VimR/FileBrowserComponent.swift +++ b/VimR/FileBrowserComponent.swift @@ -6,6 +6,11 @@ import Cocoa import RxSwift +enum FileBrowserAction { + + case open(url: URL) +} + class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineViewDelegate { fileprivate var cwd = FileUtils.userHomeUrl @@ -33,7 +38,6 @@ class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineVie let scrollView = NSScrollView.standardScrollView() scrollView.borderType = .noBorder - scrollView.backgroundColor = NSColor.windowBackgroundColor scrollView.documentView = fileView self.addSubview(scrollView) @@ -71,7 +75,7 @@ extension FileBrowserComponent { if item.dir { self.fileView.expandItem(item) } else { - NSLog("open \(item)") + self.publish(event: FileBrowserAction.open(url: item.url)) } } } @@ -116,7 +120,7 @@ extension FileBrowserComponent { return false } - return fileItem.dir && !fileItem.package + return fileItem.dir } @objc(outlineView:objectValueForTableColumn:byItem:) diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index d57e782b..09aa0aa3 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -35,6 +35,8 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate fileprivate let neoVimView: NeoVimView fileprivate var tools = [Tool: WorkspaceTool]() + fileprivate var flows = [Flow]() + // MARK: - API var uuid: String { return self.neoVimView.uuid @@ -87,10 +89,14 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate self.tools[.fileBrowser] = fileBrowserTool self.workspace.append(tool: fileBrowserTool, location: .left) + self.flows.append(fileBrowser) + // FIXME: temporarily for dev fileBrowserTool.dimension = 200 self.workspace.bars[.left]?.toggle(fileBrowserTool) + self.addReactions() + self.neoVimView.cwd = cwd // This will publish the MainWindowAction.changeCwd action for the file browser. self.neoVimView.delegate = self self.neoVimView.font = self.defaultEditorFont @@ -120,6 +126,23 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate self.neoVimView.closeAllWindowsWithoutSaving() } + // MARK: - Private + fileprivate func addReactions() { + self.flows + .map { $0.sink } + .toMergedObservables() + .subscribe(onNext: { [unowned self] action in + switch action { + case let FileBrowserAction.open(url: url): + self.open(urls: [url]) + default: + NSLog("unrecognized action: \(action)") + return + } + }) + .addDisposableTo(self.disposeBag) + } + // MARK: - WindowComponent override func addViews() { self.window.contentView?.addSubview(self.workspace) From 6623c66eb55f686d6a9b6ab4cbff6da5717cae4a Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sun, 2 Oct 2016 14:11:39 +0200 Subject: [PATCH 06/11] GH-286 Refactor slightly --- VimR/AppDelegate.swift | 2 +- VimR/MainWindowComponent.swift | 2 +- VimR/OpenQuicklyWindowComponent.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VimR/AppDelegate.swift b/VimR/AppDelegate.swift index 4c094a6c..88b4d240 100644 --- a/VimR/AppDelegate.swift +++ b/VimR/AppDelegate.swift @@ -207,7 +207,7 @@ extension AppDelegate { .filter { $0.hasPrefix(AppDelegate.cwdPrefix) } .flatMap { $0.without(prefix: AppDelegate.cwdPrefix).removingPercentEncoding } .map { URL(fileURLWithPath: $0) } - .first ?? URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + .first ?? FileUtils.userHomeUrl switch action { case .activate, .newWindow: diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index 09aa0aa3..ba0170bc 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -26,7 +26,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate fileprivate var defaultEditorFont: NSFont fileprivate var usesLigatures: Bool - fileprivate var _cwd: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) + fileprivate var _cwd: URL = FileUtils.userHomeUrl fileprivate let fontManager = NSFontManager.shared() fileprivate let fileItemService: FileItemService diff --git a/VimR/OpenQuicklyWindowComponent.swift b/VimR/OpenQuicklyWindowComponent.swift index a7ede019..d0a54f51 100644 --- a/VimR/OpenQuicklyWindowComponent.swift +++ b/VimR/OpenQuicklyWindowComponent.swift @@ -17,7 +17,7 @@ class OpenQuicklyWindowComponent: WindowComponent, var pauseScan = false fileprivate(set) var pattern = "" - fileprivate(set) var cwd = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) { + fileprivate(set) var cwd = FileUtils.userHomeUrl { didSet { self.cwdPathCompsCount = self.cwd.pathComponents.count self.cwdControl.url = self.cwd From 499e7431fcbee883fb0f9ea6182fa57bfda3cbbc Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Mon, 3 Oct 2016 09:45:38 +0200 Subject: [PATCH 07/11] GH-286 Respect minimum dimension of tools --- VimR.xcodeproj/project.pbxproj | 10 ++++++++++ VimR/Component.swift | 21 +++++++++++++++------ VimR/MainWindowComponent.swift | 10 +++------- VimR/Workspace/WorkspaceBar.swift | 20 ++++++++++++++++++-- VimR/Workspace/WorkspaceTool.swift | 8 +++++--- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index d466584c..1271ec79 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -35,6 +35,11 @@ 4B029F1A1D45E349004EE0D3 /* PrefWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B029F1C1D45E349004EE0D3 /* PrefWindow.xib */; }; 4B0677371D99D9C3001A2588 /* FileBrowserComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */; }; 4B06773C1D9DCF23001A2588 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B39DA7AC4A9B62D7CD39 /* Component.swift */; }; + 4B0677401DA170D5001A2588 /* AppKitCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A70931D60E04200E12030 /* AppKitCommons.swift */; }; + 4B0677411DA170D5001A2588 /* Workspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6423951D8EFD7100FC78C8 /* Workspace.swift */; }; + 4B0677421DA170D5001A2588 /* WorkspaceBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6423971D8EFDE000FC78C8 /* WorkspaceBar.swift */; }; + 4B0677431DA170D5001A2588 /* WorkspaceTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6423991D8EFE3000FC78C8 /* WorkspaceTool.swift */; }; + 4B0677441DA170D5001A2588 /* WorkspaceToolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB489411D952CF6005BB0E8 /* WorkspaceToolButton.swift */; }; 4B0BCC941D70320C00D3CE65 /* Logger.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0BCC931D70320C00D3CE65 /* Logger.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4B0C905B1D5DED69007753A3 /* NeoVimBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */; }; 4B0E22581D5DEDC700C072E6 /* NeoVimBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0C90591D5DED69007753A3 /* NeoVimBuffer.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1046,6 +1051,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4B0677401DA170D5001A2588 /* AppKitCommons.swift in Sources */, + 4B0677411DA170D5001A2588 /* Workspace.swift in Sources */, + 4B0677421DA170D5001A2588 /* WorkspaceBar.swift in Sources */, + 4B0677431DA170D5001A2588 /* WorkspaceTool.swift in Sources */, + 4B0677441DA170D5001A2588 /* WorkspaceToolButton.swift in Sources */, 4B06773C1D9DCF23001A2588 /* Component.swift in Sources */, 4BF8EEDC1D85908400CAC08A /* PrefUtils.swift in Sources */, 1929B43FBA7A31F39D294CD0 /* FileItem.swift in Sources */, diff --git a/VimR/Component.swift b/VimR/Component.swift index 6dd0dd83..b7a1fc07 100644 --- a/VimR/Component.swift +++ b/VimR/Component.swift @@ -11,10 +11,6 @@ protocol Flow: class { var sink: Observable { get } } -protocol Store: Flow {} - -protocol Component: Flow {} - class PublishingFlow: Flow { var sink: Observable { @@ -57,7 +53,7 @@ class StandardFlow: PublishingFlow { } } -class StandardComponent: NSObject, Component { +class StandardComponent: NSObject, Flow { var sink: Observable { return self.subject.asObservable() @@ -92,7 +88,7 @@ class StandardComponent: NSObject, Component { } } -class ViewComponent: NSView, Component { +class ViewComponent: NSView, Flow { var view: NSView { preconditionFailure("Please override") @@ -137,6 +133,19 @@ class ViewComponent: NSView, Component { } } +class WorkspaceToolComponent: WorkspaceTool, Flow { + + let viewComponent: ViewComponent + var sink: Observable { + return self.viewComponent.sink + } + + init(title: String, viewComponent: ViewComponent, minimumDimension: CGFloat = 50) { + self.viewComponent = viewComponent + super.init(title: title, view: viewComponent, minimumDimension: minimumDimension) + } +} + class WindowComponent: StandardComponent { let windowController: NSWindowController diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index ba0170bc..e204502b 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -33,9 +33,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate fileprivate let workspace: Workspace fileprivate let neoVimView: NeoVimView - fileprivate var tools = [Tool: WorkspaceTool]() - - fileprivate var flows = [Flow]() + fileprivate var tools = [Tool: WorkspaceToolComponent]() // MARK: - API var uuid: String { @@ -85,12 +83,10 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate self.workspace.delegate = self let fileBrowser = FileBrowserComponent(source: self.sink, fileItemService: fileItemService) - let fileBrowserTool = WorkspaceTool(title: "Files", view: fileBrowser) + let fileBrowserTool = WorkspaceToolComponent(title: "Files", viewComponent: fileBrowser, minimumDimension: 100) self.tools[.fileBrowser] = fileBrowserTool self.workspace.append(tool: fileBrowserTool, location: .left) - self.flows.append(fileBrowser) - // FIXME: temporarily for dev fileBrowserTool.dimension = 200 self.workspace.bars[.left]?.toggle(fileBrowserTool) @@ -128,7 +124,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate // MARK: - Private fileprivate func addReactions() { - self.flows + self.tools.values .map { $0.sink } .toMergedObservables() .subscribe(onNext: { [unowned self] action in diff --git a/VimR/Workspace/WorkspaceBar.swift b/VimR/Workspace/WorkspaceBar.swift index 9458c6a5..cda60e7b 100644 --- a/VimR/Workspace/WorkspaceBar.swift +++ b/VimR/Workspace/WorkspaceBar.swift @@ -30,6 +30,8 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { } // MARK: - API + static let minimumDimension = CGFloat(50) + let location: WorkspaceBarLocation var isButtonVisible = true { didSet { @@ -299,9 +301,11 @@ extension WorkspaceBar { } fileprivate func set(dimension: CGFloat) { - self.dimensionConstraint.constant = dimension + let saneDimension = self.saneDimension(from: dimension) - let toolDimension = self.toolDimension(fromBarDimension: dimension) + self.dimensionConstraint.constant = saneDimension + + let toolDimension = self.toolDimension(fromBarDimension: saneDimension) if self.isOpen() { self.selectedTool?.dimension = toolDimension } @@ -312,6 +316,18 @@ extension WorkspaceBar { self.window?.invalidateCursorRects(for: self) self.needsDisplay = true } + + fileprivate func saneDimension(from dimension: CGFloat) -> CGFloat { + if dimension == 0 { + return 0 + } + + if self.isOpen() { + return max(dimension, self.selectedTool!.minimumDimension, WorkspaceBar.minimumDimension) + } + + return max(dimension, self.barDimensionWithButtonsWithoutTool()) + } } // MARK: - Layout diff --git a/VimR/Workspace/WorkspaceTool.swift b/VimR/Workspace/WorkspaceTool.swift index 79df8b39..59aeef89 100644 --- a/VimR/Workspace/WorkspaceTool.swift +++ b/VimR/Workspace/WorkspaceTool.swift @@ -34,12 +34,14 @@ class WorkspaceTool { weak var delegate: WorkspaceToolDelegate? - let minimumDimension = CGFloat(30) - var dimension = CGFloat(50) + let minimumDimension: CGFloat + var dimension: CGFloat - init(title: String, view: NSView) { + init(title: String, view: NSView, minimumDimension: CGFloat = 50) { self.title = title self.view = view + self.minimumDimension = minimumDimension + self.dimension = minimumDimension self.button = WorkspaceToolButton(title: title) self.button.tool = self From eb16dd7fae54327b9f5ed92772a353d10cedca04 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Mon, 3 Oct 2016 10:17:30 +0200 Subject: [PATCH 08/11] GH-286 Add toggle tools/buttons menu items. --- VimR/Base.lproj/MainMenu.xib | 20 ++++++++++++++++---- VimR/MainWindowComponent.swift | 20 ++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/VimR/Base.lproj/MainMenu.xib b/VimR/Base.lproj/MainMenu.xib index 60c7aa63..ba85ae5a 100644 --- a/VimR/Base.lproj/MainMenu.xib +++ b/VimR/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -182,10 +182,22 @@ - + + + + + + + + + + + + + - + diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index e204502b..e139058b 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -87,10 +87,6 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate self.tools[.fileBrowser] = fileBrowserTool self.workspace.append(tool: fileBrowserTool, location: .left) - // FIXME: temporarily for dev - fileBrowserTool.dimension = 200 - self.workspace.bars[.left]?.toggle(fileBrowserTool) - self.addReactions() self.neoVimView.cwd = cwd // This will publish the MainWindowAction.changeCwd action for the file browser. @@ -172,7 +168,7 @@ extension MainWindowComponent { } } -// MARK: - File Menu Items +// MARK: - File Menu Item Actions extension MainWindowComponent { @IBAction func newTab(_ sender: Any?) { @@ -246,7 +242,19 @@ extension MainWindowComponent { } } -// MARK: - Font Menu Items +// MARK: - View Menu Item Actions +extension MainWindowComponent { + + @IBAction func toggleAllTools(_ sender: Any?) { + self.workspace.toggleAllTools() + } + + @IBAction func toggleToolButtons(_ sender: Any?) { + self.workspace.toggleToolButtons() + } +} + +// MARK: - Font Menu Item Actions extension MainWindowComponent { @IBAction func resetFontSize(_ sender: Any?) { From 3099bac9671b5e23365d44ae00ea556914adc4c6 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Mon, 3 Oct 2016 14:58:49 +0200 Subject: [PATCH 09/11] GH-286 Persist the workspace setting of the last closed main window --- VimR.xcodeproj/project.pbxproj | 2 - VimR/AppDelegate.swift | 62 ++++++++---------- VimR/FileItem.swift | 6 ++ VimR/FileItemService.swift | 33 ++++++---- VimR/MainWindowComponent.swift | 37 ++++++++++- VimR/MainWindowManager.swift | 24 ++++--- VimR/OpenQuicklyWindowManager.swift | 12 +++- VimR/PrefStore.swift | 97 ++++++++++++++++++++++------- VimR/PrefWindowComponent.swift | 6 -- VimR/Workspace/Workspace.swift | 13 ++-- VimR/Workspace/WorkspaceBar.swift | 25 ++++---- 11 files changed, 211 insertions(+), 106 deletions(-) diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index 1271ec79..e964edff 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 1929B73E5EC0B108B83F82EB /* FileItemService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B3A98687DF171307AAC8 /* FileItemService.swift */; }; 1929B741C4E58EEDAF901353 /* FileItemIgnorePattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */; }; 1929B7A2F2B423AA9740FD45 /* FileUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B5D977261F1EBFA9E8F1 /* FileUtilsTest.swift */; }; - 1929B7C981A0EFCC8D631E3F /* FileItemService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B3A98687DF171307AAC8 /* FileItemService.swift */; }; 1929B93DBAD09835E428F610 /* PrefPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB251F74BEFC82CEEF84 /* PrefPane.swift */; }; 1929BD3F9E609BFADB27584B /* Scorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9D510177918080BE39B /* Scorer.swift */; }; 1929BD4CA2204E061A86A140 /* MatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC19C1BC19246AFF1621 /* MatcherTests.swift */; }; @@ -1060,7 +1059,6 @@ 4BF8EEDC1D85908400CAC08A /* PrefUtils.swift in Sources */, 1929B43FBA7A31F39D294CD0 /* FileItem.swift in Sources */, 1929B741C4E58EEDAF901353 /* FileItemIgnorePattern.swift in Sources */, - 1929B7C981A0EFCC8D631E3F /* FileItemService.swift in Sources */, 1929B2E1A64297B8E05BB64A /* FileUtils.swift in Sources */, 1929B3BF1DB87B57559DC27D /* Matcher.swift in Sources */, 4BF8EEDB1D85903000CAC08A /* PrefUtilsTest.swift in Sources */, diff --git a/VimR/AppDelegate.swift b/VimR/AppDelegate.swift index 88b4d240..6eb09a9a 100644 --- a/VimR/AppDelegate.swift +++ b/VimR/AppDelegate.swift @@ -37,63 +37,55 @@ class AppDelegate: NSObject, NSApplicationDelegate { fileprivate let openQuicklyWindowManager: OpenQuicklyWindowManager fileprivate let prefWindowComponent: PrefWindowComponent - fileprivate let fileItemService = FileItemService() - + fileprivate let fileItemService: FileItemService + fileprivate var quitWhenAllWindowsAreClosed = false fileprivate var launching = true override init() { self.actionSink = self.actionSubject.asObservable() self.changeSink = self.changeSubject.asObservable() + let actionAndChangeSink = [self.changeSink, self.actionSink].toMergedObservables() self.prefStore = PrefStore(source: self.actionSink) + self.fileItemService = FileItemService(source: self.changeSink) self.fileItemService.set(ignorePatterns: self.prefStore.data.general.ignorePatterns) self.prefWindowComponent = PrefWindowComponent(source: self.changeSink, initialData: self.prefStore.data) + self.mainWindowManager = MainWindowManager(source: self.changeSink, fileItemService: self.fileItemService, initialData: self.prefStore.data) - self.openQuicklyWindowManager = OpenQuicklyWindowManager(source: self.changeSink, + self.openQuicklyWindowManager = OpenQuicklyWindowManager(source: actionAndChangeSink, fileItemService: self.fileItemService) super.init() - self.prefStore.sink - .filter { $0 is PrefData } - .map { $0 as! PrefData } - .subscribe(onNext: { [unowned self] data in - if data.general.ignorePatterns == self.fileItemService.ignorePatterns { - return - } - - self.fileItemService.set(ignorePatterns: data.general.ignorePatterns) - }) - .addDisposableTo(self.disposeBag) - self.mainWindowManager.sink - .filter { $0 is MainWindowEvent || $0 is MainWindowAction } + .filter { $0 is MainWindowManagerAction } + .map { $0 as! MainWindowManagerAction } .subscribe(onNext: { [unowned self] event in switch event { - case let MainWindowAction.openQuickly(mainWindow: mainWindow): - self.openQuicklyWindowManager.open(forMainWindow: mainWindow) - case MainWindowEvent.allWindowsClosed: + case .allWindowsClosed: if self.quitWhenAllWindowsAreClosed { NSApp.stop(self) } - default: - return } - }) + }) .addDisposableTo(self.disposeBag) - [ self.prefStore ] + let changeFlows: [Flow] = [ self.prefStore ] + let actionFlows: [Flow] = [ self.prefWindowComponent, self.mainWindowManager ] + + changeFlows .map { $0.sink } .toMergedObservables() .subscribe(self.changeSubject) .addDisposableTo(self.disposeBag) - [ self.prefWindowComponent ] + + actionFlows .map { $0.sink } .toMergedObservables() .subscribe(self.actionSubject) @@ -106,7 +98,7 @@ extension AppDelegate { func applicationWillFinishLaunching(_: Notification) { self.launching = true - + let appleEventManager = NSAppleEventManager.shared() appleEventManager.setEventHandler(self, andSelector: #selector(AppDelegate.handle(getUrlEvent:replyEvent:)), @@ -164,7 +156,7 @@ extension AppDelegate { // There are no open main window, then just quit. return .terminateNow } - + // For drag & dropping files on the App icon. func application(_ sender: NSApplication, openFiles filenames: [String]) { let urls = filenames.map { URL(fileURLWithPath: $0) } @@ -176,28 +168,28 @@ extension AppDelegate { // MARK: - AppleScript extension AppDelegate { - + func handle(getUrlEvent event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) { guard let urlString = event.paramDescriptor(forKeyword: UInt32(keyDirectObject))?.stringValue else { return } - + guard let url = URL(string: urlString) else { return } - + guard url.scheme == "vimr" else { return } - + guard let rawAction = url.host else { return } - + guard let action = VimRUrlAction(rawValue: rawAction) else { return } - + let queryParams = url.query?.components(separatedBy: "&") let urls = queryParams? .filter { $0.hasPrefix(AppDelegate.filePrefix) } @@ -208,7 +200,7 @@ extension AppDelegate { .flatMap { $0.without(prefix: AppDelegate.cwdPrefix).removingPercentEncoding } .map { URL(fileURLWithPath: $0) } .first ?? FileUtils.userHomeUrl - + switch action { case .activate, .newWindow: _ = self.mainWindowManager.newMainWindow(urls: urls, cwd: cwd) @@ -229,7 +221,7 @@ extension AppDelegate { @IBAction func showPrefWindow(_ sender: AnyObject!) { self.prefWindowComponent.show() } - + @IBAction func newDocument(_ sender: AnyObject!) { _ = self.mainWindowManager.newMainWindow() } @@ -242,7 +234,7 @@ extension AppDelegate { guard result == NSFileHandlingPanelOKButton else { return } - + _ = self.mainWindowManager.newMainWindow(urls: panel.urls) } } diff --git a/VimR/FileItem.swift b/VimR/FileItem.swift index cbf06905..d3d91eeb 100644 --- a/VimR/FileItem.swift +++ b/VimR/FileItem.swift @@ -5,6 +5,12 @@ import Foundation +class Token: Equatable {} + +func == (left: Token, right: Token) -> Bool { + return left === right +} + class FileItem : CustomStringConvertible { let url: URL diff --git a/VimR/FileItemService.swift b/VimR/FileItemService.swift index 201fb6d4..be8da327 100644 --- a/VimR/FileItemService.swift +++ b/VimR/FileItemService.swift @@ -7,13 +7,7 @@ import Cocoa import RxSwift import EonilFileSystemEvents -func == (left: Token, right: Token) -> Bool { - return left === right -} - -class Token: Equatable {} - -class FileItemService: PublishingFlow { +class FileItemService: StandardFlow { /// Used to cache fnmatch calls in `FileItem`. fileprivate var ignoreToken = Token() @@ -29,7 +23,7 @@ class FileItemService: PublishingFlow { fileprivate let fileSystemEventsLatency = Double(2) fileprivate var monitors = [URL: FileSystemEventMonitor]() fileprivate var monitorCounter = [URL: Int]() - + fileprivate let workspace = NSWorkspace.shared() fileprivate let iconsCache = NSCache() @@ -42,7 +36,9 @@ class FileItemService: PublishingFlow { } } - override init() { + override init(source: Observable) { + super.init(source: source) + self.iconsCache.countLimit = 2000 self.iconsCache.name = "icon-cache" } @@ -50,7 +46,7 @@ class FileItemService: PublishingFlow { func set(ignorePatterns patterns: Set) { self.ignorePatterns = patterns } - + func icon(forUrl url: URL) -> NSImage? { if let cached = self.iconsCache.object(forKey: url as NSURL) { return cached @@ -60,7 +56,7 @@ class FileItemService: PublishingFlow { let icon = workspace.icon(forFile: path) icon.size = CGSize(width: 16, height: 16) self.iconsCache.setObject(icon, forKey: url as NSURL) - + return icon } @@ -263,7 +259,7 @@ class FileItemService: PublishingFlow { return filteredChildren.first } - + fileprivate func scanChildren(_ item: FileItem) { let children = FileUtils.directDescendants(item.url).map(FileItem.init) self.syncAddChildren { item.children = children } @@ -277,4 +273,17 @@ class FileItemService: PublishingFlow { fn() OSSpinLockUnlock(&self.spinLock) } + + override func subscription(source: Observable) -> Disposable { + return source + .filter { $0 is PrefData } + .map { $0 as! PrefData } + .subscribe(onNext: { [unowned self] data in + if data.general.ignorePatterns == self.ignorePatterns { + return + } + + self.set(ignorePatterns: data.general.ignorePatterns) + }) + } } diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index e139058b..c54ccf92 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -11,7 +11,16 @@ enum MainWindowAction { case becomeKey(mainWindow: MainWindowComponent) case openQuickly(mainWindow: MainWindowComponent) case changeCwd(mainWindow: MainWindowComponent) - case close(mainWindow: MainWindowComponent) + case close(mainWindow: MainWindowComponent, mainWindowPrefData: MainWindowPrefData) +} + +struct MainWindowPrefData { + + let isAllToolsVisible: Bool + let isToolButtonsVisible: Bool + + let isFileBrowserVisible: Bool + let fileBrowserWidth: Float } fileprivate enum Tool { @@ -80,6 +89,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate super.init(source: source, nibName: MainWindowComponent.nibName) self.window.delegate = self + self.workspace.delegate = self let fileBrowser = FileBrowserComponent(source: self.sink, fileItemService: fileItemService) @@ -98,6 +108,22 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate // We don't call self.fileItemService.monitor(url: cwd) here since self.neoVimView.cwd = cwd causes the call // cwdChanged() and in that function we do monitor(...). + // By default the tool buttons are shown and no tools are shown. + let mainWindowData = initialData.mainWindow + fileBrowserTool.dimension = CGFloat(mainWindowData.fileBrowserWidth) + + if !mainWindowData.isAllToolsVisible { + self.toggleAllTools(self) + } + + if !mainWindowData.isToolButtonsVisible { + self.toggleToolButtons(self) + } + + if mainWindowData.isFileBrowserVisible { + self.workspace.bars[.left]?.toggle(fileBrowserTool) + } + self.window.makeFirstResponder(self.neoVimView) self.show() } @@ -310,7 +336,14 @@ extension MainWindowComponent { func windowWillClose(_ notification: Notification) { self.fileItemService.unmonitor(url: self._cwd) - self.publish(event: MainWindowAction.close(mainWindow: self)) + + let fileBrowser = self.tools[.fileBrowser]! + let prefData = MainWindowPrefData(isAllToolsVisible: self.workspace.isAllToolsVisible, + isToolButtonsVisible: self.workspace.isToolButtonsVisible, + isFileBrowserVisible: self.workspace.bars[.left]?.isOpen ?? true, + fileBrowserWidth: Float(fileBrowser.dimension)) + + self.publish(event: MainWindowAction.close(mainWindow: self, mainWindowPrefData: prefData)) } func windowShouldClose(_ sender: Any) -> Bool { diff --git a/VimR/MainWindowManager.swift b/VimR/MainWindowManager.swift index 965d1919..76e21320 100644 --- a/VimR/MainWindowManager.swift +++ b/VimR/MainWindowManager.swift @@ -6,7 +6,8 @@ import Cocoa import RxSwift -enum MainWindowEvent { +enum MainWindowManagerAction { + case allWindowsClosed } @@ -26,9 +27,12 @@ class MainWindowManager: StandardFlow { } func newMainWindow(urls: [URL] = [], cwd: URL = FileUtils.userHomeUrl) -> MainWindowComponent { - let mainWindowComponent = MainWindowComponent( - source: self.source, fileItemService: self.fileItemService, cwd: cwd, urls: urls, initialData: self.data - ) + let mainWindowComponent = MainWindowComponent(source: self.source, + fileItemService: self.fileItemService, + cwd: cwd, + urls: urls, + initialData: self.data) + self.mainWindowComponents[mainWindowComponent.uuid] = mainWindowComponent mainWindowComponent.sink @@ -42,8 +46,8 @@ class MainWindowManager: StandardFlow { case .openQuickly: self.publish(event: action) - case let .close(mainWindow): - self.closeMainWindow(mainWindow) + case let .close(mainWindow, mainWindowPrefData): + self.close(mainWindow, prefData: mainWindowPrefData) case .changeCwd: break @@ -54,7 +58,11 @@ class MainWindowManager: StandardFlow { return mainWindowComponent } - func closeMainWindow(_ mainWindowComponent: MainWindowComponent) { + func close(_ mainWindowComponent: MainWindowComponent, prefData: MainWindowPrefData) { + // Save the tools settings of the last closed main window. + // TODO: Think about a better time to save this. + self.publish(event: prefData) + if self.keyMainWindow === mainWindowComponent { self.keyMainWindow = nil } @@ -62,7 +70,7 @@ class MainWindowManager: StandardFlow { self.mainWindowComponents.removeValue(forKey: mainWindowComponent.uuid) if self.mainWindowComponents.isEmpty { - self.publish(event: MainWindowEvent.allWindowsClosed) + self.publish(event: MainWindowManagerAction.allWindowsClosed) } } diff --git a/VimR/OpenQuicklyWindowManager.swift b/VimR/OpenQuicklyWindowManager.swift index ecbb5708..73787e87 100644 --- a/VimR/OpenQuicklyWindowManager.swift +++ b/VimR/OpenQuicklyWindowManager.swift @@ -23,6 +23,16 @@ class OpenQuicklyWindowManager: StandardFlow { } override func subscription(source: Observable) -> Disposable { - return Disposables.create() + return source + .filter { $0 is MainWindowAction } + .map { $0 as! MainWindowAction } + .subscribe(onNext: { [unowned self] event in + switch event { + case let .openQuickly(mainWindow: mainWindow): + self.open(forMainWindow: mainWindow) + default: + return + } + }) } } diff --git a/VimR/PrefStore.swift b/VimR/PrefStore.swift index df631ee2..086ff9d8 100644 --- a/VimR/PrefStore.swift +++ b/VimR/PrefStore.swift @@ -6,6 +6,14 @@ import Cocoa import RxSwift +struct PrefData { + var general: GeneralPrefData + var appearance: AppearancePrefData + var advanced: AdvancedPrefData + + var mainWindow: MainWindowPrefData +} + private class PrefKeys { static let openNewWindowWhenLaunching = "open-new-window-when-launching" @@ -17,8 +25,15 @@ private class PrefKeys { static let editorUsesLigatures = "editor-uses-ligatures" static let useInteractiveZsh = "use-interactive-zsh" + + static let isAllToolsVisible = "is-all-tools-visible" + static let isToolButtonsShown = "is-tool-buttons-visible" + + static let isFileBrowserOpen = "is-file-browser-visible" + static let fileBrowserWidth = "file-browser-width" } +// TODO: We should generalize the persisting of pref data. class PrefStore: StandardFlow { fileprivate static let compatibleVersion = "38" @@ -34,7 +49,11 @@ class PrefStore: StandardFlow { openNewWindowOnReactivation: true, ignorePatterns: Set([ "*/.git", "*.o", "*.d", "*.dia" ].map(FileItemIgnorePattern.init))), appearance: AppearancePrefData(editorFont: PrefStore.defaultEditorFont, editorUsesLigatures: false), - advanced: AdvancedPrefData(useInteractiveZsh: false) + advanced: AdvancedPrefData(useInteractiveZsh: false), + mainWindow: MainWindowPrefData(isAllToolsVisible: true, + isToolButtonsVisible: true, + isFileBrowserVisible: true, + fileBrowserWidth: 200) ) override init(source: Observable) { @@ -54,15 +73,20 @@ class PrefStore: StandardFlow { (prefs[PrefKeys.editorFontSize] as? NSNumber)?.floatValue ?? Float(PrefStore.defaultEditorFont.pointSize) ) let editorFont = self.saneFont(editorFontName, fontSize: editorFontSize) - - let usesLigatures = (prefs[PrefKeys.editorUsesLigatures] as? NSNumber)?.boolValue ?? false - let openNewWindowWhenLaunching = (prefs[PrefKeys.openNewWindowWhenLaunching] as? NSNumber)?.boolValue ?? true - let openNewWindowOnReactivation = (prefs[PrefKeys.openNewWindowOnReactivation] as? NSNumber)?.boolValue ?? true + + let usesLigatures = self.bool(from: prefs, for: PrefKeys.editorUsesLigatures, default: false) + let openNewWindowWhenLaunching = self.bool(from: prefs, for: PrefKeys.openNewWindowWhenLaunching, default: true) + let openNewWindowOnReactivation = self.bool(from: prefs, for: PrefKeys.openNewWindowOnReactivation, default: true) let ignorePatternsList = (prefs[PrefKeys.openQuicklyIgnorePatterns] as? String) ?? "*/.git, *.o, *.d, *.dia" let ignorePatterns = PrefUtils.ignorePatterns(fromString: ignorePatternsList) - let useInteractiveZsh = (prefs[PrefKeys.useInteractiveZsh] as? NSNumber)?.boolValue ?? false + let useInteractiveZsh = self.bool(from: prefs, for: PrefKeys.useInteractiveZsh, default: false) + + let isAllToolsVisible = self.bool(from: prefs, for: PrefKeys.isAllToolsVisible, default: true) + let isToolButtonsVisible = self.bool(from: prefs, for: PrefKeys.isToolButtonsShown, default: true) + let isFileBrowserVisible = self.bool(from: prefs, for: PrefKeys.isFileBrowserOpen, default: true) + let fileBrowserWidth = (prefs[PrefKeys.fileBrowserWidth] as? NSNumber)?.floatValue ?? Float(200) return PrefData( general: GeneralPrefData( @@ -71,10 +95,18 @@ class PrefStore: StandardFlow { ignorePatterns: ignorePatterns ), appearance: AppearancePrefData(editorFont: editorFont, editorUsesLigatures: usesLigatures), - advanced: AdvancedPrefData(useInteractiveZsh: useInteractiveZsh) + advanced: AdvancedPrefData(useInteractiveZsh: useInteractiveZsh), + mainWindow: MainWindowPrefData(isAllToolsVisible: isAllToolsVisible, + isToolButtonsVisible: isToolButtonsVisible, + isFileBrowserVisible: isFileBrowserVisible, + fileBrowserWidth: fileBrowserWidth) ) } + fileprivate func bool(from prefs: [String: Any], for key: String, default defaultValue: Bool) -> Bool { + return (prefs[key] as? NSNumber)?.boolValue ?? defaultValue + } + fileprivate func saneFont(_ fontName: String, fontSize: CGFloat) -> NSFont { var editorFont = NSFont(name: fontName, size: fontSize) ?? PrefStore.defaultEditorFont if !editorFont.isFixedPitch { @@ -88,24 +120,33 @@ class PrefStore: StandardFlow { return editorFont } - fileprivate func prefsDict(_ prefData: PrefData) -> [String: AnyObject] { + fileprivate func prefsDict(_ prefData: PrefData) -> [String: Any] { let generalData = prefData.general let appearanceData = prefData.appearance let advancedData = prefData.advanced + let mainWindowData = prefData.mainWindow - let prefs: [String: AnyObject] = [ + let ignorePatterns = PrefUtils.ignorePatternString(fromSet: generalData.ignorePatterns) as Any + + let prefs: [String: Any] = [ // General - PrefKeys.openNewWindowWhenLaunching: generalData.openNewWindowWhenLaunching as AnyObject, - PrefKeys.openNewWindowOnReactivation: generalData.openNewWindowOnReactivation as AnyObject, - PrefKeys.openQuicklyIgnorePatterns: PrefUtils.ignorePatternString(fromSet: generalData.ignorePatterns) as AnyObject, + PrefKeys.openNewWindowWhenLaunching: generalData.openNewWindowWhenLaunching as Any, + PrefKeys.openNewWindowOnReactivation: generalData.openNewWindowOnReactivation as Any, + PrefKeys.openQuicklyIgnorePatterns: ignorePatterns, // Appearance - PrefKeys.editorFontName: appearanceData.editorFont.fontName as AnyObject, - PrefKeys.editorFontSize: appearanceData.editorFont.pointSize as AnyObject, - PrefKeys.editorUsesLigatures: appearanceData.editorUsesLigatures as AnyObject, + PrefKeys.editorFontName: appearanceData.editorFont.fontName as Any, + PrefKeys.editorFontSize: appearanceData.editorFont.pointSize as Any, + PrefKeys.editorUsesLigatures: appearanceData.editorUsesLigatures as Any, // Advanced - PrefKeys.useInteractiveZsh: advancedData.useInteractiveZsh as AnyObject, + PrefKeys.useInteractiveZsh: advancedData.useInteractiveZsh as Any, + + // MainWindow + PrefKeys.isAllToolsVisible: mainWindowData.isAllToolsVisible, + PrefKeys.isToolButtonsShown: mainWindowData.isToolButtonsVisible, + PrefKeys.isFileBrowserOpen: mainWindowData.isFileBrowserVisible, + PrefKeys.fileBrowserWidth: mainWindowData.fileBrowserWidth ] return prefs @@ -113,12 +154,22 @@ class PrefStore: StandardFlow { override func subscription(source: Observable) -> Disposable { return source - .filter { $0 is PrefData } - .map { $0 as! PrefData } - .subscribe(onNext: { [unowned self] prefData in - self.data = prefData - self.userDefaults.setValue(self.prefsDict(prefData), forKey: PrefStore.compatibleVersion) - self.publish(event: prefData) - }) + .filter { $0 is PrefData || $0 is MainWindowPrefData } + .subscribe(onNext: { [unowned self] data in + switch data { + case let prefData as PrefData: + self.data = prefData + + case let mainWindowPrefData as MainWindowPrefData: + NSLog("\(mainWindowPrefData)") + self.data.mainWindow = mainWindowPrefData + + default: + return + } + + self.userDefaults.setValue(self.prefsDict(self.data), forKey: PrefStore.compatibleVersion) + self.publish(event: self.data) + }) } } diff --git a/VimR/PrefWindowComponent.swift b/VimR/PrefWindowComponent.swift index d1afac1f..29615825 100644 --- a/VimR/PrefWindowComponent.swift +++ b/VimR/PrefWindowComponent.swift @@ -7,12 +7,6 @@ import Cocoa import RxSwift import PureLayout -struct PrefData { - var general: GeneralPrefData - var appearance: AppearancePrefData - var advanced: AdvancedPrefData -} - class PrefWindowComponent: WindowComponent, NSWindowDelegate, NSTableViewDataSource, NSTableViewDelegate { fileprivate var data: PrefData diff --git a/VimR/Workspace/Workspace.swift b/VimR/Workspace/Workspace.swift index 1602687d..fc3cbd34 100644 --- a/VimR/Workspace/Workspace.swift +++ b/VimR/Workspace/Workspace.swift @@ -25,11 +25,16 @@ class Workspace: NSView, WorkspaceBarDelegate { let mainViewMinimumSize: CGSize } - fileprivate(set) var isBarVisible = true { + fileprivate(set) var isAllToolsVisible = true { didSet { self.relayout() } } + fileprivate(set) var isToolButtonsVisible = true { + didSet { + self.bars.values.forEach { $0.isButtonVisible = !$0.isButtonVisible } + } + } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -66,11 +71,11 @@ class Workspace: NSView, WorkspaceBarDelegate { } func toggleAllTools() { - self.isBarVisible = !self.isBarVisible + self.isAllToolsVisible = !self.isAllToolsVisible } func toggleToolButtons() { - self.bars.values.forEach { $0.isButtonVisible = !$0.isButtonVisible } + self.isToolButtonsVisible = !self.isToolButtonsVisible } } @@ -101,7 +106,7 @@ extension Workspace { mainView.autoSetDimension(.width, toSize: self.config.mainViewMinimumSize.width, relation: .greaterThanOrEqual) mainView.autoSetDimension(.height, toSize: self.config.mainViewMinimumSize.height, relation: .greaterThanOrEqual) - guard self.isBarVisible else { + guard self.isAllToolsVisible else { mainView.autoPinEdgesToSuperviewEdges() return } diff --git a/VimR/Workspace/WorkspaceBar.swift b/VimR/Workspace/WorkspaceBar.swift index cda60e7b..c482f9f8 100644 --- a/VimR/Workspace/WorkspaceBar.swift +++ b/VimR/Workspace/WorkspaceBar.swift @@ -38,6 +38,9 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { self.relayout() } } + var isOpen: Bool { + return self.selectedTool != nil + } var dimensionConstraint = NSLayoutConstraint() weak var delegate: WorkspaceBarDelegate? @@ -64,7 +67,7 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { if self.isButtonVisible { self.layoutButtons() - if self.isOpen() { + if self.isOpen { let curTool = self.selectedTool! self.layout(curTool) @@ -76,7 +79,7 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { } } else { - if self.isOpen() { + if self.isOpen { let curTool = self.selectedTool! self.layoutWithoutButtons(curTool) @@ -94,7 +97,7 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate { tool.location = self.location tools.append(tool) - if self.isOpen() { + if self.isOpen { self.selectedTool?.isSelected = false self.selectedTool = tool } @@ -113,7 +116,7 @@ extension WorkspaceBar { self.drawInnerSeparator(dirtyRect) } - if self.isOpen() { + if self.isOpen { self.drawOuterSeparator(dirtyRect) } } @@ -127,7 +130,7 @@ extension WorkspaceBar { } override func mouseDown(with event: NSEvent) { - guard self.isOpen() else { + guard self.isOpen else { return } @@ -190,7 +193,7 @@ extension WorkspaceBar { } override func resetCursorRects() { - guard self.isOpen() else { + guard self.isOpen else { return } @@ -306,7 +309,7 @@ extension WorkspaceBar { self.dimensionConstraint.constant = saneDimension let toolDimension = self.toolDimension(fromBarDimension: saneDimension) - if self.isOpen() { + if self.isOpen { self.selectedTool?.dimension = toolDimension } @@ -322,7 +325,7 @@ extension WorkspaceBar { return 0 } - if self.isOpen() { + if self.isOpen { return max(dimension, self.selectedTool!.minimumDimension, WorkspaceBar.minimumDimension) } @@ -341,10 +344,6 @@ extension WorkspaceBar { return !self.isEmpty() } - fileprivate func isOpen() -> Bool { - return self.selectedTool != nil - } - fileprivate func layoutWithoutButtons(_ tool: WorkspaceTool) { let view = tool.view let thickness = WorkspaceBar.separatorThickness @@ -531,7 +530,7 @@ extension WorkspaceBar { func toggle(_ tool: WorkspaceTool) { self.delegate?.resizeWillStart(workspaceBar: self) - if self.isOpen() { + if self.isOpen { let curTool = self.selectedTool! if curTool === tool { // In this case, curTool.isSelected is already set to false in WorkspaceTool.toggle() From 56e21c1cbd2a8f5e23e49225adf4fde1d443c286 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Mon, 3 Oct 2016 15:03:18 +0200 Subject: [PATCH 10/11] GH-286 Add some tools menu items --- VimR.xcodeproj/project.pbxproj | 4 +++ VimR/AppKitCommons.swift | 22 +++++++++++-- VimR/Base.lproj/MainMenu.xib | 42 +++++++++++++++++------- VimR/Base.lproj/MainWindow.xib | 4 +-- VimR/Component.swift | 27 +++++++++++++++ VimR/FileBrowserComponent.swift | 33 +++++++++++++++++-- VimR/FileOutlineView.swift | 56 ++++++++++++++++++++++++++++++++ VimR/ImageAndTextTableCell.swift | 35 +++++++++++--------- VimR/MainWindowComponent.swift | 45 +++++++++++++++++++++++-- VimR/PrefStore.swift | 1 - 10 files changed, 231 insertions(+), 38 deletions(-) create mode 100644 VimR/FileOutlineView.swift diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index e964edff..0d07e61a 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 4B6A70961D6100E300E12030 /* SwiftCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A70951D6100E300E12030 /* SwiftCommons.swift */; }; 4B6A70991D65058A00E12030 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B56F29B1D29926600C1F92E /* Nimble.framework */; }; 4B6A709C1D6507A000E12030 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B56F29B1D29926600C1F92E /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4B6B0A781DA2A1A500212D6D /* FileOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6B0A771DA2A1A500212D6D /* FileOutlineView.swift */; }; 4B854A1D1D31447C00E08DE1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B854A1C1D31447C00E08DE1 /* main.m */; }; 4B97E2CC1D33F53D00FC0660 /* MainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B97E2CE1D33F53D00FC0660 /* MainWindow.xib */; }; 4B9A15241D2993DA009F9F67 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 4B56F29B1D29926600C1F92E /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -299,6 +300,7 @@ 4B6423A81D8EFE7500FC78C8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4B6A70931D60E04200E12030 /* AppKitCommons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppKitCommons.swift; sourceTree = ""; }; 4B6A70951D6100E300E12030 /* SwiftCommons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftCommons.swift; sourceTree = ""; }; + 4B6B0A771DA2A1A500212D6D /* FileOutlineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileOutlineView.swift; sourceTree = ""; }; 4B854A1A1D31447C00E08DE1 /* NeoVimServer */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = NeoVimServer; sourceTree = BUILT_PRODUCTS_DIR; }; 4B854A1C1D31447C00E08DE1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4B97E2CD1D33F53D00FC0660 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainWindow.xib; sourceTree = ""; }; @@ -423,6 +425,7 @@ 4B0677351D99D9A2001A2588 /* File Browser */ = { isa = PBXGroup; children = ( + 4B6B0A771DA2A1A500212D6D /* FileOutlineView.swift */, 4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */, ); name = "File Browser"; @@ -1041,6 +1044,7 @@ 1929B53876E6952D378C2B30 /* ScoredFileItem.swift in Sources */, 4B0677371D99D9C3001A2588 /* FileBrowserComponent.swift in Sources */, 1929BD3F9E609BFADB27584B /* Scorer.swift in Sources */, + 4B6B0A781DA2A1A500212D6D /* FileOutlineView.swift in Sources */, 4BAD84E81D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift in Sources */, 1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */, ); diff --git a/VimR/AppKitCommons.swift b/VimR/AppKitCommons.swift index 46108412..cfbc9033 100644 --- a/VimR/AppKitCommons.swift +++ b/VimR/AppKitCommons.swift @@ -71,6 +71,14 @@ extension NSView { func removeAllConstraints() { self.removeConstraints(self.constraints) } + + var isFirstResponder: Bool { + return self.window?.firstResponder == self + } + + func beFirstResponder() { + self.window?.makeFirstResponder(self) + } } extension NSTableView { @@ -104,7 +112,11 @@ extension NSOutlineView { static func standardOutlineView() -> NSOutlineView { let outlineView = NSOutlineView(frame: CGRect.zero) + NSOutlineView.configure(toStandard: outlineView) + return outlineView + } + static func configure(toStandard outlineView: NSOutlineView) { let column = NSTableColumn(identifier: "name") column.isEditable = false @@ -115,8 +127,6 @@ extension NSOutlineView { outlineView.allowsMultipleSelection = false outlineView.headerView = nil outlineView.focusRingType = .none - - return outlineView } /** @@ -130,6 +140,14 @@ extension NSOutlineView { return self.item(atRow: self.selectedRow) } + + func toggle(item: Any) { + if self.isItemExpanded(item) { + self.collapseItem(item) + } else { + self.expandItem(item) + } + } } extension NSScrollView { diff --git a/VimR/Base.lproj/MainMenu.xib b/VimR/Base.lproj/MainMenu.xib index ba85ae5a..b3511858 100644 --- a/VimR/Base.lproj/MainMenu.xib +++ b/VimR/Base.lproj/MainMenu.xib @@ -182,18 +182,6 @@ - - - - - - - - - - - - @@ -203,6 +191,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VimR/Base.lproj/MainWindow.xib b/VimR/Base.lproj/MainWindow.xib index 2de978f0..feab27bd 100644 --- a/VimR/Base.lproj/MainWindow.xib +++ b/VimR/Base.lproj/MainWindow.xib @@ -1,8 +1,8 @@ - + - + diff --git a/VimR/Component.swift b/VimR/Component.swift index b7a1fc07..c3a55bed 100644 --- a/VimR/Component.swift +++ b/VimR/Component.swift @@ -53,6 +53,33 @@ class StandardFlow: PublishingFlow { } } +class EmbeddableComponent: Flow { + + var sink: Observable { + return self.subject.asObservable() + } + + fileprivate let subject = PublishSubject() + fileprivate let source: Observable + fileprivate let disposeBag = DisposeBag() + + init(source: Observable) { + self.source = source + } + + deinit { + self.subject.onCompleted() + } + + func set(subscription: ((Observable) -> Disposable)) { + subscription(source).addDisposableTo(self.disposeBag) + } + + func publish(event: Any) { + self.subject.onNext(event) + } +} + class StandardComponent: NSObject, Flow { var sink: Observable { diff --git a/VimR/FileBrowserComponent.swift b/VimR/FileBrowserComponent.swift index 8f23c1d4..214dcb9b 100644 --- a/VimR/FileBrowserComponent.swift +++ b/VimR/FileBrowserComponent.swift @@ -16,22 +16,47 @@ class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineVie fileprivate var cwd = FileUtils.userHomeUrl fileprivate var cwdFileItem = FileItem(FileUtils.userHomeUrl) - fileprivate let fileView = NSOutlineView.standardOutlineView() fileprivate let dumb = [NSAttributedString(string: "A"), NSAttributedString(string: "B")] + fileprivate let fileView: FileOutlineView fileprivate let fileItemService: FileItemService required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override var isFirstResponder: Bool { + return self.fileView.isFirstResponder + } + init(source: Observable, fileItemService: FileItemService) { self.fileItemService = fileItemService + self.fileView = FileOutlineView(source: source) + super.init(source: source) + self.addReactions() + } + + override func beFirstResponder() { + self.window?.makeFirstResponder(self.fileView) + } + + fileprivate func addReactions() { + self.fileView.sink + .filter { $0 is FileOutlineViewAction } + .map { $0 as! FileOutlineViewAction } + .subscribe(onNext: { [unowned self] action in + switch action { + case let .openFileItem(fileItem): + self.doubleAction(for: fileItem) + } + }) + .addDisposableTo(self.disposeBag) } override func addViews() { let fileView = self.fileView + NSOutlineView.configure(toStandard: fileView) fileView.dataSource = self fileView.delegate = self fileView.doubleAction = #selector(FileBrowserComponent.fileViewDoubleAction) @@ -72,8 +97,12 @@ extension FileBrowserComponent { return } + self.doubleAction(for: item) + } + + fileprivate func doubleAction(for item: FileItem) { if item.dir { - self.fileView.expandItem(item) + self.fileView.toggle(item: item) } else { self.publish(event: FileBrowserAction.open(url: item.url)) } diff --git a/VimR/FileOutlineView.swift b/VimR/FileOutlineView.swift new file mode 100644 index 00000000..97100562 --- /dev/null +++ b/VimR/FileOutlineView.swift @@ -0,0 +1,56 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Cocoa +import RxSwift + +enum FileOutlineViewAction { + + case openFileItem(fileItem: FileItem) +} + +class FileOutlineView: NSOutlineView, Flow { + + fileprivate let flow: EmbeddableComponent + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var sink: Observable { + return self.flow.sink + } + + init(source: Observable) { + self.flow = EmbeddableComponent(source: source) + + super.init(frame: CGRect.zero) + self.translatesAutoresizingMaskIntoConstraints = false + } + + override func keyDown(with event: NSEvent) { + guard let char = event.charactersIgnoringModifiers?.characters.first else { + super.keyDown(with: event) + return + } + + guard let item = self.selectedItem as? FileItem else { + super.keyDown(with: event) + return + } + + switch char { + case " ", "\r": // Why "\r" and not "\n"? + if item.dir || item.package { + self.toggle(item: item) + } else { + self.flow.publish(event: FileOutlineViewAction.openFileItem(fileItem: item)) + } + + default: + super.keyDown(with: event) + } + } +} diff --git a/VimR/ImageAndTextTableCell.swift b/VimR/ImageAndTextTableCell.swift index 8b210bc9..018aa08f 100644 --- a/VimR/ImageAndTextTableCell.swift +++ b/VimR/ImageAndTextTableCell.swift @@ -6,47 +6,50 @@ import Cocoa import PureLayout -class ImageAndTextTableCell: NSView { +class ImageAndTextTableCell: NSTableCellView { + + fileprivate let _textField = NSTextField(forAutoLayout: ()) + fileprivate let _imageView = NSImageView(forAutoLayout: ()) var attributedText: NSAttributedString { get { - return self.textField.attributedStringValue + return self.textField!.attributedStringValue } set { - self.textField.attributedStringValue = newValue + self.textField?.attributedStringValue = newValue } } var text: String { get { - return self.textField.stringValue + return self.textField!.stringValue } set { - self.textField.stringValue = newValue + self.textField?.stringValue = newValue } } var image: NSImage? { get { - return self.imageView.image + return self.imageView?.image } set { - self.imageView.image = newValue + self.imageView?.image = newValue } } - fileprivate let textField: NSTextField = NSTextField(forAutoLayout: ()) - fileprivate let imageView: NSImageView = NSImageView(forAutoLayout: ()) - init(withIdentifier identifier: String) { super.init(frame: CGRect.zero) self.identifier = identifier - - let textField = self.textField + + self.textField = self._textField + self.imageView = self._imageView + + let textField = self._textField textField.isBordered = false textField.isBezeled = false textField.allowsDefaultTighteningForTruncation = true @@ -55,12 +58,12 @@ class ImageAndTextTableCell: NSView { textField.usesSingleLineMode = true textField.lineBreakMode = .byTruncatingTail textField.drawsBackground = false - - let imageView = self.imageView - + + let imageView = self._imageView + self.addSubview(textField) self.addSubview(imageView) - + imageView.autoPinEdge(toSuperviewEdge: .top, withInset: 2) imageView.autoPinEdge(toSuperviewEdge: .left, withInset: 2) imageView.autoSetDimension(.width, toSize: 16) diff --git a/VimR/MainWindowComponent.swift b/VimR/MainWindowComponent.swift index c54ccf92..74382daf 100644 --- a/VimR/MainWindowComponent.swift +++ b/VimR/MainWindowComponent.swift @@ -28,7 +28,7 @@ fileprivate enum Tool { case fileBrowser } -class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate { +class MainWindowComponent: WindowComponent, NSWindowDelegate, NSUserInterfaceValidations, WorkspaceDelegate { fileprivate static let nibName = "MainWindow" @@ -121,7 +121,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate } if mainWindowData.isFileBrowserVisible { - self.workspace.bars[.left]?.toggle(fileBrowserTool) + fileBrowserTool.toggle() } self.window.makeFirstResponder(self.neoVimView) @@ -130,6 +130,7 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate, WorkspaceDelegate func open(urls: [URL]) { self.neoVimView.open(urls: urls) + self.window.makeFirstResponder(self.neoVimView) } func isDirty() -> Bool { @@ -268,16 +269,38 @@ extension MainWindowComponent { } } -// MARK: - View Menu Item Actions +// MARK: - Tools Menu Item Actions extension MainWindowComponent { @IBAction func toggleAllTools(_ sender: Any?) { self.workspace.toggleAllTools() + self.focusNeoVimView(self) } @IBAction func toggleToolButtons(_ sender: Any?) { self.workspace.toggleToolButtons() } + + @IBAction func toggleFileBrowser(_ sender: Any?) { + let fileBrowserTool = self.tools[.fileBrowser]! + + if fileBrowserTool.isSelected { + if fileBrowserTool.viewComponent.isFirstResponder { + fileBrowserTool.toggle() + } else { + fileBrowserTool.viewComponent.beFirstResponder() + } + + return + } + + fileBrowserTool.toggle() + fileBrowserTool.viewComponent.beFirstResponder() + } + + @IBAction func focusNeoVimView(_ sender: Any?) { + self.window.makeFirstResponder(self.neoVimView) + } } // MARK: - Font Menu Item Actions @@ -366,3 +389,19 @@ extension MainWindowComponent { return false } } + +// MARK: - NSUserInterfaceValidationsProtocol +extension MainWindowComponent { + + public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { + guard item.action == #selector(focusNeoVimView(_:)) else { + return true + } + + if self.window.firstResponder == self.neoVimView { + return false + } + + return true + } +} diff --git a/VimR/PrefStore.swift b/VimR/PrefStore.swift index 086ff9d8..6cd47919 100644 --- a/VimR/PrefStore.swift +++ b/VimR/PrefStore.swift @@ -161,7 +161,6 @@ class PrefStore: StandardFlow { self.data = prefData case let mainWindowPrefData as MainWindowPrefData: - NSLog("\(mainWindowPrefData)") self.data.mainWindow = mainWindowPrefData default: From 00f2882e6d1f5956a5edf9faa8d8f8bea1c34d78 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Mon, 3 Oct 2016 19:35:58 +0200 Subject: [PATCH 11/11] GH-286 Comment log --- VimR/FileBrowserComponent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VimR/FileBrowserComponent.swift b/VimR/FileBrowserComponent.swift index 214dcb9b..54d99d99 100644 --- a/VimR/FileBrowserComponent.swift +++ b/VimR/FileBrowserComponent.swift @@ -79,7 +79,7 @@ class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineVie self.cwd = mainWindow.cwd self.cwdFileItem = self.fileItemService.fileItemWithChildren(for: self.cwd) ?? self.fileItemService.fileItemWithChildren(for: FileUtils.userHomeUrl)! - NSLog("cwd changed to \(self.cwd) of \(mainWindow.uuid)") +// NSLog("cwd changed to \(self.cwd) of \(mainWindow.uuid)") self.fileView.reloadData() default: