From aec12fc34a21f4a6b5b912ef2b17af2dc5f696e3 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 18 Sep 2026 19:50:52 +0200 Subject: [PATCH 1/5] Take odrcore 7.0.0, and put the editing tools under the bar The engine moves to 7.0.0. While a document is edited a row of tools grows under the bar: formatting, undo and redo for a text document, undo and redo alone for a sheet, and five markers for a pdf, which is saved with its marks. Presentations, Excel files and plain text edit and save too. The Lite app edits inside a paragraph. Formatting, new paragraphs and marks on a pdf are Pro, and Lite offers Pro when they are reached for. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01214fGHJJ8jpfdujYBqDRsz --- CHANGELOG.md | 14 + OpenDocumentReader.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- OpenDocumentReader/CoreWrapper.swift | 98 +++-- OpenDocumentReader/Document.swift | 27 +- .../DocumentViewController.swift | 335 ++++++++++++++- OpenDocumentReader/EditToolBar.swift | 389 ++++++++++++++++++ OpenDocumentReader/Features.swift | 4 + OpenDocumentReader/Main.storyboard | 1 + .../ca.lproj/Localizable.strings | 42 ++ .../cs.lproj/Localizable.strings | 42 ++ .../da.lproj/Localizable.strings | 42 ++ .../de.lproj/Localizable.strings | 42 ++ .../en.lproj/Localizable.strings | 42 ++ .../es.lproj/Localizable.strings | 42 ++ .../fr.lproj/Localizable.strings | 42 ++ .../ga.lproj/Localizable.strings | 42 ++ .../it.lproj/Localizable.strings | 42 ++ .../ja.lproj/Localizable.strings | 42 ++ .../pl.lproj/Localizable.strings | 42 ++ .../pt-BR.lproj/Localizable.strings | 42 ++ .../ru.lproj/Localizable.strings | 42 ++ .../sl.lproj/Localizable.strings | 42 ++ .../tr.lproj/Localizable.strings | 42 ++ .../zh-Hans.lproj/Localizable.strings | 42 ++ .../ArchiveDocumentTests.swift | 6 +- .../EditWorkflowTests.swift | 175 +++++++- .../LockedDocumentTests.swift | 4 +- .../OpenDocumentReaderTests.swift | 146 +++++-- README.md | 24 ++ 30 files changed, 1790 insertions(+), 111 deletions(-) create mode 100644 OpenDocumentReader/EditToolBar.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ec6c68..93eafffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,20 @@ once the version tag exists. ### Added - The name of the open document is shown at the top, between the buttons. +- A row of editing tools under the bar while a document is edited: bold, + italic, underline, strikethrough, text colour, highlight, text size, undo + and redo. +- A PDF can be marked up: highlight, underline, strike out, squiggly underline + and drawing, saved into the file. +- Presentations, Excel files and plain text files can be edited and saved. +- Undo and redo while editing. + +### Changed + +- The engine is odrcore 7.0.0, up from 6.13.0. +- The Lite app edits inside a paragraph. Formatting, new or joined paragraphs + and marks on a PDF are part of Pro, and the Lite app says so when they are + reached for. ### Fixed diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index 2502410b..e10b71a0 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -830,7 +830,7 @@ repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 6.13.0; + minimumVersion = 7.0.0; }; }; AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { diff --git a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9feccd1a..573e4b51 100644 --- a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/opendocument-app/OpenDocument.core.git", "state" : { - "revision" : "1b1599864aefe78a23f01da07d3501e617810a05", - "version" : "6.13.0" + "revision" : "2ac2be9f08a315ae49cfff2bf340d0914a74ebc4", + "version" : "7.0.0" } }, { diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index 743b18d0..f06183ef 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -84,11 +84,20 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ /// Whether odrcore saw only a container, so the page is a listing of what is inside it. @objc private(set) var isArchive = false - /// Whether `backTranslate` has a document to apply an edit to. Only a - /// document that said it takes one is kept, so having it *is* the answer. - @objc var isEditable: Bool { lock.withLock { document != nil } } + /// Whether `save` has something to apply an edit to. Only a file that said + /// it takes one is kept, so having it *is* the answer. + @objc var isEditable: Bool { lock.withLock { document != nil || textFile != nil } } + + /// Whether the file is a pdf that takes marks - the pdf's own answer, not + /// the app's: whether to offer them is decided elsewhere. + @objc var isAnnotatable: Bool { lock.withLock { pdfFile != nil } } + + /// Whether the file is plain text, which takes typing but no formatting. + @objc var isPlainText: Bool { lock.withLock { textFile != nil } } private var document: OdrCoreObjC.Document? + private var textFile: TextFile? + private var pdfFile: PdfFile? private let lock = NSRecursiveLock() /// The largest sheet region translated, as on OpenDocument.droid. @@ -99,10 +108,10 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ @objc func translate( _ inputPath: String, - cache cachePath: String, into outputPath: String, with password: String?, - editable: Bool + editable: Bool, + scope: HtmlEditingScope ) throws { lock.lock() defer { lock.unlock() } @@ -110,6 +119,8 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ pageNames = [] pageURLs = [] document = nil + textFile = nil + pdfFile = nil isArchive = false let fileTypes = (try? DecodedFile.listFileTypes(path: inputPath)) ?? [] @@ -141,6 +152,9 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ // decided from these let config = HtmlConfig() config.editable = editable + // how far an edit may reach: inside one paragraph, or across the + // document with formatting + config.editingScope = scope // resource paths are resolved relative to an output directory, and in // server mode there is none — odrcore rejects the combination config.relativeResourcePaths = false @@ -167,6 +181,8 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ let documentType: DocumentType let openedDocument: OdrCoreObjC.Document? + var openedTextFile: TextFile? + var openedPdfFile: PdfFile? let service: HtmlService if file.isDocumentFile { @@ -177,15 +193,23 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ // the document's own answer: a format odrcore renders but cannot write // back would otherwise offer Edit and fail at the save openedDocument = document.isEditable && document.isSavable ? document : nil - service = try HtmlTranslator.translate( - document: document, cachePath: cachePath, config: config) + service = try HtmlTranslator.translate(document: document, config: config) } else { - // nothing to edit, and `.unknown` keeps the single view each of - // these has - `.spreadsheet` would ask for a tab per sheet + // `.unknown` keeps the single view each of these has - + // `.spreadsheet` would ask for a tab per sheet documentType = .unknown openedDocument = nil - service = try HtmlTranslator.translate( - file: file, cachePath: cachePath, config: config) + + if file.isTextFile, let text = try? file.asTextFile(), text.isSavable { + openedTextFile = text + } + if file.isPdfFile, file.capabilities.annotate, let pdf = try? file.asPdfFile(), + pdf.isAnnotatable + { + openedPdfFile = pdf + } + + service = try HtmlTranslator.translate(file: file, config: config) } let views = selectViews(service.views, documentType) @@ -197,27 +221,31 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ throw coreWrapperError(.unknown, "could not serve the translated document") } - // only once nothing can throw any more: backTranslate must not be handed - // a document whose pages were never served + // only once nothing can throw any more: a save must not be handed a + // file whose pages were never served self.document = openedDocument + self.textFile = openedTextFile + self.pdfFile = openedPdfFile isArchive = file.isArchiveFile pageNames = views.map(\.name) pageURLs = views.map { base.appendingPathComponent($0.path) } } - @objc func backTranslate(_ diff: String, into outputPath: String) throws { + /// The script the page hands its edits back through: the editor's log for + /// a document or a text file, the marks for a pdf. + @objc var editPayloadScript: String { + isAnnotatable ? "odr.annotation.getAnnotations()" : "odr.editing.getOperations()" + } + + /// Writes the file with `payload` applied - the page's operations, or its + /// marks for a pdf. + @objc func save(_ payload: String, into outputPath: String) throws { lock.lock() defer { lock.unlock() } - guard let document else { - throw coreWrapperError(.unknown, "no document has been translated yet") - } - - try HtmlTranslator.edit(document: document, diff: diff) - - // odrcore streams the parts the edit did not touch out of the file it opened, and - // truncates the destination first - so saving onto the open document empties it + // odrcore streams the parts an edit did not touch out of the file it opened, and + // truncates the destination first - so saving onto the open file empties it let output = URL(fileURLWithPath: outputPath) let staging = try stagingDirectory(for: output) @@ -225,11 +253,35 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ let temporary = stagedFile(in: staging, for: output) - try document.save(to: temporary.path) + if let document { + if Self.holdsOperations(payload) { + try document.edit(operations: payload) + } + try document.save(to: temporary.path) + } else if let textFile { + try textFile.writeEdited(operations: payload).write(to: temporary) + } else if let pdfFile { + try pdfFile.annotate(payload).write(to: temporary) + } else { + throw coreWrapperError(.unknown, "no editable file has been translated yet") + } try moveIntoPlace(from: temporary, to: output) } + /// Whether the envelope carries any operation: an empty one is a save of + /// the file as it is. + private static func holdsOperations(_ payload: String) -> Bool { + guard let data = payload.data(using: .utf8), + let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let ops = envelope["ops"] as? [Any] + else { + return true + } + + return !ops.isEmpty + } + /// A directory on `output`'s own volume, because `replaceItemAt` cannot swap across one. /// The caller has to delete it. private func stagingDirectory(for output: URL) throws -> URL { diff --git a/OpenDocumentReader/Document.swift b/OpenDocumentReader/Document.swift index 912f1b71..0b7bb1ad 100644 --- a/OpenDocumentReader/Document.swift +++ b/OpenDocumentReader/Document.swift @@ -52,6 +52,10 @@ class Document: UIDocument { public var isArchive = false /// Whether the menu should offer to edit this one - see `CoreWrapper.isEditable`. public var isEditable = false + /// Whether this is a pdf that takes marks - see `CoreWrapper.isAnnotatable`. + public var isAnnotatable = false + /// Whether this is plain text - see `CoreWrapper.isPlainText`. + public var isPlainText = false private var wasPageCountAnnounced = false override func load(fromContents contents: Any, ofType typeName: String?) throws { @@ -66,19 +70,19 @@ class Document: UIDocument { isOdf = false isArchive = false isEditable = false + isAnnotatable = false + isPlainText = false result = nil pageURLs = nil notify { $0.documentUpdateContent(self) } - let temporaryDirectory = NSTemporaryDirectory() - do { try coreWrapper.translate( fileURL.path, - cache: temporaryDirectory, - into: temporaryDirectory, + into: NSTemporaryDirectory(), with: password, - editable: edit + editable: edit, + scope: Features.advancedEditing ? .document : .paragraph ) } catch let error as NSError where error.domain == CoreWrapperErrorDomain @@ -96,6 +100,8 @@ class Document: UIDocument { isOdf = true isArchive = coreWrapper.isArchive isEditable = coreWrapper.isEditable + isAnnotatable = coreWrapper.isAnnotatable + isPlainText = coreWrapper.isPlainText loadProgress.completedUnitCount = loadProgress.totalUnitCount @@ -152,12 +158,12 @@ class Document: UIDocument { override func writeContents( _ contents: Any, to url: URL, for saveOperation: UIDocument.SaveOperation, originalContentsURL: URL? ) throws { - let diff = try generateDiff() + let payload = try collectEdits() // the document handle CoreWrapper holds is only valid together with the - // web view that produced the diff, so the edit stays on the main thread + // web view that produced the edits, so the save stays on the main thread try onMainThread { - try coreWrapper.backTranslate(diff, into: url.path) + try coreWrapper.save(payload, into: url.path) } } @@ -173,9 +179,10 @@ class Document: UIDocument { /// Blocks the calling save thread until the web view has handed back the /// edits the user made. - private func generateDiff() throws -> String { + private func collectEdits() throws -> String { let semaphore = DispatchSemaphore(value: 0) var result: Result = .failure(DocumentError.getHtml) + let script = coreWrapper.editPayloadScript DispatchQueue.main.async { guard let webview = self.webview else { @@ -185,7 +192,7 @@ class Document: UIDocument { return } - webview.evaluateJavaScript("odr.generateDiff()") { value, error in + webview.evaluateJavaScript(script) { value, error in defer { semaphore.signal() } if let error { diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 0e484134..5edb5f9a 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -13,7 +13,8 @@ import WebKit // taken from: https://developer.apple.com/documentation/uikit/view_controllers/building_a_document_browser-based_app class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDelegate, - SKStoreProductViewControllerDelegate, WKNavigationDelegate, WKUIDelegate + SKStoreProductViewControllerDelegate, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, + UIColorPickerViewControllerDelegate { private var browserTransition: DocumentBrowserTransitioningDelegate? @@ -37,6 +38,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } @IBOutlet weak var toolBar: UIToolbar! + /// The bar and what hangs under it: the editing tools, the progress line. + @IBOutlet weak var barStack: UIStackView! @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var pageTabBar: PageTabBar! @@ -65,10 +68,34 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Whether the document on screen can be edited and searched. Neither button /// stays in the bar when it cannot be used. private var canEdit = false { didSet { updateToolBar() } } + /// Whether the document is a pdf that takes marks. The same button as the + /// pencil, with the highlighter for a glyph. + private var canMark = false { didSet { updateEditButtonRole() } } /// The same slot the pencil sits in, showing the way out of the edit it /// started — as on OpenDocument.droid, where edit mode replaces the bar /// rather than emptying it. - private var isEditingDocument = false { didSet { updateEditButtonRole() } } + private var isEditingDocument = false { + didSet { + updateEditButtonRole() + + if !isEditingDocument { + editToolBar.layout = nil + } + } + } + + /// The row of tools under the bar while a document is edited. + let editToolBar = EditToolBar() + + /// The colour the marks on a pdf take, until the reader picks another. + private var markColor = UIColor(hex: EditToolBar.markColors[0].hex) + + /// Which menu the system colour picker was opened from. + private var colorPickerTool: EditToolBar.Tool? + + /// Whether the Pro offer was shown during this edit, so a page full of + /// refused line breaks raises it once. + private var hasOfferedProForThisEdit = false private var canSearch = false { didSet { updateToolBar() @@ -124,6 +151,9 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel searchBarHeightWhenShown = searchBar.heightAnchor.constraint(equalToConstant: 56) searchBarHeightWhenHidden = searchBar.heightAnchor.constraint(equalToConstant: 0) + setUpEditToolBar() + setUpPageMessages() + setVCconstraints() hideSearchBar() @@ -257,6 +287,10 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { updateSearchButton() + if let documentNavigation, navigation === documentNavigation, document?.edit == true { + beginEditSession() + } + // the document is drawn, which is what a screenshot of it waits for - // and only the document: the "loading" page finishes first, and a // picture of it is a picture of the word "loading" @@ -382,7 +416,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - searchBar.topAnchor.constraint(equalTo: toolBar.bottomAnchor, constant: Self.toolBarBottomMargin).isActive = + searchBar.topAnchor.constraint(equalTo: barStack.bottomAnchor, constant: Self.toolBarBottomMargin).isActive = true bannerSlot.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true @@ -509,11 +543,264 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel self.document?.edit = false } + } else if canMark, !Features.advancedEditing { + offerPro(.pdf) } else { editDocument() } } + // MARK: - the editing tools + + /// Under the bar and above the progress line, so it reads as part of the bar. + private func setUpEditToolBar() { + editToolBar.layout = nil + editToolBar.menusEnabled = Features.advancedEditing + editToolBar.onTap = { [weak self] tool in self?.editToolTapped(tool) } + editToolBar.onChoice = { [weak self] tool, choice in self?.editToolChose(tool, choice) } + + barStack.insertArrangedSubview(editToolBar, at: 1) + } + + /// Hears from the page: the log, a refusal, the style under the caret. + private func setUpPageMessages() { + let controller = webview.configuration.userContentController + + controller.add(WeakScriptMessageHandler(self), name: Self.pageMessageName) + controller.addUserScript( + WKUserScript(source: Self.pageMessageBridge, injectionTime: .atDocumentEnd, forMainFrameOnly: true)) + } + + private static let pageMessageName = "odr" + + /// Points the page's callbacks at this controller. The page's own scripts + /// have run by document end, so `odr` is there to be pointed. + private static let pageMessageBridge = """ + (function () { + if (typeof odr !== 'object' || !window.webkit || !webkit.messageHandlers.odr) { return; } + var post = function (message) { webkit.messageHandlers.odr.postMessage(message); }; + odr.onEditChange = function (e) { + post({ type: 'editChange', canUndo: !!e.canUndo, canRedo: !!e.canRedo }); + }; + odr.onEditRefused = function (e) { + post({ type: 'editRefused', reason: String(e.reason || ''), message: String(e.message || '') }); + }; + odr.onSelectionChange = function (style) { + post({ type: 'selection', style: style || {} }); + }; + })(); + """ + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let body = message.body as? [String: Any], let type = body["type"] as? String else { return } + + switch type { + case "editChange": + editToolBar.setEnabled(.undo, body["canUndo"] as? Bool ?? false) + editToolBar.setEnabled(.redo, body["canRedo"] as? Bool ?? false) + + case "editRefused": + editRefused(reason: body["reason"] as? String ?? "") + + case "selection": + let style = body["style"] as? [String: Any] ?? [:] + editToolBar.setPressed(.bold, style["bold"] as? Bool ?? false) + editToolBar.setPressed(.italic, style["italic"] as? Bool ?? false) + editToolBar.setPressed(.underline, style["underline"] as? Bool ?? false) + editToolBar.setPressed(.strikethrough, style["strikethrough"] as? Bool ?? false) + + default: + break + } + } + + /// The editable page is on screen: turn the mode on and show its tools. + /// A pdf needs no mode, only a marker that acts on a selection. + private func beginEditSession() { + hasOfferedProForThisEdit = false + + if document?.isAnnotatable == true { + editToolBar.layout = .pdf + editToolBar.setEnabled(.undo, true) + run("odr.annotation.setOptions({ markOnSelection: true }); odr.annotation.setColor(\(markColor.deviceRGB))") + + return + } + + editToolBar.setEnabled(.undo, false) + editToolBar.setEnabled(.redo, false) + + let isPlainText = document?.isPlainText == true + + webview.evaluateJavaScript("odr.editing.enable(); typeof odr.sheet === 'object'") { [weak self] isSheet, _ in + guard let self, self.isEditingDocument else { return } + + self.editToolBar.layout = isPlainText || isSheet as? Bool == true ? .plain : .text + } + } + + private func editToolTapped(_ tool: EditToolBar.Tool) { + if tool.isAdvanced, !Features.advancedEditing { + offerPro(canMark ? .pdf : .formatting) + + return + } + + switch tool { + case .undo: + run(canMark ? "odr.annotation.undo()" : "odr.editing.undo()") + case .redo: + run("odr.editing.redo()") + case .bold, .italic, .underline, .strikethrough: + run("odr.editing.toggle('\(tool.pageName ?? "")')") + case .markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw: + armMarker(tool) + default: + break + } + } + + /// As on the website: a selection is marked once and the tool stays down, + /// the armed tool disarms, anything else arms. + private func armMarker(_ tool: EditToolBar.Tool) { + guard let name = tool.pageName else { return } + + let script = """ + (function () { + var a = odr.annotation; + var selection = window.getSelection(); + var selected = '\(name)' !== 'ink' && selection && !selection.isCollapsed; + if (selected) { + var armed = a.getTool(); + a.setTool('\(name)'); + a.mark(); + a.setTool(armed); + selection.removeAllRanges(); + return armed; + } + if (a.getTool() === '\(name)') { + a.setTool(null); + return null; + } + a.setWidth(2); + a.setTool('\(name)'); + return '\(name)'; + })() + """ + + webview.evaluateJavaScript(script) { [weak self] armed, error in + if let error { + CrashManager.shared.log(error) + } + + self?.showArmedMarker(armed as? String) + } + } + + private func showArmedMarker(_ armed: String?) { + for tool in EditToolBar.Layout.pdf.tools { + editToolBar.setPressed(tool, tool.pageName != nil && tool.pageName == armed) + } + } + + private func editToolChose(_ tool: EditToolBar.Tool, _ choice: EditToolBar.Choice) { + switch (tool, choice) { + case (.fontSize, .size(let size)): + run("odr.editing.format({ size: '\(size)pt' })") + case (.textColor, .color(let hex)): + run("odr.editing.format({ color: '\(hex ?? "")' })") + case (.highlight, .color(let hex)): + run("odr.editing.format({ highlight: \(hex.map { "'\($0)'" } ?? "null") })") + case (.markColor, .color(let hex)): + markColor = UIColor(hex: hex ?? EditToolBar.markColors[0].hex) + run("odr.annotation.setColor(\(markColor.deviceRGB))") + case (_, .customColor): + colorPickerTool = tool + + let picker = UIColorPickerViewController() + picker.delegate = self + picker.supportsAlpha = false + picker.selectedColor = tool == .markColor ? markColor : .label + present(picker, animated: true) + default: + break + } + } + + func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { + guard let tool = colorPickerTool else { return } + colorPickerTool = nil + + editToolChose(tool, .color(viewController.selectedColor.hexString)) + } + + /// The page said no. A line break or a format outside the paragraph is + /// what Pro is for; the rest is said in a word. + private func editRefused(reason: String) { + if reason == "outOfScope", !Features.advancedEditing { + guard !hasOfferedProForThisEdit else { return } + hasOfferedProForThisEdit = true + + offerPro(.formatting) + + return + } + + let key: String + switch reason { + case "formula": key = "edit_refused_formula" + case "formulaInput": key = "edit_refused_formula_input" + case "rich", "shapes": key = "edit_refused_rich" + default: key = "edit_refused_generic" + } + + AnalyticsManager.shared.report("edit_refused", parameters: ["reason": reason]) + + showToast(controller: self, message: NSLocalizedString(key, comment: ""), seconds: 1.5) + } + + /// What Pro adds, as the reader runs into it. + enum ProFeature { + case formatting + case pdf + + var message: String { + switch self { + case .formatting: return NSLocalizedString("pro_feature_formatting", comment: "") + case .pdf: return NSLocalizedString("pro_feature_pdf", comment: "") + } + } + } + + /// Says what Pro is for, and leads to it. The Lite app's one gate. + func offerPro(_ feature: ProFeature) { + AnalyticsManager.shared.report("pro_gate_shown", parameters: ["feature": "\(feature)"]) + + let alert = UIAlertController( + title: NSLocalizedString("pro_feature_title", comment: ""), + message: feature.message, + preferredStyle: .alert) + alert.addAction( + UIAlertAction(title: NSLocalizedString("not_now", comment: ""), style: .cancel)) + alert.addAction( + UIAlertAction(title: NSLocalizedString("house_ad_cta_get_pro", comment: ""), style: .default) { _ in + AnalyticsManager.shared.report("pro_gate_tapped", parameters: ["feature": "\(feature)"]) + + self.openProOnAppStore() + }) + + present(alert, animated: true) + } + + /// A script whose answer nobody needs. + private func run(_ script: String) { + webview.evaluateJavaScript(script) { _, error in + if let error { + CrashManager.shared.log(error) + } + } + } + /// A gap either side of the name, which is what puts it in the middle. private func setUpDocumentTitle() { // a glass capsule is what a button looks like, and this is not one @@ -585,16 +872,30 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Offered for the documents that can be edited, whether or not one is being /// edited right now — the button is the way both into an edit and out of it. private func updateEditButton() { - canEdit = document?.isEditable ?? false + canMark = document?.isAnnotatable ?? false + canEdit = (document?.isEditable ?? false) || canMark isEditingDocument = document?.edit ?? false } - /// A pencil to start an edit, and the save glyph to write one. The label goes - /// with it: VoiceOver reads that, not the glyph. + /// A pencil to start an edit, a highlighter to mark a pdf, and the save + /// glyph to write either. The label goes with it: VoiceOver reads that, + /// not the glyph. private func updateEditButtonRole() { - editButton.image = UIImage(systemName: isEditingDocument ? "square.and.arrow.down" : "pencil") - editButton.accessibilityLabel = NSLocalizedString( - isEditingDocument ? "action_edit_save" : "menu_edit", comment: "") + let symbol: String + let label: String + if isEditingDocument { + symbol = "square.and.arrow.down" + label = "action_edit_save" + } else if canMark { + symbol = "highlighter" + label = "mark_pdf" + } else { + symbol = "pencil" + label = "menu_edit" + } + + editButton.image = UIImage(systemName: symbol) + editButton.accessibilityLabel = NSLocalizedString(label, comment: "") } /// Asked of the page rather than guessed from the format: odrcore writes the @@ -848,6 +1149,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel corePageInReserve = url canEdit = false + canMark = false canSearch = false documentNavigation = webview.loadFileURL(doc.fileURL, allowingReadAccessTo: doc.fileURL) @@ -981,6 +1283,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel // neither is known until the page it produces is loaded canEdit = false + canMark = false canSearch = false } @@ -1078,3 +1381,17 @@ extension UIViewController { UIApplication.shared.open(url) } } + +/// The web view's content controller holds its handlers strongly, and this +/// controller holds the web view: a weak step in between breaks the cycle. +private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { + private weak var handler: WKScriptMessageHandler? + + init(_ handler: WKScriptMessageHandler) { + self.handler = handler + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + handler?.userContentController(userContentController, didReceive: message) + } +} diff --git a/OpenDocumentReader/EditToolBar.swift b/OpenDocumentReader/EditToolBar.swift new file mode 100644 index 00000000..fb235122 --- /dev/null +++ b/OpenDocumentReader/EditToolBar.swift @@ -0,0 +1,389 @@ +import UIKit + +/// The row of editing tools under the bar, shown while a document is edited. +/// As on the website: the bar keeps the way in and out of an edit, and this +/// row grows beneath it with what the open document takes. +final class EditToolBar: UIView { + + /// One button of the row. + enum Tool: CaseIterable { + case bold, italic, underline, strikethrough + case textColor, highlight, fontSize + case markHighlight, markUnderline, markStrikeOut, markSquiggly, markDraw, markColor + case undo, redo + + var symbol: String { + switch self { + case .bold: return "bold" + case .italic: return "italic" + case .underline: return "underline" + case .strikethrough: return "strikethrough" + case .textColor: return "character" + case .highlight, .markHighlight: return "highlighter" + case .fontSize: return "textformat.size" + case .markUnderline: return "underline" + case .markStrikeOut: return "strikethrough" + case .markSquiggly: return "scribble.variable" + case .markDraw: return "pencil.tip" + case .markColor: return "paintpalette" + case .undo: return "arrow.uturn.backward" + case .redo: return "arrow.uturn.forward" + } + } + + var label: String { + switch self { + case .bold: return NSLocalizedString("edit_bold", comment: "") + case .italic: return NSLocalizedString("edit_italic", comment: "") + case .underline: return NSLocalizedString("edit_underline", comment: "") + case .strikethrough: return NSLocalizedString("edit_strikethrough", comment: "") + case .textColor: return NSLocalizedString("edit_text_color", comment: "") + case .highlight: return NSLocalizedString("edit_highlight", comment: "") + case .fontSize: return NSLocalizedString("edit_font_size", comment: "") + case .markHighlight: return NSLocalizedString("mark_highlight", comment: "") + case .markUnderline: return NSLocalizedString("mark_underline", comment: "") + case .markStrikeOut: return NSLocalizedString("mark_strike_out", comment: "") + case .markSquiggly: return NSLocalizedString("mark_squiggly", comment: "") + case .markDraw: return NSLocalizedString("mark_draw", comment: "") + case .markColor: return NSLocalizedString("mark_color", comment: "") + case .undo: return NSLocalizedString("edit_undo", comment: "") + case .redo: return NSLocalizedString("edit_redo", comment: "") + } + } + + /// The tool's own name in the page: what `odr.editing.toggle` and + /// `odr.annotation.setTool` take. + var pageName: String? { + switch self { + case .bold: return "bold" + case .italic: return "italic" + case .underline: return "underline" + case .strikethrough: return "strikethrough" + case .markHighlight: return "highlight" + case .markUnderline: return "underline" + case .markStrikeOut: return "strikeOut" + case .markSquiggly: return "squiggly" + case .markDraw: return "ink" + default: return nil + } + } + + /// The tools that go past typing inside a paragraph - see + /// ``Features/advancedEditing``. + var isAdvanced: Bool { + switch self { + case .undo, .redo: return false + default: return true + } + } + + /// Whether the button opens a menu rather than acting at once. + var opensMenu: Bool { + switch self { + case .textColor, .highlight, .fontSize, .markColor: return true + default: return false + } + } + } + + /// What the row holds, by what the page is. + enum Layout { + /// a text document, a presentation or a plain text file + case text + /// a spreadsheet or a plain text file: nothing to format, so only the + /// way back + case plain + /// a pdf, which takes marks + case pdf + + var tools: [Tool] { + switch self { + case .text: + return [ + .bold, .italic, .underline, .strikethrough, .textColor, .highlight, .fontSize, + .undo, .redo, + ] + case .plain: + return [.undo, .redo] + case .pdf: + return [.markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw, .markColor, .undo] + } + } + } + + /// A pick from one of the menus. + enum Choice { + /// `#rrggbb`, or nil for no highlight + case color(String?) + /// the system picker, for a colour the menu does not list + case customColor + /// in points + case size(Int) + } + + /// A colour the menus offer. + struct Swatch { + let name: String + let hex: String + } + + static let textColors = [ + Swatch(name: "color_black", hex: "#191c1e"), + Swatch(name: "color_red", hex: "#e53935"), + Swatch(name: "color_blue", hex: "#1e88e5"), + Swatch(name: "color_green", hex: "#43a047"), + ] + + static let highlightColors = [ + Swatch(name: "color_yellow", hex: "#fff59d"), + Swatch(name: "color_green", hex: "#c5e1a5"), + Swatch(name: "color_pink", hex: "#f8bbd0"), + Swatch(name: "color_blue", hex: "#b3e5fc"), + ] + + static let markColors = [ + Swatch(name: "color_yellow", hex: "#ffe633"), + Swatch(name: "color_red", hex: "#e53935"), + Swatch(name: "color_blue", hex: "#1e88e5"), + Swatch(name: "color_green", hex: "#43a047"), + ] + + static let fontSizes = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48] + + static let height: CGFloat = 44 + + /// The tools shown; nil shows none and hides the row. + var layout: Layout? { + didSet { + rebuild() + } + } + + /// Whether the menus open: in a build without the advanced editing a tap + /// goes to `onTap` instead, which says what Pro is. + var menusEnabled = true { + didSet { + rebuild() + } + } + + var onTap: ((Tool) -> Void)? + var onChoice: ((Tool, Choice) -> Void)? + + private let scrollView = UIScrollView() + private let stack = UIStackView() + private var buttons: [Tool: UIButton] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + configure() + } + + private func configure() { + backgroundColor = .secondarySystemBackground + + scrollView.showsHorizontalScrollIndicator = false + scrollView.alwaysBounceHorizontal = false + scrollView.translatesAutoresizingMaskIntoConstraints = false + addSubview(scrollView) + + stack.axis = .horizontal + stack.spacing = 4 + stack.alignment = .center + stack.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stack) + + NSLayoutConstraint.activate([ + scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), + scrollView.topAnchor.constraint(equalTo: topAnchor), + scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), + scrollView.heightAnchor.constraint(equalToConstant: Self.height), + + stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 8), + stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -8), + stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + stack.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor), + ]) + } + + private func rebuild() { + for view in stack.arrangedSubviews { + stack.removeArrangedSubview(view) + view.removeFromSuperview() + } + buttons = [:] + + guard let layout else { + isHidden = true + + return + } + + isHidden = false + + for tool in layout.tools { + let button = makeButton(for: tool) + buttons[tool] = button + stack.addArrangedSubview(button) + } + } + + private func makeButton(for tool: Tool) -> UIButton { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: tool.symbol) + configuration.cornerStyle = .capsule + configuration.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10) + + let button = UIButton(configuration: configuration) + button.accessibilityLabel = tool.label + button.accessibilityIdentifier = "edit-tool-\(tool.symbol)" + // filled while it is the mode, as the pen on the website is + button.configurationUpdateHandler = { button in + var configuration = button.configuration + if button.isSelected { + configuration?.background.backgroundColor = button.tintColor + configuration?.baseForegroundColor = .white + } else { + configuration?.background.backgroundColor = .clear + configuration?.baseForegroundColor = button.tintColor + } + button.configuration = configuration + } + + if tool.opensMenu, menusEnabled { + button.menu = makeMenu(for: tool) + button.showsMenuAsPrimaryAction = true + } else { + button.addAction( + UIAction { [weak self] _ in + self?.onTap?(tool) + }, for: .touchUpInside) + } + + return button + } + + private func makeMenu(for tool: Tool) -> UIMenu { + switch tool { + case .fontSize: + return UIMenu( + title: tool.label, + children: Self.fontSizes.map { size in + UIAction(title: "\(size)") { [weak self] _ in + self?.onChoice?(tool, .size(size)) + } + }) + + case .textColor: + return colorMenu(for: tool, swatches: Self.textColors, offersNone: false) + case .highlight: + return colorMenu(for: tool, swatches: Self.highlightColors, offersNone: true) + case .markColor: + return colorMenu(for: tool, swatches: Self.markColors, offersNone: false) + + default: + return UIMenu() + } + } + + private func colorMenu(for tool: Tool, swatches: [Swatch], offersNone: Bool) -> UIMenu { + var children: [UIMenuElement] = swatches.map { swatch in + UIAction( + title: NSLocalizedString(swatch.name, comment: ""), + image: Self.swatchImage(UIColor(hex: swatch.hex)) + ) { [weak self] _ in + self?.onChoice?(tool, .color(swatch.hex)) + } + } + + if offersNone { + children.append( + UIAction( + title: NSLocalizedString("color_none", comment: ""), image: UIImage(systemName: "circle.slash") + ) { [weak self] _ in + self?.onChoice?(tool, .color(nil)) + }) + } + + children.append( + UIAction(title: NSLocalizedString("color_custom", comment: ""), image: UIImage(systemName: "paintpalette")) + { [weak self] _ in + self?.onChoice?(tool, .customColor) + }) + + return UIMenu(title: tool.label, children: children) + } + + /// A filled circle, so the menu shows the colour it names. + private static func swatchImage(_ color: UIColor) -> UIImage { + let size = CGSize(width: 20, height: 20) + + return UIGraphicsImageRenderer(size: size).image { context in + color.setFill() + context.cgContext.fillEllipse(in: CGRect(origin: .zero, size: size)) + }.withRenderingMode(.alwaysOriginal) + } + + /// Whether `tool` is drawn as the mode: a style the selection shows, or + /// the armed marker on a pdf. + func setPressed(_ tool: Tool, _ pressed: Bool) { + buttons[tool]?.isSelected = pressed + } + + func setEnabled(_ tool: Tool, _ enabled: Bool) { + buttons[tool]?.isEnabled = enabled + } + + /// For the tests: whether the row shows `tool`. + func shows(_ tool: Tool) -> Bool { + buttons[tool] != nil + } + + func isPressed(_ tool: Tool) -> Bool { + buttons[tool]?.isSelected ?? false + } +} + +extension UIColor { + + /// From `#rrggbb`. + convenience init(hex: String) { + var value: UInt64 = 0 + Scanner(string: String(hex.dropFirst())).scanHexInt64(&value) + + self.init( + red: CGFloat((value >> 16) & 0xff) / 255, + green: CGFloat((value >> 8) & 0xff) / 255, + blue: CGFloat(value & 0xff) / 255, + alpha: 1) + } + + /// As `#rrggbb`, which is what the page takes. + var hexString: String { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + getRed(&red, green: &green, blue: &blue, alpha: nil) + + return String(format: "#%02x%02x%02x", Int(red * 255), Int(green * 255), Int(blue * 255)) + } + + /// As the `[r, g, b]` in 0...1 that `odr.annotation.setColor` takes. + var deviceRGB: [Double] { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + getRed(&red, green: &green, blue: &blue, alpha: nil) + + return [Double(red), Double(green), Double(blue)] + } +} diff --git a/OpenDocumentReader/Features.swift b/OpenDocumentReader/Features.swift index a064afc9..352398b5 100644 --- a/OpenDocumentReader/Features.swift +++ b/OpenDocumentReader/Features.swift @@ -5,4 +5,8 @@ enum Features { /// The ad banner and the consent form in front of it: Lite only. static var withAds: Bool { LINKS_ADS } + + /// The editing that goes past typing inside a paragraph: formatting, new + /// and joined paragraphs, and marks on a pdf. Pro is the build without ads. + static var advancedEditing: Bool { !LINKS_ADS } } diff --git a/OpenDocumentReader/Main.storyboard b/OpenDocumentReader/Main.storyboard index 2205f5a4..9e345f62 100644 --- a/OpenDocumentReader/Main.storyboard +++ b/OpenDocumentReader/Main.storyboard @@ -106,6 +106,7 @@ + diff --git a/OpenDocumentReader/ca.lproj/Localizable.strings b/OpenDocumentReader/ca.lproj/Localizable.strings index 63fa9ad5..4712c05a 100644 --- a/OpenDocumentReader/ca.lproj/Localizable.strings +++ b/OpenDocumentReader/ca.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Format de fitxer no compatible. Proveu d'obrir-lo amb una altra aplicació."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Negreta"; +"edit_italic" = "Cursiva"; +"edit_underline" = "Subratllat"; +"edit_strikethrough" = "Ratllat"; +"edit_text_color" = "Color del text"; +"edit_highlight" = "Ressaltar"; +"edit_font_size" = "Mida del text"; +"edit_undo" = "Desfés"; +"edit_redo" = "Refés"; + +/* The marks a PDF takes */ +"mark_pdf" = "Marca aquest PDF"; +"mark_highlight" = "Ressalta"; +"mark_underline" = "Subratlla"; +"mark_strike_out" = "Ratlla"; +"mark_squiggly" = "Subratllat ondulat"; +"mark_draw" = "Dibuixa"; +"mark_color" = "Color de la marca"; + +/* The colors the menus offer */ +"color_black" = "Negre"; +"color_red" = "Vermell"; +"color_blue" = "Blau"; +"color_green" = "Verd"; +"color_yellow" = "Groc"; +"color_pink" = "Rosa"; +"color_none" = "Sense ressaltat"; +"color_custom" = "Un altre color…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Aquesta cel·la conté una fórmula i es queda com està."; +"edit_refused_formula_input" = "Escriure una fórmula encara no és compatible."; +"edit_refused_rich" = "Aquesta cel·la conté més que text simple i es queda com està."; +"edit_refused_generic" = "Aquesta edició no és possible aquí."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Part de Pro"; +"pro_feature_formatting" = "Formatar el text i afegir o unir paràgrafs forma part d’OpenDocument Reader Pro."; +"pro_feature_pdf" = "Marcar un PDF forma part d’OpenDocument Reader Pro."; +"not_now" = "Ara no"; diff --git a/OpenDocumentReader/cs.lproj/Localizable.strings b/OpenDocumentReader/cs.lproj/Localizable.strings index c38a5196..8b29ba92 100644 --- a/OpenDocumentReader/cs.lproj/Localizable.strings +++ b/OpenDocumentReader/cs.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Nepodporovaný formát souboru. Zkuste jej otevřít v jiné aplikaci."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Tučné"; +"edit_italic" = "Kurzíva"; +"edit_underline" = "Podtržené"; +"edit_strikethrough" = "Přeškrtnuté"; +"edit_text_color" = "Barva textu"; +"edit_highlight" = "Zvýraznit"; +"edit_font_size" = "Velikost textu"; +"edit_undo" = "Zpět"; +"edit_redo" = "Znovu"; + +/* The marks a PDF takes */ +"mark_pdf" = "Označit tento PDF"; +"mark_highlight" = "Zvýraznit"; +"mark_underline" = "Podtrhnout"; +"mark_strike_out" = "Přeškrtnout"; +"mark_squiggly" = "Vlnité podtržení"; +"mark_draw" = "Kreslit"; +"mark_color" = "Barva značky"; + +/* The colors the menus offer */ +"color_black" = "Černá"; +"color_red" = "Červená"; +"color_blue" = "Modrá"; +"color_green" = "Zelená"; +"color_yellow" = "Žlutá"; +"color_pink" = "Růžová"; +"color_none" = "Bez zvýraznění"; +"color_custom" = "Jiná barva…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Tato buňka obsahuje vzorec a zůstává beze změny."; +"edit_refused_formula_input" = "Zadávání vzorců zatím není podporováno."; +"edit_refused_rich" = "Tato buňka obsahuje víc než prostý text a zůstává beze změny."; +"edit_refused_generic" = "Tato úprava tu není možná."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Součást Pro"; +"pro_feature_formatting" = "Formátování textu a přidávání nebo spojování odstavců je součástí OpenDocument Reader Pro."; +"pro_feature_pdf" = "Označování PDF je součástí OpenDocument Reader Pro."; +"not_now" = "Teď ne"; diff --git a/OpenDocumentReader/da.lproj/Localizable.strings b/OpenDocumentReader/da.lproj/Localizable.strings index 52dd76ca..e2c28b32 100644 --- a/OpenDocumentReader/da.lproj/Localizable.strings +++ b/OpenDocumentReader/da.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Filformatet understøttes ikke. Prøv at åbne filen i en anden app."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Fed"; +"edit_italic" = "Kursiv"; +"edit_underline" = "Understreget"; +"edit_strikethrough" = "Gennemstreget"; +"edit_text_color" = "Tekstfarve"; +"edit_highlight" = "Fremhæv"; +"edit_font_size" = "Tekststørrelse"; +"edit_undo" = "Fortryd"; +"edit_redo" = "Gentag"; + +/* The marks a PDF takes */ +"mark_pdf" = "Markér denne PDF"; +"mark_highlight" = "Fremhæv"; +"mark_underline" = "Understreg"; +"mark_strike_out" = "Gennemstreg"; +"mark_squiggly" = "Bølget understregning"; +"mark_draw" = "Tegn"; +"mark_color" = "Markeringsfarve"; + +/* The colors the menus offer */ +"color_black" = "Sort"; +"color_red" = "Rød"; +"color_blue" = "Blå"; +"color_green" = "Grøn"; +"color_yellow" = "Gul"; +"color_pink" = "Lyserød"; +"color_none" = "Ingen fremhævning"; +"color_custom" = "Anden farve…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Den celle indeholder en formel og forbliver, som den er."; +"edit_refused_formula_input" = "Indtastning af formler understøttes ikke endnu."; +"edit_refused_rich" = "Den celle indeholder mere end ren tekst og forbliver, som den er."; +"edit_refused_generic" = "Den redigering er ikke mulig her."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "En del af Pro"; +"pro_feature_formatting" = "Formatering af tekst samt tilføjelse eller sammenføjning af afsnit er en del af OpenDocument Reader Pro."; +"pro_feature_pdf" = "Markering af PDF er en del af OpenDocument Reader Pro."; +"not_now" = "Ikke nu"; diff --git a/OpenDocumentReader/de.lproj/Localizable.strings b/OpenDocumentReader/de.lproj/Localizable.strings index 983a2006..1ff88e93 100644 --- a/OpenDocumentReader/de.lproj/Localizable.strings +++ b/OpenDocumentReader/de.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Dateiformat wird nicht unterstützt. Versuchen Sie, die Datei in einer anderen App zu öffnen."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Fett"; +"edit_italic" = "Kursiv"; +"edit_underline" = "Unterstrichen"; +"edit_strikethrough" = "Durchgestrichen"; +"edit_text_color" = "Textfarbe"; +"edit_highlight" = "Hervorheben"; +"edit_font_size" = "Textgröße"; +"edit_undo" = "Rückgängig"; +"edit_redo" = "Wiederholen"; + +/* The marks a PDF takes */ +"mark_pdf" = "Dieses PDF markieren"; +"mark_highlight" = "Hervorheben"; +"mark_underline" = "Unterstreichen"; +"mark_strike_out" = "Durchstreichen"; +"mark_squiggly" = "Wellenlinie"; +"mark_draw" = "Zeichnen"; +"mark_color" = "Markierungsfarbe"; + +/* The colors the menus offer */ +"color_black" = "Schwarz"; +"color_red" = "Rot"; +"color_blue" = "Blau"; +"color_green" = "Grün"; +"color_yellow" = "Gelb"; +"color_pink" = "Rosa"; +"color_none" = "Keine Hervorhebung"; +"color_custom" = "Andere Farbe…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Diese Zelle enthält eine Formel und bleibt, wie sie ist."; +"edit_refused_formula_input" = "Formeln eingeben wird noch nicht unterstützt."; +"edit_refused_rich" = "Diese Zelle enthält mehr als reinen Text und bleibt, wie sie ist."; +"edit_refused_generic" = "Diese Änderung ist hier nicht möglich."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Teil von Pro"; +"pro_feature_formatting" = "Text formatieren sowie Absätze einfügen oder zusammenführen ist Teil von OpenDocument Reader Pro."; +"pro_feature_pdf" = "PDFs markieren ist Teil von OpenDocument Reader Pro."; +"not_now" = "Nicht jetzt"; diff --git a/OpenDocumentReader/en.lproj/Localizable.strings b/OpenDocumentReader/en.lproj/Localizable.strings index 37636ca5..cfd7375f 100644 --- a/OpenDocumentReader/en.lproj/Localizable.strings +++ b/OpenDocumentReader/en.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Unsupported file format. Try opening it in another app."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Bold"; +"edit_italic" = "Italic"; +"edit_underline" = "Underline"; +"edit_strikethrough" = "Strikethrough"; +"edit_text_color" = "Text color"; +"edit_highlight" = "Highlight"; +"edit_font_size" = "Text size"; +"edit_undo" = "Undo"; +"edit_redo" = "Redo"; + +/* The marks a PDF takes */ +"mark_pdf" = "Mark up this PDF"; +"mark_highlight" = "Highlight"; +"mark_underline" = "Underline"; +"mark_strike_out" = "Strike out"; +"mark_squiggly" = "Squiggly underline"; +"mark_draw" = "Draw"; +"mark_color" = "Mark color"; + +/* The colors the menus offer */ +"color_black" = "Black"; +"color_red" = "Red"; +"color_blue" = "Blue"; +"color_green" = "Green"; +"color_yellow" = "Yellow"; +"color_pink" = "Pink"; +"color_none" = "No highlight"; +"color_custom" = "Other color…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "That cell holds a formula and stays as it is."; +"edit_refused_formula_input" = "Typing a formula is not supported yet."; +"edit_refused_rich" = "That cell holds more than plain text and stays as it is."; +"edit_refused_generic" = "That edit is not possible here."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Part of Pro"; +"pro_feature_formatting" = "Formatting text, and adding or joining paragraphs, is part of OpenDocument Reader Pro."; +"pro_feature_pdf" = "Marking up a PDF is part of OpenDocument Reader Pro."; +"not_now" = "Not now"; diff --git a/OpenDocumentReader/es.lproj/Localizable.strings b/OpenDocumentReader/es.lproj/Localizable.strings index 923e0465..0e320b5b 100644 --- a/OpenDocumentReader/es.lproj/Localizable.strings +++ b/OpenDocumentReader/es.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Formato de archivo no compatible. Pruebe a abrirlo en otra aplicación."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Negrita"; +"edit_italic" = "Cursiva"; +"edit_underline" = "Subrayado"; +"edit_strikethrough" = "Tachado"; +"edit_text_color" = "Color del texto"; +"edit_highlight" = "Resaltar"; +"edit_font_size" = "Tamaño del texto"; +"edit_undo" = "Deshacer"; +"edit_redo" = "Rehacer"; + +/* The marks a PDF takes */ +"mark_pdf" = "Marcar este PDF"; +"mark_highlight" = "Resaltar"; +"mark_underline" = "Subrayar"; +"mark_strike_out" = "Tachar"; +"mark_squiggly" = "Subrayado ondulado"; +"mark_draw" = "Dibujar"; +"mark_color" = "Color de la marca"; + +/* The colors the menus offer */ +"color_black" = "Negro"; +"color_red" = "Rojo"; +"color_blue" = "Azul"; +"color_green" = "Verde"; +"color_yellow" = "Amarillo"; +"color_pink" = "Rosa"; +"color_none" = "Sin resaltado"; +"color_custom" = "Otro color…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Esa celda contiene una fórmula y se queda como está."; +"edit_refused_formula_input" = "Escribir una fórmula aún no es compatible."; +"edit_refused_rich" = "Esa celda contiene más que texto sin formato y se queda como está."; +"edit_refused_generic" = "Esa edición no es posible aquí."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Parte de Pro"; +"pro_feature_formatting" = "Dar formato al texto y añadir o unir párrafos forma parte de OpenDocument Reader Pro."; +"pro_feature_pdf" = "Marcar un PDF forma parte de OpenDocument Reader Pro."; +"not_now" = "Ahora no"; diff --git a/OpenDocumentReader/fr.lproj/Localizable.strings b/OpenDocumentReader/fr.lproj/Localizable.strings index 9018f7dd..5c423150 100644 --- a/OpenDocumentReader/fr.lproj/Localizable.strings +++ b/OpenDocumentReader/fr.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Le format de ce fichier n'est pas pris en charge. Essayez de l'ouvrir avec une autre application."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Gras"; +"edit_italic" = "Italique"; +"edit_underline" = "Souligné"; +"edit_strikethrough" = "Barré"; +"edit_text_color" = "Couleur du texte"; +"edit_highlight" = "Surligner"; +"edit_font_size" = "Taille du texte"; +"edit_undo" = "Annuler"; +"edit_redo" = "Rétablir"; + +/* The marks a PDF takes */ +"mark_pdf" = "Annoter ce PDF"; +"mark_highlight" = "Surligner"; +"mark_underline" = "Souligner"; +"mark_strike_out" = "Barrer"; +"mark_squiggly" = "Soulignement ondulé"; +"mark_draw" = "Dessiner"; +"mark_color" = "Couleur de l’annotation"; + +/* The colors the menus offer */ +"color_black" = "Noir"; +"color_red" = "Rouge"; +"color_blue" = "Bleu"; +"color_green" = "Vert"; +"color_yellow" = "Jaune"; +"color_pink" = "Rose"; +"color_none" = "Aucun surlignage"; +"color_custom" = "Autre couleur…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Cette cellule contient une formule et reste telle quelle."; +"edit_refused_formula_input" = "La saisie d’une formule n’est pas encore prise en charge."; +"edit_refused_rich" = "Cette cellule contient plus que du texte brut et reste telle quelle."; +"edit_refused_generic" = "Cette modification n’est pas possible ici."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Réservé à Pro"; +"pro_feature_formatting" = "La mise en forme du texte, ainsi que l’ajout ou la fusion de paragraphes, fait partie d’OpenDocument Reader Pro."; +"pro_feature_pdf" = "L’annotation de PDF fait partie d’OpenDocument Reader Pro."; +"not_now" = "Pas maintenant"; diff --git a/OpenDocumentReader/ga.lproj/Localizable.strings b/OpenDocumentReader/ga.lproj/Localizable.strings index f60011d1..51879bc8 100644 --- a/OpenDocumentReader/ga.lproj/Localizable.strings +++ b/OpenDocumentReader/ga.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Ní thacaítear leis an bhformáid chomhaid seo. Bain triail as an gcomhad a oscailt in aip eile."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Trom"; +"edit_italic" = "Iodálach"; +"edit_underline" = "Líne faoi"; +"edit_strikethrough" = "Líne tríd"; +"edit_text_color" = "Dath an téacs"; +"edit_highlight" = "Aibhsigh"; +"edit_font_size" = "Méid an téacs"; +"edit_undo" = "Cealaigh"; +"edit_redo" = "Athdhéan"; + +/* The marks a PDF takes */ +"mark_pdf" = "Marcáil an PDF seo"; +"mark_highlight" = "Aibhsigh"; +"mark_underline" = "Cuir líne faoi"; +"mark_strike_out" = "Cuir líne tríd"; +"mark_squiggly" = "Líne thonnach faoi"; +"mark_draw" = "Tarraing"; +"mark_color" = "Dath na marcála"; + +/* The colors the menus offer */ +"color_black" = "Dubh"; +"color_red" = "Dearg"; +"color_blue" = "Gorm"; +"color_green" = "Glas"; +"color_yellow" = "Buí"; +"color_pink" = "Bándearg"; +"color_none" = "Gan aibhsiú"; +"color_custom" = "Dath eile…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Tá foirmle sa chill sin agus fanann sí mar atá."; +"edit_refused_formula_input" = "Ní thacaítear le foirmlí a chlóscríobh go fóill."; +"edit_refused_rich" = "Tá níos mó ná gnáth-théacs sa chill sin agus fanann sí mar atá."; +"edit_refused_generic" = "Ní féidir an t-athrú sin a dhéanamh anseo."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Cuid de Pro"; +"pro_feature_formatting" = "Is cuid de OpenDocument Reader Pro é téacs a fhormáidiú agus ailt a chur leis nó a nascadh."; +"pro_feature_pdf" = "Is cuid de OpenDocument Reader Pro é PDF a mharcáil."; +"not_now" = "Ní anois"; diff --git a/OpenDocumentReader/it.lproj/Localizable.strings b/OpenDocumentReader/it.lproj/Localizable.strings index eadefd5f..d47d8061 100644 --- a/OpenDocumentReader/it.lproj/Localizable.strings +++ b/OpenDocumentReader/it.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Formato di file non supportato. Prova ad aprirlo in un'altra app."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Grassetto"; +"edit_italic" = "Corsivo"; +"edit_underline" = "Sottolineato"; +"edit_strikethrough" = "Barrato"; +"edit_text_color" = "Colore del testo"; +"edit_highlight" = "Evidenzia"; +"edit_font_size" = "Dimensione del testo"; +"edit_undo" = "Annulla"; +"edit_redo" = "Ripeti"; + +/* The marks a PDF takes */ +"mark_pdf" = "Annota questo PDF"; +"mark_highlight" = "Evidenzia"; +"mark_underline" = "Sottolinea"; +"mark_strike_out" = "Barra"; +"mark_squiggly" = "Sottolineatura ondulata"; +"mark_draw" = "Disegna"; +"mark_color" = "Colore dell’annotazione"; + +/* The colors the menus offer */ +"color_black" = "Nero"; +"color_red" = "Rosso"; +"color_blue" = "Blu"; +"color_green" = "Verde"; +"color_yellow" = "Giallo"; +"color_pink" = "Rosa"; +"color_none" = "Nessuna evidenziazione"; +"color_custom" = "Altro colore…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Quella cella contiene una formula e resta com’è."; +"edit_refused_formula_input" = "Digitare una formula non è ancora supportato."; +"edit_refused_rich" = "Quella cella contiene più di testo semplice e resta com’è."; +"edit_refused_generic" = "Questa modifica non è possibile qui."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Parte di Pro"; +"pro_feature_formatting" = "Formattare il testo e aggiungere o unire paragrafi fa parte di OpenDocument Reader Pro."; +"pro_feature_pdf" = "Annotare un PDF fa parte di OpenDocument Reader Pro."; +"not_now" = "Non ora"; diff --git a/OpenDocumentReader/ja.lproj/Localizable.strings b/OpenDocumentReader/ja.lproj/Localizable.strings index 0fdd5829..780661ba 100644 --- a/OpenDocumentReader/ja.lproj/Localizable.strings +++ b/OpenDocumentReader/ja.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "対応していないファイル形式です。別のアプリで開いてみてください。"; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "太字"; +"edit_italic" = "斜体"; +"edit_underline" = "下線"; +"edit_strikethrough" = "取り消し線"; +"edit_text_color" = "文字の色"; +"edit_highlight" = "ハイライト"; +"edit_font_size" = "文字サイズ"; +"edit_undo" = "元に戻す"; +"edit_redo" = "やり直す"; + +/* The marks a PDF takes */ +"mark_pdf" = "このPDFにマークを付ける"; +"mark_highlight" = "ハイライト"; +"mark_underline" = "下線"; +"mark_strike_out" = "取り消し線"; +"mark_squiggly" = "波線"; +"mark_draw" = "描画"; +"mark_color" = "マークの色"; + +/* The colors the menus offer */ +"color_black" = "黒"; +"color_red" = "赤"; +"color_blue" = "青"; +"color_green" = "緑"; +"color_yellow" = "黄"; +"color_pink" = "ピンク"; +"color_none" = "ハイライトなし"; +"color_custom" = "その他の色…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "そのセルには数式が入っているため、そのままになります。"; +"edit_refused_formula_input" = "数式の入力はまだ対応していません。"; +"edit_refused_rich" = "そのセルには書式付きの内容が入っているため、そのままになります。"; +"edit_refused_generic" = "この編集はここではできません。"; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Proの機能"; +"pro_feature_formatting" = "文字の書式設定と、段落の追加や結合はOpenDocument Reader Proの機能です。"; +"pro_feature_pdf" = "PDFへのマーク付けはOpenDocument Reader Proの機能です。"; +"not_now" = "あとで"; diff --git a/OpenDocumentReader/pl.lproj/Localizable.strings b/OpenDocumentReader/pl.lproj/Localizable.strings index d4d543b9..6468edff 100644 --- a/OpenDocumentReader/pl.lproj/Localizable.strings +++ b/OpenDocumentReader/pl.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Nieobsługiwany format pliku. Spróbuj otworzyć go w innej aplikacji."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Pogrubienie"; +"edit_italic" = "Kursywa"; +"edit_underline" = "Podkreślenie"; +"edit_strikethrough" = "Przekreślenie"; +"edit_text_color" = "Kolor tekstu"; +"edit_highlight" = "Wyróżnienie"; +"edit_font_size" = "Rozmiar tekstu"; +"edit_undo" = "Cofnij"; +"edit_redo" = "Ponów"; + +/* The marks a PDF takes */ +"mark_pdf" = "Oznacz ten PDF"; +"mark_highlight" = "Wyróżnij"; +"mark_underline" = "Podkreśl"; +"mark_strike_out" = "Przekreśl"; +"mark_squiggly" = "Podkreślenie faliste"; +"mark_draw" = "Rysuj"; +"mark_color" = "Kolor oznaczenia"; + +/* The colors the menus offer */ +"color_black" = "Czarny"; +"color_red" = "Czerwony"; +"color_blue" = "Niebieski"; +"color_green" = "Zielony"; +"color_yellow" = "Żółty"; +"color_pink" = "Różowy"; +"color_none" = "Bez wyróżnienia"; +"color_custom" = "Inny kolor…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Ta komórka zawiera formułę i pozostaje bez zmian."; +"edit_refused_formula_input" = "Wpisywanie formuł nie jest jeszcze obsługiwane."; +"edit_refused_rich" = "Ta komórka zawiera więcej niż zwykły tekst i pozostaje bez zmian."; +"edit_refused_generic" = "Ta zmiana nie jest tu możliwa."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Część wersji Pro"; +"pro_feature_formatting" = "Formatowanie tekstu oraz dodawanie i łączenie akapitów to część OpenDocument Reader Pro."; +"pro_feature_pdf" = "Oznaczanie PDF to część OpenDocument Reader Pro."; +"not_now" = "Nie teraz"; diff --git a/OpenDocumentReader/pt-BR.lproj/Localizable.strings b/OpenDocumentReader/pt-BR.lproj/Localizable.strings index 64abe516..6b1e3cfa 100644 --- a/OpenDocumentReader/pt-BR.lproj/Localizable.strings +++ b/OpenDocumentReader/pt-BR.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Formato de arquivo não compatível. Tente abri-lo em outro aplicativo."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Negrito"; +"edit_italic" = "Itálico"; +"edit_underline" = "Sublinhado"; +"edit_strikethrough" = "Tachado"; +"edit_text_color" = "Cor do texto"; +"edit_highlight" = "Destacar"; +"edit_font_size" = "Tamanho do texto"; +"edit_undo" = "Desfazer"; +"edit_redo" = "Refazer"; + +/* The marks a PDF takes */ +"mark_pdf" = "Marcar este PDF"; +"mark_highlight" = "Destacar"; +"mark_underline" = "Sublinhar"; +"mark_strike_out" = "Tachar"; +"mark_squiggly" = "Sublinhado ondulado"; +"mark_draw" = "Desenhar"; +"mark_color" = "Cor da marcação"; + +/* The colors the menus offer */ +"color_black" = "Preto"; +"color_red" = "Vermelho"; +"color_blue" = "Azul"; +"color_green" = "Verde"; +"color_yellow" = "Amarelo"; +"color_pink" = "Rosa"; +"color_none" = "Sem destaque"; +"color_custom" = "Outra cor…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Essa célula contém uma fórmula e fica como está."; +"edit_refused_formula_input" = "Digitar uma fórmula ainda não é possível."; +"edit_refused_rich" = "Essa célula contém mais do que texto simples e fica como está."; +"edit_refused_generic" = "Essa edição não é possível aqui."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Parte do Pro"; +"pro_feature_formatting" = "Formatar texto e adicionar ou juntar parágrafos faz parte do OpenDocument Reader Pro."; +"pro_feature_pdf" = "Marcar um PDF faz parte do OpenDocument Reader Pro."; +"not_now" = "Agora não"; diff --git a/OpenDocumentReader/ru.lproj/Localizable.strings b/OpenDocumentReader/ru.lproj/Localizable.strings index 98adb00e..209acd0d 100644 --- a/OpenDocumentReader/ru.lproj/Localizable.strings +++ b/OpenDocumentReader/ru.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Формат файла не поддерживается. Попробуйте открыть его в другом приложении."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Жирный"; +"edit_italic" = "Курсив"; +"edit_underline" = "Подчёркнутый"; +"edit_strikethrough" = "Зачёркнутый"; +"edit_text_color" = "Цвет текста"; +"edit_highlight" = "Выделение"; +"edit_font_size" = "Размер текста"; +"edit_undo" = "Отменить"; +"edit_redo" = "Повторить"; + +/* The marks a PDF takes */ +"mark_pdf" = "Разметить этот PDF"; +"mark_highlight" = "Выделить"; +"mark_underline" = "Подчеркнуть"; +"mark_strike_out" = "Зачеркнуть"; +"mark_squiggly" = "Волнистое подчёркивание"; +"mark_draw" = "Рисовать"; +"mark_color" = "Цвет пометки"; + +/* The colors the menus offer */ +"color_black" = "Чёрный"; +"color_red" = "Красный"; +"color_blue" = "Синий"; +"color_green" = "Зелёный"; +"color_yellow" = "Жёлтый"; +"color_pink" = "Розовый"; +"color_none" = "Без выделения"; +"color_custom" = "Другой цвет…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "В этой ячейке формула, она остаётся как есть."; +"edit_refused_formula_input" = "Ввод формул пока не поддерживается."; +"edit_refused_rich" = "В этой ячейке не только простой текст, она остаётся как есть."; +"edit_refused_generic" = "Такое изменение здесь невозможно."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Часть Pro"; +"pro_feature_formatting" = "Форматирование текста, а также добавление и объединение абзацев — часть OpenDocument Reader Pro."; +"pro_feature_pdf" = "Разметка PDF — часть OpenDocument Reader Pro."; +"not_now" = "Не сейчас"; diff --git a/OpenDocumentReader/sl.lproj/Localizable.strings b/OpenDocumentReader/sl.lproj/Localizable.strings index c87614ab..dd4e8162 100644 --- a/OpenDocumentReader/sl.lproj/Localizable.strings +++ b/OpenDocumentReader/sl.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Nepodprta oblika datoteke. Poskusite jo odpreti v drugi aplikaciji."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Krepko"; +"edit_italic" = "Ležeče"; +"edit_underline" = "Podčrtano"; +"edit_strikethrough" = "Prečrtano"; +"edit_text_color" = "Barva besedila"; +"edit_highlight" = "Poudari"; +"edit_font_size" = "Velikost besedila"; +"edit_undo" = "Razveljavi"; +"edit_redo" = "Ponovi"; + +/* The marks a PDF takes */ +"mark_pdf" = "Označi ta PDF"; +"mark_highlight" = "Poudari"; +"mark_underline" = "Podčrtaj"; +"mark_strike_out" = "Prečrtaj"; +"mark_squiggly" = "Valovito podčrtanje"; +"mark_draw" = "Riši"; +"mark_color" = "Barva oznake"; + +/* The colors the menus offer */ +"color_black" = "Črna"; +"color_red" = "Rdeča"; +"color_blue" = "Modra"; +"color_green" = "Zelena"; +"color_yellow" = "Rumena"; +"color_pink" = "Rožnata"; +"color_none" = "Brez poudarka"; +"color_custom" = "Druga barva…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Ta celica vsebuje formulo in ostane, kot je."; +"edit_refused_formula_input" = "Vnos formul še ni podprt."; +"edit_refused_rich" = "Ta celica vsebuje več kot navadno besedilo in ostane, kot je."; +"edit_refused_generic" = "To urejanje tu ni mogoče."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Del različice Pro"; +"pro_feature_formatting" = "Oblikovanje besedila ter dodajanje ali združevanje odstavkov je del OpenDocument Reader Pro."; +"pro_feature_pdf" = "Označevanje PDF je del OpenDocument Reader Pro."; +"not_now" = "Ne zdaj"; diff --git a/OpenDocumentReader/tr.lproj/Localizable.strings b/OpenDocumentReader/tr.lproj/Localizable.strings index f435bc82..2be08d05 100644 --- a/OpenDocumentReader/tr.lproj/Localizable.strings +++ b/OpenDocumentReader/tr.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "Desteklenmeyen dosya biçimi. Başka bir uygulamada açmayı deneyin."; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "Kalın"; +"edit_italic" = "İtalik"; +"edit_underline" = "Altı çizili"; +"edit_strikethrough" = "Üstü çizili"; +"edit_text_color" = "Metin rengi"; +"edit_highlight" = "Vurgula"; +"edit_font_size" = "Metin boyutu"; +"edit_undo" = "Geri al"; +"edit_redo" = "Yinele"; + +/* The marks a PDF takes */ +"mark_pdf" = "Bu PDF’i işaretle"; +"mark_highlight" = "Vurgula"; +"mark_underline" = "Altını çiz"; +"mark_strike_out" = "Üstünü çiz"; +"mark_squiggly" = "Dalgalı alt çizgi"; +"mark_draw" = "Çiz"; +"mark_color" = "İşaret rengi"; + +/* The colors the menus offer */ +"color_black" = "Siyah"; +"color_red" = "Kırmızı"; +"color_blue" = "Mavi"; +"color_green" = "Yeşil"; +"color_yellow" = "Sarı"; +"color_pink" = "Pembe"; +"color_none" = "Vurgu yok"; +"color_custom" = "Başka renk…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "Bu hücrede bir formül var ve olduğu gibi kalır."; +"edit_refused_formula_input" = "Formül yazma henüz desteklenmiyor."; +"edit_refused_rich" = "Bu hücrede düz metinden fazlası var ve olduğu gibi kalır."; +"edit_refused_generic" = "Bu düzenleme burada yapılamaz."; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Pro’nun parçası"; +"pro_feature_formatting" = "Metni biçimlendirmek ve paragraf eklemek ya da birleştirmek OpenDocument Reader Pro’nun bir parçasıdır."; +"pro_feature_pdf" = "PDF işaretlemek OpenDocument Reader Pro’nun bir parçasıdır."; +"not_now" = "Şimdi değil"; diff --git a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings index 531720ec..56705212 100644 --- a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings +++ b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings @@ -106,3 +106,45 @@ /* Shown for a format odrcore does not read */ "toast_error_illegal_file_reopen" = "不支持的文件格式。请尝试用其他应用打开。"; + +/* The editing tools under the bar, read by VoiceOver */ +"edit_bold" = "粗体"; +"edit_italic" = "斜体"; +"edit_underline" = "下划线"; +"edit_strikethrough" = "删除线"; +"edit_text_color" = "文字颜色"; +"edit_highlight" = "高亮"; +"edit_font_size" = "文字大小"; +"edit_undo" = "撤销"; +"edit_redo" = "重做"; + +/* The marks a PDF takes */ +"mark_pdf" = "标注此 PDF"; +"mark_highlight" = "高亮"; +"mark_underline" = "下划线"; +"mark_strike_out" = "删除线"; +"mark_squiggly" = "波浪线"; +"mark_draw" = "绘制"; +"mark_color" = "标注颜色"; + +/* The colors the menus offer */ +"color_black" = "黑色"; +"color_red" = "红色"; +"color_blue" = "蓝色"; +"color_green" = "绿色"; +"color_yellow" = "黄色"; +"color_pink" = "粉色"; +"color_none" = "无高亮"; +"color_custom" = "其他颜色…"; + +/* Why an edit was not taken */ +"edit_refused_formula" = "该单元格包含公式,保持不变。"; +"edit_refused_formula_input" = "暂不支持输入公式。"; +"edit_refused_rich" = "该单元格包含的不只是纯文本,保持不变。"; +"edit_refused_generic" = "此处无法进行该编辑。"; + +/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +"pro_feature_title" = "Pro 功能"; +"pro_feature_formatting" = "设置文字格式以及添加或合并段落是 OpenDocument Reader Pro 的功能。"; +"pro_feature_pdf" = "标注 PDF 是 OpenDocument Reader Pro 的功能。"; +"not_now" = "暂不"; diff --git a/OpenDocumentReaderTests/ArchiveDocumentTests.swift b/OpenDocumentReaderTests/ArchiveDocumentTests.swift index 9673e9fb..0d376dd6 100644 --- a/OpenDocumentReaderTests/ArchiveDocumentTests.swift +++ b/OpenDocumentReaderTests/ArchiveDocumentTests.swift @@ -43,7 +43,7 @@ class ArchiveDocumentTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["files"]) } @@ -53,7 +53,7 @@ class ArchiveDocumentTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertFalse(wrapper.isEditable) } @@ -63,7 +63,7 @@ class ArchiveDocumentTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let listingURL = try XCTUnwrap(wrapper.pageURLs.first) let (data, _) = try fetch(listingURL) diff --git a/OpenDocumentReaderTests/EditWorkflowTests.swift b/OpenDocumentReaderTests/EditWorkflowTests.swift index 4b0d7f4d..0759627a 100644 --- a/OpenDocumentReaderTests/EditWorkflowTests.swift +++ b/OpenDocumentReaderTests/EditWorkflowTests.swift @@ -84,8 +84,9 @@ class EditWorkflowTests: XCTestCase { /// A document nothing can be written back to keeps the room for itself. func testACsvOffersNoEditButton() throws { - try present(try copyFixture(ofType: "csv")) - openDocument() + documentURL = try copyFixture(ofType: "csv") + try present(documentURL) + openDocument(where: "typeof odr === 'object'") XCTAssertFalse(controller.document?.isEditable ?? true) XCTAssertFalse(barContains(controller.editButton)) @@ -93,9 +94,9 @@ class EditWorkflowTests: XCTestCase { // MARK: - the page - /// The one thing edit mode is for. odrcore marks the text runs - /// `contenteditable`, but a tap has to reach one for the caret to be set and - /// the keyboard to unfold. + /// The one thing edit mode is for. odrcore makes the flow `contenteditable`, + /// but a tap has to reach a run for the caret to be set and the keyboard to + /// unfold. func testTappingTheTextReachesTheEditableRun() throws { openDocument() @@ -106,7 +107,7 @@ class EditWorkflowTests: XCTestCase { evaluate( """ (function () { - var run = document.querySelector('[contenteditable]'); + var run = document.querySelector('x-s[data-odr-id]'); // not getBoundingClientRect: the view applies a zoom to fit, // which webkit leaves out of it but elementFromPoint expects var box = odr.getViewportRect(run); @@ -129,7 +130,110 @@ class EditWorkflowTests: XCTestCase { typeIntoTheFirstRun() - XCTAssertEqual(evaluate("document.querySelector('[contenteditable]').innerText") as? String, Self.editedText) + let text = evaluate("document.querySelector('x-s[data-odr-id]').textContent") as? String ?? "" + XCTAssertTrue(text.contains(Self.editedText), text) + } + + // MARK: - the tools + + /// The row under the bar: formatting for a text document, once the page + /// says it is editable, and gone again with the edit. + func testATextDocumentShowsTheFormattingToolsWhileEditing() throws { + openDocument() + + XCTAssertNil(controller.editToolBar.layout) + + controller.editOrSave(controller.editButton) + waitForEditablePage() + waitForTools() + + XCTAssertEqual(controller.editToolBar.layout, .text) + XCTAssertTrue(controller.editToolBar.shows(.bold)) + XCTAssertTrue(controller.editToolBar.shows(.undo)) + + controller.discardChanges() + waitForPage(where: "document.querySelectorAll('x-s').length > 0") + + XCTAssertNil(controller.editToolBar.layout) + } + + /// The cells are the editor, so a spreadsheet gets only the way back. + func testASpreadsheetShowsOnlyUndoAndRedo() throws { + documentURL = try copyFixture(ofType: "ods") + try present(documentURL) + openDocument(where: "document.querySelectorAll('td').length > 0") + + controller.editOrSave(controller.editButton) + waitForTools() + + XCTAssertEqual(controller.editToolBar.layout, .plain) + XCTAssertFalse(controller.editToolBar.shows(.bold)) + XCTAssertTrue(controller.editToolBar.shows(.undo)) + } + + /// A style the caret sits in is shown pressed, the way the page reports it. + func testTheSelectionStyleReachesTheButtons() throws { + openDocument() + + controller.editOrSave(controller.editButton) + waitForEditablePage() + waitForTools() + + _ = evaluate("odr.onSelectionChange({ bold: true, italic: false })") + waitUntil { self.controller.editToolBar.isPressed(.bold) } + + XCTAssertFalse(controller.editToolBar.isPressed(.italic)) + } + + // MARK: - a pdf + + /// The pencil is a highlighter on a pdf, and the edit is a set of marks. + func testAPdfOffersMarksAndSavesThem() throws { + documentURL = try copyFixture(ofType: "pdf") + try present(documentURL) + openDocument(where: "document.querySelectorAll('[data-odr-space]').length > 0") + + XCTAssertTrue(document.isAnnotatable) + XCTAssertTrue(barContains(controller.editButton)) + XCTAssertEqual(controller.editButton.image, UIImage(systemName: "highlighter")) + + let sizeBefore = try fileSize() + + controller.editOrSave(controller.editButton) + waitForPage(where: "document.querySelectorAll('[data-odr-space]').length > 0") + waitForTools() + + XCTAssertEqual(controller.editToolBar.layout, .pdf) + XCTAssertTrue(controller.editToolBar.shows(.markHighlight)) + XCTAssertFalse(controller.editToolBar.shows(.redo)) + + let marks = + evaluate( + """ + (function () { + var page = document.querySelector('[data-odr-space]'); + var range = document.createRange(); + range.selectNodeContents(page); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + odr.annotation.setTool('highlight'); + odr.annotation.mark(); + return odr.annotation.list().length; + })() + """) as? Int ?? 0 + XCTAssertGreaterThan(marks, 0) + + let saved = expectation(description: "saved") + controller.saveContent { success in + XCTAssertTrue(success) + saved.fulfill() + } + wait(for: [saved], timeout: 60) + + // an incremental update: the marks are written after the file as it was + XCTAssertGreaterThan(try fileSize(), sizeBefore) + XCTAssertNoThrow(try reopenedText()) } // MARK: - the save @@ -192,7 +296,7 @@ class EditWorkflowTests: XCTestCase { (controller.toolBar.items ?? []).contains { $0 === item } } - private func openDocument() { + private func openDocument(where condition: String = "document.querySelectorAll('x-s').length > 0") { let opened = expectation(description: "opened") document.open { success in XCTAssertTrue(success) @@ -200,7 +304,33 @@ class EditWorkflowTests: XCTestCase { } wait(for: [opened], timeout: 60) - waitForPage(where: "document.querySelectorAll('x-s').length > 0") + waitForPage(where: condition) + } + + /// The tools appear once the editable page has answered what it is. + private func waitForTools(file: StaticString = #filePath, line: UInt = #line) { + waitUntil(file: file, line: line) { self.controller.editToolBar.layout != nil } + } + + /// A message from the page lands on a later turn of the run loop. + private func waitUntil( + file: StaticString = #filePath, line: UInt = #line, _ condition: () -> Bool + ) { + let deadline = Date().addingTimeInterval(60) + + while Date() < deadline { + if condition() { return } + + _ = XCTWaiter.wait(for: [expectation(description: "a turn of the run loop")], timeout: 0.1) + } + + XCTFail("timed out waiting for the controller", file: file, line: line) + } + + private func fileSize() throws -> Int { + let attributes = try FileManager.default.attributesOfItem(atPath: documentURL.path) + + return attributes[.size] as? Int ?? 0 } private func waitForEditablePage() { @@ -224,21 +354,30 @@ class EditWorkflowTests: XCTestCase { XCTFail("timed out waiting for \(condition)", file: file, line: line) } - /// A change to the text node, which is what typing amounts to: odrcore's - /// script watches for `characterData` and notes the run it belongs to. + /// What typing amounts to: the caret in a run, and a `beforeinput` the + /// editor takes and applies itself - the same way odrcore's own tests type. private func typeIntoTheFirstRun() { _ = evaluate( """ (function () { - var run = document.querySelector('[contenteditable]'); - run.focus(); - run.firstChild.data = '\(Self.editedText)'; + var run = document.querySelector('x-s[data-odr-id]'); + var range = document.createRange(); + range.setStart(run.firstChild, 0); + range.collapse(true); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + run.dispatchEvent(new InputEvent('beforeinput', { + inputType: 'insertText', + data: '\(Self.editedText) ', + bubbles: true, + cancelable: true + })); })() """) - // the mutation is reported in a microtask, so the diff is only complete - // on the next turn - _ = XCTWaiter.wait(for: [expectation(description: "the observer to run")], timeout: 0.5) + // the log is reported on the next turn + _ = XCTWaiter.wait(for: [expectation(description: "the editor to log it")], timeout: 0.5) } /// Errors are swallowed: a page that is not there yet is what the polling @@ -263,7 +402,7 @@ class EditWorkflowTests: XCTestCase { let temporaryDirectory = NSTemporaryDirectory() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let url = try XCTUnwrap(wrapper.pageURLs.first) diff --git a/OpenDocumentReaderTests/LockedDocumentTests.swift b/OpenDocumentReaderTests/LockedDocumentTests.swift index b7233440..eb4dd91d 100644 --- a/OpenDocumentReaderTests/LockedDocumentTests.swift +++ b/OpenDocumentReaderTests/LockedDocumentTests.swift @@ -49,8 +49,8 @@ class LockedDocumentTests: XCTestCase { for password in [nil, "secret"] { XCTAssertThrowsError( try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: password, - editable: false) + documentURL.path, into: temporaryDirectory, with: password, + editable: false, scope: .document) ) { error in XCTAssertEqual((error as NSError).code, CoreWrapperError.undecryptable.rawValue) } diff --git a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift index 6e2975df..3f5a511e 100644 --- a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift +++ b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift @@ -42,7 +42,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertFalse(wrapper.pageURLs.isEmpty) XCTAssertEqual(wrapper.pageURLs.count, wrapper.pageNames.count) @@ -53,7 +53,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["document"]) } @@ -65,7 +65,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "ods") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["Alpha", "Beta", "Gamma"]) } @@ -77,7 +77,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "odp") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["document"]) } @@ -88,7 +88,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "csv") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["document"]) } @@ -99,7 +99,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "pdf") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["document"]) } @@ -110,7 +110,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "pdf") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let (data, _) = try fetch(try XCTUnwrap(wrapper.pageURLs.first)) let html = try XCTUnwrap(String(data: data, encoding: .utf8)) @@ -128,7 +128,7 @@ class OpenDocumentReaderTests: XCTestCase { for password in [nil, "wrong"] { XCTAssertThrowsError( try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: password, editable: false) + url.path, into: temporaryDirectory, with: password, editable: false, scope: .document) ) { error in XCTAssertEqual((error as NSError).code, CoreWrapperError.wrongPassword.rawValue) } @@ -140,7 +140,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "pdf", named: "test-encrypted") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: "secret", editable: false) + url.path, into: temporaryDirectory, with: "secret", editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["document"]) @@ -153,7 +153,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "pdf") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + url.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertFalse(wrapper.isEditable) } @@ -166,7 +166,7 @@ class OpenDocumentReaderTests: XCTestCase { try Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]).write(to: image) try wrapper.translate( - image.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + image.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["image"]) } @@ -177,7 +177,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "csv") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + url.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertFalse(wrapper.isEditable) } @@ -187,7 +187,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertTrue(wrapper.isEditable) } @@ -199,7 +199,7 @@ class OpenDocumentReaderTests: XCTestCase { let url = try copyFixture(ofType: "ods") try wrapper.translate( - url.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertFalse(wrapper.pageURLs.isEmpty) @@ -219,7 +219,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let (data, _) = try fetch(try XCTUnwrap(wrapper.pageURLs.first)) let html = try XCTUnwrap(String(data: data, encoding: .utf8)) @@ -234,7 +234,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let (data, _) = try fetch(try XCTUnwrap(wrapper.pageURLs.first)) let html = try XCTUnwrap(String(data: data, encoding: .utf8)) @@ -248,11 +248,11 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let before = wrapper.pageURLs try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertNotEqual(before, wrapper.pageURLs) } @@ -264,7 +264,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let url = try XCTUnwrap(wrapper.pageURLs.first) let recorder = NavigationRecorder(finished: expectation(description: "loaded \(url)")) @@ -283,7 +283,7 @@ class OpenDocumentReaderTests: XCTestCase { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) let page = try XCTUnwrap(wrapper.pageURLs.first) XCTAssertTrue(CoreWrapper.isServedURL(page)) @@ -320,60 +320,120 @@ class OpenDocumentReaderTests: XCTestCase { return try result.get() } - func testBackTranslateWritesEditedDocument() throws { + /// The first run of the page, by the address an edit names it with. + private func firstRunId(of wrapper: CoreWrapper) throws -> Int { + let (data, _) = try fetch(try XCTUnwrap(wrapper.pageURLs.first)) + let html = String(decoding: data, as: UTF8.self) + + let match = try XCTUnwrap(html.range(of: #"]*data-odr-id="\d+""#, options: .regularExpression)) + let digits = html[match].split(separator: "\"").last ?? "" + + return try XCTUnwrap(Int(digits)) + } + + private func setText(_ id: Int, _ text: String) -> String { + #"{"version": 2, "ops": [{"op": "setText", "id": \#(id), "text": "\#(text)"}]}"# + } + + func testSaveWritesEditedDocument() throws { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) let editedURL = URL(fileURLWithPath: temporaryDirectory) .appendingPathComponent("test-edited.odt") try? FileManager.default.removeItem(at: editedURL) - let diff = """ - {"modifiedText":{"/child:3/child:0":"This is a simple test document to demonstrate the DocumentLoaderwwww example!"}} - """ - - try wrapper.backTranslate(diff, into: editedURL.path) + try wrapper.save(setText(try firstRunId(of: wrapper), "Edited by the test"), into: editedURL.path) XCTAssertTrue(FileManager.default.fileExists(atPath: editedURL.path)) } /// Where every real save lands: on the document odrcore still has open. - func testBackTranslateOverTheOpenDocumentLeavesItReadable() throws { + func testSaveOverTheOpenDocumentLeavesItReadable() throws { let wrapper = CoreWrapper() try wrapper.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) - - let diff = """ - {"modifiedText":{"/child:3/child:0":"Saved over itself."}} - """ + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) - try wrapper.backTranslate(diff, into: documentURL.path) + try wrapper.save(setText(try firstRunId(of: wrapper), "Saved over itself."), into: documentURL.path) // the whole document has to survive, not only the part the edit rewrote let reopened = CoreWrapper() try reopened.translate( - documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: true) + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) XCTAssertFalse(reopened.pageURLs.isEmpty) XCTAssertTrue(reopened.isEditable) } - /// backTranslate used to dereference an empty std::optional when nothing had + /// Nothing typed is a save of the file as it is. + func testSaveWithNoOperationsWritesTheDocument() throws { + let wrapper = CoreWrapper() + + try wrapper.translate( + documentURL.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) + + let editedURL = URL(fileURLWithPath: temporaryDirectory) + .appendingPathComponent("test-unedited.odt") + try? FileManager.default.removeItem(at: editedURL) + + try wrapper.save(#"{"version": 2, "ops": []}"#, into: editedURL.path) + + XCTAssertTrue(FileManager.default.fileExists(atPath: editedURL.path)) + } + + /// Saving used to dereference an empty std::optional when nothing had /// been translated yet. - func testBackTranslateWithoutTranslateFails() { + func testSaveWithoutTranslateFails() { let wrapper = CoreWrapper() let editedURL = URL(fileURLWithPath: temporaryDirectory) .appendingPathComponent("never-translated.odt") - XCTAssertThrowsError(try wrapper.backTranslate("{}", into: editedURL.path)) { error in + XCTAssertThrowsError(try wrapper.save("{}", into: editedURL.path)) { error in XCTAssertEqual((error as NSError).domain, CoreWrapperErrorDomain) } } + /// A pdf is not edited but marked, and says so; a text document does not. + func testAPdfTakesMarks() throws { + let wrapper = CoreWrapper() + let url = try copyFixture(ofType: "pdf") + + try wrapper.translate(url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) + + XCTAssertTrue(wrapper.isAnnotatable) + XCTAssertFalse(wrapper.isEditable) + XCTAssertEqual(wrapper.editPayloadScript, "odr.annotation.getAnnotations()") + + let text = CoreWrapper() + try text.translate(documentURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) + + XCTAssertFalse(text.isAnnotatable) + XCTAssertEqual(text.editPayloadScript, "odr.editing.getOperations()") + } + + /// The marks go in as an update behind the file, so the file is still there. + func testSavingMarksWritesThePdf() throws { + let wrapper = CoreWrapper() + let url = try copyFixture(ofType: "pdf") + + try wrapper.translate(url.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) + + let markedURL = URL(fileURLWithPath: temporaryDirectory).appendingPathComponent("test-marked.pdf") + try? FileManager.default.removeItem(at: markedURL) + + try wrapper.save(#"{"version": 1, "annotations": []}"#, into: markedURL.path) + + let reopened = CoreWrapper() + try reopened.translate( + markedURL.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) + + XCTAssertFalse(reopened.pageURLs.isEmpty) + } + /// odrcore recognising nothing at all is the message, not a page. func testUnsupportedFileTypeReportsTypedError() throws { let wrapper = CoreWrapper() @@ -385,7 +445,7 @@ class OpenDocumentReaderTests: XCTestCase { XCTAssertThrowsError( try wrapper.translate( - notADocument.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + notADocument.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) ) { error in let error = error as NSError XCTAssertEqual(error.domain, CoreWrapperErrorDomain) @@ -401,7 +461,7 @@ class OpenDocumentReaderTests: XCTestCase { try "Alpha\nBeta\n".write(to: notes, atomically: true, encoding: .utf8) try wrapper.translate( - notes.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + notes.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["text"]) } @@ -415,7 +475,7 @@ class OpenDocumentReaderTests: XCTestCase { try "# Heading\n\nSome **bold** prose.\n".write(to: notes, atomically: true, encoding: .utf8) try wrapper.translate( - notes.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + notes.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertEqual(wrapper.pageNames, ["document"]) @@ -436,7 +496,7 @@ class OpenDocumentReaderTests: XCTestCase { try "a,b\n1,2\n".write(to: rows, atomically: true, encoding: .utf8) try wrapper.translate( - rows.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + rows.path, into: temporaryDirectory, with: nil, editable: false, scope: .document) XCTAssertFalse(wrapper.pageNames.isEmpty) } @@ -448,7 +508,7 @@ class OpenDocumentReaderTests: XCTestCase { measure { do { - try wrapper.translate(path, cache: directory, into: directory, with: nil, editable: true) + try wrapper.translate(path, into: directory, with: nil, editable: true, scope: .document) } catch { XCTFail("translate threw \(error)") } diff --git a/README.md b/README.md index ed6a1af7..cecd68f5 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ flag cannot end up in a build whose code says otherwise. `AnalyticsManager` and `CrashManager` take no switch at all - both write to `os.Logger` and nowhere else, so there is nothing to withhold. +The one thing Pro does that Lite does not is `Features.advancedEditing`, which +is the same flag the other way round. Lite edits inside a paragraph: odrcore is +told the editing scope is `paragraph`, and refuses a line break or a format +with `outOfScope`, which the reader hears as the offer of Pro. Marks on a pdf +are gated the same way, in front of the highlighter rather than behind it. The +tools row still shows every button in Lite, so what Pro adds is in view. + `configs/full` and `configs/lite` hold each bundle's `Info.plist` and privacy manifest, out of the synchronized folder, since anything left in there would be copied into both apps. For the same reason `scripts/make-test-fixtures.py`, which @@ -93,6 +100,23 @@ App Transport Security exception, since ATS blocks plain HTTP: local addresses and — unlike `NSAllowsArbitraryLoads` — needs no justification in App Store review. +## Editing + +The pencil re-renders the document with odrcore's editing scaffolding and, once +the page is up, turns the mode on with `odr.editing.enable()`. A row of tools +(`EditToolBar`) grows under the bar with what the page is: formatting for a +text document, undo and redo alone for a spreadsheet or a plain text file, the +five markers for a pdf - the same shape as the website's viewer. The page talks +back through one `WKScriptMessageHandler`: the state of its log for the undo +and redo buttons, the style under the caret for the format buttons, and every +refusal, which is shown in a word. + +Saving asks the page for its log (`odr.editing.getOperations()`, or +`odr.annotation.getAnnotations()` for a pdf), hands it to odrcore, and writes +the file beside the open one before moving it into place. The save button is +the way out of the edit, as before: the file holds the edit once it is written, +so leaving edit mode reads back what was saved. + ## Formatting Swift sources are formatted with `swift-format` from the active Xcode From 8fcc0eead2876bc82f9e7d552cb891a6d2bf0bbe Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 18 Sep 2026 21:04:59 +0200 Subject: [PATCH 2/5] Follow the Android app: no reload on entering an edit, and a Pro badge Every document is rendered with its editor, so the pencil only turns the mode on and the page stays where it was. Leaving an edit asks about saving only when the page holds a change. The refusals are said in full, out-of-date formula cells are said once, and Lite's tools row starts with a Pro badge. The gate has a flag of its own beside the ads flag. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01214fGHJJ8jpfdujYBqDRsz --- Ads/Linked.swift | 2 + CHANGELOG.md | 2 + NoAds/Linked.swift | 2 + OpenDocumentReader/Document.swift | 13 ++- .../DocumentViewController.swift | 88 +++++++++++++++---- OpenDocumentReader/EditToolBar.swift | 44 +++++++++- OpenDocumentReader/Features.swift | 5 +- .../ca.lproj/Localizable.strings | 14 +++ .../cs.lproj/Localizable.strings | 14 +++ .../da.lproj/Localizable.strings | 14 +++ .../de.lproj/Localizable.strings | 14 +++ .../en.lproj/Localizable.strings | 14 +++ .../es.lproj/Localizable.strings | 14 +++ .../fr.lproj/Localizable.strings | 14 +++ .../ga.lproj/Localizable.strings | 14 +++ .../it.lproj/Localizable.strings | 14 +++ .../ja.lproj/Localizable.strings | 14 +++ .../pl.lproj/Localizable.strings | 14 +++ .../pt-BR.lproj/Localizable.strings | 14 +++ .../ru.lproj/Localizable.strings | 14 +++ .../sl.lproj/Localizable.strings | 14 +++ .../tr.lproj/Localizable.strings | 14 +++ .../zh-Hans.lproj/Localizable.strings | 14 +++ README.md | 32 ++++--- 24 files changed, 373 insertions(+), 39 deletions(-) diff --git a/Ads/Linked.swift b/Ads/Linked.swift index 0ee1f6c0..afaab96c 100644 --- a/Ads/Linked.swift +++ b/Ads/Linked.swift @@ -1,2 +1,4 @@ /// Read through ``Features``. let LINKS_ADS = true +/// Read through ``Features``. Lite edits inside a paragraph and sells the rest. +let ADVANCED_EDITING = false diff --git a/CHANGELOG.md b/CHANGELOG.md index 93eafffb..4adfc4f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ once the version tag exists. ### Changed - The engine is odrcore 7.0.0, up from 6.13.0. +- Entering an edit no longer reloads the document, so the page stays where it + was. Leaving one without changes no longer asks about saving them. - The Lite app edits inside a paragraph. Formatting, new or joined paragraphs and marks on a PDF are part of Pro, and the Lite app says so when they are reached for. diff --git a/NoAds/Linked.swift b/NoAds/Linked.swift index 512c141e..76887f04 100644 --- a/NoAds/Linked.swift +++ b/NoAds/Linked.swift @@ -1,2 +1,4 @@ /// Read through ``Features``. let LINKS_ADS = false +/// Read through ``Features``. +let ADVANCED_EDITING = true diff --git a/OpenDocumentReader/Document.swift b/OpenDocumentReader/Document.swift index 0b7bb1ad..b0bf41fc 100644 --- a/OpenDocumentReader/Document.swift +++ b/OpenDocumentReader/Document.swift @@ -8,6 +8,8 @@ protocol DocumentDelegate: AnyObject { func documentLoadingStarted(_ doc: Document) func documentLoadingCompleted(_ doc: Document) func documentPagesChanged(_ doc: Document) + /// The edit mode was turned on. The page is the one already on screen. + func documentEditingStarted(_ doc: Document) } enum DocumentError: Error { @@ -39,9 +41,16 @@ class Document: UIDocument { parse() } } + /// The page carries its editor from the first render, so entering an edit + /// turns it on in place. Leaving one renders the file again, which is what + /// drops the edits or shows the saved ones. public var edit = false { didSet { - parse() + if edit { + notify { $0.documentEditingStarted(self) } + } else { + parse() + } } } @@ -81,7 +90,7 @@ class Document: UIDocument { fileURL.path, into: NSTemporaryDirectory(), with: password, - editable: edit, + editable: true, scope: Features.advancedEditing ? .document : .paragraph ) } catch let error as NSError diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 5edb5f9a..ba03cff2 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -96,6 +96,10 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Whether the Pro offer was shown during this edit, so a page full of /// refused line breaks raises it once. private var hasOfferedProForThisEdit = false + + /// How many formula cells the edits so far left out of date; said once + /// each time the number grows. + private var staleCells = 0 private var canSearch = false { didSet { updateToolBar() @@ -287,10 +291,6 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { updateSearchButton() - if let documentNavigation, navigation === documentNavigation, document?.edit == true { - beginEditSession() - } - // the document is drawn, which is what a screenshot of it waits for - // and only the document: the "loading" page finishes first, and a // picture of it is a picture of the word "loading" @@ -325,13 +325,10 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel return case .edit: - // Entering an edit reloads the page as editable, so this comes back - // here a second time - and that pass is the one worth photographing. - guard document?.edit == true else { - editDocument() + // ready once the tools are up, which `beginEditSession` says + editDocument() - return - } + return default: break @@ -555,7 +552,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Under the bar and above the progress line, so it reads as part of the bar. private func setUpEditToolBar() { editToolBar.layout = nil - editToolBar.menusEnabled = Features.advancedEditing + editToolBar.advancedEditing = Features.advancedEditing editToolBar.onTap = { [weak self] tool in self?.editToolTapped(tool) } editToolBar.onChoice = { [weak self] tool, choice in self?.editToolChose(tool, choice) } @@ -580,7 +577,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel if (typeof odr !== 'object' || !window.webkit || !webkit.messageHandlers.odr) { return; } var post = function (message) { webkit.messageHandlers.odr.postMessage(message); }; odr.onEditChange = function (e) { - post({ type: 'editChange', canUndo: !!e.canUndo, canRedo: !!e.canRedo }); + post({ type: 'editChange', dirty: !!e.dirty, canUndo: !!e.canUndo, canRedo: !!e.canRedo }); }; odr.onEditRefused = function (e) { post({ type: 'editRefused', reason: String(e.reason || ''), message: String(e.message || '') }); @@ -588,6 +585,27 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel odr.onSelectionChange = function (style) { post({ type: 'selection', style: style || {} }); }; + odr.onCellsStale = function (detail) { + post({ type: 'cellsStale', count: detail && detail.cells ? detail.cells.length : 0 }); + }; + if (!odr.annotation) { return; } + // an armed tool marks a selection as it is made, which is what a + // touch screen needs. The annotator has no callback of its own, so + // the count of marks is reported after every gesture that can + // change it; a mark settles 50ms after the pointer lifts + odr.annotation.setOptions({ markOnSelection: true }); + var reported = -1; + var reportMarks = function () { + var count = odr.annotation.list().length; + if (count === reported) { return; } + reported = count; + post({ type: 'marks', count: count }); + }; + var reportMarksSoon = function () { window.setTimeout(reportMarks, 120); }; + document.addEventListener('pointerup', reportMarksSoon); + document.addEventListener('pointercancel', reportMarksSoon); + document.addEventListener('selectionchange', reportMarksSoon); + odr.reportMarks = reportMarks; })(); """ @@ -596,9 +614,21 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel switch type { case "editChange": + hasUnsavedEdits = body["dirty"] as? Bool ?? false editToolBar.setEnabled(.undo, body["canUndo"] as? Bool ?? false) editToolBar.setEnabled(.redo, body["canRedo"] as? Bool ?? false) + case "marks": + let count = body["count"] as? Int ?? 0 + hasUnsavedEdits = count > 0 + editToolBar.setEnabled(.undo, count > 0) + + case "cellsStale": + if body["count"] as? Int ?? 0 > staleCells { + showToast(controller: self, message: NSLocalizedString("edit_cells_stale", comment: ""), seconds: 3) + } + staleCells = body["count"] as? Int ?? 0 + case "editRefused": editRefused(reason: body["reason"] as? String ?? "") @@ -614,15 +644,18 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } } - /// The editable page is on screen: turn the mode on and show its tools. + /// Turns the mode on in the page already on screen and shows its tools. /// A pdf needs no mode, only a marker that acts on a selection. private func beginEditSession() { hasOfferedProForThisEdit = false + hasUnsavedEdits = false if document?.isAnnotatable == true { editToolBar.layout = .pdf - editToolBar.setEnabled(.undo, true) - run("odr.annotation.setOptions({ markOnSelection: true }); odr.annotation.setColor(\(markColor.deviceRGB))") + editToolBar.setEnabled(.undo, false) + run("odr.annotation.setColor(\(markColor.deviceRGB))") + showToast(controller: self, message: NSLocalizedString("mark_hint", comment: ""), seconds: 2) + editSessionReady() return } @@ -636,9 +669,21 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel guard let self, self.isEditingDocument else { return } self.editToolBar.layout = isPlainText || isSheet as? Bool == true ? .plain : .text + self.editSessionReady() } } + /// The tools are up, which is what a screenshot of an edit waits for. + private func editSessionReady() { + if ScreenshotMode.screen == .edit { + ScreenshotMode.markReady(view) + } + } + + /// Whether the page holds edits or marks that only it has, which leaving + /// would lose. + private var hasUnsavedEdits = false + private func editToolTapped(_ tool: EditToolBar.Tool) { if tool.isAdvanced, !Features.advancedEditing { offerPro(canMark ? .pdf : .formatting) @@ -648,7 +693,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel switch tool { case .undo: - run(canMark ? "odr.annotation.undo()" : "odr.editing.undo()") + run(canMark ? "odr.annotation.undo(); odr.reportMarks()" : "odr.editing.undo()") case .redo: run("odr.editing.redo()") case .bold, .italic, .underline, .strikethrough: @@ -676,6 +721,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel a.mark(); a.setTool(armed); selection.removeAllRanges(); + odr.reportMarks(); return armed; } if (a.getTool() === '\(name)') { @@ -748,9 +794,12 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel let key: String switch reason { + case "newLine": key = "edit_refused_new_line" case "formula": key = "edit_refused_formula" case "formulaInput": key = "edit_refused_formula_input" case "rich", "shapes": key = "edit_refused_rich" + case "readOnly": key = "edit_refused_read_only" + case "range": key = "edit_refused_range" default: key = "edit_refused_generic" } @@ -971,7 +1020,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel return } - if doc.edit { + if doc.edit, hasUnsavedEdits { let alert = UIAlertController( title: NSLocalizedString("alert_unsaved_changes", comment: ""), message: NSLocalizedString("alert_save_now", comment: ""), preferredStyle: .alert) @@ -1304,6 +1353,11 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel ]) } + func documentEditingStarted(_ doc: Document) { + isEditingDocument = true + beginEditSession() + } + func documentPagesChanged(_ doc: Document) { let pageNames = doc.pageNames ?? [] diff --git a/OpenDocumentReader/EditToolBar.swift b/OpenDocumentReader/EditToolBar.swift index fb235122..68325602 100644 --- a/OpenDocumentReader/EditToolBar.swift +++ b/OpenDocumentReader/EditToolBar.swift @@ -159,9 +159,9 @@ final class EditToolBar: UIView { } } - /// Whether the menus open: in a build without the advanced editing a tap - /// goes to `onTap` instead, which says what Pro is. - var menusEnabled = true { + /// Whether the advanced tools act. Without it they are shown behind a + /// "Pro" badge, and a tap goes to `onTap`, which says what Pro is. + var advancedEditing = true { didSet { rebuild() } @@ -230,6 +230,10 @@ final class EditToolBar: UIView { isHidden = false + if !advancedEditing, layout.tools.contains(where: \.isAdvanced) { + stack.addArrangedSubview(makeBadge()) + } + for tool in layout.tools { let button = makeButton(for: tool) buttons[tool] = button @@ -237,6 +241,26 @@ final class EditToolBar: UIView { } } + /// Says the tools behind it are Pro's. + private func makeBadge() -> UIView { + let label = UILabel() + label.text = NSLocalizedString("tool_pro_badge", comment: "") + label.font = UIFont.preferredFont(forTextStyle: .caption1).withWeight(.semibold) + label.textColor = .white + label.backgroundColor = tintColor + label.textAlignment = .center + label.layer.cornerRadius = 8 + label.clipsToBounds = true + label.accessibilityIdentifier = "edit-tool-pro" + + NSLayoutConstraint.activate([ + label.heightAnchor.constraint(equalToConstant: 20), + label.widthAnchor.constraint(greaterThanOrEqualToConstant: 36), + ]) + + return label + } + private func makeButton(for tool: Tool) -> UIButton { var configuration = UIButton.Configuration.plain() configuration.image = UIImage(systemName: tool.symbol) @@ -259,7 +283,7 @@ final class EditToolBar: UIView { button.configuration = configuration } - if tool.opensMenu, menusEnabled { + if tool.opensMenu, advancedEditing { button.menu = makeMenu(for: tool) button.showsMenuAsPrimaryAction = true } else { @@ -343,6 +367,11 @@ final class EditToolBar: UIView { buttons[tool]?.isEnabled = enabled } + /// For the tests: whether the row starts with the Pro badge. + var showsProBadge: Bool { + stack.arrangedSubviews.first?.accessibilityIdentifier == "edit-tool-pro" + } + /// For the tests: whether the row shows `tool`. func shows(_ tool: Tool) -> Bool { buttons[tool] != nil @@ -387,3 +416,10 @@ extension UIColor { return [Double(red), Double(green), Double(blue)] } } + +extension UIFont { + + fileprivate func withWeight(_ weight: UIFont.Weight) -> UIFont { + UIFont.systemFont(ofSize: pointSize, weight: weight) + } +} diff --git a/OpenDocumentReader/Features.swift b/OpenDocumentReader/Features.swift index 352398b5..ba3aa7f4 100644 --- a/OpenDocumentReader/Features.swift +++ b/OpenDocumentReader/Features.swift @@ -7,6 +7,7 @@ enum Features { static var withAds: Bool { LINKS_ADS } /// The editing that goes past typing inside a paragraph: formatting, new - /// and joined paragraphs, and marks on a pdf. Pro is the build without ads. - static var advancedEditing: Bool { !LINKS_ADS } + /// and joined paragraphs, and marks on a pdf. What Pro is sold on; every + /// other edit the core takes is in both builds. + static var advancedEditing: Bool { ADVANCED_EDITING } } diff --git a/OpenDocumentReader/ca.lproj/Localizable.strings b/OpenDocumentReader/ca.lproj/Localizable.strings index 4712c05a..60aa0675 100644 --- a/OpenDocumentReader/ca.lproj/Localizable.strings +++ b/OpenDocumentReader/ca.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formatar el text i afegir o unir paràgrafs forma part d’OpenDocument Reader Pro."; "pro_feature_pdf" = "Marcar un PDF forma part d’OpenDocument Reader Pro."; "not_now" = "Ara no"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Un salt de línia dins d’un paràgraf no es pot desar. Prem Retorn per a un paràgraf nou."; +"edit_refused_read_only" = "Aquest document no es pot editar."; +"edit_refused_range" = "Una edició no pot passar per sobre d’una imatge o d’una taula."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Les cel·les amb fórmules que llegeixen la teva edició estan desactualitzades. El fitxer desat conserva les fórmules i un full de càlcul les torna a calcular."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Selecciona text i després una eina per marcar-lo."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/cs.lproj/Localizable.strings b/OpenDocumentReader/cs.lproj/Localizable.strings index 8b29ba92..cf097c22 100644 --- a/OpenDocumentReader/cs.lproj/Localizable.strings +++ b/OpenDocumentReader/cs.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formátování textu a přidávání nebo spojování odstavců je součástí OpenDocument Reader Pro."; "pro_feature_pdf" = "Označování PDF je součástí OpenDocument Reader Pro."; "not_now" = "Teď ne"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Zalomení řádku uvnitř odstavce nelze uložit. Stiskněte Enter pro nový odstavec."; +"edit_refused_read_only" = "Tento dokument nelze upravovat."; +"edit_refused_range" = "Úprava nemůže sahat přes obrázek nebo tabulku."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Buňky se vzorci, které čtou vaši úpravu, jsou zastaralé. Uložený soubor vzorce zachová a tabulkový procesor je přepočítá."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Vyberte text a pak nástroj, kterým ho označíte."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/da.lproj/Localizable.strings b/OpenDocumentReader/da.lproj/Localizable.strings index e2c28b32..7958475f 100644 --- a/OpenDocumentReader/da.lproj/Localizable.strings +++ b/OpenDocumentReader/da.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formatering af tekst samt tilføjelse eller sammenføjning af afsnit er en del af OpenDocument Reader Pro."; "pro_feature_pdf" = "Markering af PDF er en del af OpenDocument Reader Pro."; "not_now" = "Ikke nu"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Et linjeskift inde i et afsnit kan ikke gemmes. Tryk på Enter for et nyt afsnit."; +"edit_refused_read_only" = "Dette dokument kan ikke redigeres."; +"edit_refused_range" = "En redigering kan ikke række hen over et billede eller en tabel."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Formelceller, der læser din redigering, er forældede. Den gemte fil beholder formlerne, og et regnearksprogram beregner dem igen."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Vælg tekst og derefter et værktøj for at markere den."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/de.lproj/Localizable.strings b/OpenDocumentReader/de.lproj/Localizable.strings index 1ff88e93..9b513a4e 100644 --- a/OpenDocumentReader/de.lproj/Localizable.strings +++ b/OpenDocumentReader/de.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Text formatieren sowie Absätze einfügen oder zusammenführen ist Teil von OpenDocument Reader Pro."; "pro_feature_pdf" = "PDFs markieren ist Teil von OpenDocument Reader Pro."; "not_now" = "Nicht jetzt"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Ein Zeilenumbruch innerhalb eines Absatzes kann nicht gespeichert werden. Drücke die Eingabetaste für einen neuen Absatz."; +"edit_refused_read_only" = "Dieses Dokument kann nicht bearbeitet werden."; +"edit_refused_range" = "Eine Änderung kann nicht über ein Bild oder eine Tabelle hinausreichen."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Formelzellen, die deine Änderung lesen, sind veraltet. Die gespeicherte Datei behält die Formeln, eine Tabellenkalkulation berechnet sie neu."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Text auswählen, dann ein Werkzeug, um ihn zu markieren."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/en.lproj/Localizable.strings b/OpenDocumentReader/en.lproj/Localizable.strings index cfd7375f..ef1f5a4c 100644 --- a/OpenDocumentReader/en.lproj/Localizable.strings +++ b/OpenDocumentReader/en.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formatting text, and adding or joining paragraphs, is part of OpenDocument Reader Pro."; "pro_feature_pdf" = "Marking up a PDF is part of OpenDocument Reader Pro."; "not_now" = "Not now"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "A line break inside a paragraph cannot be saved. Press Return for a new paragraph."; +"edit_refused_read_only" = "This document cannot be edited."; +"edit_refused_range" = "An edit cannot reach over a picture or a table."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Formula cells that read your edit are out of date. The saved file keeps the formulas, and a spreadsheet app computes them again."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Select text, then a tool, to mark it."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/es.lproj/Localizable.strings b/OpenDocumentReader/es.lproj/Localizable.strings index 0e320b5b..4dda9442 100644 --- a/OpenDocumentReader/es.lproj/Localizable.strings +++ b/OpenDocumentReader/es.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Dar formato al texto y añadir o unir párrafos forma parte de OpenDocument Reader Pro."; "pro_feature_pdf" = "Marcar un PDF forma parte de OpenDocument Reader Pro."; "not_now" = "Ahora no"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Un salto de línea dentro de un párrafo no se puede guardar. Pulsa Intro para un párrafo nuevo."; +"edit_refused_read_only" = "Este documento no se puede editar."; +"edit_refused_range" = "Una edición no puede pasar por encima de una imagen o una tabla."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Las celdas con fórmulas que leen tu edición están desactualizadas. El archivo guardado conserva las fórmulas y una hoja de cálculo las vuelve a calcular."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Selecciona texto y luego una herramienta para marcarlo."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/fr.lproj/Localizable.strings b/OpenDocumentReader/fr.lproj/Localizable.strings index 5c423150..0a180313 100644 --- a/OpenDocumentReader/fr.lproj/Localizable.strings +++ b/OpenDocumentReader/fr.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "La mise en forme du texte, ainsi que l’ajout ou la fusion de paragraphes, fait partie d’OpenDocument Reader Pro."; "pro_feature_pdf" = "L’annotation de PDF fait partie d’OpenDocument Reader Pro."; "not_now" = "Pas maintenant"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Un saut de ligne à l’intérieur d’un paragraphe ne peut pas être enregistré. Appuyez sur Entrée pour un nouveau paragraphe."; +"edit_refused_read_only" = "Ce document ne peut pas être modifié."; +"edit_refused_range" = "Une modification ne peut pas passer par-dessus une image ou un tableau."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Les cellules de formule qui lisent votre modification ne sont plus à jour. Le fichier enregistré garde les formules, et un tableur les recalcule."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Sélectionnez du texte, puis un outil, pour l’annoter."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/ga.lproj/Localizable.strings b/OpenDocumentReader/ga.lproj/Localizable.strings index 51879bc8..242d326b 100644 --- a/OpenDocumentReader/ga.lproj/Localizable.strings +++ b/OpenDocumentReader/ga.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Is cuid de OpenDocument Reader Pro é téacs a fhormáidiú agus ailt a chur leis nó a nascadh."; "pro_feature_pdf" = "Is cuid de OpenDocument Reader Pro é PDF a mharcáil."; "not_now" = "Ní anois"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Ní féidir briseadh líne laistigh d’alt a shábháil. Brúigh Iontráil le haghaidh alt nua."; +"edit_refused_read_only" = "Ní féidir an doiciméad seo a chur in eagar."; +"edit_refused_range" = "Ní féidir le hathrú dul thar phictiúr ná thar thábla."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Tá cealla foirmle a léann d’athrú as dáta. Coinníonn an comhad sábháilte na foirmlí, agus ríomhann scarbhileog arís iad."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Roghnaigh téacs, ansin uirlis, chun é a mharcáil."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/it.lproj/Localizable.strings b/OpenDocumentReader/it.lproj/Localizable.strings index d47d8061..7cb1c0dd 100644 --- a/OpenDocumentReader/it.lproj/Localizable.strings +++ b/OpenDocumentReader/it.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formattare il testo e aggiungere o unire paragrafi fa parte di OpenDocument Reader Pro."; "pro_feature_pdf" = "Annotare un PDF fa parte di OpenDocument Reader Pro."; "not_now" = "Non ora"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Un’interruzione di riga dentro un paragrafo non può essere salvata. Premi Invio per un nuovo paragrafo."; +"edit_refused_read_only" = "Questo documento non può essere modificato."; +"edit_refused_range" = "Una modifica non può passare sopra un’immagine o una tabella."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Le celle con formule che leggono la tua modifica non sono aggiornate. Il file salvato conserva le formule e un foglio di calcolo le ricalcola."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Seleziona del testo, poi uno strumento, per annotarlo."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/ja.lproj/Localizable.strings b/OpenDocumentReader/ja.lproj/Localizable.strings index 780661ba..b7cefd9d 100644 --- a/OpenDocumentReader/ja.lproj/Localizable.strings +++ b/OpenDocumentReader/ja.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "文字の書式設定と、段落の追加や結合はOpenDocument Reader Proの機能です。"; "pro_feature_pdf" = "PDFへのマーク付けはOpenDocument Reader Proの機能です。"; "not_now" = "あとで"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "段落内の改行は保存できません。新しい段落にはReturnキーを押してください。"; +"edit_refused_read_only" = "この書類は編集できません。"; +"edit_refused_range" = "画像や表をまたぐ編集はできません。"; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "編集した内容を参照する数式セルが古くなっています。保存したファイルには数式が残り、表計算アプリが再計算します。"; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "テキストを選択してから、ツールを選ぶとマークを付けられます。"; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/pl.lproj/Localizable.strings b/OpenDocumentReader/pl.lproj/Localizable.strings index 6468edff..a668becd 100644 --- a/OpenDocumentReader/pl.lproj/Localizable.strings +++ b/OpenDocumentReader/pl.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formatowanie tekstu oraz dodawanie i łączenie akapitów to część OpenDocument Reader Pro."; "pro_feature_pdf" = "Oznaczanie PDF to część OpenDocument Reader Pro."; "not_now" = "Nie teraz"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Podziału wiersza wewnątrz akapitu nie można zapisać. Naciśnij Enter, aby zacząć nowy akapit."; +"edit_refused_read_only" = "Tego dokumentu nie można edytować."; +"edit_refused_range" = "Zmiana nie może sięgać przez obraz ani tabelę."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Komórki z formułami, które odczytują twoją zmianę, są nieaktualne. Zapisany plik zachowuje formuły, a arkusz kalkulacyjny przeliczy je ponownie."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Zaznacz tekst, a potem narzędzie, aby go oznaczyć."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/pt-BR.lproj/Localizable.strings b/OpenDocumentReader/pt-BR.lproj/Localizable.strings index 6b1e3cfa..a1e98e94 100644 --- a/OpenDocumentReader/pt-BR.lproj/Localizable.strings +++ b/OpenDocumentReader/pt-BR.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Formatar texto e adicionar ou juntar parágrafos faz parte do OpenDocument Reader Pro."; "pro_feature_pdf" = "Marcar um PDF faz parte do OpenDocument Reader Pro."; "not_now" = "Agora não"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Uma quebra de linha dentro de um parágrafo não pode ser salva. Pressione Enter para um novo parágrafo."; +"edit_refused_read_only" = "Este documento não pode ser editado."; +"edit_refused_range" = "Uma edição não pode passar por cima de uma imagem ou de uma tabela."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "As células com fórmulas que leem sua edição estão desatualizadas. O arquivo salvo mantém as fórmulas, e uma planilha as recalcula."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Selecione o texto e depois uma ferramenta para marcá-lo."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/ru.lproj/Localizable.strings b/OpenDocumentReader/ru.lproj/Localizable.strings index 209acd0d..0acfb42f 100644 --- a/OpenDocumentReader/ru.lproj/Localizable.strings +++ b/OpenDocumentReader/ru.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Форматирование текста, а также добавление и объединение абзацев — часть OpenDocument Reader Pro."; "pro_feature_pdf" = "Разметка PDF — часть OpenDocument Reader Pro."; "not_now" = "Не сейчас"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Перенос строки внутри абзаца нельзя сохранить. Нажмите Ввод, чтобы начать новый абзац."; +"edit_refused_read_only" = "Этот документ нельзя редактировать."; +"edit_refused_range" = "Изменение не может проходить через картинку или таблицу."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Ячейки с формулами, которые читают ваше изменение, устарели. В сохранённом файле формулы остаются, и табличный редактор пересчитает их."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Выделите текст, затем инструмент, чтобы пометить его."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/sl.lproj/Localizable.strings b/OpenDocumentReader/sl.lproj/Localizable.strings index dd4e8162..9fe47590 100644 --- a/OpenDocumentReader/sl.lproj/Localizable.strings +++ b/OpenDocumentReader/sl.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Oblikovanje besedila ter dodajanje ali združevanje odstavkov je del OpenDocument Reader Pro."; "pro_feature_pdf" = "Označevanje PDF je del OpenDocument Reader Pro."; "not_now" = "Ne zdaj"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Preloma vrstice znotraj odstavka ni mogoče shraniti. Pritisnite Enter za nov odstavek."; +"edit_refused_read_only" = "Tega dokumenta ni mogoče urejati."; +"edit_refused_range" = "Urejanje ne more segati čez sliko ali tabelo."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Celice s formulami, ki berejo vaše urejanje, so zastarele. Shranjena datoteka ohrani formule, preglednica pa jih znova izračuna."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "Izberite besedilo, nato orodje, da ga označite."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/tr.lproj/Localizable.strings b/OpenDocumentReader/tr.lproj/Localizable.strings index 2be08d05..c19d93ec 100644 --- a/OpenDocumentReader/tr.lproj/Localizable.strings +++ b/OpenDocumentReader/tr.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "Metni biçimlendirmek ve paragraf eklemek ya da birleştirmek OpenDocument Reader Pro’nun bir parçasıdır."; "pro_feature_pdf" = "PDF işaretlemek OpenDocument Reader Pro’nun bir parçasıdır."; "not_now" = "Şimdi değil"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "Paragraf içindeki satır sonu kaydedilemez. Yeni paragraf için Enter’a basın."; +"edit_refused_read_only" = "Bu belge düzenlenemez."; +"edit_refused_range" = "Bir düzenleme bir resmin ya da tablonun üzerinden geçemez."; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "Düzenlemenizi okuyan formül hücreleri güncel değil. Kaydedilen dosya formülleri korur ve bir hesap tablosu uygulaması yeniden hesaplar."; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "İşaretlemek için metni, sonra bir aracı seçin."; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings index 56705212..ebec65c8 100644 --- a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings +++ b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings @@ -148,3 +148,17 @@ "pro_feature_formatting" = "设置文字格式以及添加或合并段落是 OpenDocument Reader Pro 的功能。"; "pro_feature_pdf" = "标注 PDF 是 OpenDocument Reader Pro 的功能。"; "not_now" = "暂不"; + +/* Said when the page refuses an edit */ +"edit_refused_new_line" = "段落内的换行无法保存。请按回车键开始新段落。"; +"edit_refused_read_only" = "此文档无法编辑。"; +"edit_refused_range" = "编辑不能跨越图片或表格。"; + +/* Said once an edit leaves formula cells with an old result */ +"edit_cells_stale" = "引用你所编辑内容的公式单元格已过期。保存的文件会保留公式,电子表格应用会重新计算。"; + +/* Shown when the marking tools come up on a PDF */ +"mark_hint" = "先选择文本,再选择工具即可标注。"; + +/* The badge in front of the tools that are Pro's, in the Lite app */ +"tool_pro_badge" = "Pro"; diff --git a/README.md b/README.md index cecd68f5..9c614648 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,14 @@ flag cannot end up in a build whose code says otherwise. `AnalyticsManager` and `CrashManager` take no switch at all - both write to `os.Logger` and nowhere else, so there is nothing to withhold. -The one thing Pro does that Lite does not is `Features.advancedEditing`, which -is the same flag the other way round. Lite edits inside a paragraph: odrcore is -told the editing scope is `paragraph`, and refuses a line break or a format -with `outOfScope`, which the reader hears as the offer of Pro. Marks on a pdf -are gated the same way, in front of the highlighter rather than behind it. The -tools row still shows every button in Lite, so what Pro adds is in view. +The one thing Pro does that Lite does not is `Features.advancedEditing`, from +`ADVANCED_EDITING` beside `LINKS_ADS` in the same two files. Lite edits inside a +paragraph: odrcore is told the editing scope is `paragraph`, and refuses a line +break or a format with `outOfScope`, which the reader hears as the offer of +Pro. Marks on a pdf are gated the same way, in front of the highlighter rather +than behind it. The tools row still shows every button in Lite, behind a "Pro" +badge, so what Pro adds is in view. Every other edit the core takes - a sheet +cell, a plain text file - is in both. `configs/full` and `configs/lite` hold each bundle's `Info.plist` and privacy manifest, out of the synchronized folder, since anything left in there would be @@ -102,14 +104,16 @@ in App Store review. ## Editing -The pencil re-renders the document with odrcore's editing scaffolding and, once -the page is up, turns the mode on with `odr.editing.enable()`. A row of tools -(`EditToolBar`) grows under the bar with what the page is: formatting for a -text document, undo and redo alone for a spreadsheet or a plain text file, the -five markers for a pdf - the same shape as the website's viewer. The page talks -back through one `WKScriptMessageHandler`: the state of its log for the undo -and redo buttons, the style under the caret for the format buttons, and every -refusal, which is shown in a word. +Every document is rendered with odrcore's editing scaffolding, so the pencil +only turns the mode on with `odr.editing.enable()` and the reader stays where +they were. A row of tools (`EditToolBar`) grows under the bar with what the +page is: formatting for a text document, undo and redo alone for a spreadsheet +or a plain text file, the five markers for a pdf - the same shape as the +website's viewer and the Android app. The page talks back through one +`WKScriptMessageHandler`: the state of its log for the undo and redo buttons +and for the prompt on leaving, the style under the caret for the format +buttons, the formula cells an edit left out of date, and every refusal, which +is shown in a word. Saving asks the page for its log (`odr.editing.getOperations()`, or `odr.annotation.getAnnotations()` for a pdf), hands it to odrcore, and writes From abb8d569aae4c6c783253dcf612bab5fb79ec65b Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 18 Sep 2026 21:41:29 +0200 Subject: [PATCH 3/5] Match the edit tools to the website and to the Android app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The website's viewer is the reference, and OpenDocument.droid #662 now does the same. - The pencil turns the edit on and off, and is drawn selected while it is on. A save button beside it writes the file. A save stays in the edit: the file is rendered again and the mode goes back on. Leaving with unsaved changes asks; leaving without them only turns the mode off. - Each marker on a pdf has a colour of its own, picked with a chevron beside it. A marker pressed with text selected marks it once and leaves no tool armed. - The highlight is a split button: a tap turns it on or off, and the chevron picks its colour. - The colour bars and the text size follow the selection. - The palettes are the ones the Android app offers. "Other color…" stays, because the website takes any colour. - The notice about formula cells says how many, from a stringsdict in every language. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01F6AQY2k86AaPq12nxBfN7A --- CHANGELOG.md | 4 +- OpenDocumentReader/Document.swift | 24 +- .../DocumentViewController.swift | 266 ++++++++++++++---- OpenDocumentReader/EditToolBar.swift | 154 +++++++++- OpenDocumentReader/Main.storyboard | 2 +- .../ca.lproj/Localizable.strings | 9 +- .../ca.lproj/Localizable.stringsdict | 22 ++ .../cs.lproj/Localizable.strings | 9 +- .../cs.lproj/Localizable.stringsdict | 26 ++ .../da.lproj/Localizable.strings | 9 +- .../da.lproj/Localizable.stringsdict | 22 ++ .../de.lproj/Localizable.strings | 9 +- .../de.lproj/Localizable.stringsdict | 22 ++ .../en.lproj/Localizable.strings | 9 +- .../en.lproj/Localizable.stringsdict | 22 ++ .../es.lproj/Localizable.strings | 9 +- .../es.lproj/Localizable.stringsdict | 22 ++ .../fr.lproj/Localizable.strings | 9 +- .../fr.lproj/Localizable.stringsdict | 22 ++ .../ga.lproj/Localizable.strings | 9 +- .../ga.lproj/Localizable.stringsdict | 28 ++ .../it.lproj/Localizable.strings | 9 +- .../it.lproj/Localizable.stringsdict | 22 ++ .../ja.lproj/Localizable.strings | 9 +- .../ja.lproj/Localizable.stringsdict | 20 ++ .../pl.lproj/Localizable.strings | 9 +- .../pl.lproj/Localizable.stringsdict | 26 ++ .../pt-BR.lproj/Localizable.strings | 9 +- .../pt-BR.lproj/Localizable.stringsdict | 22 ++ .../ru.lproj/Localizable.strings | 9 +- .../ru.lproj/Localizable.stringsdict | 26 ++ .../sl.lproj/Localizable.strings | 9 +- .../sl.lproj/Localizable.stringsdict | 26 ++ .../tr.lproj/Localizable.strings | 9 +- .../tr.lproj/Localizable.stringsdict | 22 ++ .../zh-Hans.lproj/Localizable.strings | 9 +- .../zh-Hans.lproj/Localizable.stringsdict | 20 ++ .../EditWorkflowTests.swift | 163 +++++++++-- README.md | 16 +- 39 files changed, 993 insertions(+), 150 deletions(-) create mode 100644 OpenDocumentReader/ca.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/cs.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/da.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/de.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/en.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/es.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/fr.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/ga.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/it.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/ja.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/pl.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/pt-BR.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/ru.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/sl.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/tr.lproj/Localizable.stringsdict create mode 100644 OpenDocumentReader/zh-Hans.lproj/Localizable.stringsdict diff --git a/CHANGELOG.md b/CHANGELOG.md index 4adfc4f6..a9817363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ once the version tag exists. italic, underline, strikethrough, text colour, highlight, text size, undo and redo. - A PDF can be marked up: highlight, underline, strike out, squiggly underline - and drawing, saved into the file. + and drawing, each in a colour of its own, saved into the file. - Presentations, Excel files and plain text files can be edited and saved. - Undo and redo while editing. @@ -35,6 +35,8 @@ once the version tag exists. - The engine is odrcore 7.0.0, up from 6.13.0. - Entering an edit no longer reloads the document, so the page stays where it was. Leaving one without changes no longer asks about saving them. +- The pencil turns the edit on and off, and a save button beside it writes the + file. A save no longer ends the edit. - The Lite app edits inside a paragraph. Formatting, new or joined paragraphs and marks on a PDF are part of Pro, and the Lite app says so when they are reached for. diff --git a/OpenDocumentReader/Document.swift b/OpenDocumentReader/Document.swift index b0bf41fc..aafb0b02 100644 --- a/OpenDocumentReader/Document.swift +++ b/OpenDocumentReader/Document.swift @@ -10,6 +10,9 @@ protocol DocumentDelegate: AnyObject { func documentPagesChanged(_ doc: Document) /// The edit mode was turned on. The page is the one already on screen. func documentEditingStarted(_ doc: Document) + /// The edit mode was turned off, and the page on screen stays: it holds + /// nothing the file does not. + func documentEditingEnded(_ doc: Document) } enum DocumentError: Error { @@ -43,17 +46,34 @@ class Document: UIDocument { } /// The page carries its editor from the first render, so entering an edit /// turns it on in place. Leaving one renders the file again, which is what - /// drops the edits or shows the saved ones. + /// drops the edits or shows the saved ones - unless ``endEdit(renderingAgain:)`` + /// says the page already is the file. public var edit = false { didSet { if edit { notify { $0.documentEditingStarted(self) } - } else { + } else if rendersAgainOnLeave { parse() + } else { + notify { $0.documentEditingEnded(self) } } } } + private var rendersAgainOnLeave = true + + /// Leaves the edit. Without `renderingAgain` only the mode goes off. + func endEdit(renderingAgain: Bool) { + rendersAgainOnLeave = renderingAgain + edit = false + rendersAgainOnLeave = true + } + + /// Renders the file again and keeps the mode: what a save stays in. + func reload() { + parse() + } + public var webview: WKWebView? public var isOdf = false diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index ba03cff2..8fe5d7ce 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -71,12 +71,12 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Whether the document is a pdf that takes marks. The same button as the /// pencil, with the highlighter for a glyph. private var canMark = false { didSet { updateEditButtonRole() } } - /// The same slot the pencil sits in, showing the way out of the edit it - /// started — as on OpenDocument.droid, where edit mode replaces the bar - /// rather than emptying it. + /// The pen turns the mode on and off and is drawn filled while it is on; + /// the disc beside it saves - the two controls of the website's viewer. private var isEditingDocument = false { didSet { updateEditButtonRole() + updateToolBar() if !isEditingDocument { editToolBar.layout = nil @@ -84,11 +84,36 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } } + /// Saves the edit, and stays in it. Only in the bar while editing. + lazy var saveButton: UIBarButtonItem = { + let item = UIBarButtonItem( + image: UIImage(systemName: "square.and.arrow.down"), style: .plain, target: self, + action: #selector(saveTapped(_:))) + item.accessibilityLabel = NSLocalizedString("action_edit_save", comment: "") + item.isEnabled = false + + return item + }() + + private lazy var saveButtonSpacer: UIBarButtonItem = { + let item = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) + item.width = 10 + + return item + }() + /// The row of tools under the bar while a document is edited. let editToolBar = EditToolBar() - /// The colour the marks on a pdf take, until the reader picks another. - private var markColor = UIColor(hex: EditToolBar.markColors[0].hex) + /// The colour each marker on a pdf takes, until the reader picks another. + private var markColors: [EditToolBar.Tool: UIColor] = [:] + + /// The colour the highlight button turns on; the selection's own where it + /// has one. + private var highlightColor = UIColor(hex: EditToolBar.highlightColors[0].hex) + + /// Whether the selection shows a highlight, as the page last said. + private var selectionHasHighlight = false /// Which menu the system colour picker was opened from. private var colorPickerTool: EditToolBar.Tool? @@ -97,9 +122,12 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// refused line breaks raises it once. private var hasOfferedProForThisEdit = false - /// How many formula cells the edits so far left out of date; said once - /// each time the number grows. + /// How many formula cells the edits so far left out of date; said each + /// time the number grows. private var staleCells = 0 + + /// Set by a save, so the page that loads next is put back into the mode. + private var resumesEditAfterLoad = false private var canSearch = false { didSet { updateToolBar() @@ -166,6 +194,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel barButtonItem.accessibilityLabel = NSLocalizedString("back_to_documents", comment: "") updateEditButtonRole() + setUpSaveButton() setUpDocumentTitle() // nothing is editable or searchable until a page says so @@ -291,6 +320,15 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { updateSearchButton() + // a save renders the file again, and the edit goes on in the new page + if let documentNavigation, navigation === documentNavigation, resumesEditAfterLoad { + resumesEditAfterLoad = false + + if document?.edit == true { + beginEditSession() + } + } + // the document is drawn, which is what a screenshot of it waits for - // and only the document: the "loading" page finishes first, and a // picture of it is a picture of the word "loading" @@ -528,18 +566,11 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel findAll(searchText: searchText) } - /// One button, both ways: the pencil starts an edit and the save glyph ends - /// it. See ``updateEditButtonRole()``. - @IBAction func editOrSave(_ sender: UIBarButtonItem) { + /// The pen: turns the mode on, and off again. Leaving with changes the page + /// alone holds asks first. See ``updateEditButtonRole()``. + @IBAction func toggleEdit(_ sender: UIBarButtonItem) { if isEditingDocument { - // the file holds the edit once it is written, so leaving edit mode - // reads back what was saved. A save that failed stays in the edit, - // which is the only place that text still exists. - saveContent { success in - guard success else { return } - - self.document?.edit = false - } + leaveEdit() } else if canMark, !Features.advancedEditing { offerPro(.pdf) } else { @@ -547,6 +578,65 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } } + /// Without changes the page on screen is the file, so the mode only goes + /// off. With them: save, discard, or stay. + func leaveEdit() { + guard hasUnsavedEdits else { + document?.endEdit(renderingAgain: false) + + return + } + + AnalyticsManager.shared.report("show_alert_unsaved_changes") + + let alert = UIAlertController( + title: NSLocalizedString("alert_unsaved_changes", comment: ""), + message: NSLocalizedString("alert_save_now", comment: ""), preferredStyle: .alert) + alert.addAction( + UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel)) + alert.addAction( + UIAlertAction( + title: NSLocalizedString("no", comment: ""), style: .destructive, + handler: { _ in + AnalyticsManager.shared.report("alert_unsaved_changes_no") + + self.discardChanges() + })) + alert.addAction( + UIAlertAction( + title: NSLocalizedString("yes", comment: ""), style: .default, + handler: { _ in + AnalyticsManager.shared.report("alert_unsaved_changes_yes") + + // the file holds the edit once it is written, so leaving + // reads back what was saved + self.saveContent { success in + guard success else { return } + + self.document?.endEdit(renderingAgain: true) + } + })) + + present(alert, animated: true) + } + + /// The disc: writes the edit and stays in it, as the website does. The page + /// is rendered again from the file, and the mode turned back on in it. + @objc func saveTapped(_ sender: UIBarButtonItem) { + saveAndStay() + } + + func saveAndStay(completion: ((Bool) -> Void)? = nil) { + saveContent { success in + if success { + self.resumesEditAfterLoad = true + self.document?.reload() + } + + completion?(success) + } + } + // MARK: - the editing tools /// Under the bar and above the progress line, so it reads as part of the bar. @@ -624,10 +714,13 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel editToolBar.setEnabled(.undo, count > 0) case "cellsStale": - if body["count"] as? Int ?? 0 > staleCells { - showToast(controller: self, message: NSLocalizedString("edit_cells_stale", comment: ""), seconds: 3) + let count = body["count"] as? Int ?? 0 + if count > staleCells { + let message = String.localizedStringWithFormat( + NSLocalizedString("edit_cells_stale", comment: ""), count) + showToast(controller: self, message: message, seconds: 3) } - staleCells = body["count"] as? Int ?? 0 + staleCells = count case "editRefused": editRefused(reason: body["reason"] as? String ?? "") @@ -639,6 +732,20 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel editToolBar.setPressed(.underline, style["underline"] as? Bool ?? false) editToolBar.setPressed(.strikethrough, style["strikethrough"] as? Bool ?? false) + // the bars and the size follow the selection, as the website's do + if let color = style["color"] as? String { + editToolBar.setColor(.textColor, UIColor(hex: color)) + } + let highlight = style["highlight"] as? String + selectionHasHighlight = highlight != nil + editToolBar.setPressed(.highlight, selectionHasHighlight) + if let highlight { + highlightColor = UIColor(hex: highlight) + editToolBar.setColor(.highlight, highlightColor) + } + editToolBar.setFontSize( + (style["size"] as? String).map { $0.hasSuffix("pt") ? String($0.dropLast(2)) : $0 }) + default: break } @@ -649,11 +756,15 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel private func beginEditSession() { hasOfferedProForThisEdit = false hasUnsavedEdits = false + staleCells = 0 + selectionHasHighlight = false if document?.isAnnotatable == true { editToolBar.layout = .pdf editToolBar.setEnabled(.undo, false) - run("odr.annotation.setColor(\(markColor.deviceRGB))") + for tool in EditToolBar.Layout.pdf.tools where tool.showsColor { + editToolBar.setColor(tool, markColor(of: tool)) + } showToast(controller: self, message: NSLocalizedString("mark_hint", comment: ""), seconds: 2) editSessionReady() @@ -669,6 +780,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel guard let self, self.isEditingDocument else { return } self.editToolBar.layout = isPlainText || isSheet as? Bool == true ? .plain : .text + self.editToolBar.setColor(.highlight, self.highlightColor) self.editSessionReady() } } @@ -682,7 +794,11 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Whether the page holds edits or marks that only it has, which leaving /// would lose. - private var hasUnsavedEdits = false + private var hasUnsavedEdits = false { + didSet { + saveButton.isEnabled = hasUnsavedEdits + } + } private func editToolTapped(_ tool: EditToolBar.Tool) { if tool.isAdvanced, !Features.advancedEditing { @@ -698,39 +814,54 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel run("odr.editing.redo()") case .bold, .italic, .underline, .strikethrough: run("odr.editing.toggle('\(tool.pageName ?? "")')") + case .highlight: + // off where the selection shows one, else on in the current colour + run( + "odr.editing.format({ highlight: \(selectionHasHighlight ? "null" : "'\(highlightColor.hexString)'") })" + ) case .markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw: - armMarker(tool) + pressMarker(tool, recolor: false) default: break } } - /// As on the website: a selection is marked once and the tool stays down, - /// the armed tool disarms, anything else arms. - private func armMarker(_ tool: EditToolBar.Tool) { + private func markColor(of tool: EditToolBar.Tool) -> UIColor { + markColors[tool] ?? UIColor(hex: tool.defaultColor ?? EditToolBar.markColors[0].hex) + } + + /// As on the website. A press with text selected marks it once and leaves + /// no tool armed; a press on the armed tool disarms it; any other press + /// arms it. A new colour (`recolor`) marks a selection once, recolours the + /// tool if it is armed, and is otherwise only kept. + private func pressMarker(_ tool: EditToolBar.Tool, recolor: Bool) { guard let name = tool.pageName else { return } let script = """ (function () { var a = odr.annotation; + var rgb = \(markColor(of: tool).deviceRGB); var selection = window.getSelection(); - var selected = '\(name)' !== 'ink' && selection && !selection.isCollapsed; + var selected = '\(name)' !== 'ink' && selection && !selection.isCollapsed + && selection.toString().length > 0; if (selected) { - var armed = a.getTool(); + a.setColor(rgb); + a.setWidth(2); a.setTool('\(name)'); a.mark(); - a.setTool(armed); + a.setTool(null); selection.removeAllRanges(); odr.reportMarks(); - return armed; - } - if (a.getTool() === '\(name)') { + } else if (\(recolor)) { + if (a.getTool() === '\(name)') { a.setColor(rgb); } + } else if (a.getTool() === '\(name)') { a.setTool(null); - return null; + } else { + a.setColor(rgb); + a.setWidth(2); + a.setTool('\(name)'); } - a.setWidth(2); - a.setTool('\(name)'); - return '\(name)'; + return a.getTool(); })() """ @@ -756,17 +887,28 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel case (.textColor, .color(let hex)): run("odr.editing.format({ color: '\(hex ?? "")' })") case (.highlight, .color(let hex)): + // a colour becomes the one the button turns on; none takes it off + if let hex { + highlightColor = UIColor(hex: hex) + editToolBar.setColor(.highlight, highlightColor) + } run("odr.editing.format({ highlight: \(hex.map { "'\($0)'" } ?? "null") })") - case (.markColor, .color(let hex)): - markColor = UIColor(hex: hex ?? EditToolBar.markColors[0].hex) - run("odr.annotation.setColor(\(markColor.deviceRGB))") + case (.markHighlight, .color(let hex)), (.markUnderline, .color(let hex)), + (.markStrikeOut, .color(let hex)), (.markSquiggly, .color(let hex)), (.markDraw, .color(let hex)): + markColors[tool] = UIColor(hex: hex ?? tool.defaultColor ?? EditToolBar.markColors[0].hex) + editToolBar.setColor(tool, markColor(of: tool)) + pressMarker(tool, recolor: true) case (_, .customColor): colorPickerTool = tool let picker = UIColorPickerViewController() picker.delegate = self picker.supportsAlpha = false - picker.selectedColor = tool == .markColor ? markColor : .label + switch tool { + case .highlight: picker.selectedColor = highlightColor + case .textColor: picker.selectedColor = .label + default: picker.selectedColor = markColor(of: tool) + } present(picker, animated: true) default: break @@ -850,6 +992,13 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } } + /// The disc goes after the pen, as on the website. + private func setUpSaveButton() { + guard let pen = toolBarItems.firstIndex(where: { $0 === editButtonSpacer }) else { return } + + toolBarItems.insert(contentsOf: [saveButton, saveButtonSpacer], at: pen + 1) + } + /// A gap either side of the name, which is what puts it in the middle. private func setUpDocumentTitle() { // a glass capsule is what a button looks like, and this is not one @@ -910,6 +1059,9 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel if item === editButton || item === editButtonSpacer { return canEdit } + if item === saveButton || item === saveButtonSpacer { + return canEdit && isEditingDocument + } if item === searchButton || item === searchButtonSpacer { return canSearch } @@ -926,25 +1078,13 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel isEditingDocument = document?.edit ?? false } - /// A pencil to start an edit, a highlighter to mark a pdf, and the save - /// glyph to write either. The label goes with it: VoiceOver reads that, - /// not the glyph. + /// A pencil to edit, a highlighter to mark a pdf, drawn selected while the + /// mode is on - the website's pen. The label goes with it: VoiceOver reads + /// that, not the glyph. private func updateEditButtonRole() { - let symbol: String - let label: String - if isEditingDocument { - symbol = "square.and.arrow.down" - label = "action_edit_save" - } else if canMark { - symbol = "highlighter" - label = "mark_pdf" - } else { - symbol = "pencil" - label = "menu_edit" - } - - editButton.image = UIImage(systemName: symbol) - editButton.accessibilityLabel = NSLocalizedString(label, comment: "") + editButton.image = UIImage(systemName: canMark ? "highlighter" : "pencil") + editButton.accessibilityLabel = NSLocalizedString(canMark ? "mark_pdf" : "menu_edit", comment: "") + editButton.isSelected = isEditingDocument } /// Asked of the page rather than guessed from the format: odrcore writes the @@ -1358,6 +1498,14 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel beginEditSession() } + func documentEditingEnded(_ doc: Document) { + run( + "if (window.odr) { if (odr.editing) { odr.editing.disable(); } if (odr.annotation) { odr.annotation.setTool(null); } }" + ) + view.endEditing(true) + isEditingDocument = false + } + func documentPagesChanged(_ doc: Document) { let pageNames = doc.pageNames ?? [] diff --git a/OpenDocumentReader/EditToolBar.swift b/OpenDocumentReader/EditToolBar.swift index 68325602..d6870dfd 100644 --- a/OpenDocumentReader/EditToolBar.swift +++ b/OpenDocumentReader/EditToolBar.swift @@ -2,14 +2,15 @@ import UIKit /// The row of editing tools under the bar, shown while a document is edited. /// As on the website: the bar keeps the way in and out of an edit, and this -/// row grows beneath it with what the open document takes. +/// row grows beneath it with what the open document takes. OpenDocument.droid's +/// `EditingTools` is the same row, with the same tools, colours and behaviour. final class EditToolBar: UIView { /// One button of the row. enum Tool: CaseIterable { case bold, italic, underline, strikethrough case textColor, highlight, fontSize - case markHighlight, markUnderline, markStrikeOut, markSquiggly, markDraw, markColor + case markHighlight, markUnderline, markStrikeOut, markSquiggly, markDraw case undo, redo var symbol: String { @@ -25,7 +26,6 @@ final class EditToolBar: UIView { case .markStrikeOut: return "strikethrough" case .markSquiggly: return "scribble.variable" case .markDraw: return "pencil.tip" - case .markColor: return "paintpalette" case .undo: return "arrow.uturn.backward" case .redo: return "arrow.uturn.forward" } @@ -45,7 +45,6 @@ final class EditToolBar: UIView { case .markStrikeOut: return NSLocalizedString("mark_strike_out", comment: "") case .markSquiggly: return NSLocalizedString("mark_squiggly", comment: "") case .markDraw: return NSLocalizedString("mark_draw", comment: "") - case .markColor: return NSLocalizedString("mark_color", comment: "") case .undo: return NSLocalizedString("edit_undo", comment: "") case .redo: return NSLocalizedString("edit_redo", comment: "") } @@ -80,10 +79,48 @@ final class EditToolBar: UIView { /// Whether the button opens a menu rather than acting at once. var opensMenu: Bool { switch self { - case .textColor, .highlight, .fontSize, .markColor: return true + case .textColor, .fontSize: return true default: return false } } + + /// Whether a bar under the icon shows the colour the tool applies. + var showsColor: Bool { + switch self { + case .textColor, .highlight, .markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, + .markDraw: + return true + default: return false + } + } + + /// A split button: the tool acts, and the chevron beside it picks the + /// colour it acts with. + var hasColorChevron: Bool { + showsColor && self != .textColor + } + + /// The colours the tool's menu offers. + var swatches: [Swatch] { + switch self { + case .textColor: return EditToolBar.textColors + case .highlight: return EditToolBar.highlightColors + default: return EditToolBar.markColors + } + } + + /// The colour a marker starts with: a wash for the highlighter, red for + /// the three lines, blue ink for the pen - as on the website. + var defaultColor: String? { + switch self { + case .textColor: return EditToolBar.textColors[0].hex + case .highlight: return EditToolBar.highlightColors[0].hex + case .markHighlight: return "#ffe633" + case .markUnderline, .markStrikeOut, .markSquiggly: return "#e53935" + case .markDraw: return "#1e88e5" + default: return nil + } + } } /// What the row holds, by what the page is. @@ -106,7 +143,7 @@ final class EditToolBar: UIView { case .plain: return [.undo, .redo] case .pdf: - return [.markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw, .markColor, .undo] + return [.markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw, .undo] } } } @@ -127,6 +164,8 @@ final class EditToolBar: UIView { let hex: String } + // the colours both apps offer, OpenDocument.droid's EditingTools being the + // other copy. The first of each is the website's own default static let textColors = [ Swatch(name: "color_black", hex: "#191c1e"), Swatch(name: "color_red", hex: "#e53935"), @@ -173,6 +212,7 @@ final class EditToolBar: UIView { private let scrollView = UIScrollView() private let stack = UIStackView() private var buttons: [Tool: UIButton] = [:] + private var bars: [Tool: UIView] = [:] override init(frame: CGRect) { super.init(frame: frame) @@ -221,6 +261,7 @@ final class EditToolBar: UIView { view.removeFromSuperview() } buttons = [:] + bars = [:] guard let layout else { isHidden = true @@ -237,7 +278,18 @@ final class EditToolBar: UIView { for tool in layout.tools { let button = makeButton(for: tool) buttons[tool] = button - stack.addArrangedSubview(button) + + guard tool.hasColorChevron else { + stack.addArrangedSubview(button) + + continue + } + + let pair = UIStackView(arrangedSubviews: [button, makeChevron(for: tool)]) + pair.axis = .horizontal + pair.spacing = 0 + pair.alignment = .center + stack.addArrangedSubview(pair) } } @@ -283,7 +335,11 @@ final class EditToolBar: UIView { button.configuration = configuration } - if tool.opensMenu, advancedEditing { + if tool.showsColor { + addBar(to: button, for: tool) + } + + if tool.opensMenu, advancedEditing || !tool.isAdvanced { button.menu = makeMenu(for: tool) button.showsMenuAsPrimaryAction = true } else { @@ -296,6 +352,52 @@ final class EditToolBar: UIView { return button } + /// The half of a split button that picks the tool's colour. + private func makeChevron(for tool: Tool) -> UIButton { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage( + systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(scale: .small)) + configuration.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 6) + + let chevron = UIButton(configuration: configuration) + chevron.accessibilityLabel = String( + format: NSLocalizedString("edit_color_of", comment: ""), tool.label) + chevron.accessibilityIdentifier = "edit-tool-\(tool.symbol)-color" + + if advancedEditing || !tool.isAdvanced { + chevron.menu = colorMenu(for: tool, swatches: tool.swatches, offersNone: tool == .highlight) + chevron.showsMenuAsPrimaryAction = true + } else { + chevron.addAction( + UIAction { [weak self] _ in + self?.onTap?(tool) + }, for: .touchUpInside) + } + + return chevron + } + + /// A bar under the icon in the colour the tool applies. + private func addBar(to button: UIButton, for tool: Tool) { + let bar = UIView() + bar.isUserInteractionEnabled = false + bar.layer.cornerRadius = 1 + bar.layer.borderWidth = 0.5 + bar.layer.borderColor = UIColor.separator.cgColor + bar.backgroundColor = UIColor(hex: tool.defaultColor ?? "#000000") + bar.translatesAutoresizingMaskIntoConstraints = false + button.addSubview(bar) + + NSLayoutConstraint.activate([ + bar.widthAnchor.constraint(equalToConstant: 16), + bar.heightAnchor.constraint(equalToConstant: 3), + bar.centerXAnchor.constraint(equalTo: button.centerXAnchor), + bar.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -3), + ]) + + bars[tool] = bar + } + private func makeMenu(for tool: Tool) -> UIMenu { switch tool { case .fontSize: @@ -309,10 +411,6 @@ final class EditToolBar: UIView { case .textColor: return colorMenu(for: tool, swatches: Self.textColors, offersNone: false) - case .highlight: - return colorMenu(for: tool, swatches: Self.highlightColors, offersNone: true) - case .markColor: - return colorMenu(for: tool, swatches: Self.markColors, offersNone: false) default: return UIMenu() @@ -367,6 +465,38 @@ final class EditToolBar: UIView { buttons[tool]?.isEnabled = enabled } + /// Paints the bar under `tool` in the colour it now applies. + func setColor(_ tool: Tool, _ color: UIColor) { + bars[tool]?.backgroundColor = color + } + + /// Shows the selection's size as "12 pt", or the symbol where the + /// selection states none - as the website's size select does. + func setFontSize(_ points: String?) { + guard let button = buttons[.fontSize] else { return } + + var configuration = button.configuration + if let points { + configuration?.image = nil + configuration?.title = String( + format: NSLocalizedString("edit_font_size_points", comment: ""), points) + } else { + configuration?.image = UIImage(systemName: Tool.fontSize.symbol) + configuration?.title = nil + } + button.configuration = configuration + } + + /// For the tests: the colour the bar under `tool` shows. + func color(of tool: Tool) -> UIColor? { + bars[tool]?.backgroundColor + } + + /// For the tests: what the size tool says. + var fontSizeTitle: String? { + buttons[.fontSize]?.configuration?.title + } + /// For the tests: whether the row starts with the Pro badge. var showsProBadge: Bool { stack.arrangedSubviews.first?.accessibilityIdentifier == "edit-tool-pro" diff --git a/OpenDocumentReader/Main.storyboard b/OpenDocumentReader/Main.storyboard index 9e345f62..5b57558a 100644 --- a/OpenDocumentReader/Main.storyboard +++ b/OpenDocumentReader/Main.storyboard @@ -46,7 +46,7 @@ - + diff --git a/OpenDocumentReader/ca.lproj/Localizable.strings b/OpenDocumentReader/ca.lproj/Localizable.strings index 60aa0675..ad8f7bbc 100644 --- a/OpenDocumentReader/ca.lproj/Localizable.strings +++ b/OpenDocumentReader/ca.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Ratlla"; "mark_squiggly" = "Subratllat ondulat"; "mark_draw" = "Dibuixa"; -"mark_color" = "Color de la marca"; /* The colors the menus offer */ "color_black" = "Negre"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Aquest document no es pot editar."; "edit_refused_range" = "Una edició no pot passar per sobre d’una imatge o d’una taula."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Les cel·les amb fórmules que llegeixen la teva edició estan desactualitzades. El fitxer desat conserva les fórmules i un full de càlcul les torna a calcular."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Selecciona text i després una eina per marcar-lo."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Color de %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/ca.lproj/Localizable.stringsdict b/OpenDocumentReader/ca.lproj/Localizable.stringsdict new file mode 100644 index 00000000..d355446d --- /dev/null +++ b/OpenDocumentReader/ca.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d cel·la amb fórmula mostra un resultat que la vostra edició ha deixat desactualitzat. El fitxer desat conserva la fórmula, i una aplicació de fulls de càlcul la torna a calcular. + other + %d cel·les amb fórmula mostren resultats que la vostra edició ha deixat desactualitzats. El fitxer desat conserva les fórmules, i una aplicació de fulls de càlcul les torna a calcular. + + + + diff --git a/OpenDocumentReader/cs.lproj/Localizable.strings b/OpenDocumentReader/cs.lproj/Localizable.strings index cf097c22..16efa586 100644 --- a/OpenDocumentReader/cs.lproj/Localizable.strings +++ b/OpenDocumentReader/cs.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Přeškrtnout"; "mark_squiggly" = "Vlnité podtržení"; "mark_draw" = "Kreslit"; -"mark_color" = "Barva značky"; /* The colors the menus offer */ "color_black" = "Černá"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Tento dokument nelze upravovat."; "edit_refused_range" = "Úprava nemůže sahat přes obrázek nebo tabulku."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Buňky se vzorci, které čtou vaši úpravu, jsou zastaralé. Uložený soubor vzorce zachová a tabulkový procesor je přepočítá."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Vyberte text a pak nástroj, kterým ho označíte."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Barva: %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/cs.lproj/Localizable.stringsdict b/OpenDocumentReader/cs.lproj/Localizable.stringsdict new file mode 100644 index 00000000..25596513 --- /dev/null +++ b/OpenDocumentReader/cs.lproj/Localizable.stringsdict @@ -0,0 +1,26 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d buňka se vzorcem ukazuje výsledek, který vaše úprava učinila zastaralým. Uložený soubor vzorec zachová a tabulkový program ho spočítá znovu. + few + %d buňky se vzorcem ukazují výsledky, které vaše úprava učinila zastaralými. Uložený soubor vzorce zachová a tabulkový program je spočítá znovu. + many + %d buňky se vzorcem ukazují výsledky, které vaše úprava učinila zastaralými. Uložený soubor vzorce zachová a tabulkový program je spočítá znovu. + other + %d buněk se vzorcem ukazuje výsledky, které vaše úprava učinila zastaralými. Uložený soubor vzorce zachová a tabulkový program je spočítá znovu. + + + + diff --git a/OpenDocumentReader/da.lproj/Localizable.strings b/OpenDocumentReader/da.lproj/Localizable.strings index 7958475f..0e8473ae 100644 --- a/OpenDocumentReader/da.lproj/Localizable.strings +++ b/OpenDocumentReader/da.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Gennemstreg"; "mark_squiggly" = "Bølget understregning"; "mark_draw" = "Tegn"; -"mark_color" = "Markeringsfarve"; /* The colors the menus offer */ "color_black" = "Sort"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Dette dokument kan ikke redigeres."; "edit_refused_range" = "En redigering kan ikke række hen over et billede eller en tabel."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Formelceller, der læser din redigering, er forældede. Den gemte fil beholder formlerne, og et regnearksprogram beregner dem igen."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Vælg tekst og derefter et værktøj for at markere den."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Farve til %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/da.lproj/Localizable.stringsdict b/OpenDocumentReader/da.lproj/Localizable.stringsdict new file mode 100644 index 00000000..5e383c69 --- /dev/null +++ b/OpenDocumentReader/da.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d formelcelle viser et resultat, som din redigering har gjort forældet. Den gemte fil beholder formlen, og et regnearksprogram beregner den igen. + other + %d formelceller viser resultater, som din redigering har gjort forældede. Den gemte fil beholder formlerne, og et regnearksprogram beregner dem igen. + + + + diff --git a/OpenDocumentReader/de.lproj/Localizable.strings b/OpenDocumentReader/de.lproj/Localizable.strings index 9b513a4e..2f2917d0 100644 --- a/OpenDocumentReader/de.lproj/Localizable.strings +++ b/OpenDocumentReader/de.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Durchstreichen"; "mark_squiggly" = "Wellenlinie"; "mark_draw" = "Zeichnen"; -"mark_color" = "Markierungsfarbe"; /* The colors the menus offer */ "color_black" = "Schwarz"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Dieses Dokument kann nicht bearbeitet werden."; "edit_refused_range" = "Eine Änderung kann nicht über ein Bild oder eine Tabelle hinausreichen."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Formelzellen, die deine Änderung lesen, sind veraltet. Die gespeicherte Datei behält die Formeln, eine Tabellenkalkulation berechnet sie neu."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Text auswählen, dann ein Werkzeug, um ihn zu markieren."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Farbe für %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/de.lproj/Localizable.stringsdict b/OpenDocumentReader/de.lproj/Localizable.stringsdict new file mode 100644 index 00000000..eb978e78 --- /dev/null +++ b/OpenDocumentReader/de.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Formelzelle zeigt ein Ergebnis, das durch Ihre Änderung veraltet ist. Die gespeicherte Datei behält die Formel, und eine Tabellenkalkulation berechnet sie neu. + other + %d Formelzellen zeigen Ergebnisse, die durch Ihre Änderung veraltet sind. Die gespeicherte Datei behält die Formeln, und eine Tabellenkalkulation berechnet sie neu. + + + + diff --git a/OpenDocumentReader/en.lproj/Localizable.strings b/OpenDocumentReader/en.lproj/Localizable.strings index ef1f5a4c..2748c6b8 100644 --- a/OpenDocumentReader/en.lproj/Localizable.strings +++ b/OpenDocumentReader/en.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Strike out"; "mark_squiggly" = "Squiggly underline"; "mark_draw" = "Draw"; -"mark_color" = "Mark color"; /* The colors the menus offer */ "color_black" = "Black"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "This document cannot be edited."; "edit_refused_range" = "An edit cannot reach over a picture or a table."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Formula cells that read your edit are out of date. The saved file keeps the formulas, and a spreadsheet app computes them again."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Select text, then a tool, to mark it."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "%@ color"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/en.lproj/Localizable.stringsdict b/OpenDocumentReader/en.lproj/Localizable.stringsdict new file mode 100644 index 00000000..c1e04aa4 --- /dev/null +++ b/OpenDocumentReader/en.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d formula cell shows a result your edit made out of date. The saved file keeps the formula, and a spreadsheet app computes it again. + other + %d formula cells show results your edit made out of date. The saved file keeps the formulas, and a spreadsheet app computes them again. + + + + diff --git a/OpenDocumentReader/es.lproj/Localizable.strings b/OpenDocumentReader/es.lproj/Localizable.strings index 4dda9442..190a97db 100644 --- a/OpenDocumentReader/es.lproj/Localizable.strings +++ b/OpenDocumentReader/es.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Tachar"; "mark_squiggly" = "Subrayado ondulado"; "mark_draw" = "Dibujar"; -"mark_color" = "Color de la marca"; /* The colors the menus offer */ "color_black" = "Negro"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Este documento no se puede editar."; "edit_refused_range" = "Una edición no puede pasar por encima de una imagen o una tabla."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Las celdas con fórmulas que leen tu edición están desactualizadas. El archivo guardado conserva las fórmulas y una hoja de cálculo las vuelve a calcular."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Selecciona texto y luego una herramienta para marcarlo."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Color de %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/es.lproj/Localizable.stringsdict b/OpenDocumentReader/es.lproj/Localizable.stringsdict new file mode 100644 index 00000000..9f3a7512 --- /dev/null +++ b/OpenDocumentReader/es.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d celda con fórmula muestra un resultado que su edición dejó desactualizado. El archivo guardado conserva la fórmula y una aplicación de hojas de cálculo la vuelve a calcular. + other + %d celdas con fórmula muestran resultados que su edición dejó desactualizados. El archivo guardado conserva las fórmulas y una aplicación de hojas de cálculo las vuelve a calcular. + + + + diff --git a/OpenDocumentReader/fr.lproj/Localizable.strings b/OpenDocumentReader/fr.lproj/Localizable.strings index 0a180313..2e08e7d5 100644 --- a/OpenDocumentReader/fr.lproj/Localizable.strings +++ b/OpenDocumentReader/fr.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Barrer"; "mark_squiggly" = "Soulignement ondulé"; "mark_draw" = "Dessiner"; -"mark_color" = "Couleur de l’annotation"; /* The colors the menus offer */ "color_black" = "Noir"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Ce document ne peut pas être modifié."; "edit_refused_range" = "Une modification ne peut pas passer par-dessus une image ou un tableau."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Les cellules de formule qui lisent votre modification ne sont plus à jour. Le fichier enregistré garde les formules, et un tableur les recalcule."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Sélectionnez du texte, puis un outil, pour l’annoter."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Couleur : %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/fr.lproj/Localizable.stringsdict b/OpenDocumentReader/fr.lproj/Localizable.stringsdict new file mode 100644 index 00000000..5a449598 --- /dev/null +++ b/OpenDocumentReader/fr.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d cellule de formule affiche un résultat que votre modification a rendu obsolète. Le fichier enregistré conserve la formule, et un tableur la recalcule. + other + %d cellules de formule affichent des résultats que votre modification a rendus obsolètes. Le fichier enregistré conserve les formules, et un tableur les recalcule. + + + + diff --git a/OpenDocumentReader/ga.lproj/Localizable.strings b/OpenDocumentReader/ga.lproj/Localizable.strings index 242d326b..c440ae61 100644 --- a/OpenDocumentReader/ga.lproj/Localizable.strings +++ b/OpenDocumentReader/ga.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Cuir líne tríd"; "mark_squiggly" = "Líne thonnach faoi"; "mark_draw" = "Tarraing"; -"mark_color" = "Dath na marcála"; /* The colors the menus offer */ "color_black" = "Dubh"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Ní féidir an doiciméad seo a chur in eagar."; "edit_refused_range" = "Ní féidir le hathrú dul thar phictiúr ná thar thábla."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Tá cealla foirmle a léann d’athrú as dáta. Coinníonn an comhad sábháilte na foirmlí, agus ríomhann scarbhileog arís iad."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Roghnaigh téacs, ansin uirlis, chun é a mharcáil."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Dath: %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/ga.lproj/Localizable.stringsdict b/OpenDocumentReader/ga.lproj/Localizable.stringsdict new file mode 100644 index 00000000..283fd6db --- /dev/null +++ b/OpenDocumentReader/ga.lproj/Localizable.stringsdict @@ -0,0 +1,28 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Taispeánann %d chill fhoirmle toradh atá as dáta de bharr d'eagarthóireachta. Coinníonn an comhad sábháilte an fhoirmle, agus ríomhann aip scarbhileog arís í. + two + Taispeánann %d cill fhoirmle torthaí atá as dáta de bharr d'eagarthóireachta. Coinníonn an comhad sábháilte na foirmlí, agus ríomhann aip scarbhileog arís iad. + few + Taispeánann %d cill fhoirmle torthaí atá as dáta de bharr d'eagarthóireachta. Coinníonn an comhad sábháilte na foirmlí, agus ríomhann aip scarbhileog arís iad. + many + Taispeánann %d cill fhoirmle torthaí atá as dáta de bharr d'eagarthóireachta. Coinníonn an comhad sábháilte na foirmlí, agus ríomhann aip scarbhileog arís iad. + other + Taispeánann %d cill fhoirmle torthaí atá as dáta de bharr d'eagarthóireachta. Coinníonn an comhad sábháilte na foirmlí, agus ríomhann aip scarbhileog arís iad. + + + + diff --git a/OpenDocumentReader/it.lproj/Localizable.strings b/OpenDocumentReader/it.lproj/Localizable.strings index 7cb1c0dd..c0f7193d 100644 --- a/OpenDocumentReader/it.lproj/Localizable.strings +++ b/OpenDocumentReader/it.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Barra"; "mark_squiggly" = "Sottolineatura ondulata"; "mark_draw" = "Disegna"; -"mark_color" = "Colore dell’annotazione"; /* The colors the menus offer */ "color_black" = "Nero"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Questo documento non può essere modificato."; "edit_refused_range" = "Una modifica non può passare sopra un’immagine o una tabella."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Le celle con formule che leggono la tua modifica non sono aggiornate. Il file salvato conserva le formule e un foglio di calcolo le ricalcola."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Seleziona del testo, poi uno strumento, per annotarlo."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Colore di %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/it.lproj/Localizable.stringsdict b/OpenDocumentReader/it.lproj/Localizable.stringsdict new file mode 100644 index 00000000..33fa7934 --- /dev/null +++ b/OpenDocumentReader/it.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d cella con formula mostra un risultato reso obsoleto dalla tua modifica. Il file salvato mantiene la formula e un'app per fogli di calcolo la ricalcola. + other + %d celle con formula mostrano risultati resi obsoleti dalla tua modifica. Il file salvato mantiene le formule e un'app per fogli di calcolo le ricalcola. + + + + diff --git a/OpenDocumentReader/ja.lproj/Localizable.strings b/OpenDocumentReader/ja.lproj/Localizable.strings index b7cefd9d..dacb8a32 100644 --- a/OpenDocumentReader/ja.lproj/Localizable.strings +++ b/OpenDocumentReader/ja.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "取り消し線"; "mark_squiggly" = "波線"; "mark_draw" = "描画"; -"mark_color" = "マークの色"; /* The colors the menus offer */ "color_black" = "黒"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "この書類は編集できません。"; "edit_refused_range" = "画像や表をまたぐ編集はできません。"; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "編集した内容を参照する数式セルが古くなっています。保存したファイルには数式が残り、表計算アプリが再計算します。"; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "テキストを選択してから、ツールを選ぶとマークを付けられます。"; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "%@の色"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/ja.lproj/Localizable.stringsdict b/OpenDocumentReader/ja.lproj/Localizable.stringsdict new file mode 100644 index 00000000..5f224034 --- /dev/null +++ b/OpenDocumentReader/ja.lproj/Localizable.stringsdict @@ -0,0 +1,20 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + %d 個の数式セルに、編集によって古くなった結果が表示されています。保存したファイルには数式が残り、表計算アプリで再計算されます。 + + + + diff --git a/OpenDocumentReader/pl.lproj/Localizable.strings b/OpenDocumentReader/pl.lproj/Localizable.strings index a668becd..1cdbfd08 100644 --- a/OpenDocumentReader/pl.lproj/Localizable.strings +++ b/OpenDocumentReader/pl.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Przekreśl"; "mark_squiggly" = "Podkreślenie faliste"; "mark_draw" = "Rysuj"; -"mark_color" = "Kolor oznaczenia"; /* The colors the menus offer */ "color_black" = "Czarny"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Tego dokumentu nie można edytować."; "edit_refused_range" = "Zmiana nie może sięgać przez obraz ani tabelę."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Komórki z formułami, które odczytują twoją zmianę, są nieaktualne. Zapisany plik zachowuje formuły, a arkusz kalkulacyjny przeliczy je ponownie."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Zaznacz tekst, a potem narzędzie, aby go oznaczyć."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Kolor: %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/pl.lproj/Localizable.stringsdict b/OpenDocumentReader/pl.lproj/Localizable.stringsdict new file mode 100644 index 00000000..956c0864 --- /dev/null +++ b/OpenDocumentReader/pl.lproj/Localizable.stringsdict @@ -0,0 +1,26 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d komórka z formułą pokazuje wynik, który Twoja edycja zdezaktualizowała. Zapisany plik zachowuje formułę, a arkusz kalkulacyjny przeliczy ją ponownie. + few + %d komórki z formułami pokazują wyniki, które Twoja edycja zdezaktualizowała. Zapisany plik zachowuje formuły, a arkusz kalkulacyjny przeliczy je ponownie. + many + %d komórek z formułami pokazuje wyniki, które Twoja edycja zdezaktualizowała. Zapisany plik zachowuje formuły, a arkusz kalkulacyjny przeliczy je ponownie. + other + %d komórki z formułami pokazuje wyniki, które Twoja edycja zdezaktualizowała. Zapisany plik zachowuje formuły, a arkusz kalkulacyjny przeliczy je ponownie. + + + + diff --git a/OpenDocumentReader/pt-BR.lproj/Localizable.strings b/OpenDocumentReader/pt-BR.lproj/Localizable.strings index a1e98e94..8eabbf74 100644 --- a/OpenDocumentReader/pt-BR.lproj/Localizable.strings +++ b/OpenDocumentReader/pt-BR.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Tachar"; "mark_squiggly" = "Sublinhado ondulado"; "mark_draw" = "Desenhar"; -"mark_color" = "Cor da marcação"; /* The colors the menus offer */ "color_black" = "Preto"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Este documento não pode ser editado."; "edit_refused_range" = "Uma edição não pode passar por cima de uma imagem ou de uma tabela."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "As células com fórmulas que leem sua edição estão desatualizadas. O arquivo salvo mantém as fórmulas, e uma planilha as recalcula."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Selecione o texto e depois uma ferramenta para marcá-lo."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Cor de %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/pt-BR.lproj/Localizable.stringsdict b/OpenDocumentReader/pt-BR.lproj/Localizable.stringsdict new file mode 100644 index 00000000..98ed93d7 --- /dev/null +++ b/OpenDocumentReader/pt-BR.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d célula com fórmula mostra um resultado que sua edição desatualizou. O arquivo salvo mantém a fórmula, e um aplicativo de planilhas a calcula de novo. + other + %d células com fórmula mostram resultados que sua edição desatualizou. O arquivo salvo mantém as fórmulas, e um aplicativo de planilhas as calcula de novo. + + + + diff --git a/OpenDocumentReader/ru.lproj/Localizable.strings b/OpenDocumentReader/ru.lproj/Localizable.strings index 0acfb42f..b6d3b296 100644 --- a/OpenDocumentReader/ru.lproj/Localizable.strings +++ b/OpenDocumentReader/ru.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Зачеркнуть"; "mark_squiggly" = "Волнистое подчёркивание"; "mark_draw" = "Рисовать"; -"mark_color" = "Цвет пометки"; /* The colors the menus offer */ "color_black" = "Чёрный"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Этот документ нельзя редактировать."; "edit_refused_range" = "Изменение не может проходить через картинку или таблицу."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Ячейки с формулами, которые читают ваше изменение, устарели. В сохранённом файле формулы остаются, и табличный редактор пересчитает их."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Выделите текст, затем инструмент, чтобы пометить его."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Цвет: %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ пт"; diff --git a/OpenDocumentReader/ru.lproj/Localizable.stringsdict b/OpenDocumentReader/ru.lproj/Localizable.stringsdict new file mode 100644 index 00000000..a4dfc925 --- /dev/null +++ b/OpenDocumentReader/ru.lproj/Localizable.stringsdict @@ -0,0 +1,26 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d ячейка с формулой показывает результат, который ваша правка сделала устаревшим. Сохранённый файл сохраняет формулу, и табличный редактор пересчитает её. + few + %d ячейки с формулами показывают результаты, которые ваша правка сделала устаревшими. Сохранённый файл сохраняет формулы, и табличный редактор пересчитает их. + many + %d ячеек с формулами показывают результаты, которые ваша правка сделала устаревшими. Сохранённый файл сохраняет формулы, и табличный редактор пересчитает их. + other + %d ячейки с формулами показывают результаты, которые ваша правка сделала устаревшими. Сохранённый файл сохраняет формулы, и табличный редактор пересчитает их. + + + + diff --git a/OpenDocumentReader/sl.lproj/Localizable.strings b/OpenDocumentReader/sl.lproj/Localizable.strings index 9fe47590..fb416bf3 100644 --- a/OpenDocumentReader/sl.lproj/Localizable.strings +++ b/OpenDocumentReader/sl.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Prečrtaj"; "mark_squiggly" = "Valovito podčrtanje"; "mark_draw" = "Riši"; -"mark_color" = "Barva oznake"; /* The colors the menus offer */ "color_black" = "Črna"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Tega dokumenta ni mogoče urejati."; "edit_refused_range" = "Urejanje ne more segati čez sliko ali tabelo."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Celice s formulami, ki berejo vaše urejanje, so zastarele. Shranjena datoteka ohrani formule, preglednica pa jih znova izračuna."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Izberite besedilo, nato orodje, da ga označite."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "Barva: %@"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/sl.lproj/Localizable.stringsdict b/OpenDocumentReader/sl.lproj/Localizable.stringsdict new file mode 100644 index 00000000..54ac5803 --- /dev/null +++ b/OpenDocumentReader/sl.lproj/Localizable.stringsdict @@ -0,0 +1,26 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d celica s formulo prikazuje rezultat, ki ga je vaše urejanje naredilo zastarelega. Shranjena datoteka ohrani formulo, program za preglednice pa jo znova izračuna. + two + %d celici s formulo prikazujeta rezultata, ki ju je vaše urejanje naredilo zastarela. Shranjena datoteka ohrani formuli, program za preglednice pa ju znova izračuna. + few + %d celice s formulo prikazujejo rezultate, ki jih je vaše urejanje naredilo zastarele. Shranjena datoteka ohrani formule, program za preglednice pa jih znova izračuna. + other + %d celic s formulo prikazuje rezultate, ki jih je vaše urejanje naredilo zastarele. Shranjena datoteka ohrani formule, program za preglednice pa jih znova izračuna. + + + + diff --git a/OpenDocumentReader/tr.lproj/Localizable.strings b/OpenDocumentReader/tr.lproj/Localizable.strings index c19d93ec..568c5a91 100644 --- a/OpenDocumentReader/tr.lproj/Localizable.strings +++ b/OpenDocumentReader/tr.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "Üstünü çiz"; "mark_squiggly" = "Dalgalı alt çizgi"; "mark_draw" = "Çiz"; -"mark_color" = "İşaret rengi"; /* The colors the menus offer */ "color_black" = "Siyah"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "Bu belge düzenlenemez."; "edit_refused_range" = "Bir düzenleme bir resmin ya da tablonun üzerinden geçemez."; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "Düzenlemenizi okuyan formül hücreleri güncel değil. Kaydedilen dosya formülleri korur ve bir hesap tablosu uygulaması yeniden hesaplar."; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "İşaretlemek için metni, sonra bir aracı seçin."; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "%@ rengi"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ pt"; diff --git a/OpenDocumentReader/tr.lproj/Localizable.stringsdict b/OpenDocumentReader/tr.lproj/Localizable.stringsdict new file mode 100644 index 00000000..255724a3 --- /dev/null +++ b/OpenDocumentReader/tr.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d formül hücresi, düzenlemenizin eskittiği bir sonuç gösteriyor. Kaydedilen dosya formülü korur ve bir hesap tablosu uygulaması onu yeniden hesaplar. + other + %d formül hücresi, düzenlemenizin eskittiği sonuçlar gösteriyor. Kaydedilen dosya formülleri korur ve bir hesap tablosu uygulaması onları yeniden hesaplar. + + + + diff --git a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings index ebec65c8..d9a01eb6 100644 --- a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings +++ b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings @@ -125,7 +125,6 @@ "mark_strike_out" = "删除线"; "mark_squiggly" = "波浪线"; "mark_draw" = "绘制"; -"mark_color" = "标注颜色"; /* The colors the menus offer */ "color_black" = "黑色"; @@ -154,11 +153,15 @@ "edit_refused_read_only" = "此文档无法编辑。"; "edit_refused_range" = "编辑不能跨越图片或表格。"; -/* Said once an edit leaves formula cells with an old result */ -"edit_cells_stale" = "引用你所编辑内容的公式单元格已过期。保存的文件会保留公式,电子表格应用会重新计算。"; /* Shown when the marking tools come up on a PDF */ "mark_hint" = "先选择文本,再选择工具即可标注。"; /* The badge in front of the tools that are Pro's, in the Lite app */ "tool_pro_badge" = "Pro"; + +/* The chevron beside a tool, which picks the color it uses: "Highlight color" */ +"edit_color_of" = "%@颜色"; + +/* The size of the selected text, on the size tool: "12 pt" */ +"edit_font_size_points" = "%@ 磅"; diff --git a/OpenDocumentReader/zh-Hans.lproj/Localizable.stringsdict b/OpenDocumentReader/zh-Hans.lproj/Localizable.stringsdict new file mode 100644 index 00000000..a23e5893 --- /dev/null +++ b/OpenDocumentReader/zh-Hans.lproj/Localizable.stringsdict @@ -0,0 +1,20 @@ + + + + + edit_cells_stale + + NSStringLocalizedFormatKey + %#@cells@ + cells + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + %d 个公式单元格显示的结果因您的编辑而过时。保存的文件会保留公式,电子表格应用会重新计算。 + + + + diff --git a/OpenDocumentReaderTests/EditWorkflowTests.swift b/OpenDocumentReaderTests/EditWorkflowTests.swift index 0759627a..65e73ed0 100644 --- a/OpenDocumentReaderTests/EditWorkflowTests.swift +++ b/OpenDocumentReaderTests/EditWorkflowTests.swift @@ -69,17 +69,42 @@ class EditWorkflowTests: XCTestCase { XCTAssertEqual(controller.editButton.image, UIImage(systemName: "pencil")) } - /// What used to happen instead: the button left the bar, and saving was only - /// reachable through the menu. - func testThePencilBecomesASaveButtonWhileEditing() throws { + /// The website's two controls: the pen, drawn selected while the mode is on, + /// and the disc beside it, there only while editing and live only once the + /// page holds a change. + func testThePenStaysAndTheSaveButtonJoinsItWhileEditing() throws { openDocument() - controller.editOrSave(controller.editButton) + XCTAssertFalse(barContains(controller.saveButton)) + + controller.toggleEdit(controller.editButton) waitForEditablePage() XCTAssertTrue(document.edit) XCTAssertTrue(barContains(controller.editButton)) - XCTAssertEqual(controller.editButton.image, UIImage(systemName: "square.and.arrow.down")) + XCTAssertEqual(controller.editButton.image, UIImage(systemName: "pencil")) + XCTAssertTrue(controller.editButton.isSelected) + XCTAssertTrue(barContains(controller.saveButton)) + XCTAssertFalse(controller.saveButton.isEnabled) + + typeIntoTheFirstRun() + waitUntil { self.controller.saveButton.isEnabled } + } + + /// With nothing to lose the pen only turns the mode off, and the page stays. + func testThePenLeavesAnUnchangedEditAtOnce() throws { + openDocument() + + controller.toggleEdit(controller.editButton) + waitForEditablePage() + + controller.toggleEdit(controller.editButton) + waitForPage(where: "document.querySelectorAll('[contenteditable]').length === 0") + + XCTAssertFalse(document.edit) + XCTAssertFalse(controller.editButton.isSelected) + XCTAssertFalse(barContains(controller.saveButton)) + XCTAssertNil(controller.editToolBar.layout) } /// A document nothing can be written back to keeps the room for itself. @@ -100,7 +125,7 @@ class EditWorkflowTests: XCTestCase { func testTappingTheTextReachesTheEditableRun() throws { openDocument() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() let tapped = @@ -125,7 +150,7 @@ class EditWorkflowTests: XCTestCase { func testAFocusedRunTakesTheEdit() throws { openDocument() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() typeIntoTheFirstRun() @@ -143,7 +168,7 @@ class EditWorkflowTests: XCTestCase { XCTAssertNil(controller.editToolBar.layout) - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() waitForTools() @@ -163,7 +188,7 @@ class EditWorkflowTests: XCTestCase { try present(documentURL) openDocument(where: "document.querySelectorAll('td').length > 0") - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForTools() XCTAssertEqual(controller.editToolBar.layout, .plain) @@ -175,7 +200,7 @@ class EditWorkflowTests: XCTestCase { func testTheSelectionStyleReachesTheButtons() throws { openDocument() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() waitForTools() @@ -185,6 +210,45 @@ class EditWorkflowTests: XCTestCase { XCTAssertFalse(controller.editToolBar.isPressed(.italic)) } + /// As on the website: the colour bars and the size follow the selection, + /// and the highlight shows pressed where the selection has one. + func testTheSelectionColorsAndSizeReachTheTools() throws { + openDocument() + + controller.toggleEdit(controller.editButton) + waitForEditablePage() + waitForTools() + + XCTAssertNil(controller.editToolBar.fontSizeTitle) + + _ = evaluate("odr.onSelectionChange({ color: '#e53935', highlight: '#c5e1a5', size: '12pt' })") + waitUntil { self.controller.editToolBar.isPressed(.highlight) } + + XCTAssertEqual(controller.editToolBar.color(of: .textColor)?.hexString, "#e53935") + XCTAssertEqual(controller.editToolBar.color(of: .highlight)?.hexString, "#c5e1a5") + XCTAssertEqual(controller.editToolBar.fontSizeTitle, "12 pt") + } + + /// The highlight button turns a highlight on in its colour, and off again. + func testTheHighlightButtonTogglesTheHighlight() throws { + openDocument() + + controller.toggleEdit(controller.editButton) + waitForEditablePage() + waitForTools() + selectTheFirstRun() + + controller.editToolBar.onTap?(.highlight) + waitForPage(where: "document.querySelector('x-s[data-odr-id]').style.backgroundColor !== ''") + + _ = evaluate("odr.onSelectionChange({ highlight: '#fff59d' })") + waitUntil { self.controller.editToolBar.isPressed(.highlight) } + selectTheFirstRun() + + controller.editToolBar.onTap?(.highlight) + waitForPage(where: "document.querySelector('x-s[data-odr-id]').style.backgroundColor === ''") + } + // MARK: - a pdf /// The pencil is a highlighter on a pdf, and the edit is a set of marks. @@ -199,7 +263,7 @@ class EditWorkflowTests: XCTestCase { let sizeBefore = try fileSize() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForPage(where: "document.querySelectorAll('[data-odr-space]').length > 0") waitForTools() @@ -207,6 +271,10 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(controller.editToolBar.shows(.markHighlight)) XCTAssertFalse(controller.editToolBar.shows(.redo)) + // each marker has a colour of its own, as on the website + XCTAssertEqual(controller.editToolBar.color(of: .markHighlight)?.hexString, "#ffe633") + XCTAssertEqual(controller.editToolBar.color(of: .markDraw)?.hexString, "#1e88e5") + let marks = evaluate( """ @@ -241,7 +309,7 @@ class EditWorkflowTests: XCTestCase { func testSavingWritesTheEditToTheFile() throws { openDocument() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() typeIntoTheFirstRun() @@ -255,30 +323,67 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(try reopenedText().contains(Self.editedText)) } - /// The save button is the way out of the edit as well as the way to write - /// it: a page left editable after a successful save has no button left to - /// end it. - func testSavingFromTheBarLeavesEditMode() throws { + /// As on the website, a save writes the edit and stays in it: the file is + /// rendered again, and the new page is back in the mode with a clean log. + func testSavingStaysInEditMode() throws { openDocument() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() typeIntoTheFirstRun() - controller.editOrSave(controller.editButton) - waitForPage(where: "document.querySelectorAll('[contenteditable]').length === 0") + let saved = expectation(description: "saved") + controller.saveAndStay { success in + XCTAssertTrue(success) + saved.fulfill() + } + wait(for: [saved], timeout: 60) - XCTAssertFalse(document.edit) - XCTAssertEqual(controller.editButton.image, UIImage(systemName: "pencil")) + waitForPage( + where: + "document.querySelectorAll('[contenteditable]').length > 0 && document.body.textContent.indexOf('\(Self.editedText)') >= 0" + ) + + XCTAssertTrue(document.edit) + XCTAssertTrue(controller.editButton.isSelected) + XCTAssertTrue(barContains(controller.saveButton)) XCTAssertTrue(try reopenedText().contains(Self.editedText)) } + /// A marker pressed with text selected marks it once and leaves no tool + /// armed, the website's `markOnce`. + func testAMarkerMarksASelectionOnceAndArmsNothing() throws { + documentURL = try copyFixture(ofType: "pdf") + try present(documentURL) + openDocument(where: "document.querySelectorAll('[data-odr-space]').length > 0") + + controller.toggleEdit(controller.editButton) + waitForTools() + + _ = evaluate( + """ + (function () { + var range = document.createRange(); + range.selectNodeContents(document.querySelector('[data-odr-space]')); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + })() + """) + + controller.editToolBar.onTap?(.markUnderline) + waitForPage(where: "odr.annotation.list().length > 0") + + XCTAssertEqual(evaluate("odr.annotation.getTool() === null") as? Bool, true) + XCTAssertFalse(controller.editToolBar.isPressed(.markUnderline)) + } + /// The way back to reading without saving, and the only one besides leaving /// the document altogether. func testDiscardingChangesLeavesEditModeAndTheFileAlone() throws { openDocument() - controller.editOrSave(controller.editButton) + controller.toggleEdit(controller.editButton) waitForEditablePage() typeIntoTheFirstRun() @@ -354,6 +459,20 @@ class EditWorkflowTests: XCTestCase { XCTFail("timed out waiting for \(condition)", file: file, line: line) } + private func selectTheFirstRun() { + _ = evaluate( + """ + (function () { + var run = document.querySelector('x-s[data-odr-id]'); + var range = document.createRange(); + range.selectNodeContents(run); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + })() + """) + } + /// What typing amounts to: the caret in a run, and a `beforeinput` the /// editor takes and applies itself - the same way odrcore's own tests type. private func typeIntoTheFirstRun() { diff --git a/README.md b/README.md index 9c614648..652d53b2 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,12 @@ Every document is rendered with odrcore's editing scaffolding, so the pencil only turns the mode on with `odr.editing.enable()` and the reader stays where they were. A row of tools (`EditToolBar`) grows under the bar with what the page is: formatting for a text document, undo and redo alone for a spreadsheet -or a plain text file, the five markers for a pdf - the same shape as the -website's viewer and the Android app. The page talks back through one +or a plain text file, the five markers for a pdf. The website's viewer is the +reference, and OpenDocument.droid's `EditingTools` matches it: the same tools, +the same colours, a split button for the highlight and each marker, colour bars +and a size that follow the selection, and a marker that marks a selection once +and leaves nothing armed. The one difference is "Other color…", the system +colour picker, which Android does not have. The page talks back through one `WKScriptMessageHandler`: the state of its log for the undo and redo buttons and for the prompt on leaving, the style under the caret for the format buttons, the formula cells an edit left out of date, and every refusal, which @@ -117,9 +121,11 @@ is shown in a word. Saving asks the page for its log (`odr.editing.getOperations()`, or `odr.annotation.getAnnotations()` for a pdf), hands it to odrcore, and writes -the file beside the open one before moving it into place. The save button is -the way out of the edit, as before: the file holds the edit once it is written, -so leaving edit mode reads back what was saved. +the file beside the open one before moving it into place. As on the website, +the pencil (the pen) turns the mode on and off and the save button (the disc) +beside it writes: a save stays in the edit, renders the file again and turns +the mode back on in the new page. Leaving with changes the page alone holds +asks first; leaving without any only turns the mode off. ## Formatting From 3f25598752861488faf77f40a0aa15a78c79e2da Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 19 Sep 2026 10:00:31 +0200 Subject: [PATCH 4/5] Take odrcore 7.1.0 A text file and a pdf now save and report their changes through the core's own calls. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Si9aS96QtAu8r9rF8zQ7Xm --- CHANGELOG.md | 2 +- OpenDocumentReader.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- OpenDocumentReader/CoreWrapper.swift | 3 +- .../DocumentViewController.swift | 48 +++---------------- .../EditWorkflowTests.swift | 1 + 6 files changed, 13 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9817363..a630bb30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ once the version tag exists. ### Changed -- The engine is odrcore 7.0.0, up from 6.13.0. +- The engine is odrcore 7.1.0, up from 6.13.0. - Entering an edit no longer reloads the document, so the page stays where it was. Leaving one without changes no longer asks about saving them. - The pencil turns the edit on and off, and a save button beside it writes the diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index e10b71a0..627f0823 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -830,7 +830,7 @@ repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 7.0.0; + minimumVersion = 7.1.0; }; }; AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { diff --git a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 573e4b51..8ed0f6c4 100644 --- a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/opendocument-app/OpenDocument.core.git", "state" : { - "revision" : "2ac2be9f08a315ae49cfff2bf340d0914a74ebc4", - "version" : "7.0.0" + "revision" : "cee4ef5e7dbc4e781bb8459dc5269cded98f824a", + "version" : "7.1.0" } }, { diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index f06183ef..36afa036 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -259,7 +259,8 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ } try document.save(to: temporary.path) } else if let textFile { - try textFile.writeEdited(operations: payload).write(to: temporary) + try textFile.edit(operations: payload) + try textFile.save(to: temporary.path) } else if let pdfFile { try pdfFile.annotate(payload).write(to: temporary) } else { diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 8fe5d7ce..e591b78a 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -680,22 +680,11 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel }; if (!odr.annotation) { return; } // an armed tool marks a selection as it is made, which is what a - // touch screen needs. The annotator has no callback of its own, so - // the count of marks is reported after every gesture that can - // change it; a mark settles 50ms after the pointer lifts + // touch screen needs odr.annotation.setOptions({ markOnSelection: true }); - var reported = -1; - var reportMarks = function () { - var count = odr.annotation.list().length; - if (count === reported) { return; } - reported = count; - post({ type: 'marks', count: count }); + odr.onAnnotationChange = function (e) { + post({ type: 'marks', count: e && e.count ? e.count : 0 }); }; - var reportMarksSoon = function () { window.setTimeout(reportMarks, 120); }; - document.addEventListener('pointerup', reportMarksSoon); - document.addEventListener('pointercancel', reportMarksSoon); - document.addEventListener('selectionchange', reportMarksSoon); - odr.reportMarks = reportMarks; })(); """ @@ -809,7 +798,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel switch tool { case .undo: - run(canMark ? "odr.annotation.undo(); odr.reportMarks()" : "odr.editing.undo()") + run(canMark ? "odr.annotation.undo()" : "odr.editing.undo()") case .redo: run("odr.editing.redo()") case .bold, .italic, .underline, .strikethrough: @@ -837,33 +826,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel private func pressMarker(_ tool: EditToolBar.Tool, recolor: Bool) { guard let name = tool.pageName else { return } - let script = """ - (function () { - var a = odr.annotation; - var rgb = \(markColor(of: tool).deviceRGB); - var selection = window.getSelection(); - var selected = '\(name)' !== 'ink' && selection && !selection.isCollapsed - && selection.toString().length > 0; - if (selected) { - a.setColor(rgb); - a.setWidth(2); - a.setTool('\(name)'); - a.mark(); - a.setTool(null); - selection.removeAllRanges(); - odr.reportMarks(); - } else if (\(recolor)) { - if (a.getTool() === '\(name)') { a.setColor(rgb); } - } else if (a.getTool() === '\(name)') { - a.setTool(null); - } else { - a.setColor(rgb); - a.setWidth(2); - a.setTool('\(name)'); - } - return a.getTool(); - })() - """ + let script = + "odr.annotation.\(recolor ? "recolor" : "press")('\(name)', { color: \(markColor(of: tool).deviceRGB), width: 2 })" webview.evaluateJavaScript(script) { [weak self] armed, error in if let error { diff --git a/OpenDocumentReaderTests/EditWorkflowTests.swift b/OpenDocumentReaderTests/EditWorkflowTests.swift index 65e73ed0..39f60584 100644 --- a/OpenDocumentReaderTests/EditWorkflowTests.swift +++ b/OpenDocumentReaderTests/EditWorkflowTests.swift @@ -376,6 +376,7 @@ class EditWorkflowTests: XCTestCase { XCTAssertEqual(evaluate("odr.annotation.getTool() === null") as? Bool, true) XCTAssertFalse(controller.editToolBar.isPressed(.markUnderline)) + waitUntil { self.controller.saveButton.isEnabled } } /// The way back to reading without saving, and the only one besides leaving From 57c005b0999ce21c6122617492b4c6d1f180ab12 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 19 Sep 2026 10:36:52 +0200 Subject: [PATCH 5/5] Shorten the comments, the readme and the changelog Also keep a wide-gamut colour from the picker in range, and test the save of a text file. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Si9aS96QtAu8r9rF8zQ7Xm --- Ads/Linked.swift | 2 +- CHANGELOG.md | 21 ++++----- OpenDocumentReader/CoreWrapper.swift | 3 +- OpenDocumentReader/Document.swift | 6 +-- .../DocumentViewController.swift | 28 ++++-------- OpenDocumentReader/EditToolBar.swift | 31 +++++-------- OpenDocumentReader/Features.swift | 4 +- .../ca.lproj/Localizable.strings | 11 ++--- .../cs.lproj/Localizable.strings | 11 ++--- .../da.lproj/Localizable.strings | 11 ++--- .../de.lproj/Localizable.strings | 11 ++--- .../en.lproj/Localizable.strings | 11 ++--- .../es.lproj/Localizable.strings | 11 ++--- .../fr.lproj/Localizable.strings | 11 ++--- .../ga.lproj/Localizable.strings | 11 ++--- .../it.lproj/Localizable.strings | 11 ++--- .../ja.lproj/Localizable.strings | 11 ++--- .../pl.lproj/Localizable.strings | 11 ++--- .../pt-BR.lproj/Localizable.strings | 11 ++--- .../ru.lproj/Localizable.strings | 11 ++--- .../sl.lproj/Localizable.strings | 11 ++--- .../tr.lproj/Localizable.strings | 11 ++--- .../zh-Hans.lproj/Localizable.strings | 11 ++--- .../EditWorkflowTests.swift | 17 +++---- .../OpenDocumentReaderTests.swift | 14 ++++++ README.md | 45 +++++++------------ 26 files changed, 133 insertions(+), 214 deletions(-) diff --git a/Ads/Linked.swift b/Ads/Linked.swift index afaab96c..08906a10 100644 --- a/Ads/Linked.swift +++ b/Ads/Linked.swift @@ -1,4 +1,4 @@ /// Read through ``Features``. let LINKS_ADS = true -/// Read through ``Features``. Lite edits inside a paragraph and sells the rest. +/// Read through ``Features``. let ADVANCED_EDITING = false diff --git a/CHANGELOG.md b/CHANGELOG.md index a630bb30..bc57da9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,24 +22,19 @@ once the version tag exists. ### Added - The name of the open document is shown at the top, between the buttons. -- A row of editing tools under the bar while a document is edited: bold, - italic, underline, strikethrough, text colour, highlight, text size, undo - and redo. -- A PDF can be marked up: highlight, underline, strike out, squiggly underline - and drawing, each in a colour of its own, saved into the file. +- Editing tools under the bar: bold, italic, underline, strikethrough, text + colour, highlight, text size, undo and redo. +- A PDF can be marked up with highlights, lines and drawings. - Presentations, Excel files and plain text files can be edited and saved. -- Undo and redo while editing. ### Changed - The engine is odrcore 7.1.0, up from 6.13.0. -- Entering an edit no longer reloads the document, so the page stays where it - was. Leaving one without changes no longer asks about saving them. -- The pencil turns the edit on and off, and a save button beside it writes the - file. A save no longer ends the edit. -- The Lite app edits inside a paragraph. Formatting, new or joined paragraphs - and marks on a PDF are part of Pro, and the Lite app says so when they are - reached for. +- Entering an edit keeps your place in the document. +- The pencil turns editing on and off, and a new save button saves without + leaving the edit. +- Lite edits text inside a paragraph. Formatting, paragraphs and PDF marks are + part of Pro. ### Fixed diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index 36afa036..eaf19e5e 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -88,8 +88,7 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ /// it takes one is kept, so having it *is* the answer. @objc var isEditable: Bool { lock.withLock { document != nil || textFile != nil } } - /// Whether the file is a pdf that takes marks - the pdf's own answer, not - /// the app's: whether to offer them is decided elsewhere. + /// Whether the file is a pdf that takes marks. @objc var isAnnotatable: Bool { lock.withLock { pdfFile != nil } } /// Whether the file is plain text, which takes typing but no formatting. diff --git a/OpenDocumentReader/Document.swift b/OpenDocumentReader/Document.swift index aafb0b02..d067262f 100644 --- a/OpenDocumentReader/Document.swift +++ b/OpenDocumentReader/Document.swift @@ -44,10 +44,8 @@ class Document: UIDocument { parse() } } - /// The page carries its editor from the first render, so entering an edit - /// turns it on in place. Leaving one renders the file again, which is what - /// drops the edits or shows the saved ones - unless ``endEdit(renderingAgain:)`` - /// says the page already is the file. + /// Entering an edit turns it on in the page. Leaving renders the file + /// again, unless ``endEdit(renderingAgain:)`` says not to. public var edit = false { didSet { if edit { diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index e591b78a..843efd7e 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -68,11 +68,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Whether the document on screen can be edited and searched. Neither button /// stays in the bar when it cannot be used. private var canEdit = false { didSet { updateToolBar() } } - /// Whether the document is a pdf that takes marks. The same button as the - /// pencil, with the highlighter for a glyph. + /// Whether the document is a pdf that takes marks. private var canMark = false { didSet { updateEditButtonRole() } } - /// The pen turns the mode on and off and is drawn filled while it is on; - /// the disc beside it saves - the two controls of the website's viewer. private var isEditingDocument = false { didSet { updateEditButtonRole() @@ -118,8 +115,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel /// Which menu the system colour picker was opened from. private var colorPickerTool: EditToolBar.Tool? - /// Whether the Pro offer was shown during this edit, so a page full of - /// refused line breaks raises it once. + /// Whether the Pro offer was shown during this edit, so it shows once. private var hasOfferedProForThisEdit = false /// How many formula cells the edits so far left out of date; said each @@ -620,8 +616,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel present(alert, animated: true) } - /// The disc: writes the edit and stays in it, as the website does. The page - /// is rendered again from the file, and the mode turned back on in it. + /// Saves, and stays in the edit. @objc func saveTapped(_ sender: UIBarButtonItem) { saveAndStay() } @@ -660,8 +655,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel private static let pageMessageName = "odr" - /// Points the page's callbacks at this controller. The page's own scripts - /// have run by document end, so `odr` is there to be pointed. + /// Points the page's callbacks at this controller. It runs at document + /// end, after the page's own scripts. private static let pageMessageBridge = """ (function () { if (typeof odr !== 'object' || !window.webkit || !webkit.messageHandlers.odr) { return; } @@ -819,10 +814,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel markColors[tool] ?? UIColor(hex: tool.defaultColor ?? EditToolBar.markColors[0].hex) } - /// As on the website. A press with text selected marks it once and leaves - /// no tool armed; a press on the armed tool disarms it; any other press - /// arms it. A new colour (`recolor`) marks a selection once, recolours the - /// tool if it is armed, and is otherwise only kept. + /// A tap on a marker, or a new colour for it (`recolor`). private func pressMarker(_ tool: EditToolBar.Tool, recolor: Bool) { guard let name = tool.pageName else { return } @@ -886,8 +878,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel editToolChose(tool, .color(viewController.selectedColor.hexString)) } - /// The page said no. A line break or a format outside the paragraph is - /// what Pro is for; the rest is said in a word. + /// The page refused an edit. In Lite, an edit out of scope offers Pro. private func editRefused(reason: String) { if reason == "outOfScope", !Features.advancedEditing { guard !hasOfferedProForThisEdit else { return } @@ -1042,9 +1033,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel isEditingDocument = document?.edit ?? false } - /// A pencil to edit, a highlighter to mark a pdf, drawn selected while the - /// mode is on - the website's pen. The label goes with it: VoiceOver reads - /// that, not the glyph. + /// A pencil to edit, a highlighter to mark a pdf, selected while editing. + /// VoiceOver reads the label, not the glyph. private func updateEditButtonRole() { editButton.image = UIImage(systemName: canMark ? "highlighter" : "pencil") editButton.accessibilityLabel = NSLocalizedString(canMark ? "mark_pdf" : "menu_edit", comment: "") diff --git a/OpenDocumentReader/EditToolBar.swift b/OpenDocumentReader/EditToolBar.swift index d6870dfd..288107da 100644 --- a/OpenDocumentReader/EditToolBar.swift +++ b/OpenDocumentReader/EditToolBar.swift @@ -1,9 +1,6 @@ import UIKit /// The row of editing tools under the bar, shown while a document is edited. -/// As on the website: the bar keeps the way in and out of an edit, and this -/// row grows beneath it with what the open document takes. OpenDocument.droid's -/// `EditingTools` is the same row, with the same tools, colours and behaviour. final class EditToolBar: UIView { /// One button of the row. @@ -109,8 +106,7 @@ final class EditToolBar: UIView { } } - /// The colour a marker starts with: a wash for the highlighter, red for - /// the three lines, blue ink for the pen - as on the website. + /// The colour the tool starts with. var defaultColor: String? { switch self { case .textColor: return EditToolBar.textColors[0].hex @@ -125,10 +121,9 @@ final class EditToolBar: UIView { /// What the row holds, by what the page is. enum Layout { - /// a text document, a presentation or a plain text file + /// a text document or a presentation case text - /// a spreadsheet or a plain text file: nothing to format, so only the - /// way back + /// a spreadsheet or a plain text file: nothing to format case plain /// a pdf, which takes marks case pdf @@ -164,8 +159,7 @@ final class EditToolBar: UIView { let hex: String } - // the colours both apps offer, OpenDocument.droid's EditingTools being the - // other copy. The first of each is the website's own default + // the same colours as the website and OpenDocument.droid static let textColors = [ Swatch(name: "color_black", hex: "#191c1e"), Swatch(name: "color_red", hex: "#e53935"), @@ -322,7 +316,7 @@ final class EditToolBar: UIView { let button = UIButton(configuration: configuration) button.accessibilityLabel = tool.label button.accessibilityIdentifier = "edit-tool-\(tool.symbol)" - // filled while it is the mode, as the pen on the website is + // filled while pressed button.configurationUpdateHandler = { button in var configuration = button.configuration if button.isSelected { @@ -470,8 +464,7 @@ final class EditToolBar: UIView { bars[tool]?.backgroundColor = color } - /// Shows the selection's size as "12 pt", or the symbol where the - /// selection states none - as the website's size select does. + /// Shows the selection's size as "12 pt", or the symbol if it has none. func setFontSize(_ points: String?) { guard let button = buttons[.fontSize] else { return } @@ -528,22 +521,20 @@ extension UIColor { /// As `#rrggbb`, which is what the page takes. var hexString: String { - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - getRed(&red, green: &green, blue: &blue, alpha: nil) + let rgb = deviceRGB.map { Int(($0 * 255).rounded()) } - return String(format: "#%02x%02x%02x", Int(red * 255), Int(green * 255), Int(blue * 255)) + return String(format: "#%02x%02x%02x", rgb[0], rgb[1], rgb[2]) } - /// As the `[r, g, b]` in 0...1 that `odr.annotation.setColor` takes. + /// As the `[r, g, b]` in 0...1 that the pdf markers take. A wide-gamut + /// colour from the picker is clamped into sRGB. var deviceRGB: [Double] { var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 getRed(&red, green: &green, blue: &blue, alpha: nil) - return [Double(red), Double(green), Double(blue)] + return [red, green, blue].map { Double(min(max($0, 0), 1)) } } } diff --git a/OpenDocumentReader/Features.swift b/OpenDocumentReader/Features.swift index ba3aa7f4..6a52a651 100644 --- a/OpenDocumentReader/Features.swift +++ b/OpenDocumentReader/Features.swift @@ -6,8 +6,6 @@ enum Features { /// The ad banner and the consent form in front of it: Lite only. static var withAds: Bool { LINKS_ADS } - /// The editing that goes past typing inside a paragraph: formatting, new - /// and joined paragraphs, and marks on a pdf. What Pro is sold on; every - /// other edit the core takes is in both builds. + /// Formatting, paragraph changes and pdf marks: Pro only. static var advancedEditing: Bool { ADVANCED_EDITING } } diff --git a/OpenDocumentReader/ca.lproj/Localizable.strings b/OpenDocumentReader/ca.lproj/Localizable.strings index ad8f7bbc..d03a235c 100644 --- a/OpenDocumentReader/ca.lproj/Localizable.strings +++ b/OpenDocumentReader/ca.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Escriure una fórmula encara no és compatible."; "edit_refused_rich" = "Aquesta cel·la conté més que text simple i es queda com està."; "edit_refused_generic" = "Aquesta edició no és possible aquí."; +"edit_refused_new_line" = "Un salt de línia dins d’un paràgraf no es pot desar. Prem Retorn per a un paràgraf nou."; +"edit_refused_read_only" = "Aquest document no es pot editar."; +"edit_refused_range" = "Una edició no pot passar per sobre d’una imatge o d’una taula."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Part de Pro"; "pro_feature_formatting" = "Formatar el text i afegir o unir paràgrafs forma part d’OpenDocument Reader Pro."; "pro_feature_pdf" = "Marcar un PDF forma part d’OpenDocument Reader Pro."; "not_now" = "Ara no"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Un salt de línia dins d’un paràgraf no es pot desar. Prem Retorn per a un paràgraf nou."; -"edit_refused_read_only" = "Aquest document no es pot editar."; -"edit_refused_range" = "Una edició no pot passar per sobre d’una imatge o d’una taula."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Selecciona text i després una eina per marcar-lo."; diff --git a/OpenDocumentReader/cs.lproj/Localizable.strings b/OpenDocumentReader/cs.lproj/Localizable.strings index 16efa586..b16fcbf3 100644 --- a/OpenDocumentReader/cs.lproj/Localizable.strings +++ b/OpenDocumentReader/cs.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Zadávání vzorců zatím není podporováno."; "edit_refused_rich" = "Tato buňka obsahuje víc než prostý text a zůstává beze změny."; "edit_refused_generic" = "Tato úprava tu není možná."; +"edit_refused_new_line" = "Zalomení řádku uvnitř odstavce nelze uložit. Stiskněte Enter pro nový odstavec."; +"edit_refused_read_only" = "Tento dokument nelze upravovat."; +"edit_refused_range" = "Úprava nemůže sahat přes obrázek nebo tabulku."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Součást Pro"; "pro_feature_formatting" = "Formátování textu a přidávání nebo spojování odstavců je součástí OpenDocument Reader Pro."; "pro_feature_pdf" = "Označování PDF je součástí OpenDocument Reader Pro."; "not_now" = "Teď ne"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Zalomení řádku uvnitř odstavce nelze uložit. Stiskněte Enter pro nový odstavec."; -"edit_refused_read_only" = "Tento dokument nelze upravovat."; -"edit_refused_range" = "Úprava nemůže sahat přes obrázek nebo tabulku."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Vyberte text a pak nástroj, kterým ho označíte."; diff --git a/OpenDocumentReader/da.lproj/Localizable.strings b/OpenDocumentReader/da.lproj/Localizable.strings index 0e8473ae..0d614ae5 100644 --- a/OpenDocumentReader/da.lproj/Localizable.strings +++ b/OpenDocumentReader/da.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Indtastning af formler understøttes ikke endnu."; "edit_refused_rich" = "Den celle indeholder mere end ren tekst og forbliver, som den er."; "edit_refused_generic" = "Den redigering er ikke mulig her."; +"edit_refused_new_line" = "Et linjeskift inde i et afsnit kan ikke gemmes. Tryk på Enter for et nyt afsnit."; +"edit_refused_read_only" = "Dette dokument kan ikke redigeres."; +"edit_refused_range" = "En redigering kan ikke række hen over et billede eller en tabel."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "En del af Pro"; "pro_feature_formatting" = "Formatering af tekst samt tilføjelse eller sammenføjning af afsnit er en del af OpenDocument Reader Pro."; "pro_feature_pdf" = "Markering af PDF er en del af OpenDocument Reader Pro."; "not_now" = "Ikke nu"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Et linjeskift inde i et afsnit kan ikke gemmes. Tryk på Enter for et nyt afsnit."; -"edit_refused_read_only" = "Dette dokument kan ikke redigeres."; -"edit_refused_range" = "En redigering kan ikke række hen over et billede eller en tabel."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Vælg tekst og derefter et værktøj for at markere den."; diff --git a/OpenDocumentReader/de.lproj/Localizable.strings b/OpenDocumentReader/de.lproj/Localizable.strings index 2f2917d0..049d3323 100644 --- a/OpenDocumentReader/de.lproj/Localizable.strings +++ b/OpenDocumentReader/de.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Formeln eingeben wird noch nicht unterstützt."; "edit_refused_rich" = "Diese Zelle enthält mehr als reinen Text und bleibt, wie sie ist."; "edit_refused_generic" = "Diese Änderung ist hier nicht möglich."; +"edit_refused_new_line" = "Ein Zeilenumbruch innerhalb eines Absatzes kann nicht gespeichert werden. Drücke die Eingabetaste für einen neuen Absatz."; +"edit_refused_read_only" = "Dieses Dokument kann nicht bearbeitet werden."; +"edit_refused_range" = "Eine Änderung kann nicht über ein Bild oder eine Tabelle hinausreichen."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Teil von Pro"; "pro_feature_formatting" = "Text formatieren sowie Absätze einfügen oder zusammenführen ist Teil von OpenDocument Reader Pro."; "pro_feature_pdf" = "PDFs markieren ist Teil von OpenDocument Reader Pro."; "not_now" = "Nicht jetzt"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Ein Zeilenumbruch innerhalb eines Absatzes kann nicht gespeichert werden. Drücke die Eingabetaste für einen neuen Absatz."; -"edit_refused_read_only" = "Dieses Dokument kann nicht bearbeitet werden."; -"edit_refused_range" = "Eine Änderung kann nicht über ein Bild oder eine Tabelle hinausreichen."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Text auswählen, dann ein Werkzeug, um ihn zu markieren."; diff --git a/OpenDocumentReader/en.lproj/Localizable.strings b/OpenDocumentReader/en.lproj/Localizable.strings index 2748c6b8..daad0393 100644 --- a/OpenDocumentReader/en.lproj/Localizable.strings +++ b/OpenDocumentReader/en.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Typing a formula is not supported yet."; "edit_refused_rich" = "That cell holds more than plain text and stays as it is."; "edit_refused_generic" = "That edit is not possible here."; +"edit_refused_new_line" = "A line break inside a paragraph cannot be saved. Press Return for a new paragraph."; +"edit_refused_read_only" = "This document cannot be edited."; +"edit_refused_range" = "An edit cannot reach over a picture or a table."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Part of Pro"; "pro_feature_formatting" = "Formatting text, and adding or joining paragraphs, is part of OpenDocument Reader Pro."; "pro_feature_pdf" = "Marking up a PDF is part of OpenDocument Reader Pro."; "not_now" = "Not now"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "A line break inside a paragraph cannot be saved. Press Return for a new paragraph."; -"edit_refused_read_only" = "This document cannot be edited."; -"edit_refused_range" = "An edit cannot reach over a picture or a table."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Select text, then a tool, to mark it."; diff --git a/OpenDocumentReader/es.lproj/Localizable.strings b/OpenDocumentReader/es.lproj/Localizable.strings index 190a97db..17436945 100644 --- a/OpenDocumentReader/es.lproj/Localizable.strings +++ b/OpenDocumentReader/es.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Escribir una fórmula aún no es compatible."; "edit_refused_rich" = "Esa celda contiene más que texto sin formato y se queda como está."; "edit_refused_generic" = "Esa edición no es posible aquí."; +"edit_refused_new_line" = "Un salto de línea dentro de un párrafo no se puede guardar. Pulsa Intro para un párrafo nuevo."; +"edit_refused_read_only" = "Este documento no se puede editar."; +"edit_refused_range" = "Una edición no puede pasar por encima de una imagen o una tabla."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Parte de Pro"; "pro_feature_formatting" = "Dar formato al texto y añadir o unir párrafos forma parte de OpenDocument Reader Pro."; "pro_feature_pdf" = "Marcar un PDF forma parte de OpenDocument Reader Pro."; "not_now" = "Ahora no"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Un salto de línea dentro de un párrafo no se puede guardar. Pulsa Intro para un párrafo nuevo."; -"edit_refused_read_only" = "Este documento no se puede editar."; -"edit_refused_range" = "Una edición no puede pasar por encima de una imagen o una tabla."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Selecciona texto y luego una herramienta para marcarlo."; diff --git a/OpenDocumentReader/fr.lproj/Localizable.strings b/OpenDocumentReader/fr.lproj/Localizable.strings index 2e08e7d5..d927f51a 100644 --- a/OpenDocumentReader/fr.lproj/Localizable.strings +++ b/OpenDocumentReader/fr.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "La saisie d’une formule n’est pas encore prise en charge."; "edit_refused_rich" = "Cette cellule contient plus que du texte brut et reste telle quelle."; "edit_refused_generic" = "Cette modification n’est pas possible ici."; +"edit_refused_new_line" = "Un saut de ligne à l’intérieur d’un paragraphe ne peut pas être enregistré. Appuyez sur Entrée pour un nouveau paragraphe."; +"edit_refused_read_only" = "Ce document ne peut pas être modifié."; +"edit_refused_range" = "Une modification ne peut pas passer par-dessus une image ou un tableau."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Réservé à Pro"; "pro_feature_formatting" = "La mise en forme du texte, ainsi que l’ajout ou la fusion de paragraphes, fait partie d’OpenDocument Reader Pro."; "pro_feature_pdf" = "L’annotation de PDF fait partie d’OpenDocument Reader Pro."; "not_now" = "Pas maintenant"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Un saut de ligne à l’intérieur d’un paragraphe ne peut pas être enregistré. Appuyez sur Entrée pour un nouveau paragraphe."; -"edit_refused_read_only" = "Ce document ne peut pas être modifié."; -"edit_refused_range" = "Une modification ne peut pas passer par-dessus une image ou un tableau."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Sélectionnez du texte, puis un outil, pour l’annoter."; diff --git a/OpenDocumentReader/ga.lproj/Localizable.strings b/OpenDocumentReader/ga.lproj/Localizable.strings index c440ae61..0ef96633 100644 --- a/OpenDocumentReader/ga.lproj/Localizable.strings +++ b/OpenDocumentReader/ga.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Ní thacaítear le foirmlí a chlóscríobh go fóill."; "edit_refused_rich" = "Tá níos mó ná gnáth-théacs sa chill sin agus fanann sí mar atá."; "edit_refused_generic" = "Ní féidir an t-athrú sin a dhéanamh anseo."; +"edit_refused_new_line" = "Ní féidir briseadh líne laistigh d’alt a shábháil. Brúigh Iontráil le haghaidh alt nua."; +"edit_refused_read_only" = "Ní féidir an doiciméad seo a chur in eagar."; +"edit_refused_range" = "Ní féidir le hathrú dul thar phictiúr ná thar thábla."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Cuid de Pro"; "pro_feature_formatting" = "Is cuid de OpenDocument Reader Pro é téacs a fhormáidiú agus ailt a chur leis nó a nascadh."; "pro_feature_pdf" = "Is cuid de OpenDocument Reader Pro é PDF a mharcáil."; "not_now" = "Ní anois"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Ní féidir briseadh líne laistigh d’alt a shábháil. Brúigh Iontráil le haghaidh alt nua."; -"edit_refused_read_only" = "Ní féidir an doiciméad seo a chur in eagar."; -"edit_refused_range" = "Ní féidir le hathrú dul thar phictiúr ná thar thábla."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Roghnaigh téacs, ansin uirlis, chun é a mharcáil."; diff --git a/OpenDocumentReader/it.lproj/Localizable.strings b/OpenDocumentReader/it.lproj/Localizable.strings index c0f7193d..aa7fca60 100644 --- a/OpenDocumentReader/it.lproj/Localizable.strings +++ b/OpenDocumentReader/it.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Digitare una formula non è ancora supportato."; "edit_refused_rich" = "Quella cella contiene più di testo semplice e resta com’è."; "edit_refused_generic" = "Questa modifica non è possibile qui."; +"edit_refused_new_line" = "Un’interruzione di riga dentro un paragrafo non può essere salvata. Premi Invio per un nuovo paragrafo."; +"edit_refused_read_only" = "Questo documento non può essere modificato."; +"edit_refused_range" = "Una modifica non può passare sopra un’immagine o una tabella."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Parte di Pro"; "pro_feature_formatting" = "Formattare il testo e aggiungere o unire paragrafi fa parte di OpenDocument Reader Pro."; "pro_feature_pdf" = "Annotare un PDF fa parte di OpenDocument Reader Pro."; "not_now" = "Non ora"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Un’interruzione di riga dentro un paragrafo non può essere salvata. Premi Invio per un nuovo paragrafo."; -"edit_refused_read_only" = "Questo documento non può essere modificato."; -"edit_refused_range" = "Una modifica non può passare sopra un’immagine o una tabella."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Seleziona del testo, poi uno strumento, per annotarlo."; diff --git a/OpenDocumentReader/ja.lproj/Localizable.strings b/OpenDocumentReader/ja.lproj/Localizable.strings index dacb8a32..f42cfbda 100644 --- a/OpenDocumentReader/ja.lproj/Localizable.strings +++ b/OpenDocumentReader/ja.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "数式の入力はまだ対応していません。"; "edit_refused_rich" = "そのセルには書式付きの内容が入っているため、そのままになります。"; "edit_refused_generic" = "この編集はここではできません。"; +"edit_refused_new_line" = "段落内の改行は保存できません。新しい段落にはReturnキーを押してください。"; +"edit_refused_read_only" = "この書類は編集できません。"; +"edit_refused_range" = "画像や表をまたぐ編集はできません。"; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Proの機能"; "pro_feature_formatting" = "文字の書式設定と、段落の追加や結合はOpenDocument Reader Proの機能です。"; "pro_feature_pdf" = "PDFへのマーク付けはOpenDocument Reader Proの機能です。"; "not_now" = "あとで"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "段落内の改行は保存できません。新しい段落にはReturnキーを押してください。"; -"edit_refused_read_only" = "この書類は編集できません。"; -"edit_refused_range" = "画像や表をまたぐ編集はできません。"; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "テキストを選択してから、ツールを選ぶとマークを付けられます。"; diff --git a/OpenDocumentReader/pl.lproj/Localizable.strings b/OpenDocumentReader/pl.lproj/Localizable.strings index 1cdbfd08..a03b58ea 100644 --- a/OpenDocumentReader/pl.lproj/Localizable.strings +++ b/OpenDocumentReader/pl.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Wpisywanie formuł nie jest jeszcze obsługiwane."; "edit_refused_rich" = "Ta komórka zawiera więcej niż zwykły tekst i pozostaje bez zmian."; "edit_refused_generic" = "Ta zmiana nie jest tu możliwa."; +"edit_refused_new_line" = "Podziału wiersza wewnątrz akapitu nie można zapisać. Naciśnij Enter, aby zacząć nowy akapit."; +"edit_refused_read_only" = "Tego dokumentu nie można edytować."; +"edit_refused_range" = "Zmiana nie może sięgać przez obraz ani tabelę."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Część wersji Pro"; "pro_feature_formatting" = "Formatowanie tekstu oraz dodawanie i łączenie akapitów to część OpenDocument Reader Pro."; "pro_feature_pdf" = "Oznaczanie PDF to część OpenDocument Reader Pro."; "not_now" = "Nie teraz"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Podziału wiersza wewnątrz akapitu nie można zapisać. Naciśnij Enter, aby zacząć nowy akapit."; -"edit_refused_read_only" = "Tego dokumentu nie można edytować."; -"edit_refused_range" = "Zmiana nie może sięgać przez obraz ani tabelę."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Zaznacz tekst, a potem narzędzie, aby go oznaczyć."; diff --git a/OpenDocumentReader/pt-BR.lproj/Localizable.strings b/OpenDocumentReader/pt-BR.lproj/Localizable.strings index 8eabbf74..df214e63 100644 --- a/OpenDocumentReader/pt-BR.lproj/Localizable.strings +++ b/OpenDocumentReader/pt-BR.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Digitar uma fórmula ainda não é possível."; "edit_refused_rich" = "Essa célula contém mais do que texto simples e fica como está."; "edit_refused_generic" = "Essa edição não é possível aqui."; +"edit_refused_new_line" = "Uma quebra de linha dentro de um parágrafo não pode ser salva. Pressione Enter para um novo parágrafo."; +"edit_refused_read_only" = "Este documento não pode ser editado."; +"edit_refused_range" = "Uma edição não pode passar por cima de uma imagem ou de uma tabela."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Parte do Pro"; "pro_feature_formatting" = "Formatar texto e adicionar ou juntar parágrafos faz parte do OpenDocument Reader Pro."; "pro_feature_pdf" = "Marcar um PDF faz parte do OpenDocument Reader Pro."; "not_now" = "Agora não"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Uma quebra de linha dentro de um parágrafo não pode ser salva. Pressione Enter para um novo parágrafo."; -"edit_refused_read_only" = "Este documento não pode ser editado."; -"edit_refused_range" = "Uma edição não pode passar por cima de uma imagem ou de uma tabela."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Selecione o texto e depois uma ferramenta para marcá-lo."; diff --git a/OpenDocumentReader/ru.lproj/Localizable.strings b/OpenDocumentReader/ru.lproj/Localizable.strings index b6d3b296..5fc5e43b 100644 --- a/OpenDocumentReader/ru.lproj/Localizable.strings +++ b/OpenDocumentReader/ru.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Ввод формул пока не поддерживается."; "edit_refused_rich" = "В этой ячейке не только простой текст, она остаётся как есть."; "edit_refused_generic" = "Такое изменение здесь невозможно."; +"edit_refused_new_line" = "Перенос строки внутри абзаца нельзя сохранить. Нажмите Ввод, чтобы начать новый абзац."; +"edit_refused_read_only" = "Этот документ нельзя редактировать."; +"edit_refused_range" = "Изменение не может проходить через картинку или таблицу."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Часть Pro"; "pro_feature_formatting" = "Форматирование текста, а также добавление и объединение абзацев — часть OpenDocument Reader Pro."; "pro_feature_pdf" = "Разметка PDF — часть OpenDocument Reader Pro."; "not_now" = "Не сейчас"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Перенос строки внутри абзаца нельзя сохранить. Нажмите Ввод, чтобы начать новый абзац."; -"edit_refused_read_only" = "Этот документ нельзя редактировать."; -"edit_refused_range" = "Изменение не может проходить через картинку или таблицу."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Выделите текст, затем инструмент, чтобы пометить его."; diff --git a/OpenDocumentReader/sl.lproj/Localizable.strings b/OpenDocumentReader/sl.lproj/Localizable.strings index fb416bf3..3f201c8e 100644 --- a/OpenDocumentReader/sl.lproj/Localizable.strings +++ b/OpenDocumentReader/sl.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Vnos formul še ni podprt."; "edit_refused_rich" = "Ta celica vsebuje več kot navadno besedilo in ostane, kot je."; "edit_refused_generic" = "To urejanje tu ni mogoče."; +"edit_refused_new_line" = "Preloma vrstice znotraj odstavka ni mogoče shraniti. Pritisnite Enter za nov odstavek."; +"edit_refused_read_only" = "Tega dokumenta ni mogoče urejati."; +"edit_refused_range" = "Urejanje ne more segati čez sliko ali tabelo."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Del različice Pro"; "pro_feature_formatting" = "Oblikovanje besedila ter dodajanje ali združevanje odstavkov je del OpenDocument Reader Pro."; "pro_feature_pdf" = "Označevanje PDF je del OpenDocument Reader Pro."; "not_now" = "Ne zdaj"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Preloma vrstice znotraj odstavka ni mogoče shraniti. Pritisnite Enter za nov odstavek."; -"edit_refused_read_only" = "Tega dokumenta ni mogoče urejati."; -"edit_refused_range" = "Urejanje ne more segati čez sliko ali tabelo."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "Izberite besedilo, nato orodje, da ga označite."; diff --git a/OpenDocumentReader/tr.lproj/Localizable.strings b/OpenDocumentReader/tr.lproj/Localizable.strings index 568c5a91..0efc3ea4 100644 --- a/OpenDocumentReader/tr.lproj/Localizable.strings +++ b/OpenDocumentReader/tr.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "Formül yazma henüz desteklenmiyor."; "edit_refused_rich" = "Bu hücrede düz metinden fazlası var ve olduğu gibi kalır."; "edit_refused_generic" = "Bu düzenleme burada yapılamaz."; +"edit_refused_new_line" = "Paragraf içindeki satır sonu kaydedilemez. Yeni paragraf için Enter’a basın."; +"edit_refused_read_only" = "Bu belge düzenlenemez."; +"edit_refused_range" = "Bir düzenleme bir resmin ya da tablonun üzerinden geçemez."; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Pro’nun parçası"; "pro_feature_formatting" = "Metni biçimlendirmek ve paragraf eklemek ya da birleştirmek OpenDocument Reader Pro’nun bir parçasıdır."; "pro_feature_pdf" = "PDF işaretlemek OpenDocument Reader Pro’nun bir parçasıdır."; "not_now" = "Şimdi değil"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "Paragraf içindeki satır sonu kaydedilemez. Yeni paragraf için Enter’a basın."; -"edit_refused_read_only" = "Bu belge düzenlenemez."; -"edit_refused_range" = "Bir düzenleme bir resmin ya da tablonun üzerinden geçemez."; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "İşaretlemek için metni, sonra bir aracı seçin."; diff --git a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings index d9a01eb6..6ecbb452 100644 --- a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings +++ b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings @@ -141,19 +141,16 @@ "edit_refused_formula_input" = "暂不支持输入公式。"; "edit_refused_rich" = "该单元格包含的不只是纯文本,保持不变。"; "edit_refused_generic" = "此处无法进行该编辑。"; +"edit_refused_new_line" = "段落内的换行无法保存。请按回车键开始新段落。"; +"edit_refused_read_only" = "此文档无法编辑。"; +"edit_refused_range" = "编辑不能跨越图片或表格。"; -/* The one gate of the Lite app: what Pro adds, as the reader runs into it */ +/* What Pro adds, shown in the Lite app */ "pro_feature_title" = "Pro 功能"; "pro_feature_formatting" = "设置文字格式以及添加或合并段落是 OpenDocument Reader Pro 的功能。"; "pro_feature_pdf" = "标注 PDF 是 OpenDocument Reader Pro 的功能。"; "not_now" = "暂不"; -/* Said when the page refuses an edit */ -"edit_refused_new_line" = "段落内的换行无法保存。请按回车键开始新段落。"; -"edit_refused_read_only" = "此文档无法编辑。"; -"edit_refused_range" = "编辑不能跨越图片或表格。"; - - /* Shown when the marking tools come up on a PDF */ "mark_hint" = "先选择文本,再选择工具即可标注。"; diff --git a/OpenDocumentReaderTests/EditWorkflowTests.swift b/OpenDocumentReaderTests/EditWorkflowTests.swift index 39f60584..67a1dfca 100644 --- a/OpenDocumentReaderTests/EditWorkflowTests.swift +++ b/OpenDocumentReaderTests/EditWorkflowTests.swift @@ -69,9 +69,8 @@ class EditWorkflowTests: XCTestCase { XCTAssertEqual(controller.editButton.image, UIImage(systemName: "pencil")) } - /// The website's two controls: the pen, drawn selected while the mode is on, - /// and the disc beside it, there only while editing and live only once the - /// page holds a change. + /// The pencil is selected while editing. The save button shows only while + /// editing, and is enabled only after a change. func testThePenStaysAndTheSaveButtonJoinsItWhileEditing() throws { openDocument() @@ -210,8 +209,7 @@ class EditWorkflowTests: XCTestCase { XCTAssertFalse(controller.editToolBar.isPressed(.italic)) } - /// As on the website: the colour bars and the size follow the selection, - /// and the highlight shows pressed where the selection has one. + /// The colour bars, the size and the highlight follow the selection. func testTheSelectionColorsAndSizeReachTheTools() throws { openDocument() @@ -271,7 +269,7 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(controller.editToolBar.shows(.markHighlight)) XCTAssertFalse(controller.editToolBar.shows(.redo)) - // each marker has a colour of its own, as on the website + // each marker has a colour of its own XCTAssertEqual(controller.editToolBar.color(of: .markHighlight)?.hexString, "#ffe633") XCTAssertEqual(controller.editToolBar.color(of: .markDraw)?.hexString, "#1e88e5") @@ -323,8 +321,7 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(try reopenedText().contains(Self.editedText)) } - /// As on the website, a save writes the edit and stays in it: the file is - /// rendered again, and the new page is back in the mode with a clean log. + /// A save writes the edit and stays in it, in the page rendered again. func testSavingStaysInEditMode() throws { openDocument() @@ -350,8 +347,8 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(try reopenedText().contains(Self.editedText)) } - /// A marker pressed with text selected marks it once and leaves no tool - /// armed, the website's `markOnce`. + /// A marker tapped with text selected marks it once and leaves no tool + /// armed. func testAMarkerMarksASelectionOnceAndArmsNothing() throws { documentURL = try copyFixture(ofType: "pdf") try present(documentURL) diff --git a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift index 3f5a511e..6060e148 100644 --- a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift +++ b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift @@ -466,6 +466,20 @@ class OpenDocumentReaderTests: XCTestCase { XCTAssertEqual(wrapper.pageNames, ["text"]) } + func testSavingATextFileWritesTheText() throws { + let wrapper = CoreWrapper() + + let notes = URL(fileURLWithPath: temporaryDirectory).appendingPathComponent("notes-edited.txt") + try "Alpha\nBeta\n".write(to: notes, atomically: true, encoding: .utf8) + + try wrapper.translate(notes.path, into: temporaryDirectory, with: nil, editable: true, scope: .document) + XCTAssertTrue(wrapper.isPlainText) + + try wrapper.save(#"{"version": 2, "ops": [{"op": "setContent", "text": "Gamma"}]}"#, into: notes.path) + + XCTAssertEqual(try String(contentsOf: notes, encoding: .utf8), "Gamma") + } + /// A text file comes back as `text`; a markdown one as a document, with the /// hashes and stars turned into a heading and a bold run. func testMarkdownIsReadAsProse() throws { diff --git a/README.md b/README.md index 652d53b2..b26b15a7 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,10 @@ flag cannot end up in a build whose code says otherwise. `AnalyticsManager` and `CrashManager` take no switch at all - both write to `os.Logger` and nowhere else, so there is nothing to withhold. -The one thing Pro does that Lite does not is `Features.advancedEditing`, from -`ADVANCED_EDITING` beside `LINKS_ADS` in the same two files. Lite edits inside a -paragraph: odrcore is told the editing scope is `paragraph`, and refuses a line -break or a format with `outOfScope`, which the reader hears as the offer of -Pro. Marks on a pdf are gated the same way, in front of the highlighter rather -than behind it. The tools row still shows every button in Lite, behind a "Pro" -badge, so what Pro adds is in view. Every other edit the core takes - a sheet -cell, a plain text file - is in both. +Pro also has `Features.advancedEditing`, from `ADVANCED_EDITING` in the same +two files. Lite renders with the editing scope `paragraph`, so odrcore refuses +formatting and paragraph changes with `outOfScope`, and the app offers Pro. +PDF marks and the tools behind the "Pro" badge offer Pro as well. `configs/full` and `configs/lite` hold each bundle's `Info.plist` and privacy manifest, out of the synchronized folder, since anything left in there would be @@ -104,28 +100,17 @@ in App Store review. ## Editing -Every document is rendered with odrcore's editing scaffolding, so the pencil -only turns the mode on with `odr.editing.enable()` and the reader stays where -they were. A row of tools (`EditToolBar`) grows under the bar with what the -page is: formatting for a text document, undo and redo alone for a spreadsheet -or a plain text file, the five markers for a pdf. The website's viewer is the -reference, and OpenDocument.droid's `EditingTools` matches it: the same tools, -the same colours, a split button for the highlight and each marker, colour bars -and a size that follow the selection, and a marker that marks a selection once -and leaves nothing armed. The one difference is "Other color…", the system -colour picker, which Android does not have. The page talks back through one -`WKScriptMessageHandler`: the state of its log for the undo and redo buttons -and for the prompt on leaving, the style under the caret for the format -buttons, the formula cells an edit left out of date, and every refusal, which -is shown in a word. - -Saving asks the page for its log (`odr.editing.getOperations()`, or -`odr.annotation.getAnnotations()` for a pdf), hands it to odrcore, and writes -the file beside the open one before moving it into place. As on the website, -the pencil (the pen) turns the mode on and off and the save button (the disc) -beside it writes: a save stays in the edit, renders the file again and turns -the mode back on in the new page. Leaving with changes the page alone holds -asks first; leaving without any only turns the mode off. +Every document is rendered editable, so the pencil only calls +`odr.editing.enable()` and the page stays where it is. `EditToolBar` shows the +tools for the page: formatting for a text document, undo and redo for a sheet or +plain text, markers for a PDF. The tools match the website and +OpenDocument.droid. The page reports to the app through one +`WKScriptMessageHandler`. + +A save reads the page's log (`odr.editing.getOperations()`, or +`odr.annotation.getAnnotations()` for a PDF), and odrcore writes the file next +to the open one before it moves into place. The page then renders again and +stays in the edit. ## Formatting