From 7cd3477331ee8d56048ee867a36fd53bed11371d Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Mon, 27 Feb 2017 22:45:26 +0100 Subject: [PATCH] Add general pref pane --- VimR.xcodeproj/project.pbxproj | 40 +++++- VimR/AppDelegate.swift | 8 +- VimR/AppDelegateTransformer.swift | 3 + VimR/Context.swift | 24 ++++ VimR/FileHandler.swift | 2 +- VimR/FileItemIgnorePattern.swift | 36 ++++- VimR/GeneralPref.swift | 228 ++++++++++++++++++++++++++++++ VimR/GeneralPrefTransformer.swift | 34 +++++ VimR/PrefPane.swift | 76 ++++++++++ VimR/PrefWindow.swift | 68 +++++++++ VimR/PrefWindowTransformer.swift | 27 ++++ VimR/States.swift | 7 +- VimR/UiRoot.swift | 25 ++-- 13 files changed, 552 insertions(+), 26 deletions(-) create mode 100644 VimR/GeneralPref.swift create mode 100644 VimR/GeneralPrefTransformer.swift create mode 100644 VimR/PrefPane.swift create mode 100644 VimR/PrefWindowTransformer.swift diff --git a/VimR.xcodeproj/project.pbxproj b/VimR.xcodeproj/project.pbxproj index 48bf4bc7..24ab0e2a 100644 --- a/VimR.xcodeproj/project.pbxproj +++ b/VimR.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 1929B18A0D7C7407C51DB642 /* DataWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB6CFF4CC0B5E8B00C62 /* DataWrapper.m */; }; 1929B1E05C116514C1D3A384 /* CocoaCategories.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929B5C3F2F1CA4113DABFFD /* CocoaCategories.m */; }; 1929B29B95AD176D57942E08 /* UiRootTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B457B9D0FA4D21F3751E /* UiRootTransformer.swift */; }; + 1929B3217A7A3D79E28C80DB /* PrefWindowTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B49E6924847AD085C8C9 /* PrefWindowTransformer.swift */; }; 1929B3AC66EFE35D68C020E3 /* PreviewToolTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BFB0F294F3714D5E095F /* PreviewToolTransformer.swift */; }; 1929B3CEE0C1A1850E9CCE2F /* BasicTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B2FBE11048569391E092 /* BasicTypes.swift */; }; 1929B3F5743967125F357C9F /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BEEB33113B0E33C3830F /* Matcher.swift */; }; @@ -29,6 +30,7 @@ 1929B5F016431A76292D1E84 /* FileMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B365A6434354B568B04F /* FileMonitor.swift */; }; 1929B6388EAF16C190B82955 /* FileItemIgnorePattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */; }; 1929B67DA3EB21A631EF1DBB /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA8AC40B901B20F20B71 /* FileUtils.swift */; }; + 1929B6BE1610892E6C4C0CE6 /* GeneralPrefTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B0EB3F49C42A57D083AF /* GeneralPrefTransformer.swift */; }; 1929B6D8F5FC723B7109031F /* OpenQuicklyTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B12CE56A9B36980288A4 /* OpenQuicklyTransformer.swift */; }; 1929B71381946860626E5224 /* FileBrowserTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BDC8F5D48578A90236E9 /* FileBrowserTransformer.swift */; }; 1929B728262BAA14FC93F6AC /* NeoVimView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BF00B466B40629C2AABE /* NeoVimView.swift */; }; @@ -37,6 +39,7 @@ 1929B902534935D0EADED52E /* PreviewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC589C366CF02CDB4997 /* PreviewUtils.swift */; }; 1929BA120290D6A2A61A4468 /* ArrayCommonsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B477E1E62433BC48E10B /* ArrayCommonsTest.swift */; }; 1929BA715337FE26155B2071 /* OpenedFileList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA43449BA41666CD55ED /* OpenedFileList.swift */; }; + 1929BAE4900D72A7877741B1 /* PrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BE168F31344B69E61B62 /* PrefWindow.swift */; }; 1929BAFF1E011321D3186EE6 /* UiRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD4149D5A25C82064DD8 /* UiRoot.swift */; }; 1929BB4A9B2FA42A64CCCC76 /* MainWindowTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD83A13BF133741766CC /* MainWindowTransformer.swift */; }; 1929BCF444CE7F1D14D421DE /* FileItemTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B4778E20696E3AAFB69B /* FileItemTest.swift */; }; @@ -47,11 +50,12 @@ 1929BD8BFE753FE724958320 /* OpenQuicklyWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1215220464EF4A7AD2B /* OpenQuicklyWindow.swift */; }; 1929BE0DAEE9664C5BCFA211 /* States.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB6608B4F0E037CA0F4C /* States.swift */; }; 1929BE3936E6CF0F80D4183C /* FileBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BFCBD821FC8383B7C3E3 /* FileBrowser.swift */; }; + 1929BEAE0592096BC1191B67 /* PrefPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B07A4A9209C88380E015 /* PrefPane.swift */; }; 1929BEB90DCDAF7A2B68C886 /* ColorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA6128BFDD54CA92F46E /* ColorUtils.swift */; }; + 1929BEDE1BE950EDA9497363 /* GeneralPref.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BB55946DAEBF55D24048 /* GeneralPref.swift */; }; 1929BEFEABA0448306CDB6D4 /* FileItemIgnorePatternTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BBC84557C8351EC6183E /* FileItemIgnorePatternTest.swift */; }; 1929BF3F3841B4BF0B4397ED /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9A55E12B703DDD673FD /* Debouncer.swift */; }; 1929BF81A40B4154D3EA33CE /* server_ui.m in Sources */ = {isa = PBXBuildFile; fileRef = 1929B93013228985F509C8F6 /* server_ui.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; - 1929BFD3CA6AD3CAAFFCC581 /* PrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B587502F00C3664AFEB2 /* PrefWindow.swift */; }; 4B029F1A1D45E349004EE0D3 /* PrefWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B029F1C1D45E349004EE0D3 /* PrefWindow.xib */; }; 4B0BCC941D70320C00D3CE65 /* Logger.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B0BCC931D70320C00D3CE65 /* Logger.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4B183E0E1E06E2940079E8A8 /* CocoaMarkdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B183E0D1E06E2940079E8A8 /* CocoaMarkdown.framework */; }; @@ -287,6 +291,8 @@ /* Begin PBXFileReference section */ 1929B02440BC99C42F9EBD45 /* NetUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetUtils.m; sourceTree = ""; }; 1929B04EC69F616EEFAF5F96 /* FileMonitorTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMonitorTransformer.swift; sourceTree = ""; }; + 1929B07A4A9209C88380E015 /* PrefPane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefPane.swift; sourceTree = ""; }; + 1929B0EB3F49C42A57D083AF /* GeneralPrefTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralPrefTransformer.swift; sourceTree = ""; }; 1929B0EEBE4A765934AF8335 /* DataWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataWrapper.h; sourceTree = ""; }; 1929B1215220464EF4A7AD2B /* OpenQuicklyWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyWindow.swift; sourceTree = ""; }; 1929B12CE56A9B36980288A4 /* OpenQuicklyTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyTransformer.swift; sourceTree = ""; }; @@ -302,7 +308,7 @@ 1929B457B9D0FA4D21F3751E /* UiRootTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiRootTransformer.swift; sourceTree = ""; }; 1929B4778E20696E3AAFB69B /* FileItemTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemTest.swift; sourceTree = ""; }; 1929B477E1E62433BC48E10B /* ArrayCommonsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayCommonsTest.swift; sourceTree = ""; }; - 1929B587502F00C3664AFEB2 /* PrefWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefWindow.swift; sourceTree = ""; }; + 1929B49E6924847AD085C8C9 /* PrefWindowTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefWindowTransformer.swift; sourceTree = ""; }; 1929B5C3F2F1CA4113DABFFD /* CocoaCategories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CocoaCategories.m; sourceTree = ""; }; 1929B5D977261F1EBFA9E8F1 /* FileUtilsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtilsTest.swift; sourceTree = ""; }; 1929B617C229B19DB3E987B8 /* PreviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewService.swift; sourceTree = ""; }; @@ -322,6 +328,7 @@ 1929BA8AC40B901B20F20B71 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 1929BADEB143008EFA6F3318 /* NetUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetUtils.h; sourceTree = ""; }; 1929BB251F74BEFC82CEEF84 /* PrefPaneOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefPaneOld.swift; sourceTree = ""; }; + 1929BB55946DAEBF55D24048 /* GeneralPref.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralPref.swift; sourceTree = ""; }; 1929BB6608B4F0E037CA0F4C /* States.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = States.swift; sourceTree = ""; }; 1929BB6CFF4CC0B5E8B00C62 /* DataWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataWrapper.m; sourceTree = ""; }; 1929BB8BCA48637156F92945 /* PreviewServiceOld.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewServiceOld.swift; sourceTree = ""; }; @@ -333,6 +340,7 @@ 1929BD8CBADC191CF8C85309 /* MainWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = ""; }; 1929BDC8F5D48578A90236E9 /* FileBrowserTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileBrowserTransformer.swift; sourceTree = ""; }; 1929BDF9EBAF1D9D44399045 /* ScoredFileItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScoredFileItem.swift; sourceTree = ""; }; + 1929BE168F31344B69E61B62 /* PrefWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefWindow.swift; sourceTree = ""; }; 1929BE37AA2843779CAFA76F /* PreviewTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewTransformer.swift; sourceTree = ""; }; 1929BE69CF9AB1A10D0DD4F2 /* CocoaCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CocoaCategories.h; sourceTree = ""; }; 1929BEEB33113B0E33C3830F /* Matcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; @@ -541,7 +549,7 @@ 1929B365A6434354B568B04F /* FileMonitor.swift */, 1929BFCBD821FC8383B7C3E3 /* FileBrowser.swift */, 1929BA43449BA41666CD55ED /* OpenedFileList.swift */, - 1929B587502F00C3664AFEB2 /* PrefWindow.swift */, + 1929BC56ADBA3275E7A0A598 /* Preferences */, ); name = Components; sourceTree = ""; @@ -575,6 +583,7 @@ 1929B04EC69F616EEFAF5F96 /* FileMonitorTransformer.swift */, 1929BDC8F5D48578A90236E9 /* FileBrowserTransformer.swift */, 1929B67A10E6BB2986B2416E /* OpenedFileListTransformer.swift */, + 1929BB4CF1C1FFEE6CCDD6FD /* Preferences */, ); name = Transformers; sourceTree = ""; @@ -604,6 +613,25 @@ name = Redesign; sourceTree = ""; }; + 1929BB4CF1C1FFEE6CCDD6FD /* Preferences */ = { + isa = PBXGroup; + children = ( + 1929B49E6924847AD085C8C9 /* PrefWindowTransformer.swift */, + 1929B0EB3F49C42A57D083AF /* GeneralPrefTransformer.swift */, + ); + name = Preferences; + sourceTree = ""; + }; + 1929BC56ADBA3275E7A0A598 /* Preferences */ = { + isa = PBXGroup; + children = ( + 1929BE168F31344B69E61B62 /* PrefWindow.swift */, + 1929B07A4A9209C88380E015 /* PrefPane.swift */, + 1929BB55946DAEBF55D24048 /* GeneralPref.swift */, + ); + name = Preferences; + sourceTree = ""; + }; 1929BFA93DC859DD76C46192 /* Services */ = { isa = PBXGroup; children = ( @@ -1405,7 +1433,11 @@ 1929B71381946860626E5224 /* FileBrowserTransformer.swift in Sources */, 1929BA715337FE26155B2071 /* OpenedFileList.swift in Sources */, 1929B4E54E2F13A7F5F2B682 /* OpenedFileListTransformer.swift in Sources */, - 1929BFD3CA6AD3CAAFFCC581 /* PrefWindow.swift in Sources */, + 1929BAE4900D72A7877741B1 /* PrefWindow.swift in Sources */, + 1929BEAE0592096BC1191B67 /* PrefPane.swift in Sources */, + 1929BEDE1BE950EDA9497363 /* GeneralPref.swift in Sources */, + 1929B3217A7A3D79E28C80DB /* PrefWindowTransformer.swift in Sources */, + 1929B6BE1610892E6C4C0CE6 /* GeneralPrefTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/VimR/AppDelegate.swift b/VimR/AppDelegate.swift index d971d729..ea0c7a28 100644 --- a/VimR/AppDelegate.swift +++ b/VimR/AppDelegate.swift @@ -27,6 +27,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { case newMainWindow(urls: [URL], cwd: URL) case openInKeyWindow(urls: [URL], cwd: URL) + case preferences + case quitWithoutSaving case quit } @@ -52,6 +54,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { .subscribe(onNext: { appState in self.hasMainWindows = !appState.mainWindows.isEmpty self.hasDirtyWindows = appState.mainWindows.values.reduce(false) { $1.isDirty ? true : $0 } + + self.openNewMainWindowOnLaunch = appState.openNewMainWindowOnLaunch + self.openNewMainWindowOnReactivation = appState.openNewMainWindowOnReactivation }) .addDisposableTo(self.disposeBag) @@ -224,8 +229,7 @@ extension AppDelegate { } @IBAction func showPrefWindow(_ sender: Any?) { - // FIXME -// self.prefWindowComponent.show() + self.stateContext.actionEmitter.emit(Action.preferences) } // Invoked when no main window is open. diff --git a/VimR/AppDelegateTransformer.swift b/VimR/AppDelegateTransformer.swift index 6ac06293..07e65a43 100644 --- a/VimR/AppDelegateTransformer.swift +++ b/VimR/AppDelegateTransformer.swift @@ -34,6 +34,9 @@ class AppDelegateTransformer: Transformer { state.mainWindows[uuid]?.urlsToOpen.append(Marked(urls.toDict { url in MainWindow.OpenMode.default })) state.mainWindows[uuid]?.cwd = cwd + case .preferences: + state.preferencesOpen = Marked(true) + case .quitWithoutSaving, .quit: state.mainWindows.removeAll() state.quitWhenNoMainWindow = true diff --git a/VimR/Context.swift b/VimR/Context.swift index 15c64763..4cf808fa 100644 --- a/VimR/Context.swift +++ b/VimR/Context.swift @@ -130,6 +130,27 @@ class StateContext { }) .addDisposableTo(self.disposeBag) + Observable + .of( + actionSource + .mapOmittingNil { $0 as? PrefWindow.Action } + .map { StateActionPair(state: self.appState, action: $0, modified: false) } + .transform(by: self.prefWindowTransformer) + .filter { $0.modified } + .map { $0.state }, + actionSource + .mapOmittingNil { $0 as? GeneralPref.Action } + .map { StateActionPair(state: self.appState, action: $0, modified: false) } + .transform(by: self.generalPrefTransformer) + .filter { $0.modified } + .map { $0.state } + ) + .merge() + .subscribe(onNext: { state in + self.appState = state + self.stateSubject.onNext(self.appState) + }) + .addDisposableTo(self.disposeBag) #if DEBUG // actionSource.debug().subscribe().addDisposableTo(self.disposeBag) @@ -151,6 +172,9 @@ class StateContext { fileprivate let uiRootTransformer = UiRootTransformer() fileprivate let fileMonitorTransformer = FileMonitorTransformer() + fileprivate let prefWindowTransformer = PrefWindowTransformer() + fileprivate let generalPrefTransformer = GeneralPrefTransformer() + fileprivate let mainWindowTransformer = MainWindowTransformer() fileprivate let mainWindowOpenQuicklyTransformer = MainWindowToOpenQuicklyTransformer() diff --git a/VimR/FileHandler.swift b/VimR/FileHandler.swift index 6975988e..aded7e40 100644 --- a/VimR/FileHandler.swift +++ b/VimR/FileHandler.swift @@ -9,7 +9,7 @@ import RxSwift class FileItemUtils { static func flatFileItems(ofUrl url: URL, - ignorePatterns: [FileItemIgnorePattern], + ignorePatterns: Set, ignoreToken: Token, root: FileItem) -> Observable<[FileItem]> { guard url.isFileURL else { diff --git a/VimR/FileItemIgnorePattern.swift b/VimR/FileItemIgnorePattern.swift index 910cb5aa..b8d65dd4 100644 --- a/VimR/FileItemIgnorePattern.swift +++ b/VimR/FileItemIgnorePattern.swift @@ -5,12 +5,40 @@ import Foundation -func ==(lhs: FileItemIgnorePattern, rhs: FileItemIgnorePattern) -> Bool { - return lhs.pattern == rhs.pattern -} +fileprivate let whitespaceCharSet = CharacterSet.whitespaces class FileItemIgnorePattern: Hashable, CustomStringConvertible { - + + static func ==(lhs: FileItemIgnorePattern, rhs: FileItemIgnorePattern) -> Bool { + return lhs.pattern == rhs.pattern + } + + static func from(string str: String) -> Set { + if str.trimmingCharacters(in: whitespaceCharSet).characters.count == 0 { + return Set() + } + + let patterns: [FileItemIgnorePattern] = str + .components(separatedBy: ",") + .flatMap { + let trimmed = $0.trimmingCharacters(in: whitespaceCharSet) + if trimmed.characters.count == 0 { + return nil + } + + return FileItemIgnorePattern(pattern: trimmed) + } + + return Set(patterns) + } + + static func toString(_ set: Set) -> String { + return Array(set) + .map { $0.pattern } + .sorted() + .joined(separator: ", ") + } + var hashValue: Int { return self.pattern.hashValue } diff --git a/VimR/GeneralPref.swift b/VimR/GeneralPref.swift new file mode 100644 index 00000000..05f94024 --- /dev/null +++ b/VimR/GeneralPref.swift @@ -0,0 +1,228 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Cocoa +import PureLayout +import RxSwift + +class GeneralPref: PrefPane, UiComponent, NSTextFieldDelegate { + + typealias StateType = AppState + + enum Action { + + case setOpenOnLaunch(Bool) + case setOpenOnReactivation(Bool) + case setIgnorePatterns(Set) + } + + override var displayName: String { + return "General" + } + + override var pinToContainer: Bool { + return true + } + + override func windowWillClose() { + self.ignorePatternsAction() + } + + required init(source: Observable, emitter: ActionEmitter, state: StateType) { + self.emitter = emitter + + super.init(frame: .zero) + + self.addViews() + + self.openWhenLaunchingCheckbox.boolState = state.openNewMainWindowOnLaunch + self.openOnReactivationCheckbox.boolState = state.openNewMainWindowOnReactivation + self.ignorePatterns = state.openQuickly.ignorePatterns + self.ignoreField.stringValue = FileItemIgnorePattern.toString(state.openQuickly.ignorePatterns) + + source + .observeOn(MainScheduler.instance) + .subscribe(onNext: { state in + if self.openWhenLaunchingCheckbox.boolState != state.openNewMainWindowOnLaunch { + self.openWhenLaunchingCheckbox.boolState = state.openNewMainWindowOnLaunch + } + + if self.openOnReactivationCheckbox.boolState != state.openNewMainWindowOnReactivation { + self.openOnReactivationCheckbox.boolState = state.openNewMainWindowOnReactivation + } + + + }) + .addDisposableTo(self.disposeBag) + } + + fileprivate let emitter: ActionEmitter + fileprivate let disposeBag = DisposeBag() + + fileprivate let openWhenLaunchingCheckbox = NSButton(forAutoLayout: ()) + fileprivate let openOnReactivationCheckbox = NSButton(forAutoLayout: ()) + fileprivate let ignoreField = NSTextField(forAutoLayout: ()) + + fileprivate var ignorePatterns = Set() + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func addViews() { + let paneTitle = self.paneTitleTextField(title: "General") + + let openUntitledWindowTitle = self.titleTextField(title: "Open Untitled Window:") + self.configureCheckbox(button: self.openWhenLaunchingCheckbox, + title: "On launch", + action: #selector(GeneralPref.openUntitledWindowWhenLaunchingAction(_:))) + self.configureCheckbox(button: self.openOnReactivationCheckbox, + title: "On re-activation", + action: #selector(GeneralPref.openUntitledWindowOnReactivationAction(_:))) + + let whenLaunching = self.openWhenLaunchingCheckbox + let onReactivation = self.openOnReactivationCheckbox + + let ignoreListTitle = self.titleTextField(title: "Files To Ignore:") + let ignoreField = self.ignoreField + NotificationCenter.default.addObserver(forName: NSNotification.Name.NSControlTextDidEndEditing, + object: ignoreField, + queue: nil) { [unowned self] _ in + self.ignorePatternsAction() + } + let ignoreInfo = + self.infoTextField(markdown: + "Comma-separated list of ignore patterns \n" + + "Matching files will be ignored in \"Open Quickly\" and the file browser. \n" + + "Example: `*/.git, */node_modules` \n" + + "For detailed information see [VimR Wiki](https://github.com/qvacua/vimr/wiki)." + ) + + let cliToolTitle = self.titleTextField(title: "CLI Tool:") + let cliToolButton = NSButton(forAutoLayout: ()) + cliToolButton.title = "Copy 'vimr' CLI Tool..." + cliToolButton.bezelStyle = .rounded + cliToolButton.isBordered = true + cliToolButton.setButtonType(.momentaryPushIn) + cliToolButton.target = self + cliToolButton.action = #selector(GeneralPref.copyCliTool(_:)) + let cliToolInfo = self.infoTextField( + markdown: "Put the executable `vimr` in your `$PATH` and execute `vimr -h` for help." + ) + + self.addSubview(paneTitle) + self.addSubview(openUntitledWindowTitle) + self.addSubview(whenLaunching) + self.addSubview(onReactivation) + + self.addSubview(ignoreListTitle) + self.addSubview(ignoreField) + self.addSubview(ignoreInfo) + + self.addSubview(cliToolTitle) + self.addSubview(cliToolButton) + self.addSubview(cliToolInfo) + + paneTitle.autoPinEdge(toSuperviewEdge: .top, withInset: 18) + paneTitle.autoPinEdge(toSuperviewEdge: .left, withInset: 18) + paneTitle.autoPinEdge(toSuperviewEdge: .right, withInset: 18, relation: .greaterThanOrEqual) + + openUntitledWindowTitle.autoAlignAxis(.baseline, toSameAxisOf: whenLaunching, withOffset: 0) + openUntitledWindowTitle.autoPinEdge(toSuperviewEdge: .left, withInset: 18) + + whenLaunching.autoPinEdge(.top, to: .bottom, of: paneTitle, withOffset: 18) + whenLaunching.autoPinEdge(.left, to: .right, of: openUntitledWindowTitle, withOffset: 5) + whenLaunching.autoPinEdge(toSuperviewEdge: .right, withInset: 18, relation: .greaterThanOrEqual) + + onReactivation.autoPinEdge(.top, to: .bottom, of: whenLaunching, withOffset: 5) + onReactivation.autoPinEdge(.left, to: .left, of: whenLaunching) + onReactivation.autoPinEdge(toSuperviewEdge: .right, withInset: 18, relation: .greaterThanOrEqual) + + ignoreListTitle.autoAlignAxis(.baseline, toSameAxisOf: ignoreField) + ignoreListTitle.autoPinEdge(.right, to: .right, of: openUntitledWindowTitle) + ignoreListTitle.autoPinEdge(toSuperviewEdge: .left, withInset: 18, relation: .greaterThanOrEqual) + + ignoreField.autoPinEdge(.top, to: .bottom, of: onReactivation, withOffset: 18) + ignoreField.autoPinEdge(toSuperviewEdge: .right, withInset: 18) + ignoreField.autoPinEdge(.left, to: .right, of: ignoreListTitle, withOffset: 5) + + ignoreInfo.autoPinEdge(.top, to: .bottom, of: ignoreField, withOffset: 5) + ignoreInfo.autoPinEdge(toSuperviewEdge: .right, withInset: 18) + ignoreInfo.autoPinEdge(.left, to: .right, of: ignoreListTitle, withOffset: 5) + + cliToolTitle.autoAlignAxis(.baseline, toSameAxisOf: cliToolButton) + cliToolTitle.autoPinEdge(toSuperviewEdge: .left, withInset: 18, relation: .greaterThanOrEqual) + cliToolTitle.autoPinEdge(.right, to: .right, of: openUntitledWindowTitle) + + cliToolButton.autoPinEdge(.top, to: .bottom, of: ignoreInfo, withOffset: 18) + cliToolButton.autoPinEdge(toSuperviewEdge: .right, withInset: 18, relation: .greaterThanOrEqual) + cliToolButton.autoPinEdge(.left, to: .right, of: cliToolTitle, withOffset: 5) + + cliToolInfo.autoPinEdge(.top, to: .bottom, of: cliToolButton, withOffset: 5) + cliToolInfo.autoPinEdge(toSuperviewEdge: .right, withInset: 18, relation: .greaterThanOrEqual) + cliToolInfo.autoPinEdge(.left, to: .right, of: cliToolTitle, withOffset: 5) + } +} + +// MARK: - Actions +extension GeneralPref { + + func copyCliTool(_ sender: NSButton) { + let panel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + + panel.beginSheetModal(for: self.window!) { result in + guard result == NSFileHandlingPanelOKButton else { + return + } + + guard let vimrUrl = Bundle.main.url(forResource: "vimr", withExtension: nil) else { + self.alert(title: "Something Went Wrong.", + info: "The CLI tool 'vimr' could not be found. Please re-download VimR and try again.") + return + } + + guard let targetUrl = panel.url?.appendingPathComponent("vimr") else { + self.alert(title: "Something Went Wrong.", + info: "The target directory could not be determined. Please try again with a different directory.") + return + } + + do { + try FileManager.default.copyItem(at: vimrUrl, to: targetUrl) + } catch let err as NSError { + self.alert(title: "Error copying 'vimr'", info: err.localizedDescription) + } + } + } + + func openUntitledWindowWhenLaunchingAction(_ sender: NSButton) { + self.emitter.emit(Action.setOpenOnLaunch(self.openWhenLaunchingCheckbox.boolState)) + } + + func openUntitledWindowOnReactivationAction(_ sender: NSButton) { + NSLog("\(self.openOnReactivationCheckbox.boolState)") + self.emitter.emit(Action.setOpenOnReactivation(self.openOnReactivationCheckbox.boolState)) + } + + fileprivate func ignorePatternsAction() { + let patterns = FileItemIgnorePattern.from(string: self.ignoreField.stringValue) + if patterns == self.ignorePatterns { + return + } + + self.ignorePatterns = patterns + self.emitter.emit(Action.setIgnorePatterns(ignorePatterns)) + } + + fileprivate func alert(title: String, info: String) { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = title + alert.informativeText = info + alert.runModal() + } +} diff --git a/VimR/GeneralPrefTransformer.swift b/VimR/GeneralPrefTransformer.swift new file mode 100644 index 00000000..ce47d762 --- /dev/null +++ b/VimR/GeneralPrefTransformer.swift @@ -0,0 +1,34 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Foundation +import RxSwift + +class GeneralPrefTransformer: Transformer { + + typealias Pair = StateActionPair + + func transform(_ source: Observable) -> Observable { + return source.map { pair in + var state = pair.state + + switch pair.action { + + case let .setOpenOnLaunch(value): + state.openNewMainWindowOnLaunch = value + + case let .setOpenOnReactivation(value): + state.openNewMainWindowOnReactivation = value + + case let .setIgnorePatterns(patterns): + state.openQuickly.ignorePatterns = patterns + state.openQuickly.ignoreToken = Token() + + } + + return StateActionPair(state: state, action: pair.action) + } + } +} diff --git a/VimR/PrefPane.swift b/VimR/PrefPane.swift new file mode 100644 index 00000000..22d733b0 --- /dev/null +++ b/VimR/PrefPane.swift @@ -0,0 +1,76 @@ +// +// Created by Tae Won Ha on 2/27/17. +// Copyright (c) 2017 Tae Won Ha. All rights reserved. +// + +import Foundation + +class PrefPane: NSView { + + // 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 + } + + var displayName: String { + preconditionFailure("Please override") + } + + var pinToContainer: Bool { + return false + } + + func windowWillClose() { + // noop, override + } +} + +// MARK: - Control Utils +extension PrefPane { + + func paneTitleTextField(title: String) -> NSTextField { + let field = defaultTitleTextField() + field.font = NSFont.boldSystemFont(ofSize: 16) + field.alignment = .left; + field.stringValue = title + return field + } + + func titleTextField(title: String) -> NSTextField { + let field = defaultTitleTextField() + field.alignment = .right; + field.stringValue = title + return field + } + + func infoTextField(markdown: String) -> NSTextField { + let field = NSTextField(forAutoLayout: ()) + field.backgroundColor = NSColor.clear + field.isEditable = false + field.isBordered = false + field.usesSingleLineMode = false + + // both are needed, otherwise hyperlink won't accept mousedown + field.isSelectable = true + field.allowsEditingTextAttributes = true + + field.attributedStringValue = NSAttributedString.infoLabel(markdown: markdown) + + return field + } + + func configureCheckbox(button: NSButton, title: String, action: Selector) { + button.title = title + button.setButtonType(.switch) + button.target = self + button.action = action + } + + fileprivate func defaultTitleTextField() -> NSTextField { + let field = NSTextField(forAutoLayout: ()) + field.backgroundColor = NSColor.clear; + field.isEditable = false; + field.isBordered = false; + return field + } +} diff --git a/VimR/PrefWindow.swift b/VimR/PrefWindow.swift index f829ebe3..6fb5c8f3 100644 --- a/VimR/PrefWindow.swift +++ b/VimR/PrefWindow.swift @@ -14,19 +14,42 @@ class PrefWindow: NSObject, typealias StateType = AppState + enum Action { + + case close + } + required init(source: Observable, emitter: ActionEmitter, state: StateType) { self.emitter = emitter + self.openStatusMark = state.preferencesOpen.mark self.windowController = NSWindowController(windowNibName: "PrefWindow") + self.panes = [ + GeneralPref(source: source, emitter: emitter, state: state) + ] + super.init() + self.window.delegate = self + self.addViews() source .observeOn(MainScheduler.instance) .subscribe(onNext: { state in + if state.preferencesOpen.payload == false { + self.openStatusMark = state.preferencesOpen.mark + self.windowController.close() + return + } + if state.preferencesOpen.mark == self.openStatusMark { + return + } + + self.openStatusMark = state.preferencesOpen.mark + self.windowController.showWindow(self) }) .addDisposableTo(self.disposeBag) } @@ -34,6 +57,8 @@ class PrefWindow: NSObject, fileprivate let emitter: ActionEmitter fileprivate let disposeBag = DisposeBag() + fileprivate var openStatusMark: Token + fileprivate let windowController: NSWindowController fileprivate var window: NSWindow { return self.windowController.window! } @@ -58,10 +83,53 @@ class PrefWindow: NSObject, } fileprivate func addViews() { + let categoryView = self.categoryView + categoryView.dataSource = self + categoryView.delegate = self + let categoryScrollView = self.categoryScrollView + categoryScrollView.documentView = categoryView + + let paneContainer = self.paneContainer + paneContainer.hasVerticalScroller = true + paneContainer.hasHorizontalScroller = true + paneContainer.autohidesScrollers = true + paneContainer.borderType = .noBorder + paneContainer.autoresizesSubviews = false + paneContainer.backgroundColor = NSColor.windowBackgroundColor + + self.window.contentView?.addSubview(categoryScrollView) + self.window.contentView?.addSubview(paneContainer) + + categoryScrollView.autoSetDimension(.width, toSize: 150) + categoryScrollView.autoPinEdge(toSuperviewEdge: .top, withInset: -1) + categoryScrollView.autoPinEdge(toSuperviewEdge: .bottom, withInset: -1) + categoryScrollView.autoPinEdge(toSuperviewEdge: .left, withInset: -1) + + paneContainer.autoSetDimension(.width, toSize: 200, relation: .greaterThanOrEqual) + paneContainer.autoPinEdge(toSuperviewEdge: .top) + paneContainer.autoPinEdge(toSuperviewEdge: .right) + paneContainer.autoPinEdge(toSuperviewEdge: .bottom) + paneContainer.autoPinEdge(.left, to: .right, of: categoryScrollView) + + self.currentPane = self.panes[0] } } +// MARK: - NSWindowDelegate +extension PrefWindow { + + func windowShouldClose(_: Any) -> Bool { + self.emitter.emit(Action.close) + + return false + } + +// func windowWillClose(_: Notification) { +// +// } +} + // MARK: - NSTableViewDataSource extension PrefWindow { diff --git a/VimR/PrefWindowTransformer.swift b/VimR/PrefWindowTransformer.swift new file mode 100644 index 00000000..c0024061 --- /dev/null +++ b/VimR/PrefWindowTransformer.swift @@ -0,0 +1,27 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Foundation +import RxSwift + +class PrefWindowTransformer: Transformer { + + typealias Pair = StateActionPair + + func transform(_ source: Observable) -> Observable { + return source.map { pair in + var state = pair.state + + switch pair.action { + + case .close: + state.preferencesOpen = Marked(false) + + } + + return StateActionPair(state: state, action: pair.action) + } + } +} diff --git a/VimR/States.swift b/VimR/States.swift index 1f40dff5..ba9e4773 100644 --- a/VimR/States.swift +++ b/VimR/States.swift @@ -13,6 +13,8 @@ struct AppState { var openNewMainWindowOnLaunch = true var openNewMainWindowOnReactivation = true + var preferencesOpen = Marked(false) + var mainWindowTemplate: MainWindow.State var currentMainWindowUuid: String? @@ -37,7 +39,7 @@ extension OpenQuicklyWindow { var flatFileItems = Observable<[FileItem]>.empty() var cwd = FileUtils.userHomeUrl - var ignorePatterns = [FileItemIgnorePattern]() + var ignorePatterns = Set(["*/.git", "*.o", "*.d", "*.dia"].map(FileItemIgnorePattern.init)) var ignoreToken = Token() var open = false @@ -73,8 +75,7 @@ struct PreviewState { buffer: URL? = nil, html: URL? = nil, server: URL? = nil, - updateDate: Date = Date()) - { + updateDate: Date = Date()) { self.status = status self.buffer = buffer self.html = html diff --git a/VimR/UiRoot.swift b/VimR/UiRoot.swift index c2ab29f3..470a4622 100644 --- a/VimR/UiRoot.swift +++ b/VimR/UiRoot.swift @@ -15,6 +15,7 @@ class UiRoot: UiComponent { self.emitter = emitter self.fileMonitor = FileMonitor(source: source, emitter: emitter, state: state) + self.prefWindow = PrefWindow(source: source, emitter: emitter, state: state) self.openQuicklyWindow = OpenQuicklyWindow(source: source, emitter: emitter, state: state) source @@ -47,6 +48,18 @@ class UiRoot: UiComponent { .addDisposableTo(self.disposeBag) } + fileprivate let source: Observable + fileprivate let emitter: ActionEmitter + fileprivate let disposeBag = DisposeBag() + + fileprivate let fileMonitor: FileMonitor + fileprivate let prefWindow: PrefWindow + fileprivate let openQuicklyWindow: OpenQuicklyWindow + + fileprivate var mainWindows = [String: MainWindow]() + fileprivate var subjectForMainWindows = [String: PublishSubject]() + fileprivate var disposables = [String: Disposable]() + fileprivate func createNewMainWindow(with state: MainWindow.State) { let subject = PublishSubject() let source = self.source.mapOmittingNil { $0.mainWindows[state.uuid] } @@ -67,16 +80,4 @@ class UiRoot: UiComponent { self.disposables.removeValue(forKey: uuid) self.mainWindows.removeValue(forKey: uuid) } - - fileprivate let source: Observable - fileprivate let emitter: ActionEmitter - fileprivate let disposeBag = DisposeBag() - - fileprivate var mainWindows = [String: MainWindow]() - fileprivate var subjectForMainWindows = [String: PublishSubject]() - fileprivate var disposables = [String: Disposable]() - - fileprivate let fileMonitor: FileMonitor - - fileprivate let openQuicklyWindow: OpenQuicklyWindow }