2017-01-22 16:22:05 +03:00
|
|
|
/**
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
* See LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
import RxSwift
|
|
|
|
import PureLayout
|
|
|
|
import WebKit
|
|
|
|
import Swifter
|
|
|
|
|
|
|
|
class PreviewTool: NSView, UiComponent, WKNavigationDelegate {
|
|
|
|
|
2017-02-07 00:54:22 +03:00
|
|
|
enum Action {
|
|
|
|
|
|
|
|
case refreshNow
|
2017-02-11 20:32:22 +03:00
|
|
|
case reverseSearch(to: Marked<Position>)
|
2017-02-07 00:54:22 +03:00
|
|
|
|
2017-02-11 20:32:22 +03:00
|
|
|
case scroll(to: Marked<Position>)
|
2017-02-07 00:54:22 +03:00
|
|
|
|
|
|
|
case setAutomaticReverseSearch(to: Bool)
|
|
|
|
case setAutomaticForwardSearch(to: Bool)
|
|
|
|
case setRefreshOnWrite(to: Bool)
|
|
|
|
}
|
2017-01-22 16:22:05 +03:00
|
|
|
|
|
|
|
typealias StateType = MainWindow.State
|
|
|
|
|
2017-02-07 00:54:22 +03:00
|
|
|
let menuItems: [NSMenuItem]
|
|
|
|
|
2017-01-25 00:39:19 +03:00
|
|
|
required init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType) {
|
2017-04-26 20:40:42 +03:00
|
|
|
self.emit = emitter.typedEmit()
|
2017-02-07 00:54:22 +03:00
|
|
|
self.uuid = state.uuid
|
|
|
|
|
|
|
|
let configuration = WKWebViewConfiguration()
|
|
|
|
configuration.userContentController = self.userContentController
|
|
|
|
self.webview = WKWebView(frame: CGRect.zero, configuration: configuration)
|
|
|
|
|
|
|
|
let refreshMenuItem = NSMenuItem(title: "Refresh Now", action: nil, keyEquivalent: "")
|
|
|
|
let forwardSearchMenuItem = NSMenuItem(title: "Forward Search", action: nil, keyEquivalent: "")
|
|
|
|
let reverseSearchMenuItem = NSMenuItem(title: "Reverse Search", action: nil, keyEquivalent: "")
|
|
|
|
|
|
|
|
let automaticForward = self.automaticForwardMenuItem
|
|
|
|
let automaticReverse = self.automaticReverseMenuItem
|
|
|
|
let refreshOnWrite = self.refreshOnWriteMenuItem
|
|
|
|
|
|
|
|
automaticForward.boolState = state.previewTool.isForwardSearchAutomatically
|
|
|
|
automaticReverse.boolState = state.previewTool.isReverseSearchAutomatically
|
|
|
|
refreshOnWrite.boolState = state.previewTool.isRefreshOnWrite
|
|
|
|
|
|
|
|
self.menuItems = [
|
|
|
|
refreshMenuItem,
|
|
|
|
forwardSearchMenuItem,
|
|
|
|
reverseSearchMenuItem,
|
|
|
|
NSMenuItem.separator(),
|
|
|
|
automaticForward,
|
|
|
|
automaticReverse,
|
|
|
|
NSMenuItem.separator(),
|
|
|
|
refreshOnWrite,
|
|
|
|
]
|
2017-01-22 16:22:05 +03:00
|
|
|
|
|
|
|
super.init(frame: .zero)
|
|
|
|
self.configureForAutoLayout()
|
|
|
|
|
2017-02-07 00:54:22 +03:00
|
|
|
refreshMenuItem.target = self
|
|
|
|
refreshMenuItem.action = #selector(PreviewTool.refreshNowAction)
|
|
|
|
forwardSearchMenuItem.target = self
|
|
|
|
forwardSearchMenuItem.action = #selector(PreviewTool.forwardSearchAction)
|
|
|
|
reverseSearchMenuItem.target = self
|
|
|
|
reverseSearchMenuItem.action = #selector(PreviewTool.reverseSearchAction)
|
|
|
|
automaticForward.target = self
|
|
|
|
automaticForward.action = #selector(PreviewTool.automaticForwardSearchAction)
|
|
|
|
automaticReverse.target = self
|
|
|
|
automaticReverse.action = #selector(PreviewTool.automaticReverseSearchAction)
|
|
|
|
refreshOnWrite.target = self
|
|
|
|
refreshOnWrite.action = #selector(PreviewTool.refreshOnWriteAction)
|
2017-01-22 16:22:05 +03:00
|
|
|
|
|
|
|
self.addViews()
|
2017-04-21 08:45:28 +03:00
|
|
|
self.webview.navigationDelegate = self
|
2017-02-28 21:21:22 +03:00
|
|
|
self.webview.load(URLRequest(url: state.preview.server!))
|
2017-02-06 00:39:55 +03:00
|
|
|
|
|
|
|
source
|
2017-02-07 00:54:22 +03:00
|
|
|
.observeOn(MainScheduler.instance)
|
2017-05-15 20:18:51 +03:00
|
|
|
.subscribe(onNext: { state in
|
2017-03-31 20:32:01 +03:00
|
|
|
if state.viewToBeFocused != nil, case .preview = state.viewToBeFocused! {
|
2017-02-25 00:47:32 +03:00
|
|
|
self.beFirstResponder()
|
|
|
|
}
|
|
|
|
|
2017-02-07 00:54:22 +03:00
|
|
|
self.automaticForwardMenuItem.boolState = state.previewTool.isForwardSearchAutomatically
|
|
|
|
self.automaticReverseMenuItem.boolState = state.previewTool.isReverseSearchAutomatically
|
|
|
|
self.refreshOnWriteMenuItem.boolState = state.previewTool.isRefreshOnWrite
|
|
|
|
|
2017-02-09 02:31:49 +03:00
|
|
|
if state.previewTool.isForwardSearchAutomatically
|
2017-02-12 16:07:52 +03:00
|
|
|
&& state.preview.editorPosition.hasDifferentMark(as: self.editorPosition)
|
2017-02-09 02:31:49 +03:00
|
|
|
{
|
|
|
|
self.forwardSearch(position: state.preview.editorPosition.payload)
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
2017-02-09 02:31:49 +03:00
|
|
|
self.editorPosition = state.preview.editorPosition
|
|
|
|
|
2017-02-07 00:54:22 +03:00
|
|
|
guard state.preview.updateDate > self.lastUpdateDate else { return }
|
|
|
|
guard let serverUrl = state.preview.server else { return }
|
2017-04-21 08:45:28 +03:00
|
|
|
if serverUrl != self.url {
|
|
|
|
self.url = serverUrl
|
|
|
|
self.scrollTop = 0
|
|
|
|
self.previewPosition = Marked(Position.beginning)
|
|
|
|
}
|
2017-02-07 00:54:22 +03:00
|
|
|
|
|
|
|
self.lastUpdateDate = state.preview.updateDate
|
|
|
|
self.webview.load(URLRequest(url: serverUrl))
|
2017-05-15 20:18:51 +03:00
|
|
|
}, onCompleted: {
|
2017-02-28 21:21:22 +03:00
|
|
|
// We have to do the following to avoid a crash... Dunno why... -_-
|
|
|
|
self.webviewMessageHandler.subject.onCompleted()
|
|
|
|
self.webview.navigationDelegate = nil
|
|
|
|
self.webview.removeFromSuperview()
|
2017-02-07 00:54:22 +03:00
|
|
|
})
|
2017-04-22 17:30:40 +03:00
|
|
|
.disposed(by: self.disposeBag)
|
2017-02-07 00:54:22 +03:00
|
|
|
|
|
|
|
self.webviewMessageHandler.source
|
|
|
|
.throttle(0.75, latest: true, scheduler: self.scheduler)
|
2017-04-21 08:45:28 +03:00
|
|
|
.subscribe(onNext: { [unowned self] (position, scrollTop) in
|
2017-02-11 20:32:22 +03:00
|
|
|
self.previewPosition = Marked(position)
|
2017-04-21 08:45:28 +03:00
|
|
|
self.scrollTop = scrollTop
|
2017-04-22 16:56:13 +03:00
|
|
|
self.emit(UuidAction(uuid: self.uuid, action: .scroll(to: self.previewPosition)))
|
2017-02-06 00:39:55 +03:00
|
|
|
})
|
2017-04-22 17:30:40 +03:00
|
|
|
.disposed(by: self.disposeBag)
|
2017-01-22 16:22:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate func addViews() {
|
2017-02-07 00:54:22 +03:00
|
|
|
self.webview.navigationDelegate = self
|
|
|
|
self.userContentController.add(webviewMessageHandler, name: "com_vimr_tools_preview_markdown")
|
|
|
|
self.webview.configureForAutoLayout()
|
|
|
|
|
2017-01-22 16:22:05 +03:00
|
|
|
self.addSubview(self.webview)
|
|
|
|
self.webview.autoPinEdgesToSuperviewEdges()
|
|
|
|
}
|
|
|
|
|
|
|
|
func webView(_: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
|
|
NSLog("ERROR preview component's webview: \(error)")
|
|
|
|
}
|
|
|
|
|
2017-04-21 08:45:28 +03:00
|
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
|
self.webview.evaluateJavaScript("document.body.scrollTop = \(self.scrollTop)")
|
|
|
|
}
|
|
|
|
|
2017-04-22 16:56:13 +03:00
|
|
|
fileprivate let emit: (UuidAction<Action>) -> Void
|
2017-02-07 00:54:22 +03:00
|
|
|
fileprivate let uuid: String
|
|
|
|
|
|
|
|
fileprivate let webview: WKWebView
|
2017-02-06 00:39:55 +03:00
|
|
|
fileprivate let disposeBag = DisposeBag()
|
2017-02-07 00:54:22 +03:00
|
|
|
fileprivate let scheduler = ConcurrentDispatchQueueScheduler(qos: .userInitiated)
|
2017-01-22 16:22:05 +03:00
|
|
|
fileprivate var isOpen = false
|
|
|
|
|
2017-04-21 08:45:28 +03:00
|
|
|
fileprivate var url: URL?
|
2017-02-07 00:54:22 +03:00
|
|
|
fileprivate var lastUpdateDate = Date.distantPast
|
2017-02-12 15:41:12 +03:00
|
|
|
fileprivate var editorPosition = Marked(Position.beginning)
|
|
|
|
fileprivate var previewPosition = Marked(Position.beginning)
|
2017-04-21 08:45:28 +03:00
|
|
|
fileprivate var scrollTop = 0
|
2017-02-07 00:54:22 +03:00
|
|
|
|
|
|
|
fileprivate let userContentController = WKUserContentController()
|
|
|
|
fileprivate let webviewMessageHandler = WebviewMessageHandler()
|
|
|
|
|
|
|
|
fileprivate let automaticForwardMenuItem = NSMenuItem(title: "Automatic Forward Search",
|
|
|
|
action: nil,
|
|
|
|
keyEquivalent: "")
|
|
|
|
fileprivate let automaticReverseMenuItem = NSMenuItem(title: "Automatic Reverse Search",
|
|
|
|
action: nil,
|
|
|
|
keyEquivalent: "")
|
|
|
|
fileprivate let refreshOnWriteMenuItem = NSMenuItem(title: "Refresh on Write", action: nil, keyEquivalent: "")
|
|
|
|
|
2017-01-22 16:22:05 +03:00
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
}
|
2017-02-07 00:54:22 +03:00
|
|
|
|
|
|
|
fileprivate func forwardSearch(position: Position) {
|
|
|
|
self.webview.evaluateJavaScript("scrollToPosition(\(position.row), \(position.column));")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Actions
|
|
|
|
extension PreviewTool {
|
|
|
|
|
|
|
|
func refreshNowAction(_: Any?) {
|
2017-04-22 16:56:13 +03:00
|
|
|
self.emit(UuidAction(uuid: self.uuid, action: .refreshNow))
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func forwardSearchAction(_: Any?) {
|
2017-02-09 02:31:49 +03:00
|
|
|
self.forwardSearch(position: self.editorPosition.payload)
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func reverseSearchAction(_: Any?) {
|
2017-02-12 13:24:15 +03:00
|
|
|
self.previewPosition = Marked(self.previewPosition.payload) // set a new mark
|
2017-04-22 16:56:13 +03:00
|
|
|
self.emit(UuidAction(uuid: self.uuid, action: .reverseSearch(to: self.previewPosition)))
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func automaticForwardSearchAction(_ sender: NSMenuItem) {
|
2017-04-22 16:56:13 +03:00
|
|
|
self.emit(UuidAction(uuid: self.uuid, action: .setAutomaticForwardSearch(to: !sender.boolState)))
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func automaticReverseSearchAction(_ sender: NSMenuItem) {
|
2017-04-22 16:56:13 +03:00
|
|
|
self.emit(UuidAction(uuid: self.uuid, action: .setAutomaticReverseSearch(to: !sender.boolState)))
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func refreshOnWriteAction(_ sender: NSMenuItem) {
|
2017-04-22 16:56:13 +03:00
|
|
|
self.emit(UuidAction(uuid: self.uuid, action: .setRefreshOnWrite(to: !sender.boolState)))
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileprivate class WebviewMessageHandler: NSObject, WKScriptMessageHandler {
|
|
|
|
|
2017-04-21 08:45:28 +03:00
|
|
|
var source: Observable<(Position, Int)> {
|
2017-02-07 00:54:22 +03:00
|
|
|
return self.subject.asObservable()
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
self.subject.onCompleted()
|
|
|
|
}
|
|
|
|
|
|
|
|
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
|
|
guard let msgBody = message.body as? [String: Int] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-21 08:45:28 +03:00
|
|
|
guard let lineBegin = msgBody["lineBegin"],
|
|
|
|
let columnBegin = msgBody["columnBegin"],
|
|
|
|
let scrollTop = msgBody["scrollTop"]
|
|
|
|
else {
|
2017-02-07 00:54:22 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-04-21 08:45:28 +03:00
|
|
|
self.subject.onNext((Position(row: lineBegin, column: columnBegin), scrollTop))
|
2017-02-07 00:54:22 +03:00
|
|
|
}
|
|
|
|
|
2017-04-21 08:45:28 +03:00
|
|
|
fileprivate let subject = PublishSubject<(Position, Int)>()
|
2017-01-22 16:22:05 +03:00
|
|
|
}
|