From d4771816182bdc9b026744c061dbd57457227d6b Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 3 Oct 2026 14:41:16 +0200 Subject: [PATCH] Take core 7.4.0 and use its new features The engine moves from 7.3.2 to 7.4.0. Text and cells now take an alignment, and a spreadsheet gets the strip of formatting tools, with a fill colour in place of the highlight. The page now sends its messages to the app itself, and the config sets the start options of the sheet and the marker. So the app no longer puts a script of its own into the page. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01P9mp6LYWFtb4rYnGaGuiAm --- CHANGELOG.md | 19 +++ OpenDocumentReader.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- OpenDocumentReader/CoreWrapper.swift | 15 +++ .../DocumentViewController.swift | 119 ++++++++---------- OpenDocumentReader/EditToolBar.swift | 119 +++++++++++++++--- .../ca.lproj/Localizable.strings | 6 + .../cs.lproj/Localizable.strings | 6 + .../da.lproj/Localizable.strings | 6 + .../de.lproj/Localizable.strings | 6 + .../en.lproj/Localizable.strings | 6 + .../es.lproj/Localizable.strings | 6 + .../fr.lproj/Localizable.strings | 6 + .../ga.lproj/Localizable.strings | 6 + .../it.lproj/Localizable.strings | 6 + .../ja.lproj/Localizable.strings | 6 + .../pl.lproj/Localizable.strings | 6 + .../pt-BR.lproj/Localizable.strings | 6 + .../ru.lproj/Localizable.strings | 6 + .../sl.lproj/Localizable.strings | 6 + .../tr.lproj/Localizable.strings | 6 + .../zh-Hans.lproj/Localizable.strings | 6 + .../EditWorkflowTests.swift | 97 ++++++++++++-- 23 files changed, 374 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bf7ec..9b60162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,25 @@ Rules: `Unreleased`. - Add the date and the compare link once the version tag exists. +## [Unreleased] + +### Added + +- Text and cells can be aligned left, centre or right, and text can be + justified. +- Spreadsheet cells can be formatted: bold, italic, underline, + strikethrough, text colour, fill colour and text size. + +### Changed + +- The engine is odrcore 7.4.0, up from 7.3.2. +- A document no longer loads images from the internet. + +### Fixed + +- Spaces and tabs in text and in cells stay as they were typed. +- Spreadsheet cells show their colours, also in dark mode. + ## [1.47] 1.46 did not go out in the store either, so the store notes of 1.47 are the diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index 289f372..67d82e2 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.3.2; + minimumVersion = 7.4.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 ee07a16..3cc777a 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" : "34b8f4cd7c6ac1fc910109ce4b597b2ae359e9fc", - "version" : "7.3.2" + "revision" : "426f465031f6ef9d33bd8cb6de6c513320c45d88", + "version" : "7.4.0" } }, { diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index eaf19e5..939f6cf 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -105,6 +105,12 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ /// Bounds the rows by the sheet's width: the wider, the fewer it keeps. private static let spreadsheetCellLimit: UInt64 = 500_000 + /// The name the web view's message handler is added under. + @objc static let pageMessageName = "odr" + + /// The function the page calls with each message, as a path from `window`. + private static let hostMessageHandler = "webkit.messageHandlers.\(pageMessageName).postMessage" + @objc func translate( _ inputPath: String, into outputPath: String, @@ -177,6 +183,15 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ config.spreadsheetLimit = Self.spreadsheetLimit config.spreadsheetCellLimit = Self.spreadsheetCellLimit config.spreadsheetLimitByContent = true + // the page sends every `odr.on*` callback here as one JSON string, so + // the app needs no script of its own in the page + config.hostMessageHandler = Self.hostMessageHandler + // a tap opens the cell editor: the page would ask the pointer, and a + // web view answers it as a mouse + config.sheetEditOnClick = true + // an armed marker marks each selection as it is made: a tap elsewhere + // would lose it + config.pdfAnnotationMarkOnSelection = true let documentType: DocumentType let openedDocument: OdrCoreObjC.Document? diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index e7d21d8..9e5b532 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -126,10 +126,11 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel private var markColors: [EditToolBar.Tool: UIColor] = [:] /// The colour the highlight button turns on; the selection's own where it - /// has one. + /// has one. In a sheet the fill button takes its place, and this colour. private var highlightColor = UIColor(hex: EditToolBar.highlightColors[0].hex) - /// Whether the selection shows a highlight, as the page last said. + /// Whether the selection shows a highlight, or in a sheet a fill, as the + /// page last said. private var selectionHasHighlight = false /// Which menu the system colour picker was opened from. @@ -694,65 +695,34 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } /// Hears from the page: the log, a refusal, the style under the caret. + /// odrcore's page calls the handler itself, see `CoreWrapper`. 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. 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; } - var post = function (message) { webkit.messageHandlers.odr.postMessage(message); }; - odr.onEditChange = function (e) { - 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 || '') }); - }; - odr.onSelectionChange = function (style) { - post({ type: 'selection', style: style || {} }); - }; - odr.onCellsStale = function (detail) { - post({ type: 'cellsStale', count: detail && detail.cells ? detail.cells.length : 0 }); - }; - // asked of the page, since only a sheet carries that editor. The - // pointer is no use: a web view answers it as a mouse - if (odr.editing && odr.editing.setSheetOptions) { - odr.editing.setSheetOptions({ editOnClick: true }); - } - if (!odr.annotation) { return; } - // an armed tool marks each selection as it is made: a tap elsewhere - // would lose it - odr.annotation.setOptions({ markOnSelection: true }); - odr.onAnnotationChange = function (e) { - post({ type: 'marks', count: e && e.count ? e.count : 0 }); - }; - })(); - """ + webview.configuration.userContentController.add( + WeakScriptMessageHandler(self), name: CoreWrapper.pageMessageName) + } + /// Each message is one JSON `{type, detail}` string. func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - guard let body = message.body as? [String: Any], let type = body["type"] as? String else { return } + guard let json = message.body as? String, let data = json.data(using: .utf8), + let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let type = envelope["type"] as? String + else { return } + + let detail = envelope["detail"] as? [String: Any] ?? [:] switch type { case "editChange": - hasUnsavedEdits = body["dirty"] as? Bool ?? false - undoButton.isEnabled = body["canUndo"] as? Bool ?? false - redoButton.isEnabled = body["canRedo"] as? Bool ?? false + hasUnsavedEdits = detail["dirty"] as? Bool ?? false + undoButton.isEnabled = detail["canUndo"] as? Bool ?? false + redoButton.isEnabled = detail["canRedo"] as? Bool ?? false - case "marks": - let count = body["count"] as? Int ?? 0 + case "annotationChange": + let count = detail["count"] as? Int ?? 0 hasUnsavedEdits = count > 0 undoButton.isEnabled = count > 0 case "cellsStale": - let count = body["count"] as? Int ?? 0 + let count = (detail["cells"] as? [Any])?.count ?? 0 if count > staleCells { let message = String.localizedStringWithFormat( NSLocalizedString("edit_cells_stale", comment: ""), count) @@ -761,10 +731,10 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel staleCells = count case "editRefused": - editRefused(reason: body["reason"] as? String ?? "") + editRefused(reason: detail["reason"] as? String ?? "") - case "selection": - let style = body["style"] as? [String: Any] ?? [:] + case "selectionChange": + let style = detail editToolBar.setPressed(.bold, style["bold"] as? Bool ?? false) editToolBar.setPressed(.italic, style["italic"] as? Bool ?? false) editToolBar.setPressed(.underline, style["underline"] as? Bool ?? false) @@ -774,15 +744,20 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel if let color = style["color"] as? String { editToolBar.setColor(.textColor, UIColor(hex: color)) } - let highlight = style["highlight"] as? String - selectionHasHighlight = highlight != nil + // a cell has a fill where text has a highlight, and a strip shows + // one of the two tools + let paint = style["highlight"] as? String ?? style["fill"] as? String + selectionHasHighlight = paint != nil editToolBar.setPressed(.highlight, selectionHasHighlight) - if let highlight { - highlightColor = UIColor(hex: highlight) + editToolBar.setPressed(.fill, selectionHasHighlight) + if let paint { + highlightColor = UIColor(hex: paint) editToolBar.setColor(.highlight, highlightColor) + editToolBar.setColor(.fill, highlightColor) } editToolBar.setFontSize( (style["size"] as? String).map { $0.hasSuffix("pt") ? String($0.dropLast(2)) : $0 }) + editToolBar.setAlignment((style["align"] as? String).flatMap(EditToolBar.Alignment.init(rawValue:))) default: break @@ -790,7 +765,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } /// Turns the mode on in the page already on screen and shows its tools. A - /// sheet or a plain text file takes no formatting, so it gets no strip. + /// plain text file takes no formatting, so it gets no strip. private func beginEditSession() { hasOfferedProForThisEdit = false hasUnsavedEdits = false @@ -816,10 +791,12 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel webview.evaluateJavaScript("odr.editing.enable(); typeof odr.sheet === 'object'") { [weak self] isSheet, _ in guard let self, self.isEditingDocument else { return } - let formats = !isPlainText && isSheet as? Bool != true - self.editToolBar.layout = formats ? .text : nil - if formats { + if isPlainText { + self.editToolBar.layout = nil + } else { + self.editToolBar.layout = isSheet as? Bool == true ? .sheet : .text self.editToolBar.setColor(.highlight, self.highlightColor) + self.editToolBar.setColor(.fill, self.highlightColor) } self.editSessionReady() } @@ -858,10 +835,10 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel switch tool { case .bold, .italic, .underline, .strikethrough: run("odr.editing.toggle('\(tool.pageName ?? "")')") - case .highlight: + case .highlight, .fill: // off where the selection shows one, else on in the current colour run( - "odr.editing.format({ highlight: \(selectionHasHighlight ? "null" : "'\(highlightColor.hexString)'") })" + "odr.editing.format({ \(paintKey(of: tool)): \(selectionHasHighlight ? "null" : "'\(highlightColor.hexString)'") })" ) case .markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw: pressMarker(tool, recolor: false) @@ -870,6 +847,12 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } } + /// The key `odr.editing.format` takes for the colour behind the text: a + /// cell has a fill, and text has a highlight. + private func paintKey(of tool: EditToolBar.Tool) -> String { + tool == .fill ? "fill" : "highlight" + } + private func markColor(of tool: EditToolBar.Tool) -> UIColor { markColors[tool] ?? UIColor(hex: tool.defaultColor ?? EditToolBar.markColors[0].hex) } @@ -902,13 +885,15 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel run("odr.editing.format({ size: '\(size)pt' })") case (.textColor, .color(let hex)): run("odr.editing.format({ color: '\(hex ?? "")' })") - case (.highlight, .color(let hex)): + case (.align, .align(let alignment)): + run("odr.editing.format({ align: '\(alignment.rawValue)' })") + case (.highlight, .color(let hex)), (.fill, .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) + editToolBar.setColor(tool, highlightColor) } - run("odr.editing.format({ highlight: \(hex.map { "'\($0)'" } ?? "null") })") + run("odr.editing.format({ \(paintKey(of: tool)): \(hex.map { "'\($0)'" } ?? "null") })") 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) @@ -921,7 +906,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel picker.delegate = self picker.supportsAlpha = false switch tool { - case .highlight: picker.selectedColor = highlightColor + case .highlight, .fill: picker.selectedColor = highlightColor case .textColor: picker.selectedColor = .label default: picker.selectedColor = markColor(of: tool) } diff --git a/OpenDocumentReader/EditToolBar.swift b/OpenDocumentReader/EditToolBar.swift index b629845..303d421 100644 --- a/OpenDocumentReader/EditToolBar.swift +++ b/OpenDocumentReader/EditToolBar.swift @@ -11,7 +11,7 @@ final class EditToolBar: UIView { /// One button of the strip. enum Tool: CaseIterable { case bold, italic, underline, strikethrough - case textColor, highlight, fontSize + case textColor, highlight, fill, fontSize, align case markHighlight, markUnderline, markStrikeOut, markSquiggly, markDraw var symbol: String { @@ -22,7 +22,9 @@ final class EditToolBar: UIView { case .strikethrough: return "strikethrough" case .textColor: return "character" case .highlight, .markHighlight: return "highlighter" + case .fill: return "paintbrush" case .fontSize: return "textformat.size" + case .align: return Alignment.left.symbol case .markUnderline: return "underline" case .markStrikeOut: return "strikethrough" // not in the system set, so it is drawn - see ``squigglyImage`` @@ -44,7 +46,9 @@ final class EditToolBar: UIView { case .strikethrough: return NSLocalizedString("edit_strikethrough", comment: "") case .textColor: return NSLocalizedString("edit_text_color", comment: "") case .highlight: return NSLocalizedString("edit_highlight", comment: "") + case .fill: return NSLocalizedString("edit_fill", comment: "") case .fontSize: return NSLocalizedString("edit_font_size", comment: "") + case .align: return NSLocalizedString("edit_align", comment: "") case .markHighlight: return NSLocalizedString("mark_highlight", comment: "") case .markUnderline: return NSLocalizedString("mark_underline", comment: "") case .markStrikeOut: return NSLocalizedString("mark_strike_out", comment: "") @@ -70,16 +74,22 @@ final class EditToolBar: UIView { } } - /// The one tool a locked strip still works, under both its names: the - /// formatting style, and the pdf's marking tool. + /// The one tool a locked strip still works, under its three names: the + /// formatting style, the fill of a cell, and the pdf's marking tool. var isFree: Bool { - self == .highlight || self == .markHighlight + self == .highlight || self == .fill || self == .markHighlight } - /// Whether a tap opens the tool's choices. Neither of these two has - /// any state to turn off. + /// Whether a tap opens the tool's choices. None of these has any state + /// to turn off. var opensOnTap: Bool { - self == .textColor || self == .fontSize + self == .textColor || self == .fontSize || self == .align + } + + /// Whether the tool paints behind the text, and so offers to paint + /// nothing. + var offersNone: Bool { + self == .highlight || self == .fill } /// Whether a bar under the icon shows the colour the tool applies. @@ -91,7 +101,7 @@ final class EditToolBar: UIView { var swatches: [Swatch] { switch self { case .textColor: return EditToolBar.textColors - case .highlight: return EditToolBar.highlightColors + case .highlight, .fill: return EditToolBar.highlightColors default: return EditToolBar.markColors } } @@ -100,7 +110,7 @@ final class EditToolBar: UIView { var defaultColor: String? { switch self { case .textColor: return EditToolBar.textColors[0].hex - case .highlight: return EditToolBar.highlightColors[0].hex + case .highlight, .fill: return EditToolBar.highlightColors[0].hex case .markHighlight: return "#ffe633" case .markUnderline, .markStrikeOut, .markSquiggly: return "#e53935" case .markDraw: return "#1e88e5" @@ -109,22 +119,54 @@ final class EditToolBar: UIView { } } - /// What the strip holds. A sheet or a plain text file takes no formatting, - /// so it gets no strip at all - ``layout`` is left nil. + /// What the strip holds. A plain text file takes no formatting, so it + /// gets no strip at all - ``layout`` is left nil. enum Layout { /// a text document or a presentation case text + /// a spreadsheet, whose cells take a style + case sheet /// a pdf, which takes marks case pdf var tools: [Tool] { switch self { case .text: - return [.bold, .italic, .underline, .strikethrough, .textColor, .highlight, .fontSize] + return [.bold, .italic, .underline, .strikethrough, .textColor, .highlight, .fontSize, .align] + case .sheet: + return [.bold, .italic, .underline, .strikethrough, .textColor, .fill, .fontSize, .align] case .pdf: return [.markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw] } } + + /// What the alignment tool offers: a cell cannot be justified. + var alignments: [Alignment] { + self == .sheet ? [.left, .center, .right] : Alignment.allCases + } + } + + /// How the paragraphs or the cells sit, as the page names it. + enum Alignment: String, CaseIterable { + case left, center, right, justify + + var symbol: String { + switch self { + case .left: return "text.alignleft" + case .center: return "text.aligncenter" + case .right: return "text.alignright" + case .justify: return "text.justify" + } + } + + var label: String { + switch self { + case .left: return NSLocalizedString("edit_align_left", comment: "") + case .center: return NSLocalizedString("edit_align_center", comment: "") + case .right: return NSLocalizedString("edit_align_right", comment: "") + case .justify: return NSLocalizedString("edit_align_justify", comment: "") + } + } } /// A pick from one of the menus. @@ -135,6 +177,7 @@ final class EditToolBar: UIView { case customColor /// in points case size(Int) + case align(Alignment) } /// A colour the menus offer. @@ -214,6 +257,13 @@ final class EditToolBar: UIView { /// disagree. private var selectionSize: String? + /// The icon of the alignment tool, which shows the selection's alignment. + private var alignIcon: UIImageView? + + /// The alignment of the selection, or nil where the paragraphs or the + /// cells disagree. + private var selectionAlignment: Alignment? + override init(frame: CGRect) { super.init(frame: frame) @@ -286,6 +336,8 @@ final class EditToolBar: UIView { bars = [:] sizeCaption = nil selectionSize = nil + alignIcon = nil + selectionAlignment = nil guard let layout else { isHidden = true @@ -384,6 +436,8 @@ final class EditToolBar: UIView { addBar(to: button, in: slot, for: tool) } else if tool == .fontSize { addCaption(to: button, in: slot) + } else if tool == .align { + alignIcon = icon } // a tool that only offers Pro is dimmed, so the free one is the one @@ -440,7 +494,7 @@ final class EditToolBar: UIView { } } - if tool == .highlight { + if tool.offersNone { actions.append( UIAccessibilityCustomAction(name: NSLocalizedString("color_none", comment: "")) { [weak self] _ in self?.onChoice?(tool, .color(nil)) @@ -500,11 +554,26 @@ final class EditToolBar: UIView { } private func makeMenu(for tool: Tool) -> UIMenu { - guard tool != .fontSize else { - return sizeMenu() + switch tool { + case .fontSize: return sizeMenu() + case .align: return alignMenu() + default: return colorMenu(for: tool, swatches: tool.swatches, offersNone: tool.offersNone) } + } + + /// The alignments the layout takes, with the one the selection has marked. + private func alignMenu() -> UIMenu { + UIMenu( + title: Tool.align.label, + children: (layout?.alignments ?? []).map { alignment in + let action = UIAction(title: alignment.label, image: UIImage(systemName: alignment.symbol)) { + [weak self] _ in + self?.onChoice?(.align, .align(alignment)) + } + action.state = alignment == selectionAlignment ? .on : .off - return colorMenu(for: tool, swatches: tool.swatches, offersNone: tool == .highlight) + return action + }) } /// The fourteen sizes, with the one the text is in marked. @@ -624,6 +693,24 @@ final class EditToolBar: UIView { } } + /// Shows the selection's alignment on the tool, or the left one where the + /// paragraphs or the cells disagree. + func setAlignment(_ alignment: Alignment?) { + selectionAlignment = alignment + + alignIcon?.image = UIImage(systemName: (alignment ?? .left).symbol) + + // the mark moves with the selection, and a menu is built once + if let button = buttons[.align], !isPro(.align) { + button.menu = alignMenu() + } + } + + /// For the tests: the alignment the tool shows as the selection's. + var alignment: Alignment? { + selectionAlignment + } + /// For the tests: the colour the bar under `tool` shows. func color(of tool: Tool) -> UIColor? { bars[tool]?.backgroundColor diff --git a/OpenDocumentReader/ca.lproj/Localizable.strings b/OpenDocumentReader/ca.lproj/Localizable.strings index 182e2d1..48bbeac 100644 --- a/OpenDocumentReader/ca.lproj/Localizable.strings +++ b/OpenDocumentReader/ca.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Color del text"; "edit_highlight" = "Ressaltar"; "edit_font_size" = "Mida del text"; +"edit_fill" = "Color d'emplenament"; +"edit_align" = "Alinear"; +"edit_align_left" = "Esquerra"; +"edit_align_center" = "Centre"; +"edit_align_right" = "Dreta"; +"edit_align_justify" = "Justificat"; "edit_undo" = "Desfés"; "edit_redo" = "Refés"; diff --git a/OpenDocumentReader/cs.lproj/Localizable.strings b/OpenDocumentReader/cs.lproj/Localizable.strings index f30fe32..1d0aa17 100644 --- a/OpenDocumentReader/cs.lproj/Localizable.strings +++ b/OpenDocumentReader/cs.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Barva textu"; "edit_highlight" = "Zvýraznit"; "edit_font_size" = "Velikost textu"; +"edit_fill" = "Barva výplně"; +"edit_align" = "Zarovnat"; +"edit_align_left" = "Vlevo"; +"edit_align_center" = "Na střed"; +"edit_align_right" = "Vpravo"; +"edit_align_justify" = "Do bloku"; "edit_undo" = "Zpět"; "edit_redo" = "Znovu"; diff --git a/OpenDocumentReader/da.lproj/Localizable.strings b/OpenDocumentReader/da.lproj/Localizable.strings index 6b8017b..6db60f7 100644 --- a/OpenDocumentReader/da.lproj/Localizable.strings +++ b/OpenDocumentReader/da.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Tekstfarve"; "edit_highlight" = "Fremhæv"; "edit_font_size" = "Tekststørrelse"; +"edit_fill" = "Fyldfarve"; +"edit_align" = "Juster"; +"edit_align_left" = "Venstre"; +"edit_align_center" = "Centreret"; +"edit_align_right" = "Højre"; +"edit_align_justify" = "Lige margener"; "edit_undo" = "Fortryd"; "edit_redo" = "Gentag"; diff --git a/OpenDocumentReader/de.lproj/Localizable.strings b/OpenDocumentReader/de.lproj/Localizable.strings index 00eff55..2a71c8f 100644 --- a/OpenDocumentReader/de.lproj/Localizable.strings +++ b/OpenDocumentReader/de.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Textfarbe"; "edit_highlight" = "Hervorheben"; "edit_font_size" = "Textgröße"; +"edit_fill" = "Füllfarbe"; +"edit_align" = "Ausrichten"; +"edit_align_left" = "Links"; +"edit_align_center" = "Zentriert"; +"edit_align_right" = "Rechts"; +"edit_align_justify" = "Blocksatz"; "edit_undo" = "Rückgängig"; "edit_redo" = "Wiederholen"; diff --git a/OpenDocumentReader/en.lproj/Localizable.strings b/OpenDocumentReader/en.lproj/Localizable.strings index dbb111d..b6e51ec 100644 --- a/OpenDocumentReader/en.lproj/Localizable.strings +++ b/OpenDocumentReader/en.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Text color"; "edit_highlight" = "Highlight"; "edit_font_size" = "Text size"; +"edit_fill" = "Fill color"; +"edit_align" = "Align"; +"edit_align_left" = "Left"; +"edit_align_center" = "Center"; +"edit_align_right" = "Right"; +"edit_align_justify" = "Justify"; "edit_undo" = "Undo"; "edit_redo" = "Redo"; diff --git a/OpenDocumentReader/es.lproj/Localizable.strings b/OpenDocumentReader/es.lproj/Localizable.strings index 04f2e07..e7beb3a 100644 --- a/OpenDocumentReader/es.lproj/Localizable.strings +++ b/OpenDocumentReader/es.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Color del texto"; "edit_highlight" = "Resaltar"; "edit_font_size" = "Tamaño del texto"; +"edit_fill" = "Color de relleno"; +"edit_align" = "Alinear"; +"edit_align_left" = "Izquierda"; +"edit_align_center" = "Centro"; +"edit_align_right" = "Derecha"; +"edit_align_justify" = "Justificado"; "edit_undo" = "Deshacer"; "edit_redo" = "Rehacer"; diff --git a/OpenDocumentReader/fr.lproj/Localizable.strings b/OpenDocumentReader/fr.lproj/Localizable.strings index a4f6eb2..e74333c 100644 --- a/OpenDocumentReader/fr.lproj/Localizable.strings +++ b/OpenDocumentReader/fr.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Couleur du texte"; "edit_highlight" = "Surligner"; "edit_font_size" = "Taille du texte"; +"edit_fill" = "Couleur de remplissage"; +"edit_align" = "Aligner"; +"edit_align_left" = "À gauche"; +"edit_align_center" = "Centré"; +"edit_align_right" = "À droite"; +"edit_align_justify" = "Justifié"; "edit_undo" = "Annuler"; "edit_redo" = "Rétablir"; diff --git a/OpenDocumentReader/ga.lproj/Localizable.strings b/OpenDocumentReader/ga.lproj/Localizable.strings index 7b26b9c..2087090 100644 --- a/OpenDocumentReader/ga.lproj/Localizable.strings +++ b/OpenDocumentReader/ga.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Dath an téacs"; "edit_highlight" = "Aibhsigh"; "edit_font_size" = "Méid an téacs"; +"edit_fill" = "Dath líonta"; +"edit_align" = "Ailínigh"; +"edit_align_left" = "Clé"; +"edit_align_center" = "Lár"; +"edit_align_right" = "Deas"; +"edit_align_justify" = "Comhfhadaigh"; "edit_undo" = "Cealaigh"; "edit_redo" = "Athdhéan"; diff --git a/OpenDocumentReader/it.lproj/Localizable.strings b/OpenDocumentReader/it.lproj/Localizable.strings index 7589487..ca8c445 100644 --- a/OpenDocumentReader/it.lproj/Localizable.strings +++ b/OpenDocumentReader/it.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Colore del testo"; "edit_highlight" = "Evidenzia"; "edit_font_size" = "Dimensione del testo"; +"edit_fill" = "Colore di riempimento"; +"edit_align" = "Allinea"; +"edit_align_left" = "Sinistra"; +"edit_align_center" = "Centro"; +"edit_align_right" = "Destra"; +"edit_align_justify" = "Giustificato"; "edit_undo" = "Annulla"; "edit_redo" = "Ripeti"; diff --git a/OpenDocumentReader/ja.lproj/Localizable.strings b/OpenDocumentReader/ja.lproj/Localizable.strings index 76ecc9a..53364e0 100644 --- a/OpenDocumentReader/ja.lproj/Localizable.strings +++ b/OpenDocumentReader/ja.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "文字の色"; "edit_highlight" = "ハイライト"; "edit_font_size" = "文字サイズ"; +"edit_fill" = "塗りつぶしの色"; +"edit_align" = "配置"; +"edit_align_left" = "左揃え"; +"edit_align_center" = "中央揃え"; +"edit_align_right" = "右揃え"; +"edit_align_justify" = "両端揃え"; "edit_undo" = "元に戻す"; "edit_redo" = "やり直す"; diff --git a/OpenDocumentReader/pl.lproj/Localizable.strings b/OpenDocumentReader/pl.lproj/Localizable.strings index 515285c..721c925 100644 --- a/OpenDocumentReader/pl.lproj/Localizable.strings +++ b/OpenDocumentReader/pl.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Kolor tekstu"; "edit_highlight" = "Wyróżnienie"; "edit_font_size" = "Rozmiar tekstu"; +"edit_fill" = "Kolor wypełnienia"; +"edit_align" = "Wyrównaj"; +"edit_align_left" = "Do lewej"; +"edit_align_center" = "Do środka"; +"edit_align_right" = "Do prawej"; +"edit_align_justify" = "Wyjustuj"; "edit_undo" = "Cofnij"; "edit_redo" = "Ponów"; diff --git a/OpenDocumentReader/pt-BR.lproj/Localizable.strings b/OpenDocumentReader/pt-BR.lproj/Localizable.strings index 702b213..e5e26b3 100644 --- a/OpenDocumentReader/pt-BR.lproj/Localizable.strings +++ b/OpenDocumentReader/pt-BR.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Cor do texto"; "edit_highlight" = "Destacar"; "edit_font_size" = "Tamanho do texto"; +"edit_fill" = "Cor de preenchimento"; +"edit_align" = "Alinhar"; +"edit_align_left" = "À esquerda"; +"edit_align_center" = "Centralizado"; +"edit_align_right" = "À direita"; +"edit_align_justify" = "Justificado"; "edit_undo" = "Desfazer"; "edit_redo" = "Refazer"; diff --git a/OpenDocumentReader/ru.lproj/Localizable.strings b/OpenDocumentReader/ru.lproj/Localizable.strings index faaa8c0..d6c45de 100644 --- a/OpenDocumentReader/ru.lproj/Localizable.strings +++ b/OpenDocumentReader/ru.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Цвет текста"; "edit_highlight" = "Выделение"; "edit_font_size" = "Размер текста"; +"edit_fill" = "Цвет заливки"; +"edit_align" = "Выравнивание"; +"edit_align_left" = "По левому краю"; +"edit_align_center" = "По центру"; +"edit_align_right" = "По правому краю"; +"edit_align_justify" = "По ширине"; "edit_undo" = "Отменить"; "edit_redo" = "Повторить"; diff --git a/OpenDocumentReader/sl.lproj/Localizable.strings b/OpenDocumentReader/sl.lproj/Localizable.strings index ec5e15a..c7ce2c0 100644 --- a/OpenDocumentReader/sl.lproj/Localizable.strings +++ b/OpenDocumentReader/sl.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Barva besedila"; "edit_highlight" = "Poudari"; "edit_font_size" = "Velikost besedila"; +"edit_fill" = "Barva polnila"; +"edit_align" = "Poravnaj"; +"edit_align_left" = "Levo"; +"edit_align_center" = "Sredinsko"; +"edit_align_right" = "Desno"; +"edit_align_justify" = "Obojestransko"; "edit_undo" = "Razveljavi"; "edit_redo" = "Ponovi"; diff --git a/OpenDocumentReader/tr.lproj/Localizable.strings b/OpenDocumentReader/tr.lproj/Localizable.strings index f6f77fa..058c185 100644 --- a/OpenDocumentReader/tr.lproj/Localizable.strings +++ b/OpenDocumentReader/tr.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "Metin rengi"; "edit_highlight" = "Vurgula"; "edit_font_size" = "Metin boyutu"; +"edit_fill" = "Dolgu rengi"; +"edit_align" = "Hizala"; +"edit_align_left" = "Sola"; +"edit_align_center" = "Ortaya"; +"edit_align_right" = "Sağa"; +"edit_align_justify" = "İki yana"; "edit_undo" = "Geri al"; "edit_redo" = "Yinele"; diff --git a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings index aff6636..0dc832e 100644 --- a/OpenDocumentReader/zh-Hans.lproj/Localizable.strings +++ b/OpenDocumentReader/zh-Hans.lproj/Localizable.strings @@ -115,6 +115,12 @@ "edit_text_color" = "文字颜色"; "edit_highlight" = "高亮"; "edit_font_size" = "文字大小"; +"edit_fill" = "填充颜色"; +"edit_align" = "对齐"; +"edit_align_left" = "左对齐"; +"edit_align_center" = "居中"; +"edit_align_right" = "右对齐"; +"edit_align_justify" = "两端对齐"; "edit_undo" = "撤销"; "edit_redo" = "重做"; diff --git a/OpenDocumentReaderTests/EditWorkflowTests.swift b/OpenDocumentReaderTests/EditWorkflowTests.swift index bb44f8b..768375e 100644 --- a/OpenDocumentReaderTests/EditWorkflowTests.swift +++ b/OpenDocumentReaderTests/EditWorkflowTests.swift @@ -198,6 +198,8 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(controller.editToolBar.shows(.bold)) XCTAssertTrue(controller.editToolBar.shows(.highlight)) XCTAssertTrue(controller.editToolBar.shows(.fontSize)) + XCTAssertTrue(controller.editToolBar.shows(.align)) + XCTAssertFalse(controller.editToolBar.shows(.fill)) controller.discardChanges() waitForPage(where: "document.querySelectorAll('x-s').length > 0") @@ -205,22 +207,33 @@ class EditWorkflowTests: XCTestCase { XCTAssertNil(controller.editToolBar.layout) } - /// The cells are the editor, so a spreadsheet gets no strip - only the bar. - func testASpreadsheetShowsNoToolStrip() throws { - documentURL = try copyFixture(ofType: "ods") - try present(documentURL) - openDocument(where: "document.querySelectorAll('td').length > 0") + /// A cell takes a style, so a spreadsheet gets the strip with a fill in + /// place of the highlight. + func testASpreadsheetShowsTheCellTools() throws { + try openSheetForEditing() - controller.toggleEdit(controller.editButton) - waitForTools() - - XCTAssertNil(controller.editToolBar.layout) - XCTAssertTrue(controller.editToolBar.isHidden) + XCTAssertEqual(controller.editToolBar.layout, .sheet) + XCTAssertFalse(controller.editToolBar.isHidden) + XCTAssertTrue(controller.editToolBar.shows(.bold)) + XCTAssertTrue(controller.editToolBar.shows(.fill)) + XCTAssertTrue(controller.editToolBar.shows(.align)) + XCTAssertFalse(controller.editToolBar.shows(.highlight)) XCTAssertTrue(barContains(controller.undoButton)) XCTAssertTrue(barContains(controller.redoButton)) XCTAssertTrue(barContains(controller.saveButton)) } + /// The fill button paints the pinned cell, and the edit goes into the log. + func testTheFillButtonPaintsTheCell() throws { + try openSheetForEditing() + + _ = evaluate("odr.sheet.pin({ column: 0, row: 0 })") + controller.editToolBar.onTap?(.fill) + + waitForPage(where: "odr.sheet.cellAt(0, 0).style.backgroundColor !== ''") + waitUntil { self.controller.saveButton.isEnabled } + } + /// A style the caret sits in is shown pressed, the way the page reports it. func testTheSelectionStyleReachesTheButtons() throws { openDocument() @@ -253,6 +266,36 @@ class EditWorkflowTests: XCTestCase { XCTAssertEqual(controller.editToolBar.fontSizeTitle, "12 pt") } + /// The alignment tool shows the alignment the selection has. + func testTheSelectionAlignmentReachesTheTool() throws { + openDocument() + + controller.toggleEdit(controller.editButton) + waitForEditablePage() + waitForTools() + + _ = evaluate("odr.onSelectionChange({ align: 'center' })") + waitUntil { self.controller.editToolBar.alignment == .center } + + _ = evaluate("odr.onSelectionChange({})") + waitUntil { self.controller.editToolBar.alignment == nil } + } + + /// A pick from the alignment menu aligns the paragraph of the selection. + func testTheAlignmentToolAlignsTheParagraph() throws { + openDocument() + + controller.toggleEdit(controller.editButton) + waitForEditablePage() + waitForTools() + selectTheFirstRun() + + controller.editToolBar.onChoice?(.align, .align(.center)) + + waitForPage(where: "document.querySelector('x-s[data-odr-id]').closest('x-p').style.textAlign === 'center'") + waitUntil { self.controller.saveButton.isEnabled } + } + /// The highlight button turns a highlight on in its colour, and off again. func testTheHighlightButtonTogglesTheHighlight() throws { openDocument() @@ -287,6 +330,13 @@ class EditWorkflowTests: XCTestCase { XCTAssertTrue(tools.isDimmed(.bold)) XCTAssertTrue(tools.isDimmed(.fontSize)) XCTAssertFalse(tools.isDimmed(.highlight)) + XCTAssertTrue(tools.isDimmed(.align)) + + tools.layout = .sheet + + XCTAssertTrue(tools.isDimmed(.bold)) + XCTAssertTrue(tools.isDimmed(.align)) + XCTAssertFalse(tools.isDimmed(.fill)) tools.layout = .pdf @@ -309,6 +359,11 @@ class EditWorkflowTests: XCTestCase { XCTAssertNotNil(tools.menu(of: .highlight)) XCTAssertNotNil(tools.menu(of: .textColor)) XCTAssertNotNil(tools.menu(of: .fontSize)) + XCTAssertNotNil(tools.menu(of: .align)) + + tools.layout = .sheet + + XCTAssertNotNil(tools.menu(of: .fill)) tools.layout = .pdf @@ -317,6 +372,17 @@ class EditWorkflowTests: XCTestCase { } } + /// A cell cannot be justified, so the sheet's menu leaves it out. + func testTheAlignmentMenuFollowsTheLayout() throws { + let tools = EditToolBar() + + tools.layout = .text + XCTAssertEqual(tools.menu(of: .align)?.children.count, 4) + + tools.layout = .sheet + XCTAssertEqual(tools.menu(of: .align)?.children.count, 3) + } + /// Pro dims nothing and wears no badge. func testAnUnlockedStripShowsNoBadge() throws { let tools = EditToolBar() @@ -493,7 +559,16 @@ class EditWorkflowTests: XCTestCase { waitForPage(where: condition) } - /// Waits on the page's answer, not on the strip: a sheet has none. + private func openSheetForEditing() throws { + documentURL = try copyFixture(ofType: "ods") + try present(documentURL) + openDocument(where: "document.querySelectorAll('td').length > 0") + + controller.toggleEdit(controller.editButton) + waitForTools() + } + + /// Waits on the page's answer, not on the strip: a plain text file has none. private func waitForTools(file: StaticString = #filePath, line: UInt = #line) { waitUntil(file: file, line: line) { self.controller.isEditSessionReady } }