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 } }