From 2d75d0d68bf38deeec1d38c432b8338509931776 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 3 Oct 2026 14:56:57 +0200 Subject: [PATCH] Take core 7.4.0, and format sheet cells and align paragraphs Core 7.4.0 formats a selection of cells in the sheet editor and aligns paragraphs in odt, odp, odg, docx and pptx. The strip now shows over a sheet, with a fill in place of the highlight. Both strips get an alignment tool. The fill is the free tool of a sheet in lite, as the highlighter is in a document. HtmlConfig.hostMessageHandler sends every odr.on* callback to PageView.postMessage, so editing-bridge.js and its injection are gone. HtmlConfig.sheetEditOnClick and pdfAnnotationMarkOnSelection replace the two calls that the script made after each page load. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VTmf15E1oFAe5PrtjT2Cz8 --- CHANGELOG.md | 8 ++ CLAUDE.md | 11 +- .../droid/test/MainActivityTests.kt | 42 ++++++ app/src/main/assets/editing-bridge.js | 41 ------ .../droid/background/CoreLoader.kt | 11 ++ .../droid/ui/activity/DocumentFragment.kt | 11 +- .../droid/ui/widget/EditingTools.kt | 129 ++++++++++++++---- .../opendocument/droid/ui/widget/PageView.kt | 84 +++++------- .../res/drawable/ic_format_align_center.xml | 10 ++ .../res/drawable/ic_format_align_justify.xml | 10 ++ .../res/drawable/ic_format_align_left.xml | 10 ++ .../res/drawable/ic_format_align_right.xml | 10 ++ .../res/drawable/ic_format_color_fill.xml | 10 ++ app/src/main/res/values/strings.xml | 9 ++ gradle/libs.versions.toml | 2 +- 15 files changed, 272 insertions(+), 126 deletions(-) delete mode 100644 app/src/main/assets/editing-bridge.js create mode 100644 app/src/main/res/drawable/ic_format_align_center.xml create mode 100644 app/src/main/res/drawable/ic_format_align_justify.xml create mode 100644 app/src/main/res/drawable/ic_format_align_left.xml create mode 100644 app/src/main/res/drawable/ic_format_align_right.xml create mode 100644 app/src/main/res/drawable/ic_format_color_fill.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b20fb4c979a..f96cc6fbbc6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ release; it refuses a version without one. `scripts/store-copy.py` writes Play's ## Unreleased +- Spreadsheet cells can be formatted: fill, bold, italic, underline, strikethrough, + text color, size and alignment. The fill is in every edition. +- Paragraphs in a document can be aligned left, center, right or justified. +- A space typed into a document or a cell shows, and saving no longer turns spaces + into no-break spaces. +- Spreadsheet fills show their real colors, and keep them in night mode. +- A document no longer loads images from the internet, and only a web link leaves it. + ## 4.20.1 - A selection in a PDF covers the spaces between words, and no longer drifts to the diff --git a/CLAUDE.md b/CLAUDE.md index 225de60b52c5..24d48bfea8a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,14 +140,15 @@ says whether a password is worth asking for. **The gate is on the tool, not on the mode.** Every edition opens every editable kind, so `Features.offersEditing` is the core's answer alone. A locked `EditingTools` dims what is -pro and leaves the highlighter working, in documents and in pdfs. Do not put the whole-mode -gate back. +pro and leaves the highlighter working, in documents and in pdfs, and the fill in sheets. Do +not put the whole-mode gate back. **The editor is in the page.** An editable document is rendered with `HtmlConfig.editable`, and the edit button only calls `odr.editing.enable()`, with no second render. The page owns -the operation log, undo and the refusals; `editing-bridge.js`, injected by `PageView`, -forwards its callbacks. Lite narrows `HtmlConfig.editingScope` to `PARAGRAPH`, and the page -answers the rest with `outOfScope`, which `DocumentFragment` turns into the offer of pro. +the operation log, undo and the refusals. `HtmlConfig.hostMessageHandler` sends its callbacks +to `PageView.postMessage`; do not inject a script for them. Lite narrows +`HtmlConfig.editingScope` to `PARAGRAPH`, and the page answers the rest with `outOfScope`, +which `DocumentFragment` turns into the offer of pro. **The bar holds what is done to the document, the strip what is done to the text.** Undo, redo and save are `menu/edit.xml`, dimmed by `EditActionModeCallback`. `EditingTools` under diff --git a/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt b/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt index 9dc934185c01..a60ed377846d 100644 --- a/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt +++ b/app/src/androidTest/java/app/opendocument/droid/test/MainActivityTests.kt @@ -286,6 +286,48 @@ class MainActivityTests { onView(withText(R.string.pro_offer_title)).check(doesNotExist()) } + /** + * A sheet's strip fills a cell in every edition, the fill being its free tool. The page reports + * the edit through `HtmlConfig.hostMessageHandler`, with no script of the app's in between. + */ + @Test + fun aSheetCellTakesAFill() { + 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_fill)).perform(click()) + + Assert.assertTrue( + "the cell should be filled", + waitFor(EDIT_MODE_TIMEOUT_MS) { + pageAnswers(pageView, "odr.editing.getOperations().indexOf('setCellStyle') >= 0") + }, + ) + Assert.assertTrue( + "and the page should say so to the app", + waitFor(EDIT_MODE_TIMEOUT_MS) { documentFragment.hasUnsavedEdits() }, + ) + onView(withText(R.string.pro_offer_title)).check(doesNotExist()) + } + /** Lite edits a text document inside one paragraph, and the page itself holds it to that. */ @Test fun theEditionDecidesHowFarAnEditReaches() { diff --git a/app/src/main/assets/editing-bridge.js b/app/src/main/assets/editing-bridge.js deleted file mode 100644 index 174f6820f16b..000000000000 --- a/app/src/main/assets/editing-bridge.js +++ /dev/null @@ -1,41 +0,0 @@ -// Injected by PageView into every page the core serves, after the page's own scripts. It points -// the page's editing callbacks at the app's bridge; what they mean is the page's. -(function () { - "use strict"; - - var odr = window.odr; - var bridge = window.paragraphListener; - - // a page with no scripts of the core's, or a page the bridge is not attached to - if (!odr || !bridge) { - return; - } - - odr.onEditChange = function (event) { - bridge.editChanged(!!event.dirty, !!event.canUndo, !!event.canRedo); - }; - odr.onEditRefused = function (event) { - bridge.editRefused(String(event.reason || "")); - }; - odr.onSelectionChange = function (style) { - bridge.selectionChanged(JSON.stringify(style || {})); - }; - odr.onCellsStale = function (detail) { - bridge.cellsStale(detail && detail.cells ? detail.cells.length : 0); - }; - odr.onAnnotationChange = function (event) { - bridge.marksChanged(event.count); - }; - - // only a sheet carries the sheet editor, so this is asked of the page rather than taken for - // granted. A phone has no double click to spare, and the pointer is not asked either: a - // WebView answers that one as a mouse on an emulator - if (odr.editing && odr.editing.setSheetOptions) { - odr.editing.setSheetOptions({ editOnClick: true }); - } - - if (odr.annotation) { - // an armed tool marks each selection as it is made: a tap elsewhere would lose it - odr.annotation.setOptions({ markOnSelection: true }); - } -})(); diff --git a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt index 52dc8ede0d28..b778d9eb11be 100644 --- a/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt +++ b/app/src/main/java/app/opendocument/droid/background/CoreLoader.kt @@ -22,6 +22,7 @@ import app.opendocument.core.TextEncoding import app.opendocument.core.TextFile import app.opendocument.droid.nonfree.CrashManager import app.opendocument.droid.nonfree.Features +import app.opendocument.droid.ui.widget.PageView import java.io.File import java.io.IOException @@ -144,6 +145,16 @@ class CoreLoader(private val context: Context) { htmlConfig.editingScope = if (Features.advancedEditing) HtmlEditingScope.DOCUMENT else HtmlEditingScope.PARAGRAPH + // every odr.on* callback, as one json string to PageView.postMessage + htmlConfig.hostMessageHandler = PageView.HOST_MESSAGE_HANDLER + + // a phone has no double click to spare, and the pointer is not asked: a WebView answers + // that one as a mouse on an emulator + htmlConfig.sheetEditOnClick = true + + // an armed tool marks each selection as it is made: a tap elsewhere would lose it + htmlConfig.pdfAnnotationMarkOnSelection = true + // both schemes, each behind prefers-color-scheme, rather than the one it is being read in // now: this is decided while translating, and darkening is turned on and off over the open // document. PageView.setDarkeningAllowed picks between them diff --git a/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt b/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt index b67377c5ad52..86dacd3a03e1 100644 --- a/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt +++ b/app/src/main/java/app/opendocument/droid/ui/activity/DocumentFragment.kt @@ -438,14 +438,17 @@ class DocumentFragment : Fragment(), DocumentLoader.Listener { } /** - * The strip under the bar: what the kind of document takes. A sheet or a plain text file takes - * nothing, so there is no strip over it at all - undo, redo and save are the bar's. + * The strip under the bar: what the kind of document takes. A plain text file takes nothing, so + * there is no strip over it at all - undo, redo and save are the bar's. */ private fun showEditingTools(document: LoadedDocument, editing: Boolean) { when { !editing || !document.editing.isEditable -> editingTools.hide() - document.editing == EditingKind.DOCUMENT -> - editingTools.showFormatting(locked = !Features.advancedEditing) + document.editing == EditingKind.DOCUMENT || document.editing == EditingKind.SHEET -> + editingTools.showFormatting( + sheet = document.editing == EditingKind.SHEET, + locked = !Features.advancedEditing, + ) document.editing == EditingKind.ANNOTATION -> editingTools.showMarking(locked = !Features.advancedEditing) else -> editingTools.hide() diff --git a/app/src/main/java/app/opendocument/droid/ui/widget/EditingTools.kt b/app/src/main/java/app/opendocument/droid/ui/widget/EditingTools.kt index e413526bab05..cdcd8a2695fa 100644 --- a/app/src/main/java/app/opendocument/droid/ui/widget/EditingTools.kt +++ b/app/src/main/java/app/opendocument/droid/ui/widget/EditingTools.kt @@ -33,7 +33,7 @@ import org.json.JSONObject * In a build without [app.opendocument.droid.nonfree.Features.advancedEditing] the strip is * `locked`: the highlighter still works, every other tool is dimmed and offers pro, and pro's badge * stands in front of the row. One free tool of each kind is what makes the mode worth opening - see - * [FREE_TOOL]. + * [FREE_TOOLS]. */ class EditingTools(context: Context, attributeSet: AttributeSet?) : HorizontalScrollView(context, attributeSet) { @@ -66,12 +66,21 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : /** What each tool applies, which is what its bar shows. Kept as the document is edited. */ private val toolColors = STARTING_COLORS.toMutableMap() - private var highlightTool: View? = null + /** The highlighter of a document, or the fill of a sheet, named by [groundKey]. */ + private var groundTool: View? = null + private var groundKey = HIGHLIGHT_COLOR private var sizeTool: View? = null + private var alignTool: View? = null + + /** The alignments [alignTool] offers: a cell has no `justify`. */ + private var alignments = ALIGNMENTS /** The size the selection is in, in points, or null where the runs disagree. */ private var selectionSize: String? = null + /** How the selection is aligned, or null where the paragraphs or cells disagree. */ + private var selectionAlign: String? = null + init { LayoutInflater.from(context).inflate(R.layout.view_editing_tools, this, true) @@ -87,10 +96,10 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : } /** - * The formatting tools. [locked] adds pro's badge, and makes every tool but the free one offer - * pro. + * The formatting tools. A [sheet] takes a fill where a document takes a highlight, and no + * `justify`. [locked] adds pro's badge, and makes every tool but the free one offer pro. */ - fun showFormatting(locked: Boolean) { + fun showFormatting(sheet: Boolean, locked: Boolean) { reset(locked) addBadge() @@ -108,42 +117,55 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : addColorPress(textColor, TEXT_COLOR, R.string.tool_text_color) { pickTextColor(it) } row.addView(textColor) - // a tap flips the highlight on the selection, in the colour the long press picked - val highlight = newTool(R.drawable.ic_highlight, R.string.tool_highlight, HIGHLIGHT_COLOR) - paintBar(highlight, HIGHLIGHT_COLOR) - highlight.setOnClickListener { - ifOffered(HIGHLIGHT_COLOR) { + // a tap flips the highlight or the fill on the selection, in the colour the long press + // picked + groundKey = if (sheet) FILL_COLOR else HIGHLIGHT_COLOR + val groundLabel = if (sheet) R.string.tool_fill else R.string.tool_highlight + val ground = + newTool( + if (sheet) R.drawable.ic_format_color_fill else R.drawable.ic_highlight, + groundLabel, + groundKey, + ) + paintBar(ground, groundKey) + ground.setOnClickListener { + ifOffered(groundKey) { listener?.onFormat( JSONObject() .put( - "highlight", - if (highlight.isSelected) JSONObject.NULL - else hex(colorOf(HIGHLIGHT_COLOR)), + groundKey, + if (ground.isSelected) JSONObject.NULL else hex(colorOf(groundKey)), ) ) } } - addColorPress(highlight, HIGHLIGHT_COLOR, R.string.tool_highlight) { anchor -> - showPalette(anchor, HIGHLIGHT_COLORS) { color -> + addColorPress(ground, groundKey, groundLabel) { anchor -> + showPalette(anchor, if (sheet) FILL_COLORS else HIGHLIGHT_COLORS) { color -> if (color == Color.TRANSPARENT) { - listener?.onFormat(JSONObject().put("highlight", JSONObject.NULL)) + listener?.onFormat(JSONObject().put(groundKey, JSONObject.NULL)) return@showPalette } - setToolColor(HIGHLIGHT_COLOR, highlight, color) + setToolColor(groundKey, ground, color) - listener?.onFormat(JSONObject().put("highlight", hex(color))) + listener?.onFormat(JSONObject().put(groundKey, hex(color))) } } - highlightTool = highlight - row.addView(highlight) + groundTool = ground + row.addView(ground) val size = newTool(R.drawable.ic_format_size, R.string.tool_font_size, SIZE_TOOL) size.setOnClickListener { ifOffered(SIZE_TOOL) { showSizes(it) } } sizeTool = size row.addView(size) + alignments = if (sheet) ALIGNMENTS.filter { it.value != "justify" } else ALIGNMENTS + val align = newTool(R.drawable.ic_format_align_left, R.string.tool_align, ALIGN_TOOL) + align.setOnClickListener { ifOffered(ALIGN_TOOL) { showAlignments(it) } } + alignTool = align + row.addView(align) + setSelectionStyle(JSONObject()) visibility = View.VISIBLE @@ -200,7 +222,7 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : } // isNull is also true of a key the page left out, where the runs disagree - highlightTool?.isSelected = !locked && !style.isNull("highlight") + groundTool?.isSelected = !locked && !style.isNull(groundKey) // the caption is the size the text is in; the icon alone means the runs disagree selectionSize = @@ -214,6 +236,14 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : selectionSize?.let { context.getString(R.string.tool_font_size_points, it) }, ) } + + // the icon is the alignment the selection is in; left where they disagree + selectionAlign = style.optString("align", "").takeIf { !style.isNull("align") } + alignTool + ?.findViewById(R.id.editing_tool_icon) + ?.setImageResource( + (alignments.find { it.value == selectionAlign } ?: alignments.first()).icon + ) } /** Shows which marking tool is armed, or none. */ @@ -229,14 +259,15 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : row.removeAllViews() toggles.clear() markTools.clear() - highlightTool = null + groundTool = null sizeTool = null + alignTool = null scrollTo(0, 0) } /** Whether [tool] only offers pro in this build, rather than doing its work. */ - private fun isPro(tool: String) = locked && tool != FREE_TOOL + private fun isPro(tool: String) = locked && tool !in FREE_TOOLS private fun ifOffered(tool: String, action: () -> Unit) { if (isPro(tool)) { @@ -383,6 +414,23 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : } } + /** The alignments, as a row of tools under the one that opened them. */ + private fun showAlignments(anchor: View) { + showRow(anchor, fill = false) { row, popup -> + for (alignment in alignments) { + val choice = newTool(alignment.icon, alignment.label, ALIGN_TOOL) + choice.isSelected = alignment.value == selectionAlign + choice.setOnClickListener { + popup.dismiss() + + listener?.onFormat(JSONObject().put("align", alignment.value)) + } + + row.addView(choice) + } + } + } + private fun showPalette(anchor: View, colors: List, picked: (Int) -> Unit) { showRow(anchor, fill = false) { row, popup -> val size = (44 * resources.displayMetrics.density).toInt() @@ -452,6 +500,12 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : private class NamedColor(@param:ColorInt val color: Int, @param:StringRes val name: Int) + private class Alignment( + val value: String, + @param:DrawableRes val icon: Int, + @param:StringRes val label: Int, + ) + private class Mark( val tool: String, @param:DrawableRes val icon: Int, @@ -467,15 +521,19 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : private const val TEXT_COLOR = "color" private const val HIGHLIGHT_COLOR = "highlight" - /** The size tool, which carries no colour and so is only ever a name here. */ + /** The colour behind a cell, which a sheet takes in place of a highlight. */ + private const val FILL_COLOR = "fill" + + /** The size and the alignment tools, which carry no colour and so are only names here. */ private const val SIZE_TOOL = "size" + private const val ALIGN_TOOL = "align" /** - * The one tool a locked strip still does the work of. The highlighter, under both names it + * The tools a locked strip still does the work of. The highlighter, under both names it * has: `highlight` is the formatting style and the pdf's marking tool alike, so a reader of - * either kind of document has the same free tool. + * either kind of document has the same free tool. A sheet's fill stands in for it. */ - private const val FREE_TOOL = "highlight" + private val FREE_TOOLS = setOf(HIGHLIGHT_COLOR, FILL_COLOR) /** What a tool that only offers pro is drawn at, against the free one beside it. */ private const val PRO_ALPHA = 0.45f @@ -504,6 +562,22 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : NamedColor(Color.TRANSPARENT, R.string.color_none), ) + private val FILL_COLORS = + HIGHLIGHT_COLORS.dropLast(1) + NamedColor(Color.TRANSPARENT, R.string.color_no_fill) + + /** In the values `odr.editing.format` takes for `align`. */ + private val ALIGNMENTS = + listOf( + Alignment("left", R.drawable.ic_format_align_left, R.string.tool_align_left), + Alignment("center", R.drawable.ic_format_align_center, R.string.tool_align_center), + Alignment("right", R.drawable.ic_format_align_right, R.string.tool_align_right), + Alignment( + "justify", + R.drawable.ic_format_align_justify, + R.string.tool_align_justify, + ), + ) + private val MARK_COLORS = listOf( NamedColor(0xffffe633.toInt(), R.string.color_yellow), @@ -531,6 +605,7 @@ class EditingTools(context: Context, attributeSet: AttributeSet?) : mapOf( TEXT_COLOR to TEXT_COLORS.first().color, HIGHLIGHT_COLOR to HIGHLIGHT_COLORS.first().color, + FILL_COLOR to FILL_COLORS.first().color, "highlight" to 0xffffe633.toInt(), "underline" to 0xffe53935.toInt(), "strikeOut" to 0xffe53935.toInt(), diff --git a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt index efdba20c4bf6..3c843dd50a6a 100644 --- a/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt +++ b/app/src/main/java/app/opendocument/droid/ui/widget/PageView.kt @@ -45,17 +45,13 @@ constructor(context: Context, attributeSet: AttributeSet?) : private lateinit var documentFragment: DocumentFragment private lateinit var crashManager: CrashManager - /** Told what the page's editor reports, on the main thread - see `editing-bridge.js`. */ + /** Told what the page's editor reports, on the main thread - see [postMessage]. */ var editingListener: EditingListener? = null /** What [setEditing] was last told, applied again to every page that loads. */ private var editingKind = EditingKind.NONE private var isEditing = false - private val editingBridgeScript: String by lazy { - context.assets.open(EDITING_BRIDGE_ASSET).bufferedReader().use { it.readText() } - } - /** * Progress 100 reported before the page commits leaves it blank * (https://stackoverflow.com/q/48082474/198996), so onPageFinished schedules a reload for @@ -105,12 +101,8 @@ constructor(context: Context, attributeSet: AttributeSet?) : restorePendingScroll(0) // a sheet loads a page per tab, and each one is a page of its own to wire up - if (isOwnContent(url)) { - evaluateJavascript(editingBridgeScript, null) - - if (isEditing) { - applyEditing() - } + if (isOwnContent(url) && isEditing) { + applyEditing() } buggyWebViewHandler.postDelayed( @@ -558,45 +550,40 @@ constructor(context: Context, attributeSet: AttributeSet?) : null } - // called by editing-bridge.js on the javabridge thread, so each posts to the main one - - @JavascriptInterface - @Keep - fun editChanged(dirty: Boolean, canUndo: Boolean, canRedo: Boolean) { - post { editingListener?.onEditChanged(dirty, canUndo, canRedo) } - } - - @JavascriptInterface - @Keep - fun editRefused(reason: String) { - post { editingListener?.onEditRefused(reason) } - } - + /** + * Every `odr.on*` callback of the page, as `{type, detail}` - see + * `HtmlConfig.hostMessageHandler` in `CoreLoader`. Called on the javabridge thread, so it posts + * to the main one. + */ @JavascriptInterface @Keep - fun selectionChanged(style: String) { - val parsed = + fun postMessage(json: String) { + val message = try { - JSONObject(style) + JSONObject(json) } catch (e: Exception) { crashManager.log(e) return } + val detail = message.optJSONObject("detail") ?: JSONObject() - post { editingListener?.onSelectionChanged(parsed) } - } - - @JavascriptInterface - @Keep - fun marksChanged(count: Int) { - post { editingListener?.onMarksChanged(count) } - } - - @JavascriptInterface - @Keep - fun cellsStale(count: Int) { - post { editingListener?.onCellsStale(count) } + post { + val listener = editingListener ?: return@post + + when (message.optString("type")) { + "editChange" -> + listener.onEditChanged( + detail.optBoolean("dirty"), + detail.optBoolean("canUndo"), + detail.optBoolean("canRedo"), + ) + "editRefused" -> listener.onEditRefused(detail.optString("reason")) + "selectionChange" -> listener.onSelectionChanged(detail) + "annotationChange" -> listener.onMarksChanged(detail.optInt("count")) + "cellsStale" -> listener.onCellsStale(detail.optJSONArray("cells")?.length() ?: 0) + } + } } @JavascriptInterface @@ -658,20 +645,21 @@ constructor(context: Context, attributeSet: AttributeSet?) : fun onCellsStale(count: Int) } - private companion object { + companion object { - const val BRIDGE_NAME = "paragraphListener" + private const val BRIDGE_NAME = "paragraphListener" - const val EDITING_BRIDGE_ASSET = "editing-bridge.js" + /** [postMessage], as the page finds it from `window`. */ + const val HOST_MESSAGE_HANDLER = "$BRIDGE_NAME.postMessage" - const val JAVASCRIPT_SCHEME = "javascript:" + private const val JAVASCRIPT_SCHEME = "javascript:" /** Where CoreLoader publishes a translated document. */ - const val LOCAL_SERVER_URL_PREFIX = "http://localhost:" + private const val LOCAL_SERVER_URL_PREFIX = "http://localhost:" /** Two seconds of them, which a megabyte of text lays out well inside of. */ - const val SCROLL_RESTORE_ATTEMPTS = 20 + private const val SCROLL_RESTORE_ATTEMPTS = 20 - const val SCROLL_RESTORE_INTERVAL_MS = 100L + private const val SCROLL_RESTORE_INTERVAL_MS = 100L } } diff --git a/app/src/main/res/drawable/ic_format_align_center.xml b/app/src/main/res/drawable/ic_format_align_center.xml new file mode 100644 index 000000000000..4278bc5b7d72 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_align_center.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_align_justify.xml b/app/src/main/res/drawable/ic_format_align_justify.xml new file mode 100644 index 000000000000..2ec280c9f6bd --- /dev/null +++ b/app/src/main/res/drawable/ic_format_align_justify.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_align_left.xml b/app/src/main/res/drawable/ic_format_align_left.xml new file mode 100644 index 000000000000..37ccc271d211 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_align_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_align_right.xml b/app/src/main/res/drawable/ic_format_align_right.xml new file mode 100644 index 000000000000..0b9ddacf6692 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_align_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_format_color_fill.xml b/app/src/main/res/drawable/ic_format_color_fill.xml new file mode 100644 index 000000000000..ff1041caa494 --- /dev/null +++ b/app/src/main/res/drawable/ic_format_color_fill.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b363a87a5cd..46c61557692b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,14 @@ Text color Highlight Text size + + Fill + + Alignment + Align left + Center + Align right + Justify %1$s color @@ -127,6 +135,7 @@ Blue Pink No highlight + No fill Pro