// // KeymanWebViewController.swift // KeymanEngine // // Created by Gabriel Wong on 2017-10-31. // Copyright © 2017 SIL International. All rights reserved. // import UIKit import WebKit // MARK: - UIViewController class KeymanWebViewController: UIViewController { weak var delegate: KeymanWebDelegate? var webView: WKWebView! var frame: CGRect? { didSet(frame) { if let view = view, let frame = frame { view.frame = frame } } } override func loadView() { let config = WKWebViewConfiguration() let prefs = WKPreferences() prefs.javaScriptEnabled = true config.preferences = prefs config.suppressesIncrementalRendering = false let userContentController = WKUserContentController() userContentController.add(self, name: "keyman") config.userContentController = userContentController webView = WKWebView(frame: frame ?? .zero, configuration: config) webView.isOpaque = false webView.backgroundColor = UIColor.clear webView.navigationDelegate = self webView.scrollView.isScrollEnabled = false view = webView } override func viewWillAppear(_ animated: Bool) { if let frame = frame { view.frame = frame } super.viewWillAppear(animated) } } // MARK: - JavaScript functions extension KeymanWebViewController { func languageMenuPosition(_ completion: @escaping (CGRect) -> Void) { webView.evaluateJavaScript("langMenuPos();") { result, _ in guard let result = result as? String, !result.isEmpty else { return } let components = result.components(separatedBy: ",") let x = CGFloat(Float(components[0])!) let y = CGFloat(Float(components[1])!) let w = CGFloat(Float(components[2])!) let h = CGFloat(Float(components[3])!) let isPad = UIDevice.current.userInterfaceIdiom == .pad let adjY: CGFloat = isPad ? -0.5 : -1.0 let frame = CGRect(x: x - w / 2.0, y: y - adjY, width: w, height: h) completion(frame) } } // FIXME: text is unused in the JS func executePopupKey(id: String, text: String) { webView.evaluateJavaScript("executePopupKey('\(id)','\(text)');", completionHandler: nil) } func setOskWidth(_ width: Int) { webView.evaluateJavaScript("setOskWidth(\(width));", completionHandler: nil) } func setOskHeight(_ height: Int) { webView.evaluateJavaScript("setOskHeight(\(height));", completionHandler: nil) } func setPopupVisible(_ visible: Bool) { webView.evaluateJavaScript("popupVisible(\(visible));", completionHandler: nil) } func setCursorRange(_ range: NSRange) { if range.location != NSNotFound { webView.evaluateJavaScript("setCursorRange(\(range.location),\(range.length));", completionHandler: nil) } } func setText(_ text: String?) { var text = text ?? "" text = text.replacingOccurrences(of: "\\", with: "\\\\") text = text.replacingOccurrences(of: "'", with: "\\'") text = text.replacingOccurrences(of: "\n", with: "\\n") webView.evaluateJavaScript("setKeymanVal('\(text)');", completionHandler: nil) } func setDeviceType(_ idiom: UIUserInterfaceIdiom) { let type: String switch idiom { case .phone: type = "AppleMobile" case .pad: type = "AppleTablet" default: log.error("Unexpected interface idiom: \(idiom)") return } webView.evaluateJavaScript("setDeviceType('\(type)');", completionHandler: nil) } private func fontObject(from font: Font?, keyboardID: String, isOsk: Bool) -> [String: Any]? { guard let font = font else { return nil } // family does not have to match the name in the font file. It only has to be unique. return [ "family": "\(keyboardID)__\(isOsk ? "osk" : "display")", "files": font.source ] } func setKeyboard(id: String, name: String, languageID: String, languageName: String, fileURL: URL, font: Font?, oskFont: Font?) { var stub: [String: Any] = [ "KI": "Keyboard_\(id)", "KN": name, "KLC": languageID, "KL": languageName, "KF": fileURL.absoluteString ] let displayFont = fontObject(from: font, keyboardID: id, isOsk: false) let oskFont = fontObject(from: oskFont, keyboardID: id, isOsk: true) ?? displayFont if let displayFont = displayFont { stub["KFont"] = displayFont } if let oskFont = oskFont { stub["KOskFont"] = oskFont } let data: Data do { data = try JSONSerialization.data(withJSONObject: stub, options: []) } catch { log.error("Failed to serialize keyboard stub: \(error)") return } guard let stubString = String(data: data, encoding: .utf8) else { log.error("Failed to create stub string") return } webView.evaluateJavaScript("setKeymanLanguage(\(stubString));", completionHandler: nil) } } // MARK: - WKScriptMessageHandler extension KeymanWebViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let fragment = message.body as? String, !fragment.isEmpty else { return } if fragment.hasPrefix("#insertText-") { let dnRange = fragment.range(of: "+dn=")! let sRange = fragment.range(of: "+s=")! let dn = Int(fragment[dnRange.upperBound.. CGRect { let isPad = UIDevice.current.userInterfaceIdiom == .pad let adjY: CGFloat = isPad ? -0.5 : -1.0 return CGRect(x: x - w / 2.0, y: y - adjY, width: w, height: h) } } // MARK: - WKNavigationDelegate extension KeymanWebViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { guard let url = webView.url else { return } guard url.lastPathComponent == Resources.kmwFileName && (url.fragment?.isEmpty ?? true) else { return } keyboardLoaded(self) delegate?.keyboardLoaded(self) } } // MARK: - KeymanWebDelegate extension KeymanWebViewController: KeymanWebDelegate { }