From dd51c589774a88010d78530fc6d152a9af98f095 Mon Sep 17 00:00:00 2001 From: jan Iversen Date: Thu, 11 Jan 2018 15:36:16 +0100 Subject: iOS, Rendering document. This patch is with thanks to Jon Nermut. With this patch, the iPad renders documents as it should be rendered Change-Id: I54903fde3204b949d8c608842c004cd49a211d9a --- .../LibreOfficeLight.xcodeproj/project.pbxproj | 58 +- .../LibreOfficeLight/AppDelegate.swift | 26 +- .../LibreOfficeLight/DocumentController.swift | 177 ++++++- .../LibreOfficeLight/DocumentTiledView.swift | 229 ++++++++ .../LibreOfficeLight/LOKit/AsyncUtil.swift | 92 ++++ .../LibreOfficeLight/LOKit/Document.swift | 589 +++++++++++++++++++++ .../LibreOfficeLight/LOKit/LOKitThread.swift | 287 ++++++++++ .../LOKit/LibreOfficeKitIOSTests.swift | 102 ++++ .../LOKit/LibreOfficeKitWrapper.swift | 227 ++++++++ .../LibreOfficeLight/LOKit/Util.swift | 43 ++ .../LibreOfficeLight/en.lproj/Main.storyboard | 63 ++- .../LibreOfficeLight/lokit-Bridging-Header.h | 1 + 12 files changed, 1864 insertions(+), 30 deletions(-) create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/DocumentTiledView.swift create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/LOKit/AsyncUtil.swift create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/LOKit/Document.swift create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/LOKit/LOKitThread.swift create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitIOSTests.swift create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitWrapper.swift create mode 100644 ios/LibreOfficeLight/LibreOfficeLight/LOKit/Util.swift (limited to 'ios/LibreOfficeLight') diff --git a/ios/LibreOfficeLight/LibreOfficeLight.xcodeproj/project.pbxproj b/ios/LibreOfficeLight/LibreOfficeLight.xcodeproj/project.pbxproj index 13b0a4675179..650895263ca6 100644 --- a/ios/LibreOfficeLight/LibreOfficeLight.xcodeproj/project.pbxproj +++ b/ios/LibreOfficeLight/LibreOfficeLight.xcodeproj/project.pbxproj @@ -33,6 +33,13 @@ 39B091CE1E5F0BB800682A59 /* unorc in Resources */ = {isa = PBXBuildFile; fileRef = 39B08B9C1E5F0BB600682A59 /* unorc */; }; 39E950531FC9842000D82C49 /* source in Resources */ = {isa = PBXBuildFile; fileRef = 39E950521FC9842000D82C49 /* source */; }; 39EF4E2F1FA500C9001914AC /* PropertiesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39EF4E2E1FA500C9001914AC /* PropertiesController.swift */; }; + FCC2E3FA2004A01500CEB504 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC2E3F62004A01400CEB504 /* Document.swift */; }; + FCC2E3FC2004A01500CEB504 /* LibreOfficeKitWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC2E3F82004A01400CEB504 /* LibreOfficeKitWrapper.swift */; }; + FCC2E3FD2004A01500CEB504 /* LOKitThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC2E3F92004A01400CEB504 /* LOKitThread.swift */; }; + FCC2E3FF2004B59B00CEB504 /* DocumentTiledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC2E3FE2004B59B00CEB504 /* DocumentTiledView.swift */; }; + FCC2E4012004B65E00CEB504 /* example.odt in Resources */ = {isa = PBXBuildFile; fileRef = FCC2E4002004B65E00CEB504 /* example.odt */; }; + FCC2E4032004B72700CEB504 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC2E4022004B72700CEB504 /* Util.swift */; }; + FCC2E4052004B74000CEB504 /* AsyncUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC2E4042004B74000CEB504 /* AsyncUtil.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -68,7 +75,13 @@ 39E950521FC9842000D82C49 /* source */ = {isa = PBXFileReference; lastKnownFileType = folder; name = source; path = ../source; sourceTree = ""; }; 39EE81531FA644E800B73AB8 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39EF4E2E1FA500C9001914AC /* PropertiesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertiesController.swift; sourceTree = ""; }; - 39FF0D4C200681F300A3657D /* LibreOfficeKitInit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LibreOfficeKitInit.h; path = ../../include/LibreOfficeKit/LibreOfficeKitInit.h; sourceTree = ""; }; + FCC2E3F62004A01400CEB504 /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + FCC2E3F82004A01400CEB504 /* LibreOfficeKitWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreOfficeKitWrapper.swift; sourceTree = ""; }; + FCC2E3F92004A01400CEB504 /* LOKitThread.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LOKitThread.swift; sourceTree = ""; }; + FCC2E3FE2004B59B00CEB504 /* DocumentTiledView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentTiledView.swift; sourceTree = ""; }; + FCC2E4002004B65E00CEB504 /* example.odt */ = {isa = PBXFileReference; lastKnownFileType = file; name = example.odt; path = "../../android/default-document/example.odt"; sourceTree = ""; }; + FCC2E4022004B72700CEB504 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + FCC2E4042004B74000CEB504 /* AsyncUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncUtil.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -101,7 +114,6 @@ 3956B72D1FAB3DBF00BF5DE4 /* extra */ = { isa = PBXGroup; children = ( - 39FF0D4C200681F300A3657D /* LibreOfficeKitInit.h */, 39E950521FC9842000D82C49 /* source */, 3975A8C91FBD70EE00A87B3A /* LibreOfficeKit.h */, ); @@ -142,10 +154,12 @@ 397E08FC1E597BD8001374E0 /* LibreOfficeLight */ = { isa = PBXGroup; children = ( + FCC2E3F52004A01400CEB504 /* LOKit */, 39EE81531FA644E800B73AB8 /* Info.plist */, 39503A6F1F94C4AC00F19C78 /* lokit-Bridging-Header.h */, 397E08FD1E597BD8001374E0 /* AppDelegate.swift */, 3992D8591E5B762A00BEA987 /* DocumentController.swift */, + FCC2E3FE2004B59B00CEB504 /* DocumentTiledView.swift */, 39284DB21FA5F207006F43E4 /* DocumentActions.swift */, 39EF4E2E1FA500C9001914AC /* PropertiesController.swift */, 392ED9B21E5E4B03005C8435 /* ViewPrintManager.swift */, @@ -159,6 +173,7 @@ 39B084E41E5F0B5200682A59 /* Resources */ = { isa = PBXGroup; children = ( + FCC2E4002004B65E00CEB504 /* example.odt */, 39022C201EDC2D0800100066 /* icudt60l.dat */, 39022C1E1EDC2AB000100066 /* share */, 39022C1C1EDC2A2C00100066 /* services */, @@ -174,6 +189,18 @@ name = Resources; sourceTree = SOURCE_ROOT; }; + FCC2E3F52004A01400CEB504 /* LOKit */ = { + isa = PBXGroup; + children = ( + FCC2E4042004B74000CEB504 /* AsyncUtil.swift */, + FCC2E3F62004A01400CEB504 /* Document.swift */, + FCC2E3F82004A01400CEB504 /* LibreOfficeKitWrapper.swift */, + FCC2E3F92004A01400CEB504 /* LOKitThread.swift */, + FCC2E4022004B72700CEB504 /* Util.swift */, + ); + path = LOKit; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -257,6 +284,7 @@ 39B08B9F1E5F0BB600682A59 /* oovbaapi.rdb in Resources */, 39B08B9D1E5F0BB600682A59 /* fundamentalrc in Resources */, 39B091CD1E5F0BB800682A59 /* udkapi.rdb in Resources */, + FCC2E4012004B65E00CEB504 /* example.odt in Resources */, 39B08BD91E5F0BB600682A59 /* services.rdb in Resources */, 39B091CE1E5F0BB800682A59 /* unorc in Resources */, 39022C1F1EDC2AB000100066 /* share in Resources */, @@ -271,11 +299,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FCC2E4032004B72700CEB504 /* Util.swift in Sources */, 392ED9B31E5E4B03005C8435 /* ViewPrintManager.swift in Sources */, 399648471E5B87DC00E73E83 /* ViewProperties.swift in Sources */, + FCC2E3FC2004A01500CEB504 /* LibreOfficeKitWrapper.swift in Sources */, 39284DB31FA5F207006F43E4 /* DocumentActions.swift in Sources */, 3992D85A1E5B762A00BEA987 /* DocumentController.swift in Sources */, + FCC2E3FD2004A01500CEB504 /* LOKitThread.swift in Sources */, 397E08FE1E597BD8001374E0 /* AppDelegate.swift in Sources */, + FCC2E3FA2004A01500CEB504 /* Document.swift in Sources */, + FCC2E3FF2004B59B00CEB504 /* DocumentTiledView.swift in Sources */, + FCC2E4052004B74000CEB504 /* AsyncUtil.swift in Sources */, 39EF4E2F1FA500C9001914AC /* PropertiesController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -447,7 +481,10 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "LibreOfficeLight/LibreOfficeLight-Prefix.pch"; GCC_SYMBOLS_PRIVATE_EXTERN = NO; - HEADER_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/../../include/**", + ); INFOPLIST_FILE = LibreOfficeLight/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -459,7 +496,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = 2; + TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 x86_64"; }; name = Debug; @@ -477,6 +514,10 @@ ENABLE_TESTABILITY = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "LibreOfficeLight/LibreOfficeLight-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/../../include/**", + ); INFOPLIST_FILE = LibreOfficeLight/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -488,7 +529,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = 2; + TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 x86_64"; }; name = Release; @@ -575,7 +616,10 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "LibreOfficeLight/LibreOfficeLight-Prefix.pch"; GCC_SYMBOLS_PRIVATE_EXTERN = NO; - HEADER_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/../../include/**", + ); INFOPLIST_FILE = LibreOfficeLight/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -587,7 +631,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = 2; + TARGETED_DEVICE_FAMILY = "1,2"; VALID_ARCHS = "arm64 x86_64"; }; name = Simulator; diff --git a/ios/LibreOfficeLight/LibreOfficeLight/AppDelegate.swift b/ios/LibreOfficeLight/LibreOfficeLight/AppDelegate.swift index 766aa4976a29..1804cd1e3ae3 100644 --- a/ios/LibreOfficeLight/LibreOfficeLight/AppDelegate.swift +++ b/ios/LibreOfficeLight/LibreOfficeLight/AppDelegate.swift @@ -9,7 +9,6 @@ import UIKit import Foundation - // AppDelegate is a Delegate class that receives calls from the iOS // kernel, and theirby allows stop/start/sleep of the application @UIApplicationMain @@ -22,7 +21,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate // sent when clicking on a OO document in another app // allowing this app to handle the document. // remark if the app is not started it will be started first - func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool + func application(_ app: UIApplication, + open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) + -> Bool { let document = window?.rootViewController?.childViewControllers[0] as! DocumentController document.doOpen(url) @@ -33,7 +34,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate // this function is called when the app is first started (loaded from EEProm) // it initializes the LO system and prepares for a normal run - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: + [UIApplicationLaunchOptionsKey: Any]?) + -> Bool { // Get version info let appInfo = Bundle.main.infoDictionary! as Dictionary @@ -46,7 +50,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate defaults.synchronize() // start LibreOfficeKit - BridgeLOkit_Init(Bundle.main.bundlePath) + let _ = LOKitThread.instance + return true } @@ -55,8 +60,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate // Sent when the application is about to move from active to inactive state. // This can occur for certain types of temporary interruptions // (such as an incoming phone call or SMS message) - // or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. + // or when the user quits the application and it begins the transition + // jto the background state. + // Use this method to pause ongoing tasks, disable timers, + // and invalidate graphics rendering callbacks. func applicationWillResignActive(_ application: UIApplication) { // NOT used in this App @@ -66,13 +73,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate // Sent when the application enters background (hipernating) // Use this method to release shared resources, save user data, invalidate timers, - // and store enough application state information to restore your application to its current state - // in case it is terminated later. + // and store enough application state information to restore your application + // to its current state jin case it is terminated later. // If your application supports background execution, // this method is called instead of applicationWillTerminate: when the user quits. func applicationDidEnterBackground(_ application: UIApplication) { - let document = window?.rootViewController?.childViewControllers[0] as! DocumentController + let document = window?.rootViewController?.childViewControllers[0] + as! DocumentController document.Hipernate() } diff --git a/ios/LibreOfficeLight/LibreOfficeLight/DocumentController.swift b/ios/LibreOfficeLight/LibreOfficeLight/DocumentController.swift index 273b0e04ea97..181e707a3da5 100755 --- a/ios/LibreOfficeLight/LibreOfficeLight/DocumentController.swift +++ b/ios/LibreOfficeLight/LibreOfficeLight/DocumentController.swift @@ -8,13 +8,16 @@ import UIKit - // DocumentController is the main viewer in the app, it displays the selected // documents and holds a top entry to view the properties as well as a normal // menu to handle global actions // It is a delegate class to receive Menu events as well as file handling events class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewControllerDelegate { + var document: DocumentHolder? = nil + + var documentView: DocumentTiledView? = nil + // *** Handling of DocumentController // this is normal functions every controller must implement @@ -22,6 +25,10 @@ class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewC // holds known document types var KnownDocumentTypes : [String] = [] + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var mask: UIView! + @IBOutlet weak var progressBar: UIProgressView! + @IBOutlet weak var searchBar: UISearchBar! // called once controller is loaded override func viewDidLoad() @@ -36,9 +43,19 @@ class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewC let x = ((dict["UTTypeTagSpecification"] as! NSDictionary)["public.filename-extension"] as! NSArray) KnownDocumentTypes.append( x[0] as! String ) } + LOKitThread.instance.progressDelegate = self } + override func viewDidAppear(_ animated: Bool) + { + let res = Bundle.main.url(forResource: "example", withExtension: "odt") + //let res = Bundle.main.url(forResource: "example2", withExtension: "docx") + if let exampleDoc = res + { + self.doOpen(exampleDoc) + } + } // called when there is a memory constraint override func didReceiveMemoryWarning() @@ -47,6 +64,14 @@ class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewC // not used in this App } + @IBAction func searchIconTapped(_ sender: Any) + { + searchBar.isHidden = !searchBar.isHidden + if (!searchBar.isHidden) + { + searchBar.becomeFirstResponder() + } + } // *** Handling of Background (hipernate) @@ -60,7 +85,7 @@ class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewC // Moving to hipernate public func Hipernate() -> Void { - BridgeLOkit_Hipernate() + //BridgeLOkit_Hipernate() // FIXME } @@ -68,7 +93,7 @@ class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewC // Moving back to foreground public func LeaveHipernate() -> Void { - BridgeLOkit_LeaveHipernate() + //BridgeLOkit_LeaveHipernate() // FIXME } @@ -318,9 +343,153 @@ class DocumentController: UIViewController, MenuDelegate, UIDocumentBrowserViewC // Real open and presentation of document public func doOpen(_ docURL : URL) { - BridgeLOkit_open(docURL.absoluteString); + LOKitThread.instance.documentLoad(url: docURL.absoluteString) + { + doc, error in + + if let document = doc + { + + runOnMain + { + self.setDocument(doc: document) + } + } + else + { + // TODO - alert user of failure + + } + } + + /* FIXME BridgeLOkit_Sizing(4, 4, 256, 256); + */ + } + + /// Sets the document to use and set's up it's view. Should be called on the main thread + public func setDocument(doc: DocumentHolder) + { + if let existingDoc = self.document + { + // TODO - cleanup + self.document = nil + } + if let exisitingView = self.documentView + { + exisitingView.removeFromSuperview() + self.documentView = nil // forces the close of the view and it's held documents before we setup the new one + } + + // setup the new doc view + self.document = doc + + let frameToUse = self.scrollView.frame + + let docView = DocumentTiledView(frame: frameToUse, document: doc, scale: 1.0) + + self.scrollView.addSubview(docView) + self.scrollView.contentSize = docView.frame.size + self.documentView = docView + + // debugging view borders + /* + self.scrollView.layer.borderColor = UIColor.red.cgColor + self.scrollView.layer.borderWidth = 1.0 + docView.layer.borderColor = UIColor.green.cgColor + docView.layer.borderWidth = 1.0 + */ + } + + // MARK: - UIScrollViewDelegate +} + +extension DocumentController: UIScrollViewDelegate +{ + // return a view that will be scaled. if delegate returns nil, nothing happens + func viewForZooming(in scrollView: UIScrollView) -> UIView? + { + return self.documentView + } + + // called before the scroll view begins zooming its content + func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) + { + print("scrollViewWillBeginZooming currentScale=\(scrollView.zoomScale)") + } + + // scale between minimum and maximum. called after any 'bounce' animations + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) + { + print("scrollViewDidEndZooming scale=\(scale)") + self.documentView?.scrollViewDidEndZooming(scrollView, with: view, atScale: scale) + } +} + + // MARK: - UIKeyInput +// public var hasText: Bool +// { +// true +// } +// +// +// public func insertText(_ text: String) +// { +// +// } +// +// public func deleteBackward() +// { +// +// } + +extension DocumentController: ProgressDelegate +{ + // MARK: - ProgressDelegate + func statusIndicatorStart() + { + self.mask?.isHidden = false + self.progressBar?.isHidden = false + self.progressBar?.progress = 0.0 + } + + func statusIndicatorFinish() + { + // what would be nice would be to be able to wait until the initial tiles have rendered... + self.mask?.isHidden = true + self.progressBar?.isHidden = true + } + + func statusIndicatorSetValue(value: Double) + { + self.progressBar?.progress = Float(value) / 100.0 } } +extension DocumentController: UISearchBarDelegate +{ + // called when text changes (including clear) + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) + { + + } + + + // called when keyboard search button pressed + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) + { + if let text = searchBar.text + { + if text.count > 0 + { + document?.search(searchString: text, forwardDirection: true, from: CGPoint(x:0, y:0) ) + } + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) + { + searchBar.isHidden = true + } +} diff --git a/ios/LibreOfficeLight/LibreOfficeLight/DocumentTiledView.swift b/ios/LibreOfficeLight/LibreOfficeLight/DocumentTiledView.swift new file mode 100644 index 000000000000..b49a8b0eb71f --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/DocumentTiledView.swift @@ -0,0 +1,229 @@ +// +// 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 UIKit +import QuartzCore + + +class DocumentTiledLayer : CATiledLayer +{ + override class func fadeDuration() -> CFTimeInterval + { + return 0 + } +} + +open class CachedRender +{ + open let x: CGFloat + open let y: CGFloat + open let scale: CGFloat + open let image: CGImage + + public init(x: CGFloat, y: CGFloat, scale: CGFloat, image: CGImage) + { + self.x = x + self.y = y + self.scale = scale + self.image = image + } +} + + +class DocumentTiledView: UIView +{ + var myScale: CGFloat + + weak var document: DocumentHolder? = nil + + let initialSize: CGSize + let docSize: CGSize + let initialScaleFactor: CGFloat + + var drawCount = 0 + + let drawLock = NSLock() + + // Create a new view with the desired frame and scale. + public init(frame: CGRect, document: DocumentHolder, scale: CGFloat) + { + + + self.document = document + + + myScale = scale + initialSize = frame.size + var size = document.sync { $0.getDocumentSizeAsCGSize() } + + // avoid divide by zero crashes + if (size.width == 0) + { + size.width = 1 + } + if (size.height == 0) + { + size.height = 1 + } + self.docSize = size + initialScaleFactor = (docSize.width / initialSize.width) + let scaledFrame = CGRect(x: 0, y: 0, width: frame.width, height: frame.width * (docSize.height / docSize.width)) + + print("DocumentTiledView.init frame=\(frame.desc) \n scaledFrame=\(scaledFrame.desc)\n docSize=\(docSize) \n initialScaleFactor=\(initialScaleFactor)") + super.init(frame: scaledFrame) + + //self.contentScaleFactor = 1.0 + + if let tiledLayer = self.layer as? CATiledLayer + { + tiledLayer.levelsOfDetail = 4 + tiledLayer.levelsOfDetailBias = 7 + tiledLayer.tileSize = CGSize(width: 1024.0, height: 1024.0) + //tiledLayer.tileSize = CGSize(width: 512.0, height: 512.0) + } + + } + + required init?(coder aDecoder: NSCoder) + { + fatalError("init(coder:) has not been implemented") + } + + + + override class var layerClass : AnyClass + { + return DocumentTiledLayer.self + } + + + override func draw(_ r: CGRect) + { + // UIView uses the existence of -drawRect: to determine if it should allow its CALayer + // to be invalidated, which would then lead to the layer creating a backing store and + // -drawLayer:inContext: being called. + // By implementing an empty -drawRect: method, we allow UIKit to continue to implement + // this logic, while doing our real drawing work inside of -drawLayer:inContext: + } + + // Draw the CGPDFPageRef into the layer at the correct scale. + override func draw(_ layer: CALayer, in context: CGContext) + { +// if self.superview == nil +// { +// // check that we are still active - ios is doing some really funny things where this method gets called after dealloc which causes bad bad karma +// return +// } + guard let document = self.document else + { + return + } + + guard let tiledLayer = layer as? CATiledLayer else { return } + + + + let tileSize: CGSize = tiledLayer.tileSize + let box: CGRect = context.boundingBoxOfClipPath + let ctm: CGAffineTransform = context.ctm + + drawLock.lock() + defer { drawLock.unlock() } + + drawCount += 1 + let filename = "tile\(drawCount).png" + + print("drawLayer \(filename)\n bounds=\(layer.bounds.desc)\n ctm.a=\(ctm.a)\n tileSize=\(tileSize)\n box=\(box.desc)") + + //context.setFillColor(UIColor.white.cgColor) + context.setFillColor(UIColor.blue.cgColor) + context.fill(box) + context.saveGState() + + context.interpolationQuality = CGInterpolationQuality.high + context.setRenderingIntent(CGColorRenderingIntent.defaultIntent) + + // This is where the magic happens + + let pageRect = box.applying(CGAffineTransform(scaleX: initialScaleFactor, y: initialScaleFactor )) + print(" pageRect: \(pageRect.desc)") + + // Figure out how many pixels we need for the dimensions of our tile + // tileSize represents a "full size" one in pixels + + //let fullSizeTileInPoints = CGSize(width: CGFloat(tileSize.width) / ctm.a, height: CGFloat(tileSize.height) / ctm.a) + //let cropRectTileFraction = CGSize(width: box.size.width / fullSizeTileInPoints.width, height: box.size.height / fullSizeTileInPoints.height) + //let bitmapSize = CGSize(width: tileSize.width * cropRectTileFraction.width, height: tileSize.height * cropRectTileFraction.height) + + let canvasSize = tileSize; //CGSize(width:512, height:512) // FIXME - this needs to be calculated + + // we have to do the call synchronously, as the tile has to be painted now, on the current thread + // TODO - cache the image, and check the cache before we do the sync call + let image = document.sync { + $0.paintTileToImage(canvasSize: canvasSize, tileRect: pageRect) + } + + if let img = image + { + // Debugging: write the file to disk + /* + if let data = UIImagePNGRepresentation(img) + { + let filename = getDocumentsDirectory().appendingPathComponent(filename) + try? data.write(to: filename) + print("Wrote tile to: \(filename)") + } + */ + + // We use the UIImage draw function as it automatically handles the flipping of the co-ordinate system for us. + UIGraphicsPushContext(context); + img.draw(in: box) + UIGraphicsPopContext() + } + + context.restoreGState() + + + } + + + + /* + fileprivate func emptyCache() + { + cachedRenders.removeAll() + } + + fileprivate func pruneCache() + { + let max = hasReceivedMemoryWarning ? CACHE_LOWMEM : CACHE_NORMAL + while cachedRenders.count > max + { + cachedRenders.popFirst() + } + } + */ + + deinit + { + self.document = nil + + } + + + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) + { + //self.setNeedsDisplay() + } + + +// override func pressesBegan +// { +// +// } +} diff --git a/ios/LibreOfficeLight/LibreOfficeLight/LOKit/AsyncUtil.swift b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/AsyncUtil.swift new file mode 100644 index 000000000000..52f8c1bddced --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/AsyncUtil.swift @@ -0,0 +1,92 @@ +// +// 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 + + +public typealias Runnable = () -> () + +/// Runs the closure on a queued background thread +public func runInBackground(_ runnable: @escaping Runnable) +{ + DispatchQueue.global(qos: .background).async(execute: runnable) +} + + +/// Runs the closure on the UI (main) thread. Exceptions are caught and logged +public func runOnMain(_ runnable: @escaping () -> ()) +{ + DispatchQueue.main.async(execute: runnable) +} + +/// Returns true if we are on the Main / UI thread +public func isMainThread() -> Bool +{ + return Thread.isMainThread +} + +/// Runs tasks in a serial way on a single thread. +/// Why wouldn't we just use DispatchQueue or NSOperationQueue to do this? +/// Because neither guarantee running their tasks on the same thread all the time. +/// And in fact DispatchQueue will try and run sync tasks on the current thread where it can. +/// Both classes try and abstract the thread away, whereas we have to use the same thread, or we end up with deadlocks in LOKit +public class SingleThreadedQueue: Thread +{ + public init(name: String) + { + super.init() + self.name = name + self.start() + } + + override public func main() + { + // You need the NSPort here because a runloop with no sources or ports registered with it + // will simply exit immediately instead of running forever. + let keepAlive = Port() + let rl = RunLoop.current + keepAlive.schedule(in: rl, forMode: .commonModes) + + rl.run() + } + + /// Run the task on the serial queue, and return immediately + public func async( _ runnable: @escaping Runnable) + { + let operation = BlockOperation { + runnable() + } + async(operation: operation) + } + + /// Run the task on the serial queue, and return immediately + public func async( operation: Operation) + { + if ( Thread.current == self) + { + operation.start(); + } + else + { + operation.perform(#selector(Operation.start), on: self, with: nil, waitUntilDone: false) + } + } + + public func sync( _ closure: @escaping () -> R ) -> R + { + var ret: R! = nil + let op = BlockOperation { + ret = closure(); + } + async(operation: op) + op.waitUntilFinished() + return ret + } + +} + diff --git a/ios/LibreOfficeLight/LibreOfficeLight/LOKit/Document.swift b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/Document.swift new file mode 100644 index 000000000000..8f54704dc251 --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/Document.swift @@ -0,0 +1,589 @@ +// +// 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 + + +/// The Document class represents one loaded document instance +/// Obtained through LibreOffice.documentLoad() +open class Document +{ + private let pDoc: UnsafeMutablePointer + private let docClass: LibreOfficeKitDocumentClass + + internal init(pDoc: UnsafeMutablePointer) + { + self.pDoc = pDoc + self.docClass = pDoc.pointee.pClass.pointee + } + + /** + * Stores the document's persistent data to a URL and + * continues to be a representation of the old URL. + * + * @param pUrl the location where to store the document + * @param pFormat the format to use while exporting, when omitted, then deducted from pURL's extension + * @param pFilterOptions options for the export filter, e.g. SkipImages. + * Another useful FilterOption is "TakeOwnership". It is consumed + * by the saveAs() itself, and when provided, the document identity + * changes to the provided pUrl - meaning that '.uno:ModifiedStatus' + * is triggered as with the "Save As..." in the UI. + * "TakeOwnership" mode must not be used when saving to PNG or PDF. + */ + public func saveAs(url: String, format: String? = nil, filterOptions: String? = nil) -> Bool + { + return docClass.saveAs(pDoc, url, format, filterOptions) != 0 + } + + /** + * Get document type. + * + * @since LibreOffice 6.0 + * @return an element of the LibreOfficeKitDocumentType enum. + */ + public func getDocumentType() -> LibreOfficeKitDocumentType + { + return LibreOfficeKitDocumentType(rawValue: LibreOfficeKitDocumentType.RawValue(docClass.getDocumentType(pDoc))) + } + + /** + * Get number of part that the document contains. + * + * Part refers to either individual sheets in a Calc, or slides in Impress, + * and has no relevance for Writer. + */ + public func getParts() -> Int32 + { + return docClass.getParts(pDoc); + } + + public func initializeForRendering() + { + docClass.initializeForRendering(pDoc, "") // TODO: arguments?? + } + + /** + * Get the logical rectangle of each part in the document. + * + * A part refers to an individual page in Writer and has no relevant for + * Calc or Impress. + * + * @return a rectangle list, using the same format as + * LOK_CALLBACK_TEXT_SELECTION. + */ + public func getPartRectanges() -> String + { + return toString( docClass.getPartPageRectangles(pDoc) ) ?? "" + + // TODO: convert to CGRects? Comes out like "284, 284, 11906, 16838; 284, 17406, 11906, 16838; 284, 34528, 11906, 16838" + + } + + /// Get the current part of the document. + public func getPart() -> Int32 + { + return docClass.getPart(pDoc); + } + + /// Set the current part of the document. + public func setPart( nPart: Int32 ) + { + docClass.setPart(pDoc, nPart); + } + + /// Get the current part's name. + public func getPartName( nPart: Int32) -> String? + { + return toString( docClass.getPartName(pDoc, nPart) ) + + } + + /// Get the current part's hash. + public func getPartHash( nPart: Int32 ) -> String? + { + return toString( docClass.getPartHash(pDoc, nPart) ) + + } + + public func setPartMode( nMode: Int32 ) + { + docClass.setPartMode( pDoc, nMode); + } + + /** + * Renders a subset of the document to a pre-allocated buffer. + * + * Note that the buffer size and the tile size implicitly supports + * rendering at different zoom levels, as the number of rendered pixels and + * the rendered rectangle of the document are independent. + * + * @param pBuffer pointer to the buffer, its size is determined by nCanvasWidth and nCanvasHeight. + * @param nCanvasWidth number of pixels in a row of pBuffer. + * @param nCanvasHeight number of pixels in a column of pBuffer. + * @param nTilePosX logical X position of the top left corner of the rendered rectangle, in TWIPs. + * @param nTilePosY logical Y position of the top left corner of the rendered rectangle, in TWIPs. + * @param nTileWidth logical width of the rendered rectangle, in TWIPs. + * @param nTileHeight logical height of the rendered rectangle, in TWIPs. + */ + public func paintTile( pBuffer: UnsafeMutablePointer, + canvasWidth: Int32, + canvasHeight: Int32, + tilePosX: Int32, + tilePosY: Int32, + tileWidth: Int32, + tileHeight: Int32) + { + print("paintTile canvasWidth=\(canvasWidth) canvasHeight=\(canvasHeight) tilePosX=\(tilePosX) tilePosY=\(tilePosY) tileWidth=\(tileWidth) tileHeight=\(tileHeight) ") + return docClass.paintTile(pDoc, pBuffer, canvasWidth, canvasHeight, + tilePosX, tilePosY, tileWidth, tileHeight); + } + + /** + * Renders a window (dialog, popup, etc.) with give id + * + * @param nWindowId + * @param pBuffer Buffer with enough memory allocated to render any dialog + * @param x x-coordinate from where the dialog should start painting + * @param y y-coordinate from where the dialog should start painting + * @param width The width of the dialog image to be painted + * @param height The height of the dialog image to be painted + */ + public func paintWindow( nWindowId: UInt32, + pBuffer: UnsafeMutablePointer, + x: Int32, + y: Int32, + width: Int32, + height: Int32) + { + return docClass.paintWindow(pDoc, nWindowId, pBuffer, x, y, width, height); + } + + /** + * Posts a command to the window (dialog, popup, etc.) with given id + * + * @param nWindowid + */ + public func postWindow( nWindowId: UInt32, nAction: Int32) + { + return docClass.postWindow(pDoc, nWindowId, nAction); + } + + /** + * Gets the tile mode: the pixel format used for the pBuffer of paintTile(). + * + * @return an element of the LibreOfficeKitTileMode enum. + */ + public func getTileMode() -> LibreOfficeKitTileMode + { + return LibreOfficeKitTileMode(rawValue: LibreOfficeKitTileMode.RawValue(docClass.getTileMode(pDoc))); + } + + /// Get the document sizes in TWIPs. + public func getDocumentSize() -> (Int, Int) + { + print(Thread.isMainThread) + // long* pWidth, long* pHeight + var pWidth: Int = 0 + var pHeight: Int = 0 + docClass.getDocumentSize(pDoc, &pWidth, &pHeight); + return (pWidth, pHeight) + } + + /** + * Initialize document for rendering. + * + * Sets the rendering and document parameters to default values that are + * needed to render the document correctly using tiled rendering. This + * method has to be called right after documentLoad() in case any of the + * tiled rendering methods are to be used later. + * + * Example argument string for text documents: + * + * { + * ".uno:HideWhitespace": + * { + * "type": "boolean", + * "value": "true" + * } + * } + * + * @param pArguments arguments of the rendering + */ + public func initializeForRendering(arguments: String? = nil) + { + docClass.initializeForRendering(pDoc, arguments); + } + + /** + * Registers a callback. LOK will invoke this function when it wants to + * inform the client about events. + * + * @param pCallback the callback to invoke + * @param pData the user data, will be passed to the callback on invocation + */ + public func registerCallback( callback: @escaping LibreOfficeCallback ) -> Int + { + let ret = Callbacks.register(callback: callback) + let pointer = UnsafeMutableRawPointer(bitPattern: ret) + docClass.registerCallback(pDoc, callbackFromLibreOffice, pointer) + return ret + } + + /** + * Posts a keyboard event to the focused frame. + * + * @param nType Event type, like press or release. + * @param nCharCode contains the Unicode character generated by this event or 0 + * @param nKeyCode contains the integer code representing the key of the event (non-zero for control keys) + */ + public func postKeyEvent(nType: Int32, nCharCode: Int32, nKeyCode: Int32) + { + docClass.postKeyEvent(pDoc, nType, nCharCode, nKeyCode); + } + + /** + * Posts a keyboard event to the dialog + * + * @param nWindowId + * @param nType Event type, like press or release. + * @param nCharCode contains the Unicode character generated by this event or 0 + * @param nKeyCode contains the integer code representing the key of the event (non-zero for control keys) + */ + public func postWindowKeyEvent( nWindowId: UInt32, nType: Int32, nCharCode: Int32, nKeyCode: Int32) + { + docClass.postWindowKeyEvent(pDoc, nWindowId, nType, nCharCode, nKeyCode); + } + + /** + * Posts a mouse event to the document. + * + * @param nType Event type, like down, move or up. + * @param nX horizontal position in document coordinates + * @param nY vertical position in document coordinates + * @param nCount number of clicks: 1 for single click, 2 for double click + * @param nButtons: which mouse buttons: 1 for left, 2 for middle, 4 right + * @param nModifier: which keyboard modifier: (see include/vcl/vclenum.hxx for possible values) + */ + public func postMouseEvent( nType: Int32, nX: Int32, nY: Int32, nCount: Int32, nButtons: Int32, nModifier: Int32) + { + docClass.postMouseEvent(pDoc, nType, nX, nY, nCount, nButtons, nModifier); + } + + /** + * Posts a mouse event to the window with given id. + * + * @param nWindowId + * @param nType Event type, like down, move or up. + * @param nX horizontal position in document coordinates + * @param nY vertical position in document coordinates + * @param nCount number of clicks: 1 for single click, 2 for double click + * @param nButtons: which mouse buttons: 1 for left, 2 for middle, 4 right + * @param nModifier: which keyboard modifier: (see include/vcl/vclenum.hxx for possible values) + */ + public func postWindowMouseEvent(nWindowId: UInt32, nType: Int32, nX: Int32, nY: Int32, nCount: Int32, nButtons: Int32, nModifier: Int32) + { + docClass.postWindowMouseEvent(pDoc, nWindowId, nType, nX, nY, nCount, nButtons, nModifier); + } + + /** + * Posts an UNO command to the document. + * + * Example argument string: + * + * { + * "SearchItem.SearchString": + * { + * "type": "string", + * "value": "foobar" + * }, + * "SearchItem.Backward": + * { + * "type": "boolean", + * "value": "false" + * } + * } + * + * @param pCommand uno command to be posted to the document, like ".uno:Bold" + * @param pArguments arguments of the uno command. + */ + public func postUnoCommand(command: String, arguments: String? = nil, notifyWhenFinished: Bool = false) + { + docClass.postUnoCommand(pDoc, command, arguments, notifyWhenFinished); + } + + /** + * Sets the start or end of a text selection. + * + * @param nType @see LibreOfficeKitSetTextSelectionType + * @param nX horizontal position in document coordinates + * @param nY vertical position in document coordinates + */ + public func setTextSelection( nType: Int32, nX: Int32, nY: Int32) + { + docClass.setTextSelection(pDoc, nType, nX, nY); + } + + /** + * Gets the currently selected text. + * + * @param pMimeType suggests the return format, for example text/plain;charset=utf-8. + * @param pUsedMimeType output parameter to inform about the determined format (suggested one or plain text). + */ + // FIXME - work out how to use an inout param for usedMimeType + public func getTextSelection(mimeType: String, usedMimeType: UnsafeMutablePointer?>? = nil) -> String? + { + return toString( docClass.getTextSelection(pDoc, mimeType, usedMimeType) ); + } + + /** + * Pastes content at the current cursor position. + * + * @param pMimeType format of pData, for example text/plain;charset=utf-8. + * @param pData the actual data to be pasted. + * @return if the supplied data was pasted successfully. + */ + public func paste(mimeType: String, data: String, size: Int) -> Bool + { + return docClass.paste(pDoc, mimeType, data, size); + } + + /** + * Adjusts the graphic selection. + * + * @param nType @see LibreOfficeKitSetGraphicSelectionType + * @param nX horizontal position in document coordinates + * @param nY vertical position in document coordinates + */ + public func setGraphicSelection( nType: Int32, nX: Int32, nY: Int32) + { + docClass.setGraphicSelection(pDoc, nType, nX, nY); + } + + /** + * Gets rid of any text or graphic selection. + */ + public func resetSelection() + { + docClass.resetSelection(pDoc); + } + + /** + * Returns a json mapping of the possible values for the given command + * e.g. {commandName: ".uno:StyleApply", commandValues: {"familyName1" : ["list of style names in the family1"], etc.}} + * @param pCommand a uno command for which the possible values are requested + * @return {commandName: unoCmd, commandValues: {possible_values}} + */ + public func getCommandValues(command: String) -> String? + { + return toString(docClass.getCommandValues(pDoc, command)); + } + + /** + * Save the client's view so that we can compute the right zoom level + * for the mouse events. This only affects CALC. + * @param nTilePixelWidth - tile width in pixels + * @param nTilePixelHeight - tile height in pixels + * @param nTileTwipWidth - tile width in twips + * @param nTileTwipHeight - tile height in twips + */ + public func setClientZoom( + nTilePixelWidth: Int32, + nTilePixelHeight: Int32, + nTileTwipWidth: Int32, + nTileTwipHeight: Int32) + { + docClass.setClientZoom(pDoc, nTilePixelWidth, nTilePixelHeight, nTileTwipWidth, nTileTwipHeight); + } + + /** + * Inform core about the currently visible area of the document on the + * client, so that it can perform e.g. page down (which depends on the + * visible height) in a sane way. + * + * @param nX - top left corner horizontal position + * @param nY - top left corner vertical position + * @param nWidth - area width + * @param nHeight - area height + */ + public func setClientVisibleArea( nX: Int32, nY: Int32, nWidth: Int32, nHeight: Int32) + { + docClass.setClientVisibleArea(pDoc, nX, nY, nWidth, nHeight); + } + + /** + * Show/Hide a single row/column header outline for Calc documents. + * + * @param bColumn - if we are dealing with a column or row group + * @param nLevel - the level to which the group belongs + * @param nIndex - the group entry index + * @param bHidden - the new group state (collapsed/expanded) + */ + public func setOutlineState( column: Bool, level: Int32, index: Int32, hidden: Bool) + { + docClass.setOutlineState(pDoc, column, level, index, hidden); + } + + /** + * Create a new view for an existing document. + * By default a loaded document has 1 view. + * @return the ID of the new view. + */ + public func createView() -> Int32 + { + return docClass.createView(pDoc); + } + + /** + * Destroy a view of an existing document. + * @param nId a view ID, returned by createView(). + */ + public func destroyView( id: Int32 ) + { + docClass.destroyView(pDoc, id); + } + + /** + * Set an existing view of an existing document as current. + * @param nId a view ID, returned by createView(). + */ + public func setView(id: Int32) + { + docClass.setView(pDoc, id); + } + + /** + * Get the current view. + * @return a view ID, previously returned by createView(). + */ + public func getView() -> Int32 + { + return docClass.getView(pDoc); + } + + /** + * Get number of views of this document. + */ + public func getViewsCount() -> Int32 + { + return docClass.getViewsCount(pDoc); + } + + /** + * Paints a font name or character if provided to be displayed in the font list + * @param pFontName the font to be painted + */ + // TODO +// public func renderFont(fontName: String, +// const char *pChar, +// int *pFontWidth, +// int *pFontHeight) +// { +// return docClass.renderFont(pDoc, pFontName, pChar, pFontWidth, pFontHeight); +// } + + /** + * Renders a subset of the document's part to a pre-allocated buffer. + * + * @param nPart the part number of the document of which the tile is painted. + * @see paintTile. + */ + public func paintPartTile(pBuffer: UnsafeMutablePointer, + nPart: Int32, + nCanvasWidth: Int32, + nCanvasHeight: Int32, + nTilePosX: Int32, + nTilePosY: Int32, + nTileWidth: Int32, + nTileHeight: Int32) + { + return docClass.paintPartTile(pDoc, pBuffer, nPart, + nCanvasWidth, nCanvasHeight, + nTilePosX, nTilePosY, + nTileWidth, nTileHeight); + } + + /** + * Returns the viewID for each existing view. Since viewIDs are not reused, + * viewIDs are not the same as the index of the view in the view array over + * time. Use getViewsCount() to know the minimal nSize that's large enough. + * + * @param pArray the array to write the viewIDs into + * @param nSize the size of pArray + * @returns true if pArray was large enough and result is written, false + * otherwise. + */ +// bool getViewIds(int* pArray, +// size_t nSize) +// { +// return docClass.getViewIds(pDoc, pArray, nSize); +// } + + /** + * Set the language tag of the window with the specified nId. + * + * @param nId a view ID, returned by createView(). + * @param language Bcp47 languageTag, like en-US or so. + */ + public func setViewLanguage( id: Int32, language: String) + { + docClass.setViewLanguage(pDoc, id, language); + } + +} + +/** + * iOS friendly extensions of Document. + * TODO: move me back to the framework. + */ +public extension Document +{ + public func getDocumentSizeAsCGSize() -> CGSize + { + let (x,y) = self.getDocumentSize() + return CGSize(width: x, height: y) + } + + public func paintTileToCurrentContext(canvasSize: CGSize, + tileRect: CGRect) + { + let ctx = UIGraphicsGetCurrentContext() + //print(ctx!) + let ptr = unsafeBitCast(ctx, to: UnsafeMutablePointer.self) + //print(ptr) + + self.paintTile(pBuffer:ptr, + canvasWidth: Int32(canvasSize.width), + canvasHeight: Int32(canvasSize.height), + tilePosX: Int32(tileRect.minX), + tilePosY: Int32(tileRect.minY), + tileWidth: Int32(tileRect.size.width), + tileHeight: Int32(tileRect.size.height)) + } + + public func paintTileToImage(canvasSize: CGSize, + tileRect: CGRect) -> UIImage? + { + // the scaling etc here is all black magic. + // I don't really understand whats going on, other than that this combination works... + + UIGraphicsBeginImageContextWithOptions(canvasSize, false, 1.0) + let ctx = UIGraphicsGetCurrentContext()! + + // print(ctx) + // print(ctx.ctm) + // print(ctx.userSpaceToDeviceSpaceTransform) + + self.paintTileToCurrentContext(canvasSize: canvasSize, tileRect: tileRect) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } +} + diff --git a/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LOKitThread.swift b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LOKitThread.swift new file mode 100644 index 000000000000..34109fb88c62 --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LOKitThread.swift @@ -0,0 +1,287 @@ +// +// 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 + + + + +/// Serves the same purpose as the LOKitThread in the Android project - sequentialises all access to LOKit on a background thread, off the UI thread. +/// It's a singleton, and keeps a single instance of LibreOfficeKit +/// Public methods may be called from any thread, and will dispatch their work onto the held sequential queue. +/// TODO: move me to framework +public class LOKitThread +{ + public static let instance = LOKitThread() // statics are lazy and thread safe in swift, so no need for anything more complex + + + fileprivate let queue = SingleThreadedQueue(name: "LOKitThread.queue") + + /// singleton LibreOffice instance. Can only be accessed through the queue. + var libreOffice: LibreOffice! = nil // initialised in didFinishLaunchingWithOptions + + public weak var delegate: LOKitUIDelegate? = nil + public weak var progressDelegate: ProgressDelegate? = nil + + private init() + { + + async { + self.libreOffice = try! LibreOffice() // will blow up the app if it throws, but fair enough + + // hook up event handler + self.libreOffice.registerCallback(callback: self.onLOKEvent) + + } + } + + private func onLOKEvent(type: LibreOfficeKitCallbackType, payload: String?) + { + //LibreOfficeLight.LibreOfficeKitKeyEventType. + print("onLOKEvent type:\(type) payload:\(payload ?? "")") + + switch type + { + case LOK_CALLBACK_STATUS_INDICATOR_START: + runOnMain { + self.progressDelegate?.statusIndicatorStart() + } + + case LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE: + runOnMain { + if let doub = Double(payload ?? "") + { + self.progressDelegate?.statusIndicatorSetValue(value: doub) + } + } + + case LOK_CALLBACK_STATUS_INDICATOR_FINISH: + runOnMain { + self.progressDelegate?.statusIndicatorFinish() + } + default: + print("onLOKEvent type:\(type) not handled!") + } + } + + /// Run the task on the serial queue, and return immediately + public func async(_ runnable: @escaping Runnable) + { + queue.async( runnable) + } + + /// Run the task on the serial queue, and block to get the result + /// Careful of deadlocking! + public func sync( _ closure: @escaping () -> R ) -> R + { + let ret = queue.sync( closure ) + return ret + } + + public func withLibreOffice( _ closure: @escaping (LibreOffice) -> ()) + { + async { + closure(self.libreOffice) + } + } + + /// Loads a document, and calls the callback with a wrapper if successful, or an error if not. + public func documentLoad(url: String, callback: @escaping (DocumentHolder?, Error?) -> ()) + { + withLibreOffice + { + lo in + + do + { + // this is trying to avoid null context errors which pop up on doc init + // doesnt seem to fix + UIGraphicsBeginImageContext(CGSize(width:1,height:1)) + let doc = try lo.documentLoad(url: url) + print("Opened document: \(url)") + doc.initializeForRendering() + UIGraphicsEndImageContext() + + callback(DocumentHolder(doc: doc), nil) + } + catch + { + print("Failed to load document: \(error)") + callback(nil, error) + } + } + } +} + +/** + * 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 + + init(doc: Document) + { + self.doc = doc + doc.registerCallback() { + [weak self] typ, payload in + self?.onDocumentEvent(type: typ, payload: payload) + } + } + + /// Gives async access to the document + public func async(_ closure: @escaping (Document) -> ()) + { + LOKitThread.instance.async + { + closure(self.doc) + } + } + + /// 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 + { + return closure(self.doc) + } + } + + 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) ) + } + default: + print("onDocumentEvent type:\(type) not handled!") + } + } + + 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", "1") // String.valueOf(0)); // search all == 1 + + if let jsonStr = encode(json: rootJson) + { + async { + $0.postUnoCommand(command: ".uno:ExecuteSearch", arguments: jsonStr, notifyWhenFinished: true) + } + } + } + + +} + +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? +{ + //let encoder = JSONEncoder() + + 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) } + if coords.count == 4 + { + let rect = CGRect(x: coords[0], + y: coords[1], + width: coords[2], + height: coords[3]) + ret.append( rect ) + } + } + return ret +} + +/** + * Delegate methods for global events emitted from LOKit. + * Mostly dispatched on the main thread unless noted. + */ +public protocol LOKitUIDelegate: class +{ + // Nothing ATM.. +} + +public protocol ProgressDelegate: class +{ + func statusIndicatorStart() + + func statusIndicatorFinish() + + func statusIndicatorSetValue(value: Double) +} + + +public protocol DocumentUIDelegate: class +{ + func invalidateTiles(rects: [CGRect]? ) + + func invalidateVisibleCursor(rects: [CGRect]? ) + + func textSelection(rects: [CGRect]? ) + func textSelectionStart(rects: [CGRect]? ) + func textSelectionEnd(rects: [CGRect]? ) + + + +} diff --git a/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitIOSTests.swift b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitIOSTests.swift new file mode 100644 index 000000000000..de9f1ee82c2c --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitIOSTests.swift @@ -0,0 +1,102 @@ +// +// LibreOfficeKitIOSTests.swift +// LibreOfficeKitIOSTests +// +// Created by Jon Nermut on 30/12/17. +// Copyright © 2017 LibreOffice. All rights reserved. +// + +import XCTest +@testable import LibreOfficeKitIOS + +class LibreOfficeKitIOSTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + + + func testLoadingSimpleDoc() { + + guard let lo = try? LibreOffice() else + { + XCTFail("Could not start LibreOffice") + return + } + + let b = Bundle.init(for: LibreOfficeKitIOSTests.self) + guard let url = b.url(forResource: "test-page-format", withExtension: "docx") else + { + XCTFail("Failed to get url to test doc") + return + } + + var loCallbackCount = 0 + lo.registerCallback() + { + typ, payload in + print(typ) + print(payload) + loCallbackCount += 1 + } + + guard let doc = try? lo.documentLoad(url: url.absoluteString) else + { + XCTFail("Could not load document") + return + } + + var docCallbackCount = 0 + doc.registerCallback() + { + typ, payload in + print(typ) + print(payload) + docCallbackCount += 1 + } + + //let typ: LibreOfficeDocumentType = doc.getDocumentType() + //XCTAssertTrue(typ == LibreOfficeDocumentType.LOK_DOCTYPE_TEXT) + + doc.initializeForRendering() + let rects = doc.getPartRectanges() + print(rects) // 284, 284, 12240, 15840; 284, 16408, 12240, 15840 + let tileMode = doc.getTileMode() + print(tileMode) // 1 + let canvasSize = CGSize(width: 1024,height: 1024) + let tile = CGRect(x: 284, y: 284, width: 12240, height: 12240) + + + guard let image = doc.paintTileToImage(canvasSize: canvasSize, tileRect: tile) else + { + XCTFail("No image") + return + } + if let data = UIImagePNGRepresentation(image) + { + let filename = getDocumentsDirectory().appendingPathComponent("tile1.png") + try? data.write(to: filename) + print("Wrote tile to: \(filename)") + } + } + +} + +func getDocumentsDirectory() -> URL +{ + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] +} + +public extension Document +{ + +} + diff --git a/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitWrapper.swift b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitWrapper.swift new file mode 100644 index 000000000000..f1d6b947c8e1 --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/LibreOfficeKitWrapper.swift @@ -0,0 +1,227 @@ +// +// 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 + + +public struct LibreOfficeError: Error +{ + let message: String + public init(_ message: String) + { + self.message = message + } +} + +public typealias LibreOfficeCallback = (_ type: LibreOfficeKitCallbackType, _ payload: String?) -> () + +func callbackFromLibreOffice(nType: Int32, payload: UnsafePointer?, pData: UnsafeMutableRawPointer?) +{ + if let val = pData?.hashValue + { + if let theFunc = Callbacks.callbackRegister[val] + { + let payString = toString(payload) + theFunc(LibreOfficeKitCallbackType(rawValue: LibreOfficeKitCallbackType.RawValue(nType)), payString) + } + else + { + print("Unknown callback: \(val)") + } + } + else + { + print("callbackFromLibreOffice, but pData was nil") + } +} + + +internal struct Callbacks +{ + static var count = 0 + static var callbackRegister: Dictionary = [:] + + static func register(callback: @escaping LibreOfficeCallback) -> Int + { + count += 1 + let id = count + callbackRegister[id] = callback + return id + + } +} + + +open class LibreOffice +{ + private let pLok: UnsafeMutablePointer + private let lokClass: LibreOfficeKitClass + + public init() throws + { + let b = Bundle.init(for: LibreOffice.self) + let path = b.bundlePath // not Bundle.main.bundlePath + BridgeLOkit_Init(path) + let pLok = BridgeLOkit_getLOK() + if let lokClass = pLok?.pointee.pClass?.pointee + { + self.pLok = pLok! + self.lokClass = lokClass + print("Loaded LibreOfficeKit: \(self.getVersionInfo() ?? "")") + return + } + throw LibreOfficeError("Unable to init LibreOfficeKit") + } + + /** + * Get version information of the LOKit process + * + * @since LibreOffice 6.0 + * @returns JSON string containing version information in format: + * {ProductName: <>, ProductVersion: <>, ProductExtension: <>, BuildId: <>} + * + * Eg: {"ProductName": "LibreOffice", + * "ProductVersion": "5.3", + * "ProductExtension": ".0.0.alpha0", + * "BuildId": ""} + */ + public func getVersionInfo() -> String? + { + if let pRet = lokClass.getVersionInfo(pLok) + { + return String(cString: pRet) // TODO: convert JSON + } + return nil + } + + /** + * Loads a document from an URL. + * + * @param pUrl the URL of the document to load + * @param pFilterOptions options for the import filter, e.g. SkipImages. + * Another useful FilterOption is "Language=...". It is consumed + * by the documentLoad() itself, and when provided, LibreOfficeKit + * switches the language accordingly first. + * @since pFilterOptions argument added in LibreOffice 5.0 + */ + public func documentLoad(url: String) throws -> Document + { + if let pDoc = lokClass.documentLoad(pLok, url) + { + return Document(pDoc: pDoc) + } + throw LibreOfficeError("Unable to load document") + } + + + + /// Returns the last error as a string + public func getError() -> String? + { + if let cstr = lokClass.getError(pLok) + { + let ret = String(cString: cstr) + lokClass.freeError(cstr) + return ret + } + return nil + } + + + /** + * Registers a callback. LOK will invoke this function when it wants to + * inform the client about events. + * + * @since LibreOffice 6.0 + * @param pCallback the callback to invoke + * @param pData the user data, will be passed to the callback on invocation + */ + public func registerCallback( callback: @escaping LibreOfficeCallback ) -> Int + { + let ret = Callbacks.register(callback: callback) + let pointer = UnsafeMutableRawPointer(bitPattern: ret) + lokClass.registerCallback(pLok, callbackFromLibreOffice, pointer) + return ret + } + + /** + * Returns details of filter types. + * + * Example returned string: + * + * { + * "writer8": { + * "MediaType": "application/vnd.oasis.opendocument.text" + * }, + * "calc8": { + * "MediaType": "application/vnd.oasis.opendocument.spreadsheet" + * } + * } + * + * @since LibreOffice 6.0 + */ + public func getFilterTypes() -> String? + { + return toString(lokClass.getFilterTypes(pLok)); + } + + /** + * Set bitmask of optional features supported by the client. + * + * @since LibreOffice 6.0 + * @see LibreOfficeKitOptionalFeatures + */ + public func setOptionalFeatures(features: UInt64) + { + return lokClass.setOptionalFeatures(pLok, features); + } + + /** + * Set password required for loading or editing a document. + * + * Loading the document is blocked until the password is provided. + * + * @param pURL the URL of the document, as sent to the callback + * @param pPassword the password, nullptr indicates no password + * + * In response to LOK_CALLBACK_DOCUMENT_PASSWORD, a valid password + * will continue loading the document, an invalid password will + * result in another LOK_CALLBACK_DOCUMENT_PASSWORD request, + * and a NULL password will abort loading the document. + * + * In response to LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY, a valid + * password will continue loading the document, an invalid password will + * result in another LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY request, + * and a NULL password will continue loading the document in read-only + * mode. + * + * @since LibreOffice 6.0 + */ + public func setDocumentPassword(URL: String, password: String) + { + lokClass.setDocumentPassword(pLok, URL, password); + } + + + + + /** + * Run a macro. + * + * Same syntax as on command line is permissible (ie. the macro:// URI forms) + * + * @since LibreOffice 6.0 + * @param pURL macro url to run + */ + + public func runMacro( URL: String ) -> Bool + { + return lokClass.runMacro( pLok, URL ) != 0; + } +} + diff --git a/ios/LibreOfficeLight/LibreOfficeLight/LOKit/Util.swift b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/Util.swift new file mode 100644 index 000000000000..596ca45e34a9 --- /dev/null +++ b/ios/LibreOfficeLight/LibreOfficeLight/LOKit/Util.swift @@ -0,0 +1,43 @@ +// +// 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 UIKit + + +func getDocumentsDirectory() -> URL +{ + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] +} + +public extension CGRect +{ + public var desc: String + { + return "(x: \(self.origin.x), y: \(self.origin.y), width: \(self.size.width), height: \(self.size.height), maxX: \(self.maxX), maxY: \(self.maxY))" + } +} + +public func toString(_ pointer: UnsafeMutablePointer?) -> String? +{ + if let p = pointer + { + return String(cString: p) + } + return nil +} + +public func toString(_ pointer: UnsafePointer?) -> String? +{ + if let p = pointer + { + return String(cString: p) + } + return nil +} + diff --git a/ios/LibreOfficeLight/LibreOfficeLight/en.lproj/Main.storyboard b/ios/LibreOfficeLight/LibreOfficeLight/en.lproj/Main.storyboard index e2748dad3c8c..ccc91115c5e0 100755 --- a/ios/LibreOfficeLight/LibreOfficeLight/en.lproj/Main.storyboard +++ b/ios/LibreOfficeLight/LibreOfficeLight/en.lproj/Main.storyboard @@ -1,11 +1,13 @@ - + - + + + @@ -20,6 +22,34 @@ + + + + + + + + + + + + + + + + + + + + + @@ -29,19 +59,32 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + - + diff --git a/ios/LibreOfficeLight/LibreOfficeLight/lokit-Bridging-Header.h b/ios/LibreOfficeLight/LibreOfficeLight/lokit-Bridging-Header.h index d501ae863444..bc276e9d31e2 100644 --- a/ios/LibreOfficeLight/LibreOfficeLight/lokit-Bridging-Header.h +++ b/ios/LibreOfficeLight/LibreOfficeLight/lokit-Bridging-Header.h @@ -10,4 +10,5 @@ // LibreOfficeKit is a prelink of all used LO libraries, generated // as its own xCode project. +#define LOK_USE_UNSTABLE_API #import "../../source/LibreOfficeKit.h" -- cgit