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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ Rules:

## [Unreleased]

### Added

- Spreadsheets can insert and delete rows and columns in Pro.
- A CSV file can be edited and saved.

### Changed

- The engine is odrcore 7.5.2, up from 7.4.0.

### Fixed

- A PDF prints as the file itself, one page to a sheet. A PDF with a password
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.4.0;
minimumVersion = 7.5.2;
};
};
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.

23 changes: 16 additions & 7 deletions OpenDocumentReader/CoreWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,8 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
var openedPageSize: CGSize?
let service: HtmlService

if file.isDocumentFile {
let documentFile = try file.asDocumentFile()
let document = try documentFile.document()

documentType = documentFile.documentType
if let document = try Self.document(of: file) {
documentType = document.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
Expand Down Expand Up @@ -244,6 +241,18 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
pageURLs = views.map { base.appendingPathComponent($0.path) }
}

/// The document behind the file. A csv is a one-sheet spreadsheet, but an
/// encoding odrcore cannot decode leaves it only the text view.
private static func document(of file: DecodedFile) throws -> OdrCoreObjC.Document? {
if file.isDocumentFile {
return try file.asDocumentFile().document()
}
if file.isCsvFile {
return try? file.asCsvFile().document()
}
return nil
}

/// The first page's: a presentation has one size, and a drawing seldom more.
private static func pageSize(of document: OdrCoreObjC.Document) -> CGSize? {
guard let root = try? document.rootElement() else {
Expand Down Expand Up @@ -274,8 +283,8 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
lock.lock()
defer { lock.unlock() }

// 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
// every kind of file is written beside the output and swapped in, so a
// failed save leaves the open file as it was
let output = URL(fileURLWithPath: outputPath)

let staging = try stagingDirectory(for: output)
Expand Down
28 changes: 23 additions & 5 deletions OpenDocumentReader/DocumentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
}

/// Turns the mode on in the page already on screen and shows its tools. A
/// plain text file takes no formatting, so it gets no strip.
/// plain text file takes no formatting, so it gets no strip. A csv states
/// that its cells take no style, so it gets only the row and column tools.
private func beginEditSession() {
hasOfferedProForThisEdit = false
hasUnsavedEdits = false
Expand All @@ -802,13 +803,23 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel

let isPlainText = document?.isPlainText == true

webview.evaluateJavaScript("odr.editing.enable(); typeof odr.sheet === 'object'") { [weak self] isSheet, _ in
let script = """
odr.editing.enable();
typeof odr.sheet !== 'object' ? 'text'
: document.body.getAttribute('data-odr-sheet-styles') === 'false' ? 'values' : 'sheet'
"""

webview.evaluateJavaScript(script) { [weak self] kind, _ in
guard let self, self.isEditingDocument else { return }

if isPlainText {
self.editToolBar.layout = nil
} else {
self.editToolBar.layout = isSheet as? Bool == true ? .sheet : .text
switch kind as? String {
case "sheet": self.editToolBar.layout = .sheet
case "values": self.editToolBar.layout = .values
default: self.editToolBar.layout = .text
}
self.editToolBar.setColor(.highlight, self.highlightColor)
self.editToolBar.setColor(.fill, self.highlightColor)
}
Expand Down Expand Up @@ -840,7 +851,7 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
/// Pro, and the highlighter beside it works in every edition.
private func editToolTapped(_ tool: EditToolBar.Tool) {
if !tool.isFree, !Features.advancedEditing {
offerPro(canMark ? .pdf : .formatting)
offerPro(canMark ? .pdf : tool.structures.isEmpty ? .formatting : .structure)

return
}
Expand Down Expand Up @@ -912,6 +923,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
markColors[tool] = UIColor(hex: hex ?? tool.defaultColor ?? EditToolBar.markColors[0].hex)
editToolBar.setColor(tool, markColor(of: tool))
pressMarker(tool, recolor: true)
case (_, .structure(let structure)):
run(structure.script)
case (_, .customColor):
colorPickerTool = tool

Expand Down Expand Up @@ -955,22 +968,27 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
case "rich", "shapes": key = "edit_refused_rich"
case "readOnly": key = "edit_refused_read_only"
case "range": key = "edit_refused_range"
case "sheetCut": key = "edit_refused_sheet_cut"
default: key = "edit_refused_generic"
}

AnalyticsManager.shared.report("edit_refused", parameters: ["reason": reason])

showToast(controller: self, message: NSLocalizedString(key, comment: ""), seconds: 1.5)
// the cut sheet is said once, as the edit starts, so it stays longer
let seconds: Double = reason == "sheetCut" ? 3 : 1.5
showToast(controller: self, message: NSLocalizedString(key, comment: ""), seconds: seconds)
}

/// What Pro adds, as the reader runs into it.
enum ProFeature {
case formatting
case structure
case pdf

var message: String {
switch self {
case .formatting: return NSLocalizedString("pro_feature_formatting", comment: "")
case .structure: return NSLocalizedString("pro_feature_structure", comment: "")
case .pdf: return NSLocalizedString("pro_feature_pdf", comment: "")
}
}
Expand Down
85 changes: 83 additions & 2 deletions OpenDocumentReader/EditToolBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ final class EditToolBar: UIView {
enum Tool: CaseIterable {
case bold, italic, underline, strikethrough
case textColor, highlight, fill, fontSize, align
case rows, columns
case markHighlight, markUnderline, markStrikeOut, markSquiggly, markDraw

var symbol: String {
Expand All @@ -21,6 +22,8 @@ final class EditToolBar: UIView {
case .fill: return "paintbrush"
case .fontSize: return "textformat.size"
case .align: return Alignment.left.symbol
case .rows: return "rectangle.split.1x2"
case .columns: return "rectangle.split.2x1"
case .markUnderline: return "underline"
case .markStrikeOut: return "strikethrough"
// not in the system set, so it is drawn - see ``squigglyImage``
Expand All @@ -45,6 +48,8 @@ final class EditToolBar: UIView {
case .fill: return NSLocalizedString("edit_fill", comment: "")
case .fontSize: return NSLocalizedString("edit_font_size", comment: "")
case .align: return NSLocalizedString("edit_align", comment: "")
case .rows: return NSLocalizedString("edit_rows", comment: "")
case .columns: return NSLocalizedString("edit_columns", comment: "")
case .markHighlight: return NSLocalizedString("mark_highlight", comment: "")
case .markUnderline: return NSLocalizedString("mark_underline", comment: "")
case .markStrikeOut: return NSLocalizedString("mark_strike_out", comment: "")
Expand Down Expand Up @@ -79,7 +84,17 @@ final class EditToolBar: UIView {
/// 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 == .align
self == .textColor || self == .fontSize || self == .align || !structures.isEmpty
}

/// What the tool's menu inserts or deletes; empty for a tool that
/// changes no rows or columns.
var structures: [Structure] {
switch self {
case .rows: return [.insertRowsAbove, .insertRowsBelow, .deleteRows]
case .columns: return [.insertColumnsLeft, .insertColumnsRight, .deleteColumns]
default: return []
}
}

/// Whether the tool paints behind the text, and so offers to paint
Expand Down Expand Up @@ -122,6 +137,8 @@ final class EditToolBar: UIView {
case text
/// a spreadsheet, whose cells take a style
case sheet
/// a sheet whose cells take no style, such as a csv
case values
/// a pdf, which takes marks
case pdf

Expand All @@ -130,7 +147,11 @@ final class EditToolBar: UIView {
case .text:
return [.bold, .italic, .underline, .strikethrough, .textColor, .highlight, .fontSize, .align]
case .sheet:
return [.bold, .italic, .underline, .strikethrough, .textColor, .fill, .fontSize, .align]
return [
.bold, .italic, .underline, .strikethrough, .textColor, .fill, .fontSize, .align, .rows, .columns,
]
case .values:
return [.rows, .columns]
case .pdf:
return [.markHighlight, .markUnderline, .markStrikeOut, .markSquiggly, .markDraw]
}
Expand Down Expand Up @@ -165,6 +186,50 @@ final class EditToolBar: UIView {
}
}

/// A row or column edit, applied to the rows or columns the selection
/// spans.
enum Structure: CaseIterable {
case insertRowsAbove, insertRowsBelow, deleteRows
case insertColumnsLeft, insertColumnsRight, deleteColumns

/// The call to `odr.editing` that does it.
var script: String {
switch self {
case .insertRowsAbove: return "odr.editing.insertRows('above')"
case .insertRowsBelow: return "odr.editing.insertRows('below')"
case .deleteRows: return "odr.editing.deleteRows()"
case .insertColumnsLeft: return "odr.editing.insertColumns('left')"
case .insertColumnsRight: return "odr.editing.insertColumns('right')"
case .deleteColumns: return "odr.editing.deleteColumns()"
}
}

var symbol: String {
switch self {
case .insertRowsAbove: return "arrow.up"
case .insertRowsBelow: return "arrow.down"
case .insertColumnsLeft: return "arrow.left"
case .insertColumnsRight: return "arrow.right"
case .deleteRows, .deleteColumns: return "trash"
}
}

var label: String {
switch self {
case .insertRowsAbove: return NSLocalizedString("edit_insert_rows_above", comment: "")
case .insertRowsBelow: return NSLocalizedString("edit_insert_rows_below", comment: "")
case .deleteRows: return NSLocalizedString("edit_delete_rows", comment: "")
case .insertColumnsLeft: return NSLocalizedString("edit_insert_columns_left", comment: "")
case .insertColumnsRight: return NSLocalizedString("edit_insert_columns_right", comment: "")
case .deleteColumns: return NSLocalizedString("edit_delete_columns", comment: "")
}
}

var isDelete: Bool {
self == .deleteRows || self == .deleteColumns
}
}

/// A pick from one of the menus.
enum Choice {
/// `#rrggbb`, or nil for no highlight
Expand All @@ -174,6 +239,7 @@ final class EditToolBar: UIView {
/// in points
case size(Int)
case align(Alignment)
case structure(Structure)
}

/// A colour the menus offer.
Expand Down Expand Up @@ -553,6 +619,7 @@ final class EditToolBar: UIView {
switch tool {
case .fontSize: return sizeMenu()
case .align: return alignMenu()
case .rows, .columns: return structureMenu(for: tool)
default: return colorMenu(for: tool, swatches: tool.swatches, offersNone: tool.offersNone)
}
}
Expand All @@ -572,6 +639,20 @@ final class EditToolBar: UIView {
})
}

/// Insert before, insert after, and delete what the selection spans.
private func structureMenu(for tool: Tool) -> UIMenu {
UIMenu(
title: tool.label,
children: tool.structures.map { structure in
UIAction(
title: structure.label, image: UIImage(systemName: structure.symbol),
attributes: structure.isDelete ? .destructive : []
) { [weak self] _ in
self?.onChoice?(tool, .structure(structure))
}
})
}

/// The fourteen sizes, with the one the text is in marked.
private func sizeMenu() -> UIMenu {
UIMenu(
Expand Down
10 changes: 10 additions & 0 deletions OpenDocumentReader/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@
"edit_align_center" = "Centre";
"edit_align_right" = "Dreta";
"edit_align_justify" = "Justificat";
"edit_rows" = "Files";
"edit_columns" = "Columnes";
"edit_insert_rows_above" = "Insereix una fila a sobre";
"edit_insert_rows_below" = "Insereix una fila a sota";
"edit_delete_rows" = "Suprimeix la fila";
"edit_insert_columns_left" = "Insereix una columna a l’esquerra";
"edit_insert_columns_right" = "Insereix una columna a la dreta";
"edit_delete_columns" = "Suprimeix la columna";
"edit_undo" = "Desfés";
"edit_redo" = "Refés";

Expand Down Expand Up @@ -150,11 +158,13 @@
"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.";
"edit_refused_sheet_cut" = "Aquest full és massa gran per mostrar-lo sencer. Només es pot editar la part que es mostra.";

/* What Pro adds, shown in the Lite app */
"pro_feature_title" = "Part de Pro";
"pro_feature_formatting" = "La negreta, la cursiva, els colors i la mida del text, i afegir o unir paràgrafs, formen part d’OpenDocument Reader Pro. El marcador hi és a totes les edicions.";
"pro_feature_pdf" = "Subratllar, ratllar i dibuixar en un PDF formen part d’OpenDocument Reader Pro. El marcador hi és a totes les edicions.";
"pro_feature_structure" = "Afegir i suprimir files i columnes forma part d’OpenDocument Reader Pro. El marcador hi és a totes les edicions.";
"not_now" = "Ara no";

/* Shown when the marking tools come up on a PDF */
Expand Down
10 changes: 10 additions & 0 deletions OpenDocumentReader/cs.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@
"edit_align_center" = "Na střed";
"edit_align_right" = "Vpravo";
"edit_align_justify" = "Do bloku";
"edit_rows" = "Řádky";
"edit_columns" = "Sloupce";
"edit_insert_rows_above" = "Vložit řádek nad";
"edit_insert_rows_below" = "Vložit řádek pod";
"edit_delete_rows" = "Odstranit řádek";
"edit_insert_columns_left" = "Vložit sloupec vlevo";
"edit_insert_columns_right" = "Vložit sloupec vpravo";
"edit_delete_columns" = "Odstranit sloupec";
"edit_undo" = "Zpět";
"edit_redo" = "Znovu";

Expand Down Expand Up @@ -150,11 +158,13 @@
"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.";
"edit_refused_sheet_cut" = "Tento list je příliš velký na úplné zobrazení. Upravit lze jen zobrazenou část.";

/* What Pro adds, shown in the Lite app */
"pro_feature_title" = "Součást Pro";
"pro_feature_formatting" = "Tučné písmo, kurzíva, barvy a velikost textu a přidávání nebo spojování odstavců jsou součástí OpenDocument Reader Pro. Zvýrazňovač je v každé edici.";
"pro_feature_pdf" = "Podtržení, přeškrtnutí a kreslení v PDF jsou součástí OpenDocument Reader Pro. Zvýrazňovač je v každé edici.";
"pro_feature_structure" = "Přidávání a odstraňování řádků a sloupců je součástí OpenDocument Reader Pro. Zvýrazňovač je v každé edici.";
"not_now" = "Teď ne";

/* Shown when the marking tools come up on a PDF */
Expand Down
Loading
Loading