Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion OpenDocumentReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions OpenDocumentReader/CoreWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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?
Expand Down
119 changes: 52 additions & 67 deletions OpenDocumentReader/DocumentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -774,23 +744,28 @@ 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
}
}

/// 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
Expand All @@ -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()
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
Loading
Loading