// // This file is part of the LibreOffice project. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // import Foundation import UIKit import QuartzCore /** * Holds the document object so to enforce access in a thread safe way. */ public class DocumentHolder { private let doc: Document public weak var delegate: DocumentUIDelegate? = nil public weak var searchDelegate: SearchDelegate? = nil private let cache = RenderCache() public let documentType: LibreOfficeKitDocumentType public let documentSize: CGSize public let views: Int32 public let parts: Int32 public let partNames: [String] public private(set) var currentPart: Int32 = 0 init(doc: Document) { self.doc = doc // we go and get a bunch of document properties and store them in properties // this allows easy access to these without threading issues // when we get to editing they will have to be invalidated self.documentType = doc.getDocumentType() documentSize = doc.getDocumentSizeAsCGSize() views = doc.getViewsCount() parts = doc.getParts() var partNames = [String]() for i in 0.. ()) { LOKitThread.instance.async { closure(self.doc) } self.invokeHandlers() } public func invokeHandlers() { LOKitThread.instance.async { self.doc.invokeHandlers() } } /// Gives sync access to the document - blocks until the closure runs. /// Careful of deadlocks. public func sync( _ closure: @escaping (Document) -> R ) -> R { return LOKitThread.instance.sync { self.invokeHandlers() return closure(self.doc) } } /// Paints a tile and return synchronously, using a cached version if it can public func paintTileToImage(canvasSize: CGSize, tileRect: CGRect) -> UIImage? { if let cached = cache.get(part: currentPart, canvasSize: canvasSize, tileRect: tileRect) { return cached } let img = sync { $0.paintTileToImage(canvasSize: canvasSize, tileRect: tileRect) } if let image = img { cache.add(cachedRender: CachedRender(part: currentPart, canvasSize: canvasSize, tileRect: tileRect, image: image)) } return img } private func onDocumentEvent(type: LibreOfficeKitCallbackType, payload: String?) { print("onDocumentEvent type:\(type) payload:\(payload ?? "")") switch type { case LOK_CALLBACK_INVALIDATE_TILES: runOnMain { self.delegate?.invalidateTiles( rects: decodeRects(payload) ) } case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR: runOnMain { self.delegate?.invalidateVisibleCursor( rects: decodeRects(payload) ) } case LOK_CALLBACK_TEXT_SELECTION: runOnMain { self.delegate?.textSelection( rects: decodeRects(payload) ) } case LOK_CALLBACK_TEXT_SELECTION_START: runOnMain { self.delegate?.textSelectionStart( rects: decodeRects(payload) ) } case LOK_CALLBACK_TEXT_SELECTION_END: runOnMain { self.delegate?.textSelectionEnd( rects: decodeRects(payload) ) } case LOK_CALLBACK_SEARCH_NOT_FOUND: runOnMain { self.searchDelegate?.searchNotFound() } case LOK_CALLBACK_SEARCH_RESULT_SELECTION: runOnMain { self.searchResults(payload: payload) } case LOK_CALLBACK_SET_PART: if let p = payload, let newPart = Int32(p) { self.currentPart = newPart // TODO: callback? } case LOK_CALLBACK_STATE_CHANGED: // TODO: call backback print("onDocumentEvent type: LOK_CALLBACK_STATE_CHANGED: \(payload ?? "")") default: print("onDocumentEvent type:\(type) not handled!") } } private func searchResults(payload: String?) { if let d = payload, let data = d.data(using: .utf8) { let decoder = JSONDecoder() do { let searchResults = try decoder.decode(SearchResults.self, from: data ) self.searchDelegate?.searchResultSelection(searchResults: searchResults) } catch { print("Error decoding payload: \(error)") } } } public func search(searchString: String, forwardDirection: Bool = true, from: CGPoint) { var rootJson = JSONObject() addProperty(&rootJson, "SearchItem.SearchString", "string", searchString); addProperty(&rootJson, "SearchItem.Backward", "boolean", String(!forwardDirection) ); addProperty(&rootJson, "SearchItem.SearchStartPointX", "long", String(describing: from.x) ); addProperty(&rootJson, "SearchItem.SearchStartPointY", "long", String(describing: from.y) ); addProperty(&rootJson, "SearchItem.Command", "long", "0") // String.valueOf(0)); // search all == 1 if let jsonStr = encode(json: rootJson) { async { $0.postUnoCommand(command: ".uno:ExecuteSearch", arguments: jsonStr, notifyWhenFinished: true) } } } public func incrementPart(amount: Int32) { async { document in let currentPart = document.getPart() let numParts = document.getParts() let newPart = currentPart + amount if (newPart < numParts && newPart > 0) { document.setPart(nPart: newPart) } } } } public typealias JSONObject = Dictionary public func addProperty( _ json: inout JSONObject, _ parentValue: String, _ type: String, _ value: String) { var child = JSONObject(); child["type"] = type as AnyObject child["value"] = value as AnyObject json[parentValue] = child as AnyObject } func encode(json: JSONObject) -> String? { if let data = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) { return String(data: data, encoding: String.Encoding.utf8) } return nil } /// Decodes a series of rectangles in the form: "x, y, width, height; x, y, width, height" public func decodeRects(_ payload: String?) -> [CGRect]? { guard var pl = payload else { return nil } pl = pl.trimmingCharacters(in: .whitespacesAndNewlines ) if pl == "EMPTY" || pl.count == 0 { return nil } var ret = [CGRect]() for rectStr in pl.split(separator: ";") { let coords = rectStr.split(separator: ",").flatMap { Double($0.trimmingCharacters(in: .whitespacesAndNewlines)) } if coords.count == 4 { let rect = CGRect(x: coords[0], y: coords[1], width: coords[2], height: coords[3]) ret.append( rect ) } } return ret } // MARK :- Delegates public protocol DocumentUIDelegate: class { func invalidateTiles(rects: [CGRect]? ) func invalidateVisibleCursor(rects: [CGRect]? ) func textSelection(rects: [CGRect]? ) func textSelectionStart(rects: [CGRect]? ) func textSelectionEnd(rects: [CGRect]? ) } public protocol SearchDelegate: class { func searchNotFound() func searchResultSelection(searchResults: SearchResults) } /** Encodes this example json: { "searchString": "Office", "highlightAll": "true", "searchResultSelection": [ { "part": "0", "rectangles": "1951, 10743, 627, 239" }, { "part": "0", "rectangles": "5343, 9496, 627, 287" }, { "part": "0", "rectangles": "1951, 9256, 627, 239" }, { "part": "0", "rectangles": "6502, 5946, 626, 287" }, { "part": "0", "rectangles": "6686, 5658, 627, 287" }, { "part": "0", "rectangles": "4103, 5418, 573, 239" }, { "part": "0", "rectangles": "1951, 5418, 627, 239" }, { "part": "0", "rectangles": "4934, 1658, 1586, 559" } ] } */ public struct SearchResults: Codable { public var searchString: String? public var highlightAll: String? public var searchResultSelection: Array? } public struct PartAndRectangles: Codable { public var part: String? public var rectangles: String? public var rectsAsCGRects: [CGRect]? { return decodeRects(self.rectangles) } }