1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-11 10:18:05 +09:00

Enable hot keys for different editor patterns (#332)

This commit is contained in:
Evgenii Kozlov 2020-04-06 17:16:42 +02:00 committed by GitHub
parent f1d082e829
commit dbdce727f7
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 711 additions and 129 deletions

View file

@ -5,6 +5,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_ui.common.Markup
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.core_ui.features.page.pattern.Matcher
import com.agileburo.anytype.core_ui.features.page.pattern.Pattern
import com.agileburo.anytype.core_ui.state.ControlPanelState
import com.agileburo.anytype.core_utils.common.EventWrapper
import com.agileburo.anytype.core_utils.ext.*
@ -30,6 +32,7 @@ import com.agileburo.anytype.presentation.common.SupportCommand
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
import com.agileburo.anytype.presentation.page.ControlPanelMachine.Interactor
import com.agileburo.anytype.presentation.page.model.TextUpdate
import com.agileburo.anytype.presentation.page.render.BlockViewRenderer
import com.agileburo.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.agileburo.anytype.presentation.page.toggle.ToggleStateHolder
@ -49,8 +52,9 @@ class PageViewModel(
private val archiveDocument: ArchiveDocument,
private val undo: Undo,
private val redo: Redo,
private val updateBlock: UpdateBlock,
private val updateText: UpdateText,
private val createBlock: CreateBlock,
private val replaceBlock: ReplaceBlock,
private val interceptEvents: InterceptEvents,
private val updateCheckbox: UpdateCheckbox,
private val unlinkBlocks: UnlinkBlocks,
@ -67,7 +71,8 @@ class PageViewModel(
private val documentExternalEventReducer: StateReducer<List<Block>, Event>,
private val urlBuilder: UrlBuilder,
private val renderer: DefaultBlockViewRenderer,
private val counter: Counter
private val counter: Counter,
private val patternMatcher: Matcher<Pattern>
) : ViewStateViewModel<PageViewModel.ViewState>(),
SupportNavigation<EventWrapper<AppNavigation.Command>>,
SupportCommand<PageViewModel.Command>,
@ -79,13 +84,16 @@ class PageViewModel(
val controlPanelViewState = MutableLiveData<ControlPanelState>()
private val renderingChannel = Channel<List<Block>>()
private val renderings = renderingChannel.consumeAsFlow()
private val focusChannel = ConflatedBroadcastChannel(EMPTY_FOCUS_ID)
private val focusChanges = focusChannel.asFlow()
private val textChannel = Channel<Triple<Id, String, List<Content.Text.Mark>>>()
private val textChannel = Channel<TextUpdate>()
private val textUpdateChannel = Channel<TextUpdate>()
private val textChanges = textChannel.consumeAsFlow()
private val textUpdateChanges = textUpdateChannel.consumeAsFlow()
private val selectionChannel = Channel<Pair<Id, IntRange>>()
private val selectionsChanges = selectionChannel.consumeAsFlow()
@ -146,16 +154,17 @@ class PageViewModel(
private fun proceedWithInitialFocusing(events: List<Event>) {
val event = events.find { event -> event is Event.Command.ShowBlock }
if (event is Event.Command.ShowBlock) {
val title = event.blocks.first { block ->
event.blocks.find { block ->
block.content is Content.Text
&& block.content<Content.Text>().style == Content.Text.Style.TITLE
}
updateFocus(title.id)
controlPanelInteractor.onEvent(
ControlPanelMachine.Event.OnFocusChanged(
id = title.id, style = Content.Text.Style.TITLE
}?.let { title ->
updateFocus(title.id)
controlPanelInteractor.onEvent(
ControlPanelMachine.Event.OnFocusChanged(
id = title.id, style = Content.Text.Style.TITLE
)
)
)
}
}
}
@ -217,8 +226,8 @@ class PageViewModel(
val newContent = targetContent.copy(marks = it)
val newBlock = targetBlock.copy(content = newContent)
rerenderingBlocks(newBlock)
proceedWithUpdatingBlock(
params = UpdateBlock.Params(
proceedWithUpdatingText(
params = UpdateText.Params(
contextId = context,
text = newBlock.content.asText().text,
blockId = targetBlock.id,
@ -270,8 +279,8 @@ class PageViewModel(
refresh()
proceedWithUpdatingBlock(
params = UpdateBlock.Params(
proceedWithUpdatingText(
params = UpdateText.Params(
contextId = context,
blockId = newBlock.id,
text = newContent.text,
@ -311,35 +320,96 @@ class PageViewModel(
private fun startHandlingTextChanges() {
textChanges
.onEach { update ->
when {
update.patterns.isEmpty() -> textUpdateChannel.send(update)
update.patterns.contains(Pattern.NUMBERED) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.NUMBERED
)
)
update.patterns.contains(Pattern.DIVIDER) -> replaceBy(
target = update.target,
prototype = Prototype.Divider
)
update.patterns.contains(Pattern.CHECKBOX) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.CHECKBOX
)
)
update.patterns.contains(Pattern.BULLET) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.BULLET
)
)
update.patterns.contains(Pattern.H1) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.H1
)
)
update.patterns.contains(Pattern.H2) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.H2
)
)
update.patterns.contains(Pattern.H3) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.H3
)
)
update.patterns.contains(Pattern.QUOTE) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.QUOTE
)
)
update.patterns.contains(Pattern.TOGGLE) -> replaceBy(
target = update.target,
prototype = Prototype.Text(
style = Content.Text.Style.TOGGLE
)
)
else -> textUpdateChannel.send(update)
}
}
.launchIn(viewModelScope)
textUpdateChanges
.debounce(TEXT_CHANGES_DEBOUNCE_DURATION)
.map { (id, text, marks) ->
.map { update ->
blocks = blocks.map { block ->
if (block.id == id) {
if (block.id == update.target) {
block.copy(
content = block.content.asText().copy(
text = text,
marks = marks.filter { it.range.first != it.range.last }
text = update.text,
marks = update.markup.filter { it.range.first != it.range.last }
)
)
} else
block
}
UpdateBlock.Params(
UpdateText.Params(
contextId = context,
blockId = id,
text = text,
marks = marks.filter { it.range.first != it.range.last }
blockId = update.target,
text = update.text,
marks = update.markup.filter { it.range.first != it.range.last }
)
}
.onEach { params -> proceedWithUpdatingBlock(params) }
.onEach { params -> proceedWithUpdatingText(params) }
.launchIn(viewModelScope)
}
private fun proceedWithUpdatingBlock(params: UpdateBlock.Params) {
private fun proceedWithUpdatingText(params: UpdateText.Params) {
Timber.d("Starting updating block with params: $params")
updateBlock.invoke(viewModelScope, params) { result ->
updateText.invoke(viewModelScope, params) { result ->
result.either(
fnL = { Timber.e(it, "Error while updating text: $params") },
fnR = { Timber.d("Text has been updated") }
@ -347,6 +417,26 @@ class PageViewModel(
}
}
private fun replaceBy(
target: Id,
prototype: Prototype
) {
replaceBlock.invoke(
scope = viewModelScope,
params = ReplaceBlock.Params(
context = context,
target = target,
prototype = prototype
),
onResult = { result ->
result.either(
fnL = { Timber.e(it, "Error while converting $target to: $prototype") },
fnR = { id -> updateFocus(id) }
)
}
)
}
fun open(id: String) {
context = id
@ -356,7 +446,7 @@ class PageViewModel(
openPage.invoke(viewModelScope, OpenPage.Params(id)) { result ->
result.either(
fnR = { Timber.d("Page with id $id has been opened") },
fnL = { Timber.e(it, "Error while openining page with id: $id") }
fnL = { Timber.e(it, "Error while opening page with id: $id") }
)
}
}
@ -379,8 +469,8 @@ class PageViewModel(
val newContent = targetContent.copy(marks = it)
val newBlock = targetBlock.copy(content = newContent)
rerenderingBlocks(newBlock)
proceedWithUpdatingBlock(
params = UpdateBlock.Params(
proceedWithUpdatingText(
params = UpdateText.Params(
contextId = context,
text = newBlock.content.asText().text,
blockId = targetBlock.id,
@ -422,9 +512,29 @@ class PageViewModel(
}
}
fun onTextChanged(id: String, text: String, marks: List<Content.Text.Mark>) {
Timber.d("onTextChanged: $id\nNew text: $text\nMarks: $marks")
viewModelScope.launch { textChannel.send(Triple(id, text, marks)) }
fun onTextChanged(
id: String,
text: String,
marks: List<Content.Text.Mark>
) {
val update = TextUpdate(target = id, text = text, markup = marks, patterns = emptyList())
Timber.d("onTextChanged: $update")
viewModelScope.launch { textChannel.send(update) }
}
fun onParagraphTextChanged(
id: String,
text: String,
marks: List<Content.Text.Mark>
) {
val update = TextUpdate(
target = id,
text = text,
markup = marks,
patterns = patternMatcher.match(text)
)
Timber.d("onParagraphTextChanged: $update")
viewModelScope.launch { textChannel.send(update) }
}
fun onSelectionChanged(id: String, selection: IntRange) {

View file

@ -2,6 +2,8 @@ package com.agileburo.anytype.presentation.page
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.core_ui.features.page.pattern.Matcher
import com.agileburo.anytype.core_ui.features.page.pattern.Pattern
import com.agileburo.anytype.core_utils.tools.Counter
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.model.Block
@ -21,8 +23,9 @@ open class PageViewModelFactory(
private val archiveDocument: ArchiveDocument,
private val redo: Redo,
private val undo: Undo,
private val updateBlock: UpdateBlock,
private val updateText: UpdateText,
private val createBlock: CreateBlock,
private val replaceBlock: ReplaceBlock,
private val interceptEvents: InterceptEvents,
private val updateCheckbox: UpdateCheckbox,
private val unlinkBlocks: UnlinkBlocks,
@ -39,7 +42,8 @@ open class PageViewModelFactory(
private val urlBuilder: UrlBuilder,
private val downloadFile: DownloadFile,
private val renderer: DefaultBlockViewRenderer,
private val counter: Counter
private val counter: Counter,
private val patternMatcher: Matcher<Pattern>
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -49,7 +53,7 @@ open class PageViewModelFactory(
closePage = closePage,
undo = undo,
redo = redo,
updateBlock = updateBlock,
updateText = updateText,
createBlock = createBlock,
archiveDocument = archiveDocument,
interceptEvents = interceptEvents,
@ -70,7 +74,9 @@ open class PageViewModelFactory(
downloadFile = downloadFile,
renderer = renderer,
counter = counter,
createDocument = createDocument
createDocument = createDocument,
replaceBlock = replaceBlock,
patternMatcher = patternMatcher
) as T
}
}

View file

@ -0,0 +1,19 @@
package com.agileburo.anytype.presentation.page.model
import com.agileburo.anytype.core_ui.features.page.pattern.Pattern
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.common.Id
/**
* Editor text update event data.
* @property target id of the text block, in which this change occurs
* @property text new text for this [target]
* @property markup markup, associated with this [text]
* @property patterns editor patterns found in this [text]
*/
class TextUpdate(
val target: Id,
val text: String,
val markup: List<Block.Content.Text.Mark>,
val patterns: List<Pattern>
)

View file

@ -4,6 +4,7 @@ import MockDataFactory
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.agileburo.anytype.core_ui.common.Markup
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.core_ui.features.page.pattern.DefaultPatternMatcher
import com.agileburo.anytype.core_ui.state.ControlPanelState
import com.agileburo.anytype.core_utils.tools.Counter
import com.agileburo.anytype.domain.base.Either
@ -60,7 +61,7 @@ class PageViewModelTest {
lateinit var createBlock: CreateBlock
@Mock
lateinit var updateBlock: UpdateBlock
lateinit var updateText: UpdateText
@Mock
lateinit var updateCheckbox: UpdateCheckbox
@ -116,6 +117,9 @@ class PageViewModelTest {
@Mock
lateinit var archiveDocument: ArchiveDocument
@Mock
lateinit var replaceBlock: ReplaceBlock
private lateinit var vm: PageViewModel
@Before
@ -297,7 +301,7 @@ class PageViewModelTest {
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verify(updateBlock, times(1)).invoke(
verify(updateText, times(1)).invoke(
any(),
argThat { this.contextId == pageId && this.blockId == blockId && this.text == text },
any()
@ -326,7 +330,7 @@ class PageViewModelTest {
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verify(updateBlock, times(2)).invoke(
verify(updateText, times(2)).invoke(
any(),
argThat { this.contextId == pageId && this.blockId == blockId && this.text == text },
any()
@ -906,10 +910,10 @@ class PageViewModelTest {
)
)
verify(updateBlock, times(1)).invoke(
verify(updateText, times(1)).invoke(
scope = any(),
params = eq(
UpdateBlock.Params(
UpdateText.Params(
blockId = paragraph.id,
marks = marks,
contextId = page.id,
@ -1174,10 +1178,10 @@ class PageViewModelTest {
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verify(updateBlock, times(1)).invoke(
verify(updateText, times(1)).invoke(
scope = any(),
params = eq(
UpdateBlock.Params(
UpdateText.Params(
blockId = paragraph.id,
text = userInput,
marks = marks,
@ -1196,7 +1200,9 @@ class PageViewModelTest {
}
@Test
fun `add-block-or-turn-into panel should be opened on add-block-toolbar-clicked event`() {
fun `should dispatch open-add-block-panel command on add-block-toolbar-clicked event`() {
// SETUP
val root = MockDataFactory.randomUuid()
val child = MockDataFactory.randomUuid()
@ -1223,39 +1229,23 @@ class PageViewModelTest {
coroutineTestRule.advanceTime(1001)
// TESTING
vm.onBlockFocusChanged(
id = child,
hasFocus = true
)
val commands = vm.commands.test()
vm.onAddBlockToolbarClicked()
val expected = ControlPanelState(
colorToolbar = ControlPanelState.Toolbar.Color(
isVisible = false
),
blockToolbar = ControlPanelState.Toolbar.Block(
isVisible = true,
selectedAction = ControlPanelState.Toolbar.Block.Action.ADD
),
addBlockToolbar = ControlPanelState.Toolbar.AddBlock(
isVisible = true
),
markupToolbar = ControlPanelState.Toolbar.Markup(
isVisible = false
),
actionToolbar = ControlPanelState.Toolbar.BlockAction(
isVisible = false
),
turnIntoToolbar = ControlPanelState.Toolbar.TurnInto(
isVisible = false
),
focus = ControlPanelState.Focus(
id = child,
type = ControlPanelState.Focus.Type.P
)
)
val result = commands.value()
vm.controlPanelViewState.test().assertValue(expected)
assertEquals(
expected = PageViewModel.Command.OpenAddBlockPanel,
actual = result.peekContent()
)
}
@Test
@ -3319,7 +3309,7 @@ class PageViewModelTest {
}
@Test
fun `should start closing page after succesful archive operation`() {
fun `should start closing page after successful archive operation`() {
// SETUP
@ -3393,6 +3383,232 @@ class PageViewModelTest {
)
}
@Test
fun `should convert paragraph to numbered list without any delay when regex matches`() {
// SETUP
val root = MockDataFactory.randomUuid()
val paragraph = MockBlockFactory.makeParagraphBlock()
val title = MockBlockFactory.makeTitleBlock()
val page = listOf(
Block(
id = root,
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(title.id, paragraph.id)
),
title,
paragraph
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
buildViewModel()
vm.open(root)
coroutineTestRule.advanceTime(100)
// TESTING
val update = "1. "
vm.onParagraphTextChanged(
id = paragraph.id,
marks = paragraph.content<Block.Content.Text>().marks,
text = update
)
verify(replaceBlock, times(1)).invoke(
scope = any(),
params = eq(
ReplaceBlock.Params(
context = root,
target = paragraph.id,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.NUMBERED
)
)
),
onResult = any()
)
}
@Test
fun `should ignore create-numbered-list-item pattern and update text with delay`() {
// SETUP
val root = MockDataFactory.randomUuid()
val numbered = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
marks = emptyList(),
style = Block.Content.Text.Style.NUMBERED
),
children = emptyList()
)
val title = MockBlockFactory.makeTitleBlock()
val page = listOf(
Block(
id = root,
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(title.id, numbered.id)
),
title,
numbered
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
buildViewModel()
vm.open(root)
coroutineTestRule.advanceTime(100)
// TESTING
val update = "1. "
vm.onTextChanged(
id = numbered.id,
marks = numbered.content<Block.Content.Text>().marks,
text = update
)
verify(updateText, never()).invoke(
scope = any(),
params = any(),
onResult = any()
)
verify(replaceBlock, never()).invoke(
scope = any(),
params = any(),
onResult = any()
)
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verify(replaceBlock, never()).invoke(
scope = any(),
params = any(),
onResult = any()
)
verify(updateText, times(1)).invoke(
scope = any(),
params = eq(
UpdateText.Params(
contextId = root,
blockId = numbered.id,
marks = numbered.content<Block.Content.Text>().marks,
text = update
)
),
onResult = any()
)
}
@Test
fun `should not update text while processing paragraph-to-numbered-list editor pattern`() {
// SETUP
val root = MockDataFactory.randomUuid()
val paragraph = MockBlockFactory.makeParagraphBlock()
val title = MockBlockFactory.makeTitleBlock()
val page = listOf(
Block(
id = root,
fields = Block.Fields.empty(),
content = Block.Content.Page(
style = Block.Content.Page.Style.SET
),
children = listOf(title.id, paragraph.id)
),
title,
paragraph
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowBlock(
rootId = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
buildViewModel()
vm.open(root)
coroutineTestRule.advanceTime(100)
// TESTING
val update = "1. "
vm.onParagraphTextChanged(
id = paragraph.id,
marks = paragraph.content<Block.Content.Text>().marks,
text = update
)
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verify(updateText, never()).invoke(
scope = any(),
params = any(),
onResult = any()
)
}
private fun simulateNormalPageOpeningFlow() {
val root = MockDataFactory.randomUuid()
@ -3455,7 +3671,7 @@ class PageViewModelTest {
openPage = openPage,
closePage = closePage,
createPage = createPage,
updateBlock = updateBlock,
updateText = updateText,
undo = undo,
redo = redo,
interceptEvents = interceptEvents,
@ -3481,7 +3697,9 @@ class PageViewModelTest {
toggleStateHolder = ToggleStateHolder.Default()
),
archiveDocument = archiveDocument,
createDocument = createDocument
createDocument = createDocument,
replaceBlock = replaceBlock,
patternMatcher = DefaultPatternMatcher()
)
}
}