1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-12-01 01:32:04 +03:00

Merge branch 'issue/286-filebrowser'

This commit is contained in:
Tae Won Ha 2016-10-03 19:40:59 +02:00
commit 238e7c1c74
No known key found for this signature in database
GPG Key ID: E40743465B5B8B44
25 changed files with 1004 additions and 282 deletions

View File

@ -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)

View File

@ -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")
}
@ -1232,7 +1254,7 @@ extension NeoVimView: NeoVimUiBridgeProtocol {
public func setTitle(_ title: String) {
DispatchUtils.gui {
self.delegate?.setTitle(title)
self.delegate?.set(title: title)
}
}
@ -1241,7 +1263,7 @@ extension NeoVimView: NeoVimUiBridgeProtocol {
public func setDirtyStatus(_ dirty: Bool) {
DispatchUtils.gui {
self.delegate?.setDirtyStatus(dirty)
self.delegate?.set(dirtyStatus: dirty)
}
}

View File

@ -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()
}

View File

@ -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 */; };
@ -33,6 +32,13 @@
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 */; };
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, ); }; };
@ -79,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, ); }; };
@ -253,6 +260,7 @@
1929BEEB33113B0E33C3830F /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = "<group>"; };
1929BF00B466B40629C2AABE /* NeoVimView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeoVimView.swift; sourceTree = "<group>"; };
4B029F1B1D45E349004EE0D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PrefWindow.xib; sourceTree = "<group>"; };
4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileBrowserComponent.swift; sourceTree = "<group>"; };
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 = "<group>"; };
4B0C905A1D5DED69007753A3 /* NeoVimBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NeoVimBuffer.m; sourceTree = "<group>"; };
@ -292,6 +300,7 @@
4B6423A81D8EFE7500FC78C8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4B6A70931D60E04200E12030 /* AppKitCommons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppKitCommons.swift; sourceTree = "<group>"; };
4B6A70951D6100E300E12030 /* SwiftCommons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftCommons.swift; sourceTree = "<group>"; };
4B6B0A771DA2A1A500212D6D /* FileOutlineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileOutlineView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
4B97E2CD1D33F53D00FC0660 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainWindow.xib; sourceTree = "<group>"; };
@ -413,6 +422,15 @@
path = resources;
sourceTree = "<group>";
};
4B0677351D99D9A2001A2588 /* File Browser */ = {
isa = PBXGroup;
children = (
4B6B0A771DA2A1A500212D6D /* FileOutlineView.swift */,
4B0677361D99D9C3001A2588 /* FileBrowserComponent.swift */,
);
name = "File Browser";
sourceTree = "<group>";
};
4B1AC1AF1D7F395300898F0B /* Open Quickly */ = {
isa = PBXGroup;
children = (
@ -569,6 +587,7 @@
1929B39DA7AC4A9B62D7CD39 /* Component.swift */,
4BD3BF961D32B0DB00082605 /* MainWindowManager.swift */,
4BD3BF921D32A95800082605 /* MainWindowComponent.swift */,
4B0677351D99D9A2001A2588 /* File Browser */,
4B1AC1AF1D7F395300898F0B /* Open Quickly */,
4B238BED1D3ED55300CBDD98 /* Preferences */,
);
@ -1023,7 +1042,9 @@
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 */,
4B6B0A781DA2A1A500212D6D /* FileOutlineView.swift in Sources */,
4BAD84E81D7CA8FC00A79CC3 /* OpenQuicklyFilterOperation.swift in Sources */,
1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */,
);
@ -1033,10 +1054,15 @@
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 */,
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 */,

View File

@ -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) }
@ -207,8 +199,8 @@ 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:
_ = 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)
}
}

View File

@ -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 {
@ -100,6 +108,48 @@ extension NSTableView {
}
}
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
outlineView.addTableColumn(column)
outlineView.outlineTableColumn = column
outlineView.sizeLastColumnToFit()
outlineView.allowsEmptySelection = false
outlineView.allowsMultipleSelection = false
outlineView.headerView = nil
outlineView.focusRingType = .none
}
/**
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)
}
func toggle(item: Any) {
if self.isItemExpanded(item) {
self.collapseItem(item)
} else {
self.expandItem(item)
}
}
}
extension NSScrollView {
static func standardScrollView() -> NSScrollView {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15G31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11201" systemVersion="15G1004" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11201"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -182,10 +182,40 @@
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="NXP-NG-aHV"/>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="izE-hB-PjH">
<menuItem title="Enter Full Screen" keyEquivalent="f" id="Co5-Y0-sqo">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="hl6-CI-dSN"/>
<action selector="toggleFullScreen:" target="-1" id="b4d-vv-b5h"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Tools" id="zhG-4I-P01">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Tools" id="7MO-P5-uU3">
<items>
<menuItem title="Toggle All Tools" keyEquivalent="\" id="Lb6-TZ-LgL">
<connections>
<action selector="toggleAllTools:" target="-1" id="V97-0e-bES"/>
</connections>
</menuItem>
<menuItem title="Toggle Tool Buttons" id="oK0-ZG-w1f">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleToolButtons:" target="-1" id="bxj-O1-Vo9"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="UAq-9D-Jex"/>
<menuItem title="Toggle File Browser" keyEquivalent="1" id="PWx-V8-0cQ">
<connections>
<action selector="toggleFileBrowser:" target="-1" id="Ggq-4w-iN7"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="XHW-5e-Vad"/>
<menuItem title="Focus NeoVim View" keyEquivalent="." id="TtL-Gg-pCj">
<connections>
<action selector="focusNeoVimView:" target="-1" id="zsS-Ax-dPe"/>
</connections>
</menuItem>
</items>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15G1004" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11201" systemVersion="15G1004" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11201"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSWindowController">

View File

@ -6,27 +6,41 @@
import Cocoa
import RxSwift
protocol Flow {
protocol Flow: class {
var sink: Observable<Any> { get }
}
protocol Store: Flow {}
protocol Component: Flow {}
class StandardFlow: Flow {
class PublishingFlow: Flow {
var sink: Observable<Any> {
return self.subject.asObservable()
}
let subject = PublishSubject<Any>()
init() {
}
deinit {
self.subject.onCompleted()
}
func publish(event: Any) {
self.subject.onNext(event)
}
}
class StandardFlow: PublishingFlow {
let source: Observable<Any>
let disposeBag = DisposeBag()
init(source: Observable<Any>) {
self.source = source
super.init()
self.subscription(source: source).addDisposableTo(self.disposeBag)
}
@ -37,13 +51,36 @@ class StandardFlow: Flow {
func subscription(source: Observable<Any>) -> Disposable {
preconditionFailure("Please override")
}
}
class EmbeddableComponent: Flow {
var sink: Observable<Any> {
return self.subject.asObservable()
}
fileprivate let subject = PublishSubject<Any>()
fileprivate let source: Observable<Any>
fileprivate let disposeBag = DisposeBag()
init(source: Observable<Any>) {
self.source = source
}
deinit {
self.subject.onCompleted()
}
func set(subscription: ((Observable<Any>) -> Disposable)) {
subscription(source).addDisposableTo(self.disposeBag)
}
func publish(event: Any) {
self.subject.onNext(event)
}
}
class StandardComponent: NSObject, Component {
class StandardComponent: NSObject, Flow {
var sink: Observable<Any> {
return self.subject.asObservable()
@ -78,6 +115,64 @@ class StandardComponent: NSObject, Component {
}
}
class ViewComponent: NSView, Flow {
var view: NSView {
preconditionFailure("Please override")
}
var sink: Observable<Any> {
return self.subject.asObservable()
}
let subject = PublishSubject<Any>()
let source: Observable<Any>
let disposeBag = DisposeBag()
init(source: Observable<Any>) {
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<Any>) -> Disposable {
preconditionFailure("Please override")
}
func publish(event: Any) {
self.subject.onNext(event)
}
}
class WorkspaceToolComponent: WorkspaceTool, Flow {
let viewComponent: ViewComponent
var sink: Observable<Any> {
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

View File

@ -0,0 +1,186 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Cocoa
import RxSwift
enum FileBrowserAction {
case open(url: URL)
}
class FileBrowserComponent: ViewComponent, NSOutlineViewDataSource, NSOutlineViewDelegate {
fileprivate var cwd = FileUtils.userHomeUrl
fileprivate var cwdFileItem = FileItem(FileUtils.userHomeUrl)
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<Any>, 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)
let scrollView = NSScrollView.standardScrollView()
scrollView.borderType = .noBorder
scrollView.documentView = fileView
self.addSubview(scrollView)
scrollView.autoPinEdgesToSuperviewEdges()
}
override func subscription(source: Observable<Any>) -> 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: FileUtils.userHomeUrl)!
// NSLog("cwd changed to \(self.cwd) of \(mainWindow.uuid)")
self.fileView.reloadData()
default:
break
}
})
}
}
// MARK: - Actions
extension FileBrowserComponent {
func fileViewDoubleAction() {
guard let item = self.fileView.selectedItem as? FileItem else {
return
}
self.doubleAction(for: item)
}
fileprivate func doubleAction(for item: FileItem) {
if item.dir {
self.fileView.toggle(item: item)
} else {
self.publish(event: FileBrowserAction.open(url: item.url))
}
}
}
// 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
}
@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
}
}

View File

@ -5,6 +5,12 @@
import Foundation
class Token: Equatable {}
func == (left: Token, right: Token) -> Bool {
return left === right
}
class FileItem : CustomStringConvertible {
let url: URL

View File

@ -7,19 +7,7 @@ import Cocoa
import RxSwift
import EonilFileSystemEvents
func == (left: Token, right: Token) -> Bool {
return left === right
}
class Token: Equatable {}
class FileItemService {
fileprivate(set) var ignorePatterns: Set<FileItemIgnorePattern> = [] {
didSet {
self.ignoreToken = Token()
}
}
class FileItemService: StandardFlow {
/// Used to cache fnmatch calls in `FileItem`.
fileprivate var ignoreToken = Token()
@ -35,13 +23,22 @@ class FileItemService {
fileprivate let fileSystemEventsLatency = Double(2)
fileprivate var monitors = [URL: FileSystemEventMonitor]()
fileprivate var monitorCounter = [URL: Int]()
fileprivate let workspace = NSWorkspace.shared()
fileprivate let iconsCache = NSCache<NSURL, NSImage>()
fileprivate var spinLock = OS_SPINLOCK_INIT
init() {
// MARK: - API
fileprivate(set) var ignorePatterns: Set<FileItemIgnorePattern> = [] {
didSet {
self.ignoreToken = Token()
}
}
override init(source: Observable<Any>) {
super.init(source: source)
self.iconsCache.countLimit = 2000
self.iconsCache.name = "icon-cache"
}
@ -49,7 +46,7 @@ class FileItemService {
func set(ignorePatterns patterns: Set<FileItemIgnorePattern>) {
self.ignorePatterns = patterns
}
func icon(forUrl url: URL) -> NSImage? {
if let cached = self.iconsCache.object(forKey: url as NSURL) {
return cached
@ -59,7 +56,7 @@ class FileItemService {
let icon = workspace.icon(forFile: path)
icon.size = CGSize(width: 16, height: 16)
self.iconsCache.setObject(icon, forKey: url as NSURL)
return icon
}
@ -74,7 +71,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 +103,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 +123,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 +186,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
@ -241,7 +259,7 @@ class FileItemService {
return filteredChildren.first
}
fileprivate func scanChildren(_ item: FileItem) {
let children = FileUtils.directDescendants(item.url).map(FileItem.init)
self.syncAddChildren { item.children = children }
@ -255,4 +273,17 @@ class FileItemService {
fn()
OSSpinLockUnlock(&self.spinLock)
}
override func subscription(source: Observable<Any>) -> 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)
})
}
}

View File

@ -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<Any> {
return self.flow.sink
}
init(source: Observable<Any>) {
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)
}
}
}

View File

@ -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(

View File

@ -6,47 +6,64 @@
import Cocoa
import PureLayout
class ImageAndTextTableCell: NSView {
class ImageAndTextTableCell: NSTableCellView {
var text: NSAttributedString {
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
}
}
var text: String {
get {
return self.textField!.stringValue
}
set {
self.textField.attributedStringValue = 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
textField.allowsEditingTextAttributes = false
textField.isEditable = false
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)

View File

@ -10,21 +10,45 @@ import RxSwift
enum MainWindowAction {
case becomeKey(mainWindow: MainWindowComponent)
case openQuickly(mainWindow: MainWindowComponent)
case close(mainWindow: MainWindowComponent)
case changeCwd(mainWindow: MainWindowComponent)
case close(mainWindow: MainWindowComponent, mainWindowPrefData: MainWindowPrefData)
}
class MainWindowComponent: WindowComponent, NSWindowDelegate {
struct MainWindowPrefData {
fileprivate let fontManager = NSFontManager.shared()
let isAllToolsVisible: Bool
let isToolButtonsVisible: Bool
let isFileBrowserVisible: Bool
let fileBrowserWidth: Float
}
fileprivate enum Tool {
case fileBrowser
}
class MainWindowComponent: WindowComponent, NSWindowDelegate, NSUserInterfaceValidations, WorkspaceDelegate {
fileprivate static let nibName = "MainWindow"
fileprivate var defaultEditorFont: NSFont
fileprivate var usesLigatures: Bool
fileprivate var _cwd: URL = FileUtils.userHomeUrl
fileprivate let fontManager = NSFontManager.shared()
fileprivate let fileItemService: FileItemService
fileprivate let workspace: Workspace
fileprivate let neoVimView: NeoVimView
fileprivate var tools = [Tool: WorkspaceToolComponent]()
// 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 +67,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<Any>,
@ -66,11 +86,20 @@ 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
self.neoVimView.cwd = cwd
self.workspace.delegate = self
let fileBrowser = FileBrowserComponent(source: self.sink, fileItemService: fileItemService)
let fileBrowserTool = WorkspaceToolComponent(title: "Files", viewComponent: fileBrowser, minimumDimension: 100)
self.tools[.fileBrowser] = fileBrowserTool
self.workspace.append(tool: fileBrowserTool, location: .left)
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
self.neoVimView.usesLigatures = self.usesLigatures
@ -79,12 +108,29 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate {
// 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 {
fileBrowserTool.toggle()
}
self.window.makeFirstResponder(self.neoVimView)
self.show()
}
func open(urls: [URL]) {
self.neoVimView.open(urls: urls)
self.window.makeFirstResponder(self.neoVimView)
}
func isDirty() -> Bool {
@ -99,6 +145,24 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate {
self.neoVimView.closeAllWindowsWithoutSaving()
}
// MARK: - Private
fileprivate func addReactions() {
self.tools.values
.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)
self.workspace.autoPinEdgesToSuperviewEdges()
@ -119,14 +183,26 @@ class MainWindowComponent: WindowComponent, NSWindowDelegate {
}
}
// MARK: - File Menu Items
// MARK: - WorkspaceDelegate
extension MainWindowComponent {
func resizeWillStart(workspace: Workspace) {
self.neoVimView.enterResizeMode()
}
func resizeDidEnd(workspace: Workspace) {
self.neoVimView.exitResizeMode()
}
}
// MARK: - File Menu Item Actions
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 +215,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 +230,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)
@ -193,21 +269,55 @@ extension MainWindowComponent {
}
}
// MARK: - Font Menu Items
// MARK: - Tools Menu Item Actions
extension MainWindowComponent {
@IBAction func resetFontSize(_ sender: AnyObject!) {
@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
extension MainWindowComponent {
@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 +328,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() {
@ -231,6 +341,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() {
@ -247,7 +359,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 {
@ -270,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
}
}

View File

@ -6,13 +6,12 @@
import Cocoa
import RxSwift
enum MainWindowEvent {
enum MainWindowManagerAction {
case allWindowsClosed
}
class MainWindowManager: StandardFlow {
static fileprivate let userHomeUrl = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
fileprivate var mainWindowComponents = [String:MainWindowComponent]()
fileprivate weak var keyMainWindow: MainWindowComponent?
@ -27,10 +26,13 @@ class MainWindowManager: StandardFlow {
super.init(source: source)
}
func newMainWindow(urls: [URL] = [], cwd: URL = MainWindowManager.userHomeUrl) -> MainWindowComponent {
let mainWindowComponent = MainWindowComponent(
source: self.source, fileItemService: self.fileItemService, cwd: cwd, urls: urls, initialData: self.data
)
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)
self.mainWindowComponents[mainWindowComponent.uuid] = mainWindowComponent
mainWindowComponent.sink
@ -44,8 +46,11 @@ 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
}
})
.addDisposableTo(self.disposeBag)
@ -53,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
}
@ -61,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)
}
}
@ -69,7 +78,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

View File

@ -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
@ -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
}
}

View File

@ -23,6 +23,16 @@ class OpenQuicklyWindowManager: StandardFlow {
}
override func subscription(source: Observable<Any>) -> 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
}
})
}
}

View File

@ -6,17 +6,8 @@
import Cocoa
import RxSwift
class PrefPane: NSView, Component {
class PrefPane: ViewComponent {
let disposeBag = DisposeBag()
fileprivate let source: Observable<Any>
fileprivate let subject = PublishSubject<Any>()
var sink: Observable<Any> {
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<Any>) {
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<Any>) -> 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
}

View File

@ -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,23 +25,22 @@ 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"
}
class PrefStore: Store {
// TODO: We should generalize the persisting of pref data.
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<Any>
fileprivate let disposeBag = DisposeBag()
fileprivate let subject = PublishSubject<Any>()
var sink: Observable<Any> {
return self.subject.asObservable()
}
fileprivate let userDefaults = UserDefaults.standard
fileprivate let fontManager = NSFontManager.shared()
@ -42,23 +49,21 @@ class PrefStore: Store {
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)
)
init(source: Observable<Any>) {
self.source = source
override init(source: Observable<Any>) {
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 {
@ -68,15 +73,20 @@ class PrefStore: Store {
(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(
@ -85,10 +95,18 @@ class PrefStore: Store {
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 {
@ -102,38 +120,55 @@ class PrefStore: Store {
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
}
fileprivate func addReactions() {
self.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)
})
.addDisposableTo(self.disposeBag)
override func subscription(source: Observable<Any>) -> Disposable {
return source
.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:
self.data.mainWindow = mainWindowPrefData
default:
return
}
self.userDefaults.setValue(self.prefsDict(self.data), forKey: PrefStore.compatibleVersion)
self.publish(event: self.data)
})
}
}

View File

@ -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

View File

@ -13,19 +13,28 @@ 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
}
fileprivate(set) var isBarVisible = true {
fileprivate(set) var isAllToolsVisible = true {
didSet {
self.relayout()
}
}
fileprivate let bars: [WorkspaceBarLocation: WorkspaceBar]
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")
@ -33,8 +42,11 @@ class Workspace: NSView {
// 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 +61,8 @@ class Workspace: NSView {
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.bars.values.forEach { [unowned self] in $0.delegate = self }
self.relayout()
}
@ -57,11 +71,23 @@ class Workspace: NSView {
}
func toggleAllTools() {
self.isBarVisible = !self.isBarVisible
self.isAllToolsVisible = !self.isAllToolsVisible
}
func toggleToolButtons() {
self.bars.values.forEach { $0.isButtonVisible = !$0.isButtonVisible }
self.isToolButtonsVisible = !self.isToolButtonsVisible
}
}
// MARK: - WorkspaceBarDelegate
extension Workspace {
func resizeWillStart(workspaceBar: WorkspaceBar) {
self.delegate?.resizeWillStart(workspace: self)
}
func resizeDidEnd(workspaceBar: WorkspaceBar) {
self.delegate?.resizeDidEnd(workspace: self)
}
}
@ -80,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
}

View File

@ -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
@ -24,14 +30,21 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate {
}
// MARK: - API
static let minimumDimension = CGFloat(50)
let location: WorkspaceBarLocation
var isButtonVisible = true {
didSet {
self.relayout()
}
}
var isOpen: Bool {
return self.selectedTool != nil
}
var dimensionConstraint = NSLayoutConstraint()
weak var delegate: WorkspaceBarDelegate?
init(location: WorkspaceBarLocation) {
self.location = location
@ -47,38 +60,36 @@ class WorkspaceBar: NSView, WorkspaceToolDelegate {
self.removeAllSubviews()
if self.isEmpty() {
self.set(0)
self.set(dimension: 0)
return
}
if self.isButtonVisible {
self.layoutButtons()
if self.isOpen() {
if self.isOpen {
let curTool = self.selectedTool!
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 {
if self.isOpen() {
if self.isOpen {
let curTool = self.selectedTool!
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) {
@ -86,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
}
@ -105,13 +116,21 @@ extension WorkspaceBar {
self.drawInnerSeparator(dirtyRect)
}
if self.isOpen() {
if self.isOpen {
self.drawOuterSeparator(dirtyRect)
}
}
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 {
guard self.isOpen else {
return
}
@ -128,6 +147,7 @@ extension WorkspaceBar {
}
self.isMouseDownOngoing = true
self.delegate?.resizeWillStart(workspaceBar: self)
self.dimensionConstraint.priority = NSLayoutPriorityDragThatCannotResizeWindow - 1
var dragged = false
@ -138,10 +158,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,19 +182,18 @@ 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() {
guard self.isOpen() else {
guard self.isOpen else {
return
}
@ -284,13 +303,33 @@ extension WorkspaceBar {
}
}
fileprivate func set(_ dimension: CGFloat) {
self.dimensionConstraint.constant = dimension
fileprivate func set(dimension: CGFloat) {
let saneDimension = self.saneDimension(from: dimension)
let toolDimension = self.toolDimension(fromBarDimension: dimension)
if self.isOpen() {
self.dimensionConstraint.constant = saneDimension
let toolDimension = self.toolDimension(fromBarDimension: saneDimension)
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
}
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())
}
}
@ -305,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
@ -493,7 +528,9 @@ extension WorkspaceBar {
extension WorkspaceBar {
func toggle(_ tool: WorkspaceTool) {
if self.isOpen() {
self.delegate?.resizeWillStart(workspaceBar: self)
if self.isOpen {
let curTool = self.selectedTool!
if curTool === tool {
// In this case, curTool.isSelected is already set to false in WorkspaceTool.toggle()
@ -508,5 +545,7 @@ extension WorkspaceBar {
}
self.relayout()
self.delegate?.resizeDidEnd(workspaceBar: self)
}
}

View File

@ -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