From 51380b07fff3c71034efb4c7a752dedb85dd04e4 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Thu, 31 Dec 2020 11:32:30 +0000 Subject: [PATCH 1/9] Start formatting tables with horizontal scroll and fixed-width font --- .../android/espresso/ExternalLinksTest.kt | 2 +- .../android/espresso/InternalLinksTest.kt | 10 +- .../android/espresso/QueryFragmentTest.java | 2 +- .../android/espresso/SettingsChangeTest.java | 4 +- .../android/ui/notes/NoteContentTest.kt | 154 ++++++++++++++++++ .../orgzly/android/ui/notes/NoteContent.kt | 88 ++++++++++ .../android/ui/notes/NoteItemViewBinder.kt | 71 +++++--- app/src/main/res/layout/item_head.xml | 17 +- .../item_note_content_section_table.xml | 17 ++ .../layout/item_note_content_section_text.xml | 16 ++ .../main/res/raw/orgzly_getting_started.org | 14 ++ 11 files changed, 356 insertions(+), 39 deletions(-) create mode 100644 app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt create mode 100644 app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt create mode 100644 app/src/main/res/layout/item_note_content_section_table.xml create mode 100644 app/src/main/res/layout/item_note_content_section_text.xml diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt index 8930285d5..b6f59ffc6 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt @@ -59,7 +59,7 @@ class ExternalLinksTest(private val param: Parameter) : OrgzlyTest() { onBook(0).perform(click()) // Click on link - onNoteInBook(1, R.id.item_head_content).perform(clickClickableSpan(param.link)) + onNoteInBook(1, R.id.item_head_content_list).perform(clickClickableSpan(param.link)) param.check() } diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt index 368e69383..07e3bb922 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt @@ -66,28 +66,28 @@ class InternalLinksTest : OrgzlyTest() { @Test fun testDifferentCaseUuidInternalLink() { - onNoteInBook(1, R.id.item_head_content) + onNoteInBook(1, R.id.item_head_content_list) .perform(clickClickableSpan("id:bdce923b-C3CD-41ED-B58E-8BDF8BABA54F")) onView(withId(R.id.fragment_note_title)).check(matches(withText("Note [b-2]"))) } @Test fun testDifferentCaseCustomIdInternalLink() { - onNoteInBook(2, R.id.item_head_content) + onNoteInBook(2, R.id.item_head_content_list) .perform(clickClickableSpan("#Different case custom id")) onView(withId(R.id.fragment_note_title)).check(matches(withText("Note [b-1]"))) } @Test fun testCustomIdLink() { - onNoteInBook(3, R.id.item_head_content) + onNoteInBook(3, R.id.item_head_content_list) .perform(clickClickableSpan("#Link to note in a different book")) onView(withId(R.id.fragment_note_title)).check(matches(withText("Note [b-3]"))) } @Test fun testBookLink() { - onNoteInBook(4, R.id.item_head_content) + onNoteInBook(4, R.id.item_head_content_list) .perform(clickClickableSpan("file:book-b.org")) onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed())) onNoteInBook(1, R.id.item_head_title).check(matches(withText("Note [b-1]"))) @@ -95,7 +95,7 @@ class InternalLinksTest : OrgzlyTest() { @Test fun testBookRelativeLink() { - onNoteInBook(5, R.id.item_head_content) + onNoteInBook(5, R.id.item_head_content_list) .perform(clickClickableSpan("file:./book-b.org")) onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed())) onNoteInBook(1, R.id.item_head_title).check(matches(withText("Note [b-1]"))) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java index c798f5905..fd9726d37 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java @@ -720,7 +720,7 @@ public void testContentOfFoldedNoteDisplayed() { onView(withId(R.id.fragment_query_search_view_flipper)).check(matches(isDisplayed())); onNotesInSearch().check(matches(recyclerViewItemCount(3))); onNoteInSearch(1, R.id.item_head_title).check(matches(allOf(withText(containsString("Note B")), isDisplayed()))); - onNoteInSearch(1, R.id.item_head_content).check(matches(allOf(withText(containsString("Content for Note B")), isDisplayed()))); + onNoteInSearch(1, R.id.item_head_content_list).check(matches(allOf(withText(containsString("Content for Note B")), isDisplayed()))); } @Test diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java index d5c4128ee..8b32dbe96 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java @@ -82,7 +82,7 @@ public void testChangeDefaultPriorityAgendaResultsShouldBeReordered() { public void testDisplayedContentInBook() { onBook(0).perform(click()); - onNoteInBook(1, R.id.item_head_content) + onNoteInBook(1, R.id.item_head_content_list) .check(matches(allOf(withText(containsString("Content for [a-1]")), isDisplayed()))); onActionItemClick(R.id.activity_action_settings, R.string.settings); @@ -91,7 +91,7 @@ public void testDisplayedContentInBook() { pressBack(); pressBack(); - onNoteInBook(1, R.id.item_head_content).check(matches(not(isDisplayed()))); + onNoteInBook(1, R.id.item_head_content_list).check(matches(not(isDisplayed()))); } private void setDefaultPriority(String priority) { diff --git a/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt b/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt new file mode 100644 index 000000000..25777267d --- /dev/null +++ b/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt @@ -0,0 +1,154 @@ +package com.orgzly.android.ui.notes + +import com.orgzly.android.ui.notes.NoteContent.TableNoteContent +import com.orgzly.android.ui.notes.NoteContent.TextNoteContent +import org.hamcrest.Matchers.emptyCollectionOf +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThat +import org.junit.Test +import java.util.Random + +// TODO - check CRLF vs LF vs whatever MacOS does +class NoteContentTest { + + @Test + fun emptyString() { + val parse = NoteContent.parse("") + assertThat(parse, (emptyCollectionOf(NoteContent.javaClass))) + } + + @Test + fun emptyLinesShouldStayInSingleSection() { + checkExpected("\n\n", listOf(TextNoteContent("\n\n"))) + } + + @Test + fun pipeInText() { + checkExpected("""foo +| + +foo|bar""", listOf( + TextNoteContent("foo\n"), + TableNoteContent("|\n"), + TextNoteContent("\nfoo|bar") + )) + } + + @Test + fun singleTable() { + checkExpected("""|a|b| +|c|d| +""", listOf(TableNoteContent("""|a|b| +|c|d| +"""))) + } + + @Test + fun singleTableNoFinalNewline() { + checkExpected("""|a|b| +|c|d|""", listOf(TableNoteContent("""|a|b| +|c|d|"""))) + } + + @Test + fun singleLineTextTableText() { + checkExpected("""foo +| +bar""", listOf( + TextNoteContent("foo\n"), + TableNoteContent("|\n"), + TextNoteContent("bar") + )) + } + + + @Test + fun blankLineTextTableText() { + checkExpected(""" +| +bar +""", listOf( + TextNoteContent("\n"), + TableNoteContent("|\n"), + TextNoteContent("bar\n") + )) + } + + @Test + fun tableBlankLineTable() { + checkExpected("""|zoo| + +|zog|""", listOf( + TableNoteContent("|zoo|\n"), + TextNoteContent("\n"), + TableNoteContent("|zog|") + )) + } + + @Test + fun textTableBlankLineText() { + checkExpected("""foo +| + +chops""", listOf( + TextNoteContent("foo\n"), + TableNoteContent("|\n"), + TextNoteContent("\nchops") + )) + } + + + @Test + fun textTableTextTableText() { + checkExpected("""text1 +|table2a| +|table2b| +text3a +text3b +text3c +|table4| +text5 +""", listOf( + TextNoteContent("text1\n"), + TableNoteContent("|table2a|\n|table2b|\n"), + TextNoteContent("text3a\ntext3b\ntext3c\n"), + TableNoteContent("|table4|\n"), + TextNoteContent("text5\n") + )) + } + + @Test + fun randomStringsRoundTrip() { + + val stringAtoms: List = listOf("\n", "a", "|") + + for (i in 0..1000) { + val rawStringLength = Random().nextInt(100) + val builder = StringBuilder() + for (j in 0..rawStringLength) { + builder.append(stringAtoms.random()) + } + + val raw = builder.toString() + + val actual: List = NoteContent.parse(raw) + + val roundTripped: String = actual.fold("") { acc: String, current: NoteContent -> acc + current.text } + + assertEquals(raw, roundTripped) + + } + + } + + + private fun checkExpected(input: String, expected: List) { + val actual: List = NoteContent.parse(input) + assertEquals(expected, actual) + + val roundTripped: String = actual.fold("") { acc: String, current: NoteContent -> acc + current.text } + + assertEquals(input, roundTripped) + + } +} diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt new file mode 100644 index 000000000..00d65359f --- /dev/null +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt @@ -0,0 +1,88 @@ +package com.orgzly.android.ui.notes + +/** + * Represents a subsection of note content: either text, or a table + */ +sealed class NoteContent { + + abstract val text: String + + data class TextNoteContent(override val text: String) : NoteContent() { + } + + data class TableNoteContent(override val text: String) : NoteContent() { + + fun reformat() { + // placeholder - but would fix all the spacing, missing cells, etc. Complicated + } + } + + + companion object { + + private fun lineIsTable(raw: String) = raw.length > 0 && raw.get(0) == '|' + + /** + * Converts the provided raw string (with embedded newlines) into a list of sections of + * either text or tables. Each section is contiguous and can contain newlines. + * + * This is horrible, never try to write your own parser. Consider using a regex instead. + */ + fun parse(raw: String): List { + val list: MutableList = mutableListOf() + + var currentText = "" + var currentTable = "" + + var previousIsTable: Boolean = this.lineIsTable(raw) + + val rawSplitByNewlines = raw.split("\n") + + val missingLastNewline = rawSplitByNewlines.last() != "" + + val linesForParsing = + if (missingLastNewline) { + rawSplitByNewlines + } else { + rawSplitByNewlines.dropLast(1) + } + + linesForParsing.forEach { + val currentIsTable = lineIsTable(it) + when { + currentIsTable && previousIsTable -> { + currentTable += it + "\n" + } + currentIsTable && !previousIsTable -> { + currentTable = it + "\n" + list.add(TextNoteContent(currentText)) + currentText = "" + } + !currentIsTable && previousIsTable -> { + currentText = it + "\n" + list.add(TableNoteContent(currentTable)) + currentTable = "" + } + !currentIsTable && !previousIsTable -> { + currentText += it + "\n" + } + } + previousIsTable = currentIsTable + } + + if (linesForParsing.isNotEmpty()) { + if (previousIsTable) { + list.add(TableNoteContent(if (missingLastNewline) { + currentTable.dropLast(1) + } else currentTable)) + } else { + list.add(TextNoteContent(if (missingLastNewline) { + currentText.dropLast(1) + } else currentText)) + } + } + + return list + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt index 5c8084704..674ae3b21 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt @@ -1,10 +1,11 @@ package com.orgzly.android.ui.notes import android.content.Context -import android.graphics.Typeface import android.graphics.drawable.Drawable +import android.view.LayoutInflater import android.view.View import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.ColorInt import androidx.constraintlayout.widget.ConstraintLayout @@ -13,10 +14,10 @@ import com.orgzly.android.App import com.orgzly.android.db.entity.Note import com.orgzly.android.db.entity.NoteView import com.orgzly.android.prefs.AppPreferences -import com.orgzly.android.ui.ImageLoader import com.orgzly.android.ui.TimeType import com.orgzly.android.ui.util.TitleGenerator import com.orgzly.android.ui.util.styledAttributes +import com.orgzly.android.ui.views.TextViewWithMarkup import com.orgzly.android.usecase.NoteToggleFolding import com.orgzly.android.usecase.NoteToggleFoldingSubtree import com.orgzly.android.usecase.NoteUpdateContent @@ -109,35 +110,63 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole } private fun setupContent(holder: NoteItemViewHolder, note: Note) { - holder.binding.itemHeadContent.text = note.content if (note.hasContent() && titleGenerator.shouldDisplayContent(note)) { - if (AppPreferences.isFontMonospaced(context)) { - holder.binding.itemHeadContent.typeface = Typeface.MONOSPACE - } - holder.binding.itemHeadContent.setRawText(note.content as CharSequence) + // this is absolutely not the place to split the note, but doing it here for PoC + val alternatingTableAndTextContent: List = NoteContent.parse(note.content!!) + + val linearLayout = holder.itemView.findViewById(R.id.item_head_content_list) + + linearLayout.removeAllViews() + + val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + + alternatingTableAndTextContent.forEach { aocNoteContent -> + when (aocNoteContent) { + is NoteContent.TableNoteContent -> { + + val aocSectionTableTextView = layoutInflater.inflate(R.layout.item_note_content_section_table, linearLayout, false) + + aocSectionTableTextView.findViewById(R.id.aoc_section_table_text).text = aocNoteContent.text + + linearLayout.addView(aocSectionTableTextView) + } + else -> { + + val layout = layoutInflater.inflate(R.layout.item_note_content_section_text, linearLayout, false) + + val textView = layout.findViewById(R.id.aoc_section_text) + + textView.setRawText(aocNoteContent.text) - /* If content changes (for example by toggling the checkbox), update the note. */ - holder.binding.itemHeadContent.onUserTextChangeListener = Runnable { - if (holder.binding.itemHeadContent.getRawText() != null) { - val useCase = NoteUpdateContent( - note.position.bookId, - note.id, - holder.binding.itemHeadContent.getRawText()?.toString()) + linearLayout.addView(layout) - App.EXECUTORS.diskIO().execute { - UseCaseRunner.run(useCase) + /* If content changes (for example by toggling the checkbox), update the note. */ + textView.onUserTextChangeListener = Runnable { + if (textView.getRawText() != null) { + val useCase = NoteUpdateContent( + note.position.bookId, + note.id, + textView.getRawText()?.toString()) + + App.EXECUTORS.diskIO().execute { + UseCaseRunner.run(useCase) + } + } + } + + // TODO restore this +// ImageLoader.loadImages(holder.binding.itemHeadContent) } } - } - ImageLoader.loadImages(holder.binding.itemHeadContent) + } - holder.binding.itemHeadContent.visibility = View.VISIBLE + holder.binding.itemHeadContentList.visibility = View.VISIBLE } else { - holder.binding.itemHeadContent.visibility = View.GONE + holder.binding.itemHeadContentList.visibility = View.GONE } } @@ -425,7 +454,7 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole binding.itemHeadEventIcon, binding.itemHeadClosedIcon, binding.itemHeadClosedText, - binding.itemHeadContent) + binding.itemHeadContentList) for (view in views) { (view.layoutParams as ConstraintLayout.LayoutParams).apply { diff --git a/app/src/main/res/layout/item_head.xml b/app/src/main/res/layout/item_head.xml index ffc835e91..c2adf2da7 100644 --- a/app/src/main/res/layout/item_head.xml +++ b/app/src/main/res/layout/item_head.xml @@ -26,7 +26,7 @@ - + + diff --git a/app/src/main/res/layout/item_note_content_section_table.xml b/app/src/main/res/layout/item_note_content_section_table.xml new file mode 100644 index 000000000..6eb708a66 --- /dev/null +++ b/app/src/main/res/layout/item_note_content_section_table.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_note_content_section_text.xml b/app/src/main/res/layout/item_note_content_section_text.xml new file mode 100644 index 000000000..d1b02acdd --- /dev/null +++ b/app/src/main/res/layout/item_note_content_section_text.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/raw/orgzly_getting_started.org b/app/src/main/res/raw/orgzly_getting_started.org index b782d5060..ef29b4d74 100644 --- a/app/src/main/res/raw/orgzly_getting_started.org +++ b/app/src/main/res/raw/orgzly_getting_started.org @@ -70,6 +70,20 @@ You can make words *bold*, /italic/, _underlined_, =verbatim=, ~code~ and +strik Click the checkbox to toggle it. Press new-line button at the end of the line to create a new item. +** Tables can be created + +A table starts the line with the pipe | character. + +| Here is a table | | | | +| We rely on horizontal scrolling | to show very wide | table content | | + +You can have more than one table in a note. + +| Like this | one | +| for | example | + +Editing tables is not yet supported. + * Search ** There are many search operators supported From 54bda8c0f7f907840b3f2fa7303a8fb5169ef6be Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Mon, 1 Feb 2021 12:07:01 +1100 Subject: [PATCH 2/9] Fix aoc left in IDs, and fix test --- .../orgzly/android/espresso/ExternalLinksTest.kt | 2 +- .../orgzly/android/ui/notes/NoteItemViewBinder.kt | 14 +++++++------- .../res/layout/item_note_content_section_table.xml | 4 ++-- .../res/layout/item_note_content_section_text.xml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt index b6f59ffc6..191d29760 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/ExternalLinksTest.kt @@ -59,7 +59,7 @@ class ExternalLinksTest(private val param: Parameter) : OrgzlyTest() { onBook(0).perform(click()) // Click on link - onNoteInBook(1, R.id.item_head_content_list).perform(clickClickableSpan(param.link)) + onNoteInBook(1, R.id.note_content_section_text).perform(clickClickableSpan(param.link)) param.check() } diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt index 674ae3b21..47403d7c0 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt @@ -122,23 +122,23 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - alternatingTableAndTextContent.forEach { aocNoteContent -> - when (aocNoteContent) { + alternatingTableAndTextContent.forEach { noteContent -> + when (noteContent) { is NoteContent.TableNoteContent -> { - val aocSectionTableTextView = layoutInflater.inflate(R.layout.item_note_content_section_table, linearLayout, false) + val noteContentSectionTableTextView = layoutInflater.inflate(R.layout.item_note_content_section_table, linearLayout, false) - aocSectionTableTextView.findViewById(R.id.aoc_section_table_text).text = aocNoteContent.text + noteContentSectionTableTextView.findViewById(R.id.note_content_section_table_text).text = noteContent.text - linearLayout.addView(aocSectionTableTextView) + linearLayout.addView(noteContentSectionTableTextView) } else -> { val layout = layoutInflater.inflate(R.layout.item_note_content_section_text, linearLayout, false) - val textView = layout.findViewById(R.id.aoc_section_text) + val textView = layout.findViewById(R.id.note_content_section_text) - textView.setRawText(aocNoteContent.text) + textView.setRawText(noteContent.text) linearLayout.addView(layout) diff --git a/app/src/main/res/layout/item_note_content_section_table.xml b/app/src/main/res/layout/item_note_content_section_table.xml index 6eb708a66..ffe319690 100644 --- a/app/src/main/res/layout/item_note_content_section_table.xml +++ b/app/src/main/res/layout/item_note_content_section_table.xml @@ -3,12 +3,12 @@ xmlns:tools="http://schemas.android.com/tools"> Date: Tue, 2 Feb 2021 17:25:41 +1100 Subject: [PATCH 3/9] Fix test --- .../com/orgzly/android/espresso/InternalLinksTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt b/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt index 07e3bb922..aed1ea912 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/espresso/InternalLinksTest.kt @@ -66,28 +66,28 @@ class InternalLinksTest : OrgzlyTest() { @Test fun testDifferentCaseUuidInternalLink() { - onNoteInBook(1, R.id.item_head_content_list) + onNoteInBook(1, R.id.note_content_section_text) .perform(clickClickableSpan("id:bdce923b-C3CD-41ED-B58E-8BDF8BABA54F")) onView(withId(R.id.fragment_note_title)).check(matches(withText("Note [b-2]"))) } @Test fun testDifferentCaseCustomIdInternalLink() { - onNoteInBook(2, R.id.item_head_content_list) + onNoteInBook(2, R.id.note_content_section_text) .perform(clickClickableSpan("#Different case custom id")) onView(withId(R.id.fragment_note_title)).check(matches(withText("Note [b-1]"))) } @Test fun testCustomIdLink() { - onNoteInBook(3, R.id.item_head_content_list) + onNoteInBook(3, R.id.note_content_section_text) .perform(clickClickableSpan("#Link to note in a different book")) onView(withId(R.id.fragment_note_title)).check(matches(withText("Note [b-3]"))) } @Test fun testBookLink() { - onNoteInBook(4, R.id.item_head_content_list) + onNoteInBook(4, R.id.note_content_section_text) .perform(clickClickableSpan("file:book-b.org")) onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed())) onNoteInBook(1, R.id.item_head_title).check(matches(withText("Note [b-1]"))) @@ -95,7 +95,7 @@ class InternalLinksTest : OrgzlyTest() { @Test fun testBookRelativeLink() { - onNoteInBook(5, R.id.item_head_content_list) + onNoteInBook(5, R.id.note_content_section_text) .perform(clickClickableSpan("file:./book-b.org")) onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed())) onNoteInBook(1, R.id.item_head_title).check(matches(withText("Note [b-1]"))) From decfcce19dd7c97da74b34c7ea922c901f3e25a1 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Tue, 2 Feb 2021 17:37:38 +1100 Subject: [PATCH 4/9] Fix tests --- .../java/com/orgzly/android/espresso/QueryFragmentTest.java | 2 +- .../java/com/orgzly/android/espresso/SettingsChangeTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java index fd9726d37..bd9a212ff 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/QueryFragmentTest.java @@ -720,7 +720,7 @@ public void testContentOfFoldedNoteDisplayed() { onView(withId(R.id.fragment_query_search_view_flipper)).check(matches(isDisplayed())); onNotesInSearch().check(matches(recyclerViewItemCount(3))); onNoteInSearch(1, R.id.item_head_title).check(matches(allOf(withText(containsString("Note B")), isDisplayed()))); - onNoteInSearch(1, R.id.item_head_content_list).check(matches(allOf(withText(containsString("Content for Note B")), isDisplayed()))); + onNoteInSearch(1, R.id.note_content_section_text).check(matches(allOf(withText(containsString("Content for Note B")), isDisplayed()))); } @Test diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java index 8b32dbe96..7a28e3968 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java @@ -82,7 +82,7 @@ public void testChangeDefaultPriorityAgendaResultsShouldBeReordered() { public void testDisplayedContentInBook() { onBook(0).perform(click()); - onNoteInBook(1, R.id.item_head_content_list) + onNoteInBook(1, R.id.note_content_section_text) .check(matches(allOf(withText(containsString("Content for [a-1]")), isDisplayed()))); onActionItemClick(R.id.activity_action_settings, R.string.settings); @@ -91,7 +91,7 @@ public void testDisplayedContentInBook() { pressBack(); pressBack(); - onNoteInBook(1, R.id.item_head_content_list).check(matches(not(isDisplayed()))); + onNoteInBook(1, R.id.note_content_section_text).check(matches(not(isDisplayed()))); } private void setDefaultPriority(String priority) { From 413fde757843d74fb5312ce284e00012dc00d301 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Fri, 5 Feb 2021 16:01:29 +1100 Subject: [PATCH 5/9] Fix test to check list is not displayed. It's no good to check note_content_section_text isn't displayed, as it's not found --- .../java/com/orgzly/android/espresso/SettingsChangeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java index 7a28e3968..134d01682 100644 --- a/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java +++ b/app/src/androidTest/java/com/orgzly/android/espresso/SettingsChangeTest.java @@ -91,7 +91,7 @@ public void testDisplayedContentInBook() { pressBack(); pressBack(); - onNoteInBook(1, R.id.note_content_section_text).check(matches(not(isDisplayed()))); + onNoteInBook(1, R.id.item_head_content_list).check(matches(not(isDisplayed()))); } private void setDefaultPriority(String priority) { From d78d27fd7e99f4bf67224627fe4f622acae58615 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Sun, 14 Feb 2021 15:27:51 +1100 Subject: [PATCH 6/9] Restore image which seems to work --- .../java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt index 47403d7c0..169c1ba44 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt @@ -14,6 +14,7 @@ import com.orgzly.android.App import com.orgzly.android.db.entity.Note import com.orgzly.android.db.entity.NoteView import com.orgzly.android.prefs.AppPreferences +import com.orgzly.android.ui.ImageLoader import com.orgzly.android.ui.TimeType import com.orgzly.android.ui.util.TitleGenerator import com.orgzly.android.ui.util.styledAttributes @@ -156,8 +157,7 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole } } - // TODO restore this -// ImageLoader.loadImages(holder.binding.itemHeadContent) + ImageLoader.loadImages(textView) } } From 8081ff41c4fd6588fba9fe6d50e4375b0d535fc6 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Wed, 17 Feb 2021 10:08:07 +1100 Subject: [PATCH 7/9] minor optimizations --- app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt index 00d65359f..0c4ab9f85 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt @@ -20,7 +20,7 @@ sealed class NoteContent { companion object { - private fun lineIsTable(raw: String) = raw.length > 0 && raw.get(0) == '|' + private fun lineIsTable(raw: String) = raw.isNotEmpty() && raw[0] == '|' /** * Converts the provided raw string (with embedded newlines) into a list of sections of From 50b066ed73bc21c6f0f31563ef29f32d7fd87bf1 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Thu, 1 Apr 2021 15:45:05 +1100 Subject: [PATCH 8/9] calculate start and end offset of each note part --- .../android/ui/notes/NoteContentTest.kt | 55 ++++++++++--------- .../orgzly/android/ui/notes/NoteContent.kt | 33 ++++++----- .../android/ui/notes/NoteItemViewBinder.kt | 6 +- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt b/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt index 25777267d..e20963c42 100644 --- a/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt +++ b/app/src/androidTest/java/com/orgzly/android/ui/notes/NoteContentTest.kt @@ -1,7 +1,5 @@ package com.orgzly.android.ui.notes -import com.orgzly.android.ui.notes.NoteContent.TableNoteContent -import com.orgzly.android.ui.notes.NoteContent.TextNoteContent import org.hamcrest.Matchers.emptyCollectionOf import org.junit.Assert.assertEquals import org.junit.Assert.assertThat @@ -19,7 +17,7 @@ class NoteContentTest { @Test fun emptyLinesShouldStayInSingleSection() { - checkExpected("\n\n", listOf(TextNoteContent("\n\n"))) + checkExpected("\n\n", listOf(NoteContent("\n\n", 0, 1, NoteContent.TextType.TEXT))) } @Test @@ -28,9 +26,9 @@ class NoteContentTest { | foo|bar""", listOf( - TextNoteContent("foo\n"), - TableNoteContent("|\n"), - TextNoteContent("\nfoo|bar") + NoteContent("foo\n", 0, 3, NoteContent.TextType.TEXT), + NoteContent("|\n", 4, 5, NoteContent.TextType.TABLE), + NoteContent("\nfoo|bar", 6, 13, NoteContent.TextType.TEXT) )) } @@ -38,16 +36,16 @@ foo|bar""", listOf( fun singleTable() { checkExpected("""|a|b| |c|d| -""", listOf(TableNoteContent("""|a|b| +""", listOf(NoteContent("""|a|b| |c|d| -"""))) +""", 0, 11, NoteContent.TextType.TABLE))) } @Test fun singleTableNoFinalNewline() { checkExpected("""|a|b| -|c|d|""", listOf(TableNoteContent("""|a|b| -|c|d|"""))) +|c|d|""", listOf(NoteContent("""|a|b| +|c|d|""", 0, 10, NoteContent.TextType.TABLE))) } @Test @@ -55,9 +53,9 @@ foo|bar""", listOf( checkExpected("""foo | bar""", listOf( - TextNoteContent("foo\n"), - TableNoteContent("|\n"), - TextNoteContent("bar") + NoteContent("foo\n", 0, 3, NoteContent.TextType.TEXT), + NoteContent("|\n", 4, 5, NoteContent.TextType.TABLE), + NoteContent("bar", 6, 8, NoteContent.TextType.TEXT) )) } @@ -68,9 +66,9 @@ bar""", listOf( | bar """, listOf( - TextNoteContent("\n"), - TableNoteContent("|\n"), - TextNoteContent("bar\n") + NoteContent("\n", 0, 0, NoteContent.TextType.TEXT), + NoteContent("|\n", 1, 2, NoteContent.TextType.TABLE), + NoteContent("bar\n", 3, 6, NoteContent.TextType.TEXT) )) } @@ -79,9 +77,9 @@ bar checkExpected("""|zoo| |zog|""", listOf( - TableNoteContent("|zoo|\n"), - TextNoteContent("\n"), - TableNoteContent("|zog|") + NoteContent("|zoo|\n", 0, 5, NoteContent.TextType.TABLE), + NoteContent("\n", 6, 6, NoteContent.TextType.TEXT), + NoteContent("|zog|", 7, 11, NoteContent.TextType.TABLE) )) } @@ -91,9 +89,9 @@ bar | chops""", listOf( - TextNoteContent("foo\n"), - TableNoteContent("|\n"), - TextNoteContent("\nchops") + NoteContent("foo\n", 0, 3, NoteContent.TextType.TEXT), + NoteContent("|\n", 4, 5, NoteContent.TextType.TABLE), + NoteContent("\nchops", 6, 11, NoteContent.TextType.TEXT) )) } @@ -109,11 +107,11 @@ text3c |table4| text5 """, listOf( - TextNoteContent("text1\n"), - TableNoteContent("|table2a|\n|table2b|\n"), - TextNoteContent("text3a\ntext3b\ntext3c\n"), - TableNoteContent("|table4|\n"), - TextNoteContent("text5\n") + NoteContent("text1\n", 0, 5, NoteContent.TextType.TEXT), + NoteContent("|table2a|\n|table2b|\n", 6, 25, NoteContent.TextType.TABLE), + NoteContent("text3a\ntext3b\ntext3c\n", 26, 46, NoteContent.TextType.TEXT), + NoteContent("|table4|\n", 47, 55, NoteContent.TextType.TABLE), + NoteContent("text5\n", 56, 61, NoteContent.TextType.TEXT) )) } @@ -150,5 +148,8 @@ text5 assertEquals(input, roundTripped) + actual.forEach { + assertEquals(it.text, input.substring(it.startOffset, it.endOffset + 1)) + } } } diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt index 0c4ab9f85..8d642c3b6 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteContent.kt @@ -3,18 +3,10 @@ package com.orgzly.android.ui.notes /** * Represents a subsection of note content: either text, or a table */ -sealed class NoteContent { +data class NoteContent(val text: String, val startOffset: Int, val endOffset: Int, val textType: TextType) { - abstract val text: String - - data class TextNoteContent(override val text: String) : NoteContent() { - } - - data class TableNoteContent(override val text: String) : NoteContent() { - - fun reformat() { - // placeholder - but would fix all the spacing, missing cells, etc. Complicated - } + enum class TextType { + TEXT, TABLE } @@ -55,12 +47,14 @@ sealed class NoteContent { } currentIsTable && !previousIsTable -> { currentTable = it + "\n" - list.add(TextNoteContent(currentText)) + val startOffset = getLastOffset(list) + list.add(NoteContent(currentText, startOffset, startOffset + currentText.length - 1, TextType.TEXT)) currentText = "" } !currentIsTable && previousIsTable -> { currentText = it + "\n" - list.add(TableNoteContent(currentTable)) + val startOffset = getLastOffset(list) + list.add(NoteContent(currentTable, startOffset, startOffset + currentTable.length - 1, TextType.TABLE)) currentTable = "" } !currentIsTable && !previousIsTable -> { @@ -71,18 +65,23 @@ sealed class NoteContent { } if (linesForParsing.isNotEmpty()) { + + val endOffsetAdjustment = if (missingLastNewline) 2 else 1 + if (previousIsTable) { - list.add(TableNoteContent(if (missingLastNewline) { + list.add(NoteContent(if (missingLastNewline) { currentTable.dropLast(1) - } else currentTable)) + } else currentTable, getLastOffset(list), getLastOffset(list) + currentTable.length - endOffsetAdjustment, TextType.TABLE)) } else { - list.add(TextNoteContent(if (missingLastNewline) { + list.add(NoteContent(if (missingLastNewline) { currentText.dropLast(1) - } else currentText)) + } else currentText, getLastOffset(list), getLastOffset(list) + currentText.length - endOffsetAdjustment, TextType.TEXT)) } } return list } + + private fun getLastOffset(list: MutableList) = if (list.isEmpty()) 0 else list.last().endOffset + 1 } } \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt index 169c1ba44..7e5761113 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt @@ -124,8 +124,8 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater alternatingTableAndTextContent.forEach { noteContent -> - when (noteContent) { - is NoteContent.TableNoteContent -> { + when (noteContent.textType) { + NoteContent.TextType.TABLE -> { val noteContentSectionTableTextView = layoutInflater.inflate(R.layout.item_note_content_section_table, linearLayout, false) @@ -133,7 +133,7 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole linearLayout.addView(noteContentSectionTableTextView) } - else -> { + NoteContent.TextType.TEXT -> { val layout = layoutInflater.inflate(R.layout.item_note_content_section_text, linearLayout, false) From 5030f86f762a18700155f870dcbb9794500e1e8e Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Sun, 25 Apr 2021 14:01:35 +1000 Subject: [PATCH 9/9] Basic edit screen with horizontal scrolling --- .../java/com/orgzly/android/AppIntent.java | 3 + .../com/orgzly/android/di/AppComponent.kt | 2 + .../com/orgzly/android/ui/DisplayManager.java | 11 + .../orgzly/android/ui/main/MainActivity.java | 30 +- .../android/ui/note/EditTableFragment.kt | 260 ++++++++++++++++++ .../orgzly/android/ui/note/NoteViewModel.kt | 6 +- .../orgzly/android/ui/note/TableViewModel.kt | 73 +++++ .../android/ui/note/TableViewModelFactory.kt | 28 ++ .../android/ui/notes/NoteItemViewBinder.kt | 9 +- .../main/res/layout/fragment_edit_table.xml | 27 ++ .../item_note_content_section_table.xml | 1 + .../main/res/raw/orgzly_getting_started.org | 2 +- 12 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/orgzly/android/ui/note/EditTableFragment.kt create mode 100644 app/src/main/java/com/orgzly/android/ui/note/TableViewModel.kt create mode 100644 app/src/main/java/com/orgzly/android/ui/note/TableViewModelFactory.kt create mode 100644 app/src/main/res/layout/fragment_edit_table.xml diff --git a/app/src/main/java/com/orgzly/android/AppIntent.java b/app/src/main/java/com/orgzly/android/AppIntent.java index ddb92d7c6..1165f3b89 100644 --- a/app/src/main/java/com/orgzly/android/AppIntent.java +++ b/app/src/main/java/com/orgzly/android/AppIntent.java @@ -32,6 +32,7 @@ public class AppIntent { public static final String ACTION_OPEN_BOOKS = "com.orgzly.intent.action.OPEN_BOOKS"; public static final String ACTION_OPEN_BOOK = "com.orgzly.intent.action.OPEN_BOOK"; public static final String ACTION_OPEN_SETTINGS = "com.orgzly.intent.action.OPEN_SETTINGS"; + public static final String ACTION_EDIT_TABLE = "com.orgzly.intent.action.EDIT_TABLE"; public static final String ACTION_SHOW_SNACKBAR = "com.orgzly.intent.action.SHOW_SNACKBAR"; @@ -40,6 +41,8 @@ public class AppIntent { public static final String EXTRA_BOOK_PREFACE = "com.orgzly.intent.extra.BOOK_PREFACE"; public static final String EXTRA_NOTE_ID = "com.orgzly.intent.extra.NOTE_ID"; public static final String EXTRA_NOTE_CONTENT = "com.orgzly.intent.extra.NOTE_CONTENT"; + public static final String EXTRA_TABLE_START_OFFSET = "com.orgzly.intent.action.EXTRA_TABLE_START_OFFSET"; + public static final String EXTRA_TABLE_END_OFFSET = "com.orgzly.intent.action.EXTRA_TABLE_END_OFFSET"; public static final String EXTRA_QUERY_STRING = "com.orgzly.intent.extra.QUERY_STRING"; public static final String EXTRA_PROPERTY_NAME = "com.orgzly.intent.extra.PROPERTY_NAME"; public static final String EXTRA_PROPERTY_VALUE = "com.orgzly.intent.extra.PROPERTY_VALUE"; diff --git a/app/src/main/java/com/orgzly/android/di/AppComponent.kt b/app/src/main/java/com/orgzly/android/di/AppComponent.kt index dab32b2e2..260993935 100644 --- a/app/src/main/java/com/orgzly/android/di/AppComponent.kt +++ b/app/src/main/java/com/orgzly/android/di/AppComponent.kt @@ -13,6 +13,7 @@ import com.orgzly.android.ui.BookChooserActivity import com.orgzly.android.ui.TemplateChooserActivity import com.orgzly.android.ui.books.BooksFragment import com.orgzly.android.ui.main.MainActivity +import com.orgzly.android.ui.note.EditTableFragment import com.orgzly.android.ui.note.NoteFragment import com.orgzly.android.ui.notes.NotesFragment import com.orgzly.android.ui.notes.book.BookFragment @@ -68,6 +69,7 @@ interface AppComponent { fun inject(arg: SearchFragment) fun inject(arg: AgendaFragment) fun inject(arg: NoteFragment) + fun inject(arg: EditTableFragment) fun inject(arg: SavedSearchesFragment) fun inject(arg: SavedSearchFragment) fun inject(arg: RefileFragment) diff --git a/app/src/main/java/com/orgzly/android/ui/DisplayManager.java b/app/src/main/java/com/orgzly/android/ui/DisplayManager.java index a08a052b6..ec8f67df9 100644 --- a/app/src/main/java/com/orgzly/android/ui/DisplayManager.java +++ b/app/src/main/java/com/orgzly/android/ui/DisplayManager.java @@ -12,6 +12,7 @@ import com.orgzly.android.query.Query; import com.orgzly.android.query.QueryParser; import com.orgzly.android.query.user.InternalQueryParser; +import com.orgzly.android.ui.note.EditTableFragment; import com.orgzly.android.ui.savedsearch.SavedSearchFragment; import com.orgzly.android.ui.main.MainActivity; import com.orgzly.android.ui.notes.book.BookFragment; @@ -151,6 +152,16 @@ public static void displayExistingNote(FragmentManager fragmentManager, long boo } } + public static void displayEditTable(FragmentManager fragmentManager, + long bookId, + long noteId, + int tableStartOffset, + int tableEndOffset) { + Fragment fragment = EditTableFragment.newInstance(bookId, noteId, tableStartOffset, tableEndOffset); + + displayNoteFragment(fragmentManager, fragment); + } + public static void displayNewNote(FragmentManager fragmentManager, NotePlace target) { Fragment fragment = NoteFragment.forNewNote(target); diff --git a/app/src/main/java/com/orgzly/android/ui/main/MainActivity.java b/app/src/main/java/com/orgzly/android/ui/main/MainActivity.java index d857586c2..f00a8702e 100644 --- a/app/src/main/java/com/orgzly/android/ui/main/MainActivity.java +++ b/app/src/main/java/com/orgzly/android/ui/main/MainActivity.java @@ -92,7 +92,6 @@ import com.orgzly.android.usecase.SavedSearchCreate; import com.orgzly.android.usecase.SavedSearchDelete; import com.orgzly.android.usecase.SavedSearchExport; -import com.orgzly.android.usecase.SavedSearchImport; import com.orgzly.android.usecase.SavedSearchMoveDown; import com.orgzly.android.usecase.SavedSearchMoveUp; import com.orgzly.android.usecase.SavedSearchUpdate; @@ -101,7 +100,6 @@ import com.orgzly.android.usecase.UseCaseRunner; import com.orgzly.android.util.AppPermissions; import com.orgzly.android.util.LogUtils; -import com.orgzly.android.util.MiscUtils; import com.orgzly.org.datetime.OrgDateTime; import org.jetbrains.annotations.NotNull; @@ -635,6 +633,7 @@ protected void onResumeFragments() { LocalBroadcastManager bm = LocalBroadcastManager.getInstance(this); bm.registerReceiver(receiver, new IntentFilter(AppIntent.ACTION_OPEN_NOTE)); + bm.registerReceiver(receiver, new IntentFilter(AppIntent.ACTION_EDIT_TABLE)); bm.registerReceiver(receiver, new IntentFilter(AppIntent.ACTION_FOLLOW_LINK_TO_NOTE_WITH_PROPERTY)); bm.registerReceiver(receiver, new IntentFilter(AppIntent.ACTION_FOLLOW_LINK_TO_FILE)); bm.registerReceiver(receiver, new IntentFilter(AppIntent.ACTION_OPEN_SAVED_SEARCHES)); @@ -1267,6 +1266,15 @@ public void onNoteOpen(long noteId) { viewModel.openNote(noteId); } + public void editTableOfView(View view) { + editTable( + (long) view.getTag(AppIntent.EXTRA_BOOK_ID.hashCode()), + (long) view.getTag(AppIntent.EXTRA_NOTE_ID.hashCode()), + (int) view.getTag(AppIntent.EXTRA_TABLE_START_OFFSET.hashCode()), + (int) view.getTag(AppIntent.EXTRA_TABLE_END_OFFSET.hashCode()) + ); + } + // TODO: Consider creating NavigationBroadcastReceiver public static void openSpecificNote(long bookId, long noteId) { Intent intent = new Intent(AppIntent.ACTION_OPEN_NOTE); @@ -1275,6 +1283,15 @@ public static void openSpecificNote(long bookId, long noteId) { LocalBroadcastManager.getInstance(App.getAppContext()).sendBroadcast(intent); } + public static void editTable(long bookId, long noteId, int tableStartOffset, int tableEndOffset) { + Intent intent = new Intent(AppIntent.ACTION_EDIT_TABLE); + intent.putExtra(AppIntent.EXTRA_NOTE_ID, noteId); + intent.putExtra(AppIntent.EXTRA_BOOK_ID, bookId); + intent.putExtra(AppIntent.EXTRA_TABLE_START_OFFSET, tableStartOffset); + intent.putExtra(AppIntent.EXTRA_TABLE_END_OFFSET, tableEndOffset); + LocalBroadcastManager.getInstance(App.getAppContext()).sendBroadcast(intent); + } + public static void followLinkToFile(String path) { Intent intent = new Intent(AppIntent.ACTION_FOLLOW_LINK_TO_FILE); intent.putExtra(AppIntent.EXTRA_PATH, path); @@ -1307,6 +1324,15 @@ private void handleIntent(@NonNull Intent intent, @NonNull String action) { break; } + case AppIntent.ACTION_EDIT_TABLE: { + long bookId = intent.getLongExtra(AppIntent.EXTRA_BOOK_ID, -1); + long noteId = intent.getLongExtra(AppIntent.EXTRA_NOTE_ID, -1); + int tableStartOffset = intent.getIntExtra(AppIntent.EXTRA_TABLE_START_OFFSET, -1); + int tableEndOffset = intent.getIntExtra(AppIntent.EXTRA_TABLE_END_OFFSET, -1); + DisplayManager.displayEditTable(getSupportFragmentManager(), bookId, noteId, tableStartOffset, tableEndOffset); + break; + } + case AppIntent.ACTION_OPEN_SAVED_SEARCHES: { DisplayManager.displaySavedSearches(getSupportFragmentManager()); break; diff --git a/app/src/main/java/com/orgzly/android/ui/note/EditTableFragment.kt b/app/src/main/java/com/orgzly/android/ui/note/EditTableFragment.kt new file mode 100644 index 000000000..606d07769 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/ui/note/EditTableFragment.kt @@ -0,0 +1,260 @@ +package com.orgzly.android.ui.note + +import android.app.AlertDialog +import android.content.Context +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.activity.OnBackPressedCallback +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.orgzly.BuildConfig +import com.orgzly.R +import com.orgzly.android.App +import com.orgzly.android.data.DataRepository +import com.orgzly.android.db.entity.Note +import com.orgzly.android.ui.CommonActivity +import com.orgzly.android.ui.main.SharedMainActivityViewModel +import com.orgzly.android.ui.util.ActivityUtils +import com.orgzly.android.util.LogUtils +import com.orgzly.databinding.FragmentEditTableBinding +import javax.inject.Inject + +class EditTableFragment : Fragment() { + + private val TAG = EditTableFragment::class.java.name + + private lateinit var binding: FragmentEditTableBinding + + @Inject + internal lateinit var dataRepository: DataRepository + + private var listener: NoteFragment.Listener? = null + + private lateinit var viewModel: TableViewModel + + private lateinit var sharedMainActivityViewModel: SharedMainActivityViewModel + + private var dialog: AlertDialog? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + + App.appComponent.inject(this) + + listener = activity as NoteFragment.Listener + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + sharedMainActivityViewModel.setFragment(FRAGMENT_TAG, null, null, 0) + + viewModel.noteViewModel.noteDetailsDataEvent.observeSingle(viewLifecycleOwner, { + viewModel.loadTableData() + }) + // TODO Load payload from saved Bundle if available + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // we ought to do something with the Bundle if it's non-null + + if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "onCreate") + + val args = requireNotNull(arguments) + + val nvmf: NoteViewModelFactory = NoteViewModelFactory.getInstance( + dataRepository, + arguments?.getLong(ARG_BOOK_ID) ?: 0, + args.getLong(ARG_NOTE_ID), + null, + null, + null) as NoteViewModelFactory + + val factory = TableViewModelFactory.getInstance( + nvmf, + args.getInt(ARG_TABLE_START_OFFSET), + args.getInt(ARG_TABLE_END_OFFSET)) + + viewModel = ViewModelProviders.of(this, factory).get(TableViewModel::class.java) + + sharedMainActivityViewModel = ViewModelProviders.of(requireActivity()) + .get(SharedMainActivityViewModel::class.java) + + setHasOptionsMenu(true) + + requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onBackPressed() + } + }) + + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + binding = FragmentEditTableBinding.inflate(inflater, container, false) + + binding.tableViewModel = viewModel + + binding.lifecycleOwner = viewLifecycleOwner + + // from https://stackoverflow.com/a/41022589/116509, to have a DONE tickbox icon on the soft keyboard instead a newline icon + binding.tableContentEditText.imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE + binding.tableContentEditText.setRawInputType(InputType.TYPE_CLASS_TEXT); + + binding.tableContentEditText.setOnEditorActionListener { textView, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + userSave(null) + return@setOnEditorActionListener true + } + false + } + + return binding.root + } + + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, menu, inflater) + + inflater.inflate(R.menu.note_actions, menu) + + menu.removeItem(R.id.activity_action_search) + + menu.removeItem(R.id.note_view_edit) + + menu.removeItem(R.id.metadata) + + menu.removeItem(R.id.delete) + } + + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, item) + + when (item.itemId) { + R.id.done -> { + userSave(null) + return true + } + + R.id.keep_screen_on -> { + dialog = ActivityUtils.keepScreenOnToggle(activity, item) + return true + } + + else -> return super.onOptionsItemSelected(item) + } + } + + private fun userSave(postSave: ((note: Note) -> Unit)?) { + ActivityUtils.closeSoftKeyboard(activity) + viewModel.updateNote(postSave) + } + + private fun onBackPressed() { + userCancel() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupObservers() + + viewModel.loadNoteData() + } + + private fun setupObservers() { + + viewModel.tableUpdatedEvent.observe(viewLifecycleOwner, Observer { note -> + listener?.onNoteUpdated(note) + }) + + + viewModel.errorEvent.observeSingle(viewLifecycleOwner, Observer { error -> + showSnackbar((error.cause ?: error).localizedMessage) + }) + + viewModel.snackBarMessage.observeSingle(viewLifecycleOwner, Observer { resId -> + showSnackbar(resId) + }) + } + + private fun showSnackbar(message: String?) { + CommonActivity.showSnackbar(context, message) + } + + private fun showSnackbar(@StringRes resId: Int) { + CommonActivity.showSnackbar(context, resId) + } + + + override fun onDetach() { + super.onDetach() + + if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG) + + listener = null + } + + private fun userCancel(): Boolean { + ActivityUtils.closeSoftKeyboard(activity) + + if (viewModel.isNoteModified()) { + dialog = AlertDialog.Builder(context) + .setTitle(R.string.note_has_been_modified) + .setMessage(R.string.discard_or_save_changes) + .setPositiveButton(R.string.save) { _, _ -> + viewModel.updateNote { + listener?.onNoteCanceled() + } + } + .setNegativeButton(R.string.discard) { _, _ -> + listener?.onNoteCanceled() + } + .setNeutralButton(R.string.cancel, null) + .show() + + return true + + } else { + listener?.onNoteCanceled() + return false + } + } + + + companion object { + + val FRAGMENT_TAG: String = EditTableFragment::class.java.name + private const val ARG_BOOK_ID = "book_id" + private const val ARG_NOTE_ID = "note_id" + private const val ARG_TABLE_START_OFFSET = "table_start_offset" + private const val ARG_TABLE_END_OFFSET = "table_end_offset" + + + @JvmStatic + fun newInstance(bookId: Long, + noteId: Long, + tableStartOffset: Int, + tableEndOffset: Int) = + EditTableFragment().apply { + arguments = Bundle().apply { + putLong(ARG_BOOK_ID, bookId) + putLong(ARG_NOTE_ID, noteId) + putInt(ARG_TABLE_START_OFFSET, tableStartOffset) + putInt(ARG_TABLE_END_OFFSET, tableEndOffset) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt b/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt index 5988a20d5..b944b5d89 100644 --- a/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt +++ b/app/src/main/java/com/orgzly/android/ui/note/NoteViewModel.kt @@ -29,7 +29,7 @@ class NoteViewModel( private var noteId: Long, private val place: Place?, private val title: String?, - private val content: String? + internal val content: String? ) : CommonViewModel() { enum class ViewEditMode { @@ -60,7 +60,7 @@ class NoteViewModel( val noteDeleteRequest: SingleLiveEvent = SingleLiveEvent() val bookChangeRequestEvent: SingleLiveEvent> = SingleLiveEvent() - var notePayload: NotePayload? = null + internal var notePayload: NotePayload? = null private var originalHash: Long = 0L @@ -263,7 +263,7 @@ class NoteViewModel( } } - private fun updateNote(postSave: ((note: Note) -> Unit)?) { + internal fun updateNote(postSave: ((note: Note) -> Unit)?) { notePayload?.let { payload -> App.EXECUTORS.diskIO().execute { catchAndPostError { diff --git a/app/src/main/java/com/orgzly/android/ui/note/TableViewModel.kt b/app/src/main/java/com/orgzly/android/ui/note/TableViewModel.kt new file mode 100644 index 000000000..9d22cbac9 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/ui/note/TableViewModel.kt @@ -0,0 +1,73 @@ +package com.orgzly.android.ui.note + +import androidx.lifecycle.MutableLiveData +import com.orgzly.android.db.entity.Note +import com.orgzly.android.ui.CommonViewModel +import com.orgzly.android.ui.SingleLiveEvent + +class TableViewModel( + val noteViewModel: NoteViewModel, + private val tableStartOffset: Int, + private val tableEndOffset: Int +) : CommonViewModel() { + + private val TAG = TableViewModel::class.java.name + + val tableView: MutableLiveData = MutableLiveData("") + + val tableUpdatedEvent: SingleLiveEvent = noteViewModel.noteUpdatedEvent + + lateinit var tableTextBeforeEditing: String + + fun loadNoteData() { + noteViewModel.loadData() + } + + fun loadTableData() { + val content = noteViewModel.notePayload!!.content!! + + tableTextBeforeEditing = content.substring(tableStartOffset, tableEndOffset) + + tableView.postValue(tableTextBeforeEditing) + } + + fun isNoteModified(): Boolean { + val unchanged: Boolean? = tableView.value?.equals(tableTextBeforeEditing) + if (unchanged == null) { + return false + } else { + return !unchanged + } + } + + fun updateNote(postSave: ((note: Note) -> Unit)?) { + updatePayload() + noteViewModel.updateNote(postSave) + } + + private fun updatePayload() { + + val np = noteViewModel.notePayload!! + + val updatedContent = np.content + + val beforeTable = updatedContent!!.substring(0, tableStartOffset) + val table = tableView.value + val afterTable = updatedContent.substring(tableEndOffset, noteViewModel.notePayload!!.content!!.length) + + noteViewModel.notePayload = + np.copy( + title = np.title, + content = beforeTable + table + afterTable, + state = np.state, + priority = np.priority, + scheduled = np.scheduled, + deadline = np.deadline, + closed = np.closed, + tags = np.tags, + properties = np.properties) + + } + + +} diff --git a/app/src/main/java/com/orgzly/android/ui/note/TableViewModelFactory.kt b/app/src/main/java/com/orgzly/android/ui/note/TableViewModelFactory.kt new file mode 100644 index 000000000..f402112c4 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/ui/note/TableViewModelFactory.kt @@ -0,0 +1,28 @@ +package com.orgzly.android.ui.note + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class TableViewModelFactory( + private val nvmf: NoteViewModelFactory, + private val tableStartOffset: Int, + private val tableEndOffset: Int +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return TableViewModel(nvmf.create(NoteViewModel::class.java), tableStartOffset, tableEndOffset) as T + } + + companion object { + @JvmStatic + fun getInstance( + nvmf: NoteViewModelFactory, + tableStartOffset: Int, + tableEndOffset: Int + + ): ViewModelProvider.Factory { + return TableViewModelFactory(nvmf, tableStartOffset, tableEndOffset) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt index 7e5761113..64e721281 100644 --- a/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt +++ b/app/src/main/java/com/orgzly/android/ui/notes/NoteItemViewBinder.kt @@ -11,6 +11,7 @@ import androidx.annotation.ColorInt import androidx.constraintlayout.widget.ConstraintLayout import com.orgzly.R import com.orgzly.android.App +import com.orgzly.android.AppIntent import com.orgzly.android.db.entity.Note import com.orgzly.android.db.entity.NoteView import com.orgzly.android.prefs.AppPreferences @@ -129,7 +130,13 @@ class NoteItemViewBinder(private val context: Context, private val inBook: Boole val noteContentSectionTableTextView = layoutInflater.inflate(R.layout.item_note_content_section_table, linearLayout, false) - noteContentSectionTableTextView.findViewById(R.id.note_content_section_table_text).text = noteContent.text + val tableTextView = noteContentSectionTableTextView.findViewById(R.id.note_content_section_table_text) + tableTextView.text = noteContent.text + + tableTextView.setTag(AppIntent.EXTRA_BOOK_ID.hashCode(), note.position.bookId) + tableTextView.setTag(AppIntent.EXTRA_NOTE_ID.hashCode(), note.id) + tableTextView.setTag(AppIntent.EXTRA_TABLE_START_OFFSET.hashCode(), noteContent.startOffset) + tableTextView.setTag(AppIntent.EXTRA_TABLE_END_OFFSET.hashCode(), noteContent.endOffset) linearLayout.addView(noteContentSectionTableTextView) } diff --git a/app/src/main/res/layout/fragment_edit_table.xml b/app/src/main/res/layout/fragment_edit_table.xml new file mode 100644 index 000000000..052ca5eb2 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_table.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_note_content_section_table.xml b/app/src/main/res/layout/item_note_content_section_table.xml index ffe319690..575904166 100644 --- a/app/src/main/res/layout/item_note_content_section_table.xml +++ b/app/src/main/res/layout/item_note_content_section_table.xml @@ -12,6 +12,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:typeface="monospace" + android:onClick="editTableOfView" tools:text="| a bit of | table |" /> \ No newline at end of file diff --git a/app/src/main/res/raw/orgzly_getting_started.org b/app/src/main/res/raw/orgzly_getting_started.org index ef29b4d74..fa9992b24 100644 --- a/app/src/main/res/raw/orgzly_getting_started.org +++ b/app/src/main/res/raw/orgzly_getting_started.org @@ -82,7 +82,7 @@ You can have more than one table in a note. | Like this | one | | for | example | -Editing tables is not yet supported. +Click on a table to edit it. * Search ** There are many search operators supported