2016-06-04 00:43:39 +03:00
|
|
|
/**
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
* See LICENSE
|
|
|
|
*/
|
2016-06-03 23:13:59 +03:00
|
|
|
|
|
|
|
import Cocoa
|
2016-07-19 20:34:05 +03:00
|
|
|
import RxSwift
|
2016-07-04 23:06:39 +03:00
|
|
|
import PureLayout
|
2016-10-15 11:05:11 +03:00
|
|
|
import Sparkle
|
2016-06-03 23:13:59 +03:00
|
|
|
|
2016-08-21 01:17:19 +03:00
|
|
|
/// Keep the rawValues in sync with Action in the `vimr` Python script.
|
|
|
|
private enum VimRUrlAction: String {
|
|
|
|
case activate = "activate"
|
|
|
|
case open = "open"
|
2016-08-21 15:02:20 +03:00
|
|
|
case newWindow = "open-in-new-window"
|
|
|
|
case separateWindows = "open-in-separate-windows"
|
2016-08-21 01:17:19 +03:00
|
|
|
}
|
|
|
|
|
2016-06-03 23:13:59 +03:00
|
|
|
@NSApplicationMain
|
2016-07-10 19:47:24 +03:00
|
|
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
2016-06-03 23:13:59 +03:00
|
|
|
|
2017-01-17 21:47:59 +03:00
|
|
|
enum Action {
|
|
|
|
|
|
|
|
case newMainWindow(urls: [URL], cwd: URL)
|
2017-02-04 17:34:13 +03:00
|
|
|
case closeAllMainWindowsWithoutSaving
|
|
|
|
case closeAllMainWindows
|
2017-01-17 21:47:59 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 19:29:42 +03:00
|
|
|
@IBOutlet var debugMenu: NSMenuItem?
|
2016-10-15 11:05:11 +03:00
|
|
|
@IBOutlet var updater: SUUpdater?
|
2016-09-25 19:29:42 +03:00
|
|
|
|
|
|
|
fileprivate static let filePrefix = "file="
|
|
|
|
fileprivate static let cwdPrefix = "cwd="
|
2016-08-09 23:29:46 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate let disposeBag = DisposeBag()
|
2016-07-26 23:51:05 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate let changeSubject = PublishSubject<Any>()
|
|
|
|
fileprivate let changeSink: Observable<Any>
|
2016-07-27 00:40:20 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate let actionSubject = PublishSubject<Any>()
|
|
|
|
fileprivate let actionSink: Observable<Any>
|
2016-07-26 23:51:05 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate let prefStore: PrefStore
|
2016-07-24 21:32:07 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate let mainWindowManager: MainWindowManager
|
|
|
|
fileprivate let openQuicklyWindowManager: OpenQuicklyWindowManager
|
|
|
|
fileprivate let prefWindowComponent: PrefWindowComponent
|
2016-09-03 11:17:22 +03:00
|
|
|
|
2016-10-03 15:58:49 +03:00
|
|
|
fileprivate let fileItemService: FileItemService
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate var quitWhenAllWindowsAreClosed = false
|
|
|
|
fileprivate var launching = true
|
2016-08-14 16:38:41 +03:00
|
|
|
|
2016-07-26 22:42:30 +03:00
|
|
|
override init() {
|
2017-02-06 20:57:50 +03:00
|
|
|
let initialAppState = AppState.default
|
|
|
|
self.stateContext = StateContext(initialAppState)
|
|
|
|
|
2017-01-22 16:22:05 +03:00
|
|
|
let source = self.stateContext.stateSource
|
2017-01-17 21:47:59 +03:00
|
|
|
self.uiRoot = UiRoot(source: source,
|
|
|
|
emitter: self.stateContext.actionEmitter,
|
2017-02-06 20:57:50 +03:00
|
|
|
state: initialAppState) // FIXME
|
2017-01-17 21:47:59 +03:00
|
|
|
|
2016-07-27 00:40:20 +03:00
|
|
|
self.actionSink = self.actionSubject.asObservable()
|
2016-07-26 23:51:05 +03:00
|
|
|
self.changeSink = self.changeSubject.asObservable()
|
2016-10-03 15:58:49 +03:00
|
|
|
let actionAndChangeSink = [self.changeSink, self.actionSink].toMergedObservables()
|
2016-07-26 23:51:05 +03:00
|
|
|
|
|
|
|
self.prefStore = PrefStore(source: self.actionSink)
|
|
|
|
|
2016-10-03 15:58:49 +03:00
|
|
|
self.fileItemService = FileItemService(source: self.changeSink)
|
2016-09-11 15:28:56 +03:00
|
|
|
self.fileItemService.set(ignorePatterns: self.prefStore.data.general.ignorePatterns)
|
2016-09-03 11:17:22 +03:00
|
|
|
|
2016-07-27 00:40:20 +03:00
|
|
|
self.prefWindowComponent = PrefWindowComponent(source: self.changeSink, initialData: self.prefStore.data)
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-09-07 21:12:18 +03:00
|
|
|
self.mainWindowManager = MainWindowManager(source: self.changeSink,
|
|
|
|
fileItemService: self.fileItemService,
|
|
|
|
initialData: self.prefStore.data)
|
2016-10-03 15:58:49 +03:00
|
|
|
self.openQuicklyWindowManager = OpenQuicklyWindowManager(source: actionAndChangeSink,
|
2016-09-03 11:17:22 +03:00
|
|
|
fileItemService: self.fileItemService)
|
2016-07-27 00:40:20 +03:00
|
|
|
|
2016-07-26 22:42:30 +03:00
|
|
|
super.init()
|
2016-09-01 21:10:40 +03:00
|
|
|
|
2017-02-04 17:34:13 +03:00
|
|
|
source
|
|
|
|
.subscribe(onNext: { appState in
|
|
|
|
self.hasMainWindows = !appState.mainWindows.isEmpty
|
|
|
|
self.hasDirtyWindows = appState.mainWindows.values.reduce(false) { $1.isDirty ? true : $0 }
|
|
|
|
|
|
|
|
if self.quitWhenAllWindowsAreClosed && appState.mainWindows.isEmpty {
|
|
|
|
NSApp.stop(self)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.addDisposableTo(self.disposeBag)
|
|
|
|
|
2016-09-03 11:17:22 +03:00
|
|
|
self.mainWindowManager.sink
|
2016-10-03 15:58:49 +03:00
|
|
|
.filter { $0 is MainWindowManagerAction }
|
|
|
|
.map { $0 as! MainWindowManagerAction }
|
2016-09-25 19:10:07 +03:00
|
|
|
.subscribe(onNext: { [unowned self] event in
|
2016-09-01 21:10:40 +03:00
|
|
|
switch event {
|
2016-10-03 15:58:49 +03:00
|
|
|
case .allWindowsClosed:
|
2016-09-01 21:10:40 +03:00
|
|
|
if self.quitWhenAllWindowsAreClosed {
|
|
|
|
NSApp.stop(self)
|
|
|
|
}
|
2016-08-12 19:00:05 +03:00
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
})
|
2016-09-25 19:10:07 +03:00
|
|
|
.addDisposableTo(self.disposeBag)
|
2016-07-26 23:51:05 +03:00
|
|
|
|
2016-10-15 11:05:11 +03:00
|
|
|
self.prefStore.sink
|
|
|
|
.filter { $0 is PrefData }
|
|
|
|
.map { $0 as! PrefData }
|
|
|
|
.subscribe(onNext: { [unowned self] prefData in
|
|
|
|
self.setSparkleUrl()
|
|
|
|
})
|
|
|
|
.addDisposableTo(self.disposeBag)
|
|
|
|
|
|
|
|
self.setSparkleUrl()
|
|
|
|
|
2016-10-04 01:23:54 +03:00
|
|
|
let changeFlows: [Flow] = [ self.prefStore, self.fileItemService ]
|
2016-10-03 15:58:49 +03:00
|
|
|
let actionFlows: [Flow] = [ self.prefWindowComponent, self.mainWindowManager ]
|
|
|
|
|
|
|
|
changeFlows
|
2016-07-26 23:51:05 +03:00
|
|
|
.map { $0.sink }
|
2016-07-27 19:22:25 +03:00
|
|
|
.toMergedObservables()
|
2016-07-26 23:51:05 +03:00
|
|
|
.subscribe(self.changeSubject)
|
|
|
|
.addDisposableTo(self.disposeBag)
|
2016-07-27 00:40:20 +03:00
|
|
|
|
2016-10-03 15:58:49 +03:00
|
|
|
actionFlows
|
2016-07-27 00:40:20 +03:00
|
|
|
.map { $0.sink }
|
2016-07-27 19:22:25 +03:00
|
|
|
.toMergedObservables()
|
2016-07-27 00:40:20 +03:00
|
|
|
.subscribe(self.actionSubject)
|
|
|
|
.addDisposableTo(self.disposeBag)
|
2016-07-26 22:42:30 +03:00
|
|
|
}
|
2016-10-15 11:05:11 +03:00
|
|
|
|
|
|
|
fileprivate func setSparkleUrl() {
|
|
|
|
DispatchUtils.gui {
|
|
|
|
if self.prefStore.data.advanced.useSnapshotUpdateChannel {
|
|
|
|
self.updater?.feedURL = URL(
|
2016-11-05 11:18:28 +03:00
|
|
|
string: "https://raw.githubusercontent.com/qvacua/vimr/develop/appcast_snapshot.xml"
|
2016-10-15 11:05:11 +03:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
self.updater?.feedURL = URL(
|
|
|
|
string: "https://raw.githubusercontent.com/qvacua/vimr/master/appcast.xml"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-01-17 21:47:59 +03:00
|
|
|
|
2017-02-06 20:57:50 +03:00
|
|
|
fileprivate let stateContext: StateContext
|
2017-01-17 21:47:59 +03:00
|
|
|
fileprivate let uiRoot: UiRoot
|
2017-02-04 17:34:13 +03:00
|
|
|
fileprivate var hasDirtyWindows = true
|
|
|
|
fileprivate var hasMainWindows = false
|
2016-08-12 11:00:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - NSApplicationDelegate
|
|
|
|
extension AppDelegate {
|
2016-07-24 23:31:19 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func applicationWillFinishLaunching(_: Notification) {
|
2016-08-14 00:02:00 +03:00
|
|
|
self.launching = true
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
let appleEventManager = NSAppleEventManager.shared()
|
2016-08-25 10:53:51 +03:00
|
|
|
appleEventManager.setEventHandler(self,
|
2016-09-25 19:29:42 +03:00
|
|
|
andSelector: #selector(AppDelegate.handle(getUrlEvent:replyEvent:)),
|
2016-08-25 10:53:51 +03:00
|
|
|
forEventClass: UInt32(kInternetEventClass),
|
|
|
|
andEventID: UInt32(kAEGetURL))
|
2016-08-14 00:02:00 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func applicationDidFinishLaunching(_: Notification) {
|
2016-08-14 00:02:00 +03:00
|
|
|
self.launching = false
|
2016-07-12 00:24:40 +03:00
|
|
|
|
2016-09-08 23:38:18 +03:00
|
|
|
#if DEBUG
|
2016-09-25 19:29:42 +03:00
|
|
|
self.debugMenu?.isHidden = false
|
2016-09-08 23:38:18 +03:00
|
|
|
#endif
|
2016-08-14 00:02:00 +03:00
|
|
|
}
|
2016-08-09 23:29:46 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func applicationOpenUntitledFile(_ sender: NSApplication) -> Bool {
|
2016-08-14 16:38:41 +03:00
|
|
|
if self.launching {
|
|
|
|
if self.prefStore.data.general.openNewWindowWhenLaunching {
|
|
|
|
self.newDocument(self)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if self.prefStore.data.general.openNewWindowOnReactivation {
|
|
|
|
self.newDocument(self)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2016-07-12 00:24:40 +03:00
|
|
|
}
|
2016-07-18 23:44:23 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplicationTerminateReply {
|
2016-09-03 11:17:22 +03:00
|
|
|
if self.mainWindowManager.hasDirtyWindows() {
|
2016-07-18 23:44:23 +03:00
|
|
|
let alert = NSAlert()
|
2016-09-25 18:50:33 +03:00
|
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
|
alert.addButton(withTitle: "Discard and Quit")
|
2016-07-18 23:44:23 +03:00
|
|
|
alert.messageText = "There are windows with unsaved buffers!"
|
2016-09-25 18:50:33 +03:00
|
|
|
alert.alertStyle = .warning
|
2016-07-18 23:44:23 +03:00
|
|
|
|
|
|
|
if alert.runModal() == NSAlertSecondButtonReturn {
|
2016-08-12 19:00:05 +03:00
|
|
|
self.quitWhenAllWindowsAreClosed = true
|
2016-09-03 11:17:22 +03:00
|
|
|
self.mainWindowManager.closeAllWindowsWithoutSaving()
|
2016-07-18 23:44:23 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
return .terminateCancel
|
2016-07-18 23:44:23 +03:00
|
|
|
}
|
|
|
|
|
2016-09-03 11:17:22 +03:00
|
|
|
if self.mainWindowManager.hasMainWindow() {
|
2016-08-20 17:59:30 +03:00
|
|
|
self.quitWhenAllWindowsAreClosed = true
|
2016-09-03 11:17:22 +03:00
|
|
|
self.mainWindowManager.closeAllWindows()
|
2016-08-20 17:59:30 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
return .terminateCancel
|
2016-08-20 17:59:30 +03:00
|
|
|
}
|
|
|
|
|
2017-02-04 17:34:13 +03:00
|
|
|
if self.hasDirtyWindows {
|
|
|
|
let alert = NSAlert()
|
|
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
|
alert.addButton(withTitle: "Discard and Quit")
|
|
|
|
alert.messageText = "There are windows with unsaved buffers!"
|
|
|
|
alert.alertStyle = .warning
|
|
|
|
|
|
|
|
if alert.runModal() == NSAlertSecondButtonReturn {
|
|
|
|
self.quitWhenAllWindowsAreClosed = true
|
|
|
|
self.stateContext.actionEmitter.emit(AppDelegate.Action.closeAllMainWindowsWithoutSaving)
|
|
|
|
}
|
|
|
|
|
|
|
|
return .terminateCancel
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.hasMainWindows {
|
|
|
|
self.quitWhenAllWindowsAreClosed = true
|
|
|
|
self.stateContext.actionEmitter.emit(AppDelegate.Action.closeAllMainWindows)
|
|
|
|
|
|
|
|
return .terminateCancel
|
|
|
|
}
|
|
|
|
|
2016-08-20 17:59:30 +03:00
|
|
|
// There are no open main window, then just quit.
|
2016-09-25 18:50:33 +03:00
|
|
|
return .terminateNow
|
2016-07-18 23:44:23 +03:00
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-08-12 11:00:15 +03:00
|
|
|
// For drag & dropping files on the App icon.
|
2016-09-25 18:50:33 +03:00
|
|
|
func application(_ sender: NSApplication, openFiles filenames: [String]) {
|
|
|
|
let urls = filenames.map { URL(fileURLWithPath: $0) }
|
2016-09-25 19:10:07 +03:00
|
|
|
_ = self.mainWindowManager.newMainWindow(urls: urls)
|
2016-09-25 19:29:42 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
sender.reply(toOpenOrPrint: .success)
|
2016-08-12 11:00:15 +03:00
|
|
|
}
|
2016-06-03 23:13:59 +03:00
|
|
|
}
|
2016-08-09 23:18:46 +03:00
|
|
|
|
2016-08-20 23:35:31 +03:00
|
|
|
// MARK: - AppleScript
|
|
|
|
extension AppDelegate {
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-09-25 19:29:42 +03:00
|
|
|
func handle(getUrlEvent event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) {
|
2016-09-25 18:50:33 +03:00
|
|
|
guard let urlString = event.paramDescriptor(forKeyword: UInt32(keyDirectObject))?.stringValue else {
|
2016-08-21 01:17:19 +03:00
|
|
|
return
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
guard let url = URL(string: urlString) else {
|
2016-08-20 23:35:31 +03:00
|
|
|
return
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-08-21 01:17:19 +03:00
|
|
|
guard url.scheme == "vimr" else {
|
2016-08-20 23:35:31 +03:00
|
|
|
return
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-08-21 01:17:19 +03:00
|
|
|
guard let rawAction = url.host else {
|
|
|
|
return
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-08-21 01:17:19 +03:00
|
|
|
guard let action = VimRUrlAction(rawValue: rawAction) else {
|
|
|
|
return
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
let queryParams = url.query?.components(separatedBy: "&")
|
2016-08-25 00:06:39 +03:00
|
|
|
let urls = queryParams?
|
2016-09-25 19:29:42 +03:00
|
|
|
.filter { $0.hasPrefix(AppDelegate.filePrefix) }
|
|
|
|
.flatMap { $0.without(prefix: AppDelegate.filePrefix).removingPercentEncoding }
|
2016-09-25 18:50:33 +03:00
|
|
|
.map { URL(fileURLWithPath: $0) } ?? []
|
2016-08-25 00:06:39 +03:00
|
|
|
let cwd = queryParams?
|
2016-09-25 19:29:42 +03:00
|
|
|
.filter { $0.hasPrefix(AppDelegate.cwdPrefix) }
|
|
|
|
.flatMap { $0.without(prefix: AppDelegate.cwdPrefix).removingPercentEncoding }
|
2016-09-25 18:50:33 +03:00
|
|
|
.map { URL(fileURLWithPath: $0) }
|
2016-10-02 15:11:39 +03:00
|
|
|
.first ?? FileUtils.userHomeUrl
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-08-21 15:02:20 +03:00
|
|
|
switch action {
|
2016-08-25 00:06:39 +03:00
|
|
|
case .activate, .newWindow:
|
2016-09-25 19:10:07 +03:00
|
|
|
_ = self.mainWindowManager.newMainWindow(urls: urls, cwd: cwd)
|
2016-08-21 15:02:20 +03:00
|
|
|
return
|
2016-08-25 00:06:39 +03:00
|
|
|
case .open:
|
2016-09-03 11:17:22 +03:00
|
|
|
self.mainWindowManager.openInKeyMainWindow(urls: urls, cwd: cwd)
|
2016-08-21 15:02:20 +03:00
|
|
|
return
|
|
|
|
case .separateWindows:
|
2016-09-25 19:10:07 +03:00
|
|
|
urls.forEach { _ = self.mainWindowManager.newMainWindow(urls: [$0], cwd: cwd) }
|
2016-08-21 15:02:20 +03:00
|
|
|
return
|
|
|
|
}
|
2016-08-20 23:35:31 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-09 23:18:46 +03:00
|
|
|
// MARK: - IBActions
|
|
|
|
extension AppDelegate {
|
2016-08-09 23:33:07 +03:00
|
|
|
|
2017-01-17 21:47:59 +03:00
|
|
|
@IBAction func newDocument(_ sender: Any?) {
|
|
|
|
self.stateContext.actionEmitter.emit(Action.newMainWindow(urls: [], cwd: FileUtils.userHomeUrl))
|
|
|
|
}
|
|
|
|
|
2016-10-09 22:25:34 +03:00
|
|
|
@IBAction func openInNewWindow(_ sender: Any?) {
|
|
|
|
self.openDocument(sender)
|
|
|
|
}
|
|
|
|
|
|
|
|
@IBAction func showPrefWindow(_ sender: Any?) {
|
2016-08-09 23:33:07 +03:00
|
|
|
self.prefWindowComponent.show()
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-08-12 16:02:24 +03:00
|
|
|
// Invoked when no main window is open.
|
2016-10-09 22:25:34 +03:00
|
|
|
@IBAction func openDocument(_ sender: Any?) {
|
2016-08-11 23:37:41 +03:00
|
|
|
let panel = NSOpenPanel()
|
|
|
|
panel.canChooseDirectories = true
|
2016-11-05 13:11:03 +03:00
|
|
|
panel.allowsMultipleSelection = true
|
2016-09-25 18:50:33 +03:00
|
|
|
panel.begin { result in
|
2016-08-11 23:37:41 +03:00
|
|
|
guard result == NSFileHandlingPanelOKButton else {
|
|
|
|
return
|
|
|
|
}
|
2016-10-03 15:58:49 +03:00
|
|
|
|
2016-11-05 17:17:43 +03:00
|
|
|
let urls = panel.urls
|
|
|
|
let commonParentUrl = FileUtils.commonParent(of: urls)
|
|
|
|
|
2017-01-17 21:47:59 +03:00
|
|
|
self.stateContext.actionEmitter.emit(Action.newMainWindow(urls: urls, cwd: commonParentUrl))
|
2016-08-11 23:37:41 +03:00
|
|
|
}
|
2016-08-09 23:33:07 +03:00
|
|
|
}
|
2016-08-09 23:18:46 +03:00
|
|
|
}
|