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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ section the body of the GitHub release, and refuses a version without one. `scri
unsaved marks prints from the screen as before.
- Printing starts on the paper and orientation of the document, for example A4 or a
landscape slide.
- Rows and columns of a spreadsheet can be inserted and deleted.
- A CSV file can be edited and saved.
- Saving a spreadsheet computes again the formulas that an edit made out of date.
- Read aloud uses the language of the document, where a voice for it is installed.
- A printed document has one page on each sheet, without the grey background, and a dark
document prints in light colors.
- PDF images with transparency show correctly, and PDF text in small capitals can be
found and copied.

## 4.21.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class DocumentParcelTest {
editing = EditingKind.DOCUMENT,
readsAsDocument = true,
pageSize = null,
locale = null,
)

val restored = roundTrip(document, LoadedDocument.CREATOR)
Expand All @@ -120,6 +121,7 @@ class DocumentParcelTest {
assertEquals(EditingKind.DOCUMENT, restored.editing)
assertTrue(restored.readsAsDocument)
assertNull(restored.pageSize)
assertNull(restored.locale)

assertNull(restored.partCuts[0])
assertNull(restored.partCuts[2])
Expand Down Expand Up @@ -152,6 +154,7 @@ class DocumentParcelTest {
editing = EditingKind.NONE,
readsAsDocument = true,
pageSize = PageSize(8268, 11693),
locale = "de-AT",
),
LoadedDocument.CREATOR,
)
Expand All @@ -163,6 +166,7 @@ class DocumentParcelTest {
assertTrue(restored.readsAsDocument)
assertEquals(8268, restored.pageSize?.widthMils)
assertEquals(11693, restored.pageSize?.heightMils)
assertEquals("de-AT", restored.locale)
}

private fun <T> roundTrip(value: T, creator: Parcelable.Creator<T>): T {
Expand Down
31 changes: 31 additions & 0 deletions app/src/androidTest/java/app/opendocument/droid/test/CoreTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,37 @@ class CoreTest {
result.delete()
}

/** A csv is edited as a one-sheet spreadsheet, rows included, and saved as a csv. */
@Test
fun testCsvEdit() {
val csv = File(cacheDir(), "rows.csv")
csv.writeText("a,b\nc,d\n")
extracted += csv

coreLoader.host("csv-edit", csv.absolutePath, askEditing = true)
Assert.assertEquals(EditingKind.SHEET, coreLoader.editing)

val result =
coreLoader.writeEdits(
csv.absolutePath,
null,
null,
EditingKind.SHEET,
"""{"version":2,"ops":[{"op":"insertRows","sheet":0,"row":0,"count":1},""" +
"""{"op":"setCell","sheet":0,"column":0,"row":0,""" +
""""value":{"type":"string","text":"$EDITED"}}]}""",
File(cacheDir(), "csv-edit-result").path,
)

// the core may write a byte order mark
Assert.assertEquals(
listOf("$EDITED,", "a,b", "c,d"),
result.readText().removePrefix("\uFEFF").lines().filter { it.isNotEmpty() },
)

result.delete()
}

/**
* The formats that used to be sent off for conversion instead: presentations, and the legacy
* binary microsoft ones. The core renders all four, so the app hands them to it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.VerificationModes.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withClassName
Expand Down Expand Up @@ -389,6 +390,55 @@ class MainActivityTests {
}
}

/** The rows and columns tool inserts a row over the pin in pro, and offers pro in lite. */
@Test
fun aSheetRowIsInsertedFromTheStrip() {
respondToOpenDocumentWith(requireTestFile("spreadsheet-test.ods"))

openDocumentThroughPicker()
waitForDocumentActions()

onView(withContentDescription(R.string.menu_edit)).perform(clickThroughLongPress())

val activity = mainActivityActivityTestRule.activity
val documentFragment = requireNotNull(waitForDocumentFragment(activity, 10000))
val pageView = requireNotNull(documentFragment.pageView)

Assert.assertTrue(
"the sheet should turn editable",
waitFor(EDIT_MODE_TIMEOUT_MS) { pageAnswers(pageView, "odr.editing.isEnabled()") },
)
Assert.assertTrue("the formatting strip should come up", awaitEditingTools())
Assert.assertTrue(
"the first cell should take the pin",
pageAnswers(pageView, "odr.sheet.pin({column: 0, row: 0})"),
)

onView(withContentDescription(R.string.tool_rows_columns)).perform(scrollTo(), click())

if (!Features.advancedEditing) {
awaitViewWithText(R.string.pro_offer_sheet)
onView(withText(R.string.pro_offer_sheet)).check(matches(isDisplayed()))

return
}

onView(withContentDescription(R.string.tool_insert_rows_above))
.inRoot(isPlatformPopup())
.perform(click())

Assert.assertTrue(
"a row should be inserted",
waitFor(EDIT_MODE_TIMEOUT_MS) {
pageAnswers(pageView, "odr.editing.getOperations().indexOf('insertRows') >= 0")
},
)
Assert.assertTrue(
"and the page should say so to the app",
waitFor(EDIT_MODE_TIMEOUT_MS) { documentFragment.hasUnsavedEdits() },
)
}

/** Lite edits a text document inside one paragraph, and the page itself holds it to that. */
@Test
fun theEditionDecidesHowFarAnEditReaches() {
Expand Down
29 changes: 21 additions & 8 deletions app/src/main/java/app/opendocument/droid/background/CoreLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class CoreLoader(private val context: Context) {
var pageSize: PageSize? = null
private set

/** The language the document [host] last opened states, as a BCP 47 tag. */
var locale: String? = null
private set

/**
* Whether the core reads what [host] last opened as a document rather than only showing it -
* true of csv and markdown as well as the document formats, false of an image or an archive.
Expand Down Expand Up @@ -94,6 +98,7 @@ class CoreLoader(private val context: Context) {
editing,
readsAsDocument,
pageSize,
locale,
)
}

Expand Down Expand Up @@ -128,14 +133,13 @@ class CoreLoader(private val context: Context) {
throw OdrException.UnsupportedFileType("no charset could be detected: $inputPath")
}

// one decode for both questions
// one decode for every question
val paged = lastDocumentType in PAGED_DOCUMENT_TYPES
val document =
if (file.isDocumentFile && (askEditing || paged)) file.asDocumentFile().document()
else null
val document = if (askEditing || paged) documentOf(file) else null
document.use {
editing = if (askEditing) editingOf(file, it) else EditingKind.NONE
pageSize = if (paged && it != null) pageSizeOf(it) else null
locale = it?.locale()
}

val htmlConfig = HtmlConfig()
Expand Down Expand Up @@ -333,10 +337,11 @@ class CoreLoader(private val context: Context) {
}
EditingKind.DOCUMENT,
EditingKind.SHEET ->
file.asDocumentFile().document().use { document ->
document.edit(payload)
document.save(outputFile.path)
}
checkNotNull(documentOf(file)) { "not a document: $inputPath" }
.use { document ->
document.edit(payload)
document.save(outputFile.path)
}
}

return outputFile
Expand Down Expand Up @@ -374,6 +379,14 @@ class CoreLoader(private val context: Context) {
else EditingKind.DOCUMENT
}

/** The document [file] holds: a document file's, or the one sheet of a csv. */
private fun documentOf(file: DecodedFile): Document? =
when {
file.isDocumentFile -> file.asDocumentFile().document()
file.isCsvFile -> file.asCsvFile().document()
else -> null
}

/** The first page's: a presentation has one size, and a drawing seldom more. */
private fun pageSizeOf(document: Document): PageSize? {
val root = document.rootElement() ?: return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum class EditingKind {
/** A text document or a presentation. */
DOCUMENT,

/** A spreadsheet: one cell at a time. */
/** A spreadsheet or a csv: its cells, rows and columns. */
SHEET,

/** A pdf, which takes marks rather than edits. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import android.os.Parcel
import android.os.Parcelable

/**
* A rendered document with one URI per part. [partCuts] identifies truncated sheets; [editing] and
* [readsAsDocument] come from the core.
* A rendered document with one URI per part. [partCuts] identifies truncated sheets; [editing],
* [readsAsDocument], [pageSize] and [locale] come from the core.
*/
class LoadedDocument(
val request: DocumentRequest,
Expand All @@ -17,6 +17,7 @@ class LoadedDocument(
val editing: EditingKind,
val readsAsDocument: Boolean,
val pageSize: PageSize?,
val locale: String?,
) : Parcelable {

override fun describeContents(): Int = 0
Expand All @@ -32,6 +33,7 @@ class LoadedDocument(
// 0 for none, which no page measures
parcel.writeInt(pageSize?.widthMils ?: 0)
parcel.writeInt(pageSize?.heightMils ?: 0)
parcel.writeString(locale)
}

companion object {
Expand Down Expand Up @@ -60,6 +62,7 @@ class LoadedDocument(
val readsAsDocument = ParcelUtil.readBoolean(parcel)
val widthMils = parcel.readInt()
val heightMils = parcel.readInt()
val locale = parcel.readString()

return LoadedDocument(
request,
Expand All @@ -71,6 +74,7 @@ class LoadedDocument(
readsAsDocument,
if (widthMils > 0 && heightMils > 0) PageSize(widthMils, heightMils)
else null,
locale,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ import android.widget.TextView
import androidx.appcompat.view.ActionMode
import app.opendocument.droid.R
import app.opendocument.droid.ui.widget.PageView
import java.util.Locale

/**
* Reads the document out loud, one paragraph at a time: [PageView] hands the text of the paragraph
* at [lastParagraphIndex] back through [ParagraphListener], and every finished utterance asks for
* the next one.
* the next one. [locale] is the document's language as a BCP 47 tag, read in where a voice has it.
*/
// the replacements for the deprecated utterance callback and parameter map need the utterance
// queued with an id, which is a behaviour change rather than a rename
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
class TtsActionModeCallback(private val context: Context, private val pageView: PageView) :
ActionMode.Callback, OnInitListener, ParagraphListener, OnUtteranceCompletedListener {
class TtsActionModeCallback(
private val context: Context,
private val pageView: PageView,
private val locale: String?,
) : ActionMode.Callback, OnInitListener, ParagraphListener, OnUtteranceCompletedListener {

private lateinit var textToSpeech: TextToSpeech
private lateinit var menu: Menu
Expand Down Expand Up @@ -53,6 +57,12 @@ class TtsActionModeCallback(private val context: Context, private val pageView:

textToSpeech.setOnUtteranceCompletedListener(this)

// otherwise the engine's own language, which is the phone's rather than the document's
locale
?.let { Locale.forLanguageTag(it) }
?.takeIf { textToSpeech.isLanguageAvailable(it) >= TextToSpeech.LANG_AVAILABLE }
?.let { textToSpeech.setLanguage(it) }

menu.findItem(R.id.tts_play).isEnabled = true
menu.findItem(R.id.tts_pause).isEnabled = true
menu.findItem(R.id.tts_previous).isEnabled = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,12 @@ class DocumentFragment : Fragment(), DocumentLoader.Listener {
pageView?.formatStyle(style)
}

override fun onEditStructure(function: String, where: String?) {
analyticsManager.report("edit_$function")

pageView?.editStructure(function, where)
}

override fun onMarkTool(tool: String, color: Int, recolor: Boolean) {
analyticsManager.report("edit_mark_$tool")

Expand Down Expand Up @@ -568,6 +574,7 @@ class DocumentFragment : Fragment(), DocumentLoader.Listener {
"shapes" -> R.string.edit_refused_shapes
"readOnly" -> R.string.edit_refused_read_only
"range" -> R.string.edit_refused_range
"sheetCut" -> R.string.edit_refused_sheet_cut
else -> R.string.edit_refused_unsupported
}

Expand Down Expand Up @@ -1463,6 +1470,10 @@ class DocumentFragment : Fragment(), DocumentLoader.Listener {
val pageSize: PageSize?
get() = if (::state.isInitialized) state.lastDocument?.pageSize else null

/** The language the document states - see [CoreLoader.locale]. */
val locale: String?
get() = if (::state.isInitialized) state.lastDocument?.locale else null

val lastFileType: String?
get() = state.lastFile?.mimeType

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,8 @@ class MainActivity : AppCompatActivity() {
analyticsManager.report("menu_tts")

documentFragment?.pageView?.let { pageView ->
val ttsActionMode = TtsActionModeCallback(this, pageView)
val ttsActionMode =
TtsActionModeCallback(this, pageView, documentFragment.locale)
this.ttsActionMode = ttsActionMode

currentActionMode = startSupportActionMode(ttsActionMode)
Expand Down
Loading
Loading