mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 13:57:10 +09:00
Editor | Split the target block on enter-key-pressed event (#230)
This commit is contained in:
parent
961f135b7f
commit
cb46fbe10e
26 changed files with 777 additions and 390 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,5 +1,15 @@
|
|||
# Change log for Android @Anytype app.
|
||||
|
||||
## Version 0.0.20 (WIP)
|
||||
|
||||
### New features 🚀
|
||||
|
||||
* Allow users to split blocks (#229)
|
||||
|
||||
### Middleware ⚙️
|
||||
|
||||
* Added `blockSplit` command (#229)
|
||||
|
||||
## Version 0.0.19
|
||||
|
||||
### New features 🚀
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.agileburo.anytype.R
|
|||
import com.agileburo.anytype.core_ui.features.page.BlockViewHolder
|
||||
import com.agileburo.anytype.domain.block.interactor.*
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.agileburo.anytype.domain.event.model.Event
|
||||
import com.agileburo.anytype.domain.page.ClosePage
|
||||
|
@ -41,6 +42,7 @@ import com.bartoszlipinski.disableanimationsrule.DisableAnimationsRule
|
|||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.stub
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
|
@ -92,6 +94,11 @@ class PageFragmentTest {
|
|||
@Mock
|
||||
lateinit var mergeBlocks: MergeBlocks
|
||||
|
||||
@Mock
|
||||
lateinit var repo: BlockRepository
|
||||
|
||||
private lateinit var splitBlock: SplitBlock
|
||||
|
||||
private lateinit var actionToolbar: ViewInteraction
|
||||
private lateinit var optionToolbar: ViewInteraction
|
||||
|
||||
|
@ -104,20 +111,23 @@ class PageFragmentTest {
|
|||
actionToolbar = onView(withId(R.id.actionToolbar))
|
||||
optionToolbar = onView(withId(R.id.optionToolbar))
|
||||
|
||||
splitBlock = SplitBlock(repo)
|
||||
|
||||
TestPageFragment.testViewModelFactory = PageViewModelFactory(
|
||||
openPage,
|
||||
closePage,
|
||||
updateBlock,
|
||||
createBlock,
|
||||
interceptEvents,
|
||||
updateCheckbox,
|
||||
unlinkBlocks,
|
||||
duplicateBlock,
|
||||
updateTextStyle,
|
||||
updateTextColor,
|
||||
updateLinkMarks,
|
||||
removeLinkMark,
|
||||
mergeBlocks
|
||||
openPage = openPage,
|
||||
closePage = closePage,
|
||||
updateBlock = updateBlock,
|
||||
createBlock = createBlock,
|
||||
interceptEvents = interceptEvents,
|
||||
updateCheckbox = updateCheckbox,
|
||||
unlinkBlocks = unlinkBlocks,
|
||||
duplicateBlock = duplicateBlock,
|
||||
updateTextStyle = updateTextStyle,
|
||||
updateTextColor = updateTextColor,
|
||||
updateLinkMarks = updateLinkMarks,
|
||||
removeLinkMark = removeLinkMark,
|
||||
mergeBlocks = mergeBlocks,
|
||||
splitBlock = splitBlock
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -371,10 +381,110 @@ class PageFragmentTest {
|
|||
|
||||
onView(allOf(withId(R.id.keyboard), isDisplayed())).perform(click())
|
||||
onView(withId(R.id.toolbar)).check(matches(not(isDisplayed())))
|
||||
//onView(withId(R.id.placeholder)).check(matches(hasFocus()))
|
||||
target.check(matches(not(hasFocus())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldSplitBlocks() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val args = bundleOf(PageFragment.ID_KEY to root)
|
||||
|
||||
val delayBeforeGettingEvents = 100L
|
||||
val delayBeforeSplittingBlocks = 100L
|
||||
|
||||
val paragraph = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
children = emptyList(),
|
||||
content = Block.Content.Text(
|
||||
text = "FooBar",
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
)
|
||||
)
|
||||
|
||||
val page = listOf(
|
||||
Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Page(
|
||||
style = Block.Content.Page.Style.SET
|
||||
),
|
||||
children = listOf(paragraph.id)
|
||||
),
|
||||
paragraph
|
||||
)
|
||||
|
||||
val new = Block(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
fields = Block.Fields.empty(),
|
||||
children = emptyList(),
|
||||
content = Block.Content.Text(
|
||||
text = "Bar",
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
)
|
||||
)
|
||||
|
||||
stubEvents(
|
||||
events = flow {
|
||||
delay(delayBeforeGettingEvents)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.ShowBlock(
|
||||
rootId = root,
|
||||
blocks = page,
|
||||
context = root
|
||||
)
|
||||
)
|
||||
)
|
||||
delay(delayBeforeSplittingBlocks)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.GranularChange(
|
||||
context = root,
|
||||
id = paragraph.id,
|
||||
text = "Foo"
|
||||
),
|
||||
Event.Command.UpdateStructure(
|
||||
context = root,
|
||||
id = page.first().id,
|
||||
children = listOf(paragraph.id, new.id)
|
||||
),
|
||||
Event.Command.AddBlock(
|
||||
context = root,
|
||||
blocks = listOf(new)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
launchFragmentInContainer<TestPageFragment>(
|
||||
fragmentArgs = args,
|
||||
themeResId = R.style.AppTheme
|
||||
)
|
||||
|
||||
// TESTING
|
||||
|
||||
advance(delayBeforeGettingEvents)
|
||||
|
||||
val target = onView(withRecyclerView(R.id.recycler).atPositionOnView(0, R.id.textContent))
|
||||
|
||||
target.check(matches(withText(paragraph.content.asText().text)))
|
||||
|
||||
advance(delayBeforeSplittingBlocks)
|
||||
|
||||
onView(withRecyclerView(R.id.recycler).atPositionOnView(0, R.id.textContent)).apply {
|
||||
check(matches(withText("Foo")))
|
||||
}
|
||||
onView(withRecyclerView(R.id.recycler).atPositionOnView(1, R.id.textContent)).apply {
|
||||
check(matches(withText("Bar")))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* STUBBING
|
||||
*/
|
||||
|
@ -396,6 +506,12 @@ class PageFragmentTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun stubEvents(events: Flow<List<Event>>) {
|
||||
interceptEvents.stub {
|
||||
onBlocking { build() } doReturn events
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves coroutines clock time.
|
||||
*/
|
||||
|
|
|
@ -45,7 +45,8 @@ class PageModule {
|
|||
duplicateBlock: DuplicateBlock,
|
||||
updateTextStyle: UpdateTextStyle,
|
||||
updateTextColor: UpdateTextColor,
|
||||
mergeBlocks: MergeBlocks
|
||||
mergeBlocks: MergeBlocks,
|
||||
splitBlock: SplitBlock
|
||||
): PageViewModelFactory = PageViewModelFactory(
|
||||
openPage = openPage,
|
||||
closePage = closePage,
|
||||
|
@ -59,7 +60,8 @@ class PageModule {
|
|||
updateTextColor = updateTextColor,
|
||||
updateLinkMarks = updateLinkMarks,
|
||||
removeLinkMark = removeLinkMark,
|
||||
mergeBlocks = mergeBlocks
|
||||
mergeBlocks = mergeBlocks,
|
||||
splitBlock = splitBlock
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
@ -135,6 +137,14 @@ class PageModule {
|
|||
repo = repo
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideSplitBlockUseCase(
|
||||
repo: BlockRepository
|
||||
): SplitBlock = SplitBlock(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideUpdateLinkMarks(): UpdateLinkMarks = UpdateLinkMarks()
|
||||
|
|
|
@ -43,7 +43,7 @@ class BlockAdapter(
|
|||
private val onFocusChanged: (String, Boolean) -> Unit,
|
||||
private val onEmptyBlockBackspaceClicked: (String) -> Unit,
|
||||
private val onNonEmptyBlockBackspaceClicked: (String) -> Unit,
|
||||
private val onSplitLineEnterClicked: (String) -> Unit,
|
||||
private val onSplitLineEnterClicked: (String, Int) -> Unit,
|
||||
private val onEndLineEnterClicked: (String, Editable) -> Unit,
|
||||
private val onFooterClicked: () -> Unit
|
||||
) : RecyclerView.Adapter<BlockViewHolder>() {
|
||||
|
@ -413,8 +413,8 @@ class BlockAdapter(
|
|||
onEndLineEnterClicked = { editable ->
|
||||
onEndLineEnterClicked(blocks[holder.adapterPosition].id, editable)
|
||||
},
|
||||
onSplitLineEnterClicked = {
|
||||
onSplitLineEnterClicked(blocks[holder.adapterPosition].id)
|
||||
onSplitLineEnterClicked = { index ->
|
||||
onSplitLineEnterClicked(blocks[holder.adapterPosition].id, index)
|
||||
}
|
||||
)
|
||||
holder.enableBackspaceDetector(
|
||||
|
|
|
@ -70,7 +70,12 @@ class BlockViewDiffUtil(
|
|||
*/
|
||||
data class Payload(
|
||||
val changes: List<Int>
|
||||
)
|
||||
) {
|
||||
fun markupChanged() = changes.contains(MARKUP_CHANGED)
|
||||
fun textChanged() = changes.contains(TEXT_CHANGED)
|
||||
fun textColorChanged() = changes.contains(TEXT_COLOR_CHANGED)
|
||||
fun focusChanged() = changes.contains(FOCUS_CHANGED)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TEXT_CHANGED = 0
|
||||
|
|
|
@ -9,11 +9,8 @@ import com.agileburo.anytype.core_ui.R
|
|||
import com.agileburo.anytype.core_ui.common.*
|
||||
import com.agileburo.anytype.core_ui.extensions.color
|
||||
import com.agileburo.anytype.core_ui.extensions.tint
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.FOCUS_CHANGED
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.MARKUP_CHANGED
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.NUMBER_CHANGED
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.TEXT_CHANGED
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.TEXT_COLOR_CHANGED
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Payload
|
||||
import com.agileburo.anytype.core_ui.tools.DefaultSpannableFactory
|
||||
import com.agileburo.anytype.core_ui.tools.DefaultTextWatcher
|
||||
|
@ -58,7 +55,7 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
|
||||
fun enableEnterKeyDetector(
|
||||
onEndLineEnterClicked: (Editable) -> Unit,
|
||||
onSplitLineEnterClicked: () -> Unit
|
||||
onSplitLineEnterClicked: (Int) -> Unit
|
||||
) {
|
||||
content.filters = arrayOf(
|
||||
DefaultEnterKeyDetector(
|
||||
|
@ -136,29 +133,27 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
Timber.d("Processing $payload for new view:\n$item")
|
||||
|
||||
if (item is BlockView.Text) {
|
||||
if (payload.changes.contains(TEXT_CHANGED))
|
||||
if (content.text.toString() != item.text) {
|
||||
Timber.d("Text changed.\nBefore:${content.text.toString()}\nAfter:${item.text}")
|
||||
content.pauseTextWatchers {
|
||||
if (item is Markup)
|
||||
content.setText(item.toSpannable(), BufferType.SPANNABLE)
|
||||
}
|
||||
|
||||
if (payload.textChanged()) {
|
||||
val cursor = content.selectionEnd
|
||||
content.pauseTextWatchers {
|
||||
if (item is Markup)
|
||||
content.setText(item.toSpannable(), BufferType.SPANNABLE)
|
||||
else
|
||||
content.setText(item.text)
|
||||
}
|
||||
content.setSelection(cursor)
|
||||
} else if (payload.markupChanged()) {
|
||||
if (item is Markup) setMarkup(item)
|
||||
}
|
||||
|
||||
if (payload.changes.contains(TEXT_COLOR_CHANGED))
|
||||
if (payload.textColorChanged()) {
|
||||
item.color?.let { setTextColor(it) }
|
||||
}
|
||||
|
||||
if (item is Markup) {
|
||||
if (payload.changes.contains(MARKUP_CHANGED) && !payload.changes.contains(
|
||||
TEXT_CHANGED
|
||||
)
|
||||
)
|
||||
setMarkup(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (item is Focusable) {
|
||||
if (payload.changes.contains(FOCUS_CHANGED))
|
||||
if (payload.focusChanged())
|
||||
setFocus(item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -710,6 +710,60 @@ class BlockAdapterTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should preserve cursor position after updating paragraph text`() {
|
||||
|
||||
// Setup
|
||||
|
||||
val title = BlockView.Paragraph(
|
||||
text = MockDataFactory.randomString(),
|
||||
id = MockDataFactory.randomUuid()
|
||||
)
|
||||
|
||||
val updated = title.copy(
|
||||
text = MockDataFactory.randomString()
|
||||
)
|
||||
|
||||
val views = listOf(title)
|
||||
|
||||
val adapter = buildAdapter(views)
|
||||
|
||||
val recycler = RecyclerView(context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
|
||||
val holder = adapter.onCreateViewHolder(recycler, BlockViewHolder.HOLDER_PARAGRAPH)
|
||||
|
||||
adapter.onBindViewHolder(holder, 0)
|
||||
|
||||
check(holder is BlockViewHolder.Paragraph)
|
||||
|
||||
// Testing
|
||||
|
||||
assertEquals(
|
||||
expected = title.text,
|
||||
actual = holder.content.text.toString()
|
||||
)
|
||||
|
||||
val cursorBeforeUpdate = holder.content.selectionEnd
|
||||
|
||||
holder.processChangePayload(
|
||||
item = updated,
|
||||
payloads = listOf(
|
||||
BlockViewDiffUtil.Payload(
|
||||
changes = listOf(TEXT_CHANGED)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val cursorAfterUpdate = holder.content.selectionEnd
|
||||
|
||||
assertEquals(
|
||||
expected = cursorBeforeUpdate,
|
||||
actual = cursorAfterUpdate
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildAdapter(
|
||||
views: List<BlockView>,
|
||||
onFocusChanged: (String, Boolean) -> Unit = { _, _ -> },
|
||||
|
@ -719,7 +773,7 @@ class BlockAdapterTest {
|
|||
blocks = views,
|
||||
onNonEmptyBlockBackspaceClicked = {},
|
||||
onEmptyBlockBackspaceClicked = {},
|
||||
onSplitLineEnterClicked = {},
|
||||
onSplitLineEnterClicked = { _, _ -> },
|
||||
onEndLineEnterClicked = { _, _ -> },
|
||||
onTextChanged = onTextChanged,
|
||||
onCheckboxClicked = {},
|
||||
|
|
|
@ -5,15 +5,16 @@ import android.text.Spanned
|
|||
|
||||
class DefaultEnterKeyDetector(
|
||||
private val onEndLineEnterClicked: () -> Unit,
|
||||
private val onSplitLineEnterClicked: () -> Unit
|
||||
private val onSplitLineEnterClicked: (Int) -> Unit
|
||||
) : EnterKeyDetector() {
|
||||
|
||||
override fun onEndEnterPress(textBeforeEnter: Spanned): CharSequence? {
|
||||
onEndLineEnterClicked()
|
||||
return EMPTY_REPLACEMENT
|
||||
}
|
||||
|
||||
override fun onSplitEnterPress(textBeforeEnter: Spanned): CharSequence? {
|
||||
onSplitLineEnterClicked()
|
||||
override fun onSplitEnterPress(textBeforeEnter: Spanned, index: Int): CharSequence? {
|
||||
onSplitLineEnterClicked(index)
|
||||
return EMPTY_REPLACEMENT
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +35,14 @@ abstract class EnterKeyDetector : InputFilter {
|
|||
if (dend - 1 == dest.lastIndex)
|
||||
onEndEnterPress(textBeforeEnter = dest)
|
||||
else
|
||||
onSplitEnterPress(textBeforeEnter = dest)
|
||||
onSplitEnterPress(textBeforeEnter = dest, index = dend)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onEndEnterPress(textBeforeEnter: Spanned): CharSequence?
|
||||
abstract fun onSplitEnterPress(textBeforeEnter: Spanned): CharSequence?
|
||||
abstract fun onSplitEnterPress(textBeforeEnter: Spanned, index: Int): CharSequence?
|
||||
|
||||
companion object {
|
||||
const val EMPTY_REPLACEMENT = ""
|
||||
|
|
|
@ -266,6 +266,12 @@ fun Command.Merge.toEntity(): CommandEntity.Merge = CommandEntity.Merge(
|
|||
pair = pair
|
||||
)
|
||||
|
||||
fun Command.Split.toEntity(): CommandEntity.Split = CommandEntity.Split(
|
||||
context = context,
|
||||
target = target,
|
||||
index = index
|
||||
)
|
||||
|
||||
fun Position.toEntity(): PositionEntity {
|
||||
return PositionEntity.valueOf(name)
|
||||
}
|
||||
|
|
|
@ -59,4 +59,10 @@ class CommandEntity {
|
|||
val context: String,
|
||||
val pair: Pair<String, String>
|
||||
)
|
||||
|
||||
data class Split(
|
||||
val context: String,
|
||||
val target: String,
|
||||
val index: Int
|
||||
)
|
||||
}
|
|
@ -63,4 +63,8 @@ class BlockDataRepository(
|
|||
override suspend fun merge(command: Command.Merge) {
|
||||
factory.remote.merge(command.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun split(command: Command.Split) {
|
||||
factory.remote.split(command.toEntity())
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ interface BlockDataStore {
|
|||
suspend fun dnd(command: CommandEntity.Dnd)
|
||||
suspend fun duplicate(command: CommandEntity.Duplicate): Id
|
||||
suspend fun merge(command: CommandEntity.Merge)
|
||||
suspend fun split(command: CommandEntity.Split)
|
||||
suspend fun unlink(command: CommandEntity.Unlink)
|
||||
suspend fun getConfig(): ConfigEntity
|
||||
suspend fun createPage(parentId: String): String
|
||||
|
|
|
@ -12,6 +12,7 @@ interface BlockRemote {
|
|||
suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox)
|
||||
suspend fun dnd(command: CommandEntity.Dnd)
|
||||
suspend fun merge(command: CommandEntity.Merge)
|
||||
suspend fun split(command: CommandEntity.Split)
|
||||
suspend fun duplicate(command: CommandEntity.Duplicate): Id
|
||||
suspend fun unlink(command: CommandEntity.Unlink)
|
||||
suspend fun getConfig(): ConfigEntity
|
||||
|
|
|
@ -56,4 +56,8 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
|
|||
override suspend fun merge(command: CommandEntity.Merge) {
|
||||
remote.merge(command)
|
||||
}
|
||||
|
||||
override suspend fun split(command: CommandEntity.Split) {
|
||||
remote.split(command)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.agileburo.anytype.domain.block.interactor
|
||||
|
||||
import com.agileburo.anytype.domain.base.BaseUseCase
|
||||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.model.Command
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
|
||||
/**
|
||||
* Use-case for splitting the target block into two blocks based on cursor position.
|
||||
*/
|
||||
class SplitBlock(private val repo: BlockRepository) : BaseUseCase<Unit, SplitBlock.Params>() {
|
||||
|
||||
override suspend fun run(params: Params) = try {
|
||||
repo.split(
|
||||
command = Command.Split(
|
||||
context = params.context,
|
||||
target = params.target,
|
||||
index = params.index
|
||||
)
|
||||
).let {
|
||||
Either.Right(it)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Either.Left(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Params for splitting one block into two blocks
|
||||
* @property context context id
|
||||
* @property target id of the target block, which we need to split
|
||||
* @property index index or cursor position
|
||||
*/
|
||||
data class Params(
|
||||
val context: Id,
|
||||
val target: Id,
|
||||
val index: Int
|
||||
)
|
||||
}
|
|
@ -103,4 +103,16 @@ sealed class Command {
|
|||
val context: Id,
|
||||
val pair: Pair<Id, Id>
|
||||
)
|
||||
|
||||
/**
|
||||
* Command for splitting one block into two blocks
|
||||
* @property context context id
|
||||
* @property target id of the target block, which we need to split
|
||||
* @property index index or cursor position
|
||||
*/
|
||||
data class Split(
|
||||
val context: Id,
|
||||
val target: Id,
|
||||
val index: Int
|
||||
)
|
||||
}
|
|
@ -10,6 +10,7 @@ interface BlockRepository {
|
|||
suspend fun unlink(command: Command.Unlink)
|
||||
suspend fun create(command: Command.Create)
|
||||
suspend fun merge(command: Command.Merge)
|
||||
suspend fun split(command: Command.Split)
|
||||
suspend fun updateText(command: Command.UpdateText)
|
||||
suspend fun updateTextStyle(command: Command.UpdateStyle)
|
||||
suspend fun updateTextColor(command: Command.UpdateTextColor)
|
||||
|
|
|
@ -82,4 +82,8 @@ class BlockMiddleware(
|
|||
override suspend fun merge(command: CommandEntity.Merge) {
|
||||
middleware.merge(command)
|
||||
}
|
||||
|
||||
override suspend fun split(command: CommandEntity.Split) {
|
||||
middleware.split(command)
|
||||
}
|
||||
}
|
|
@ -502,4 +502,17 @@ public class Middleware {
|
|||
|
||||
service.blockMerge(request);
|
||||
}
|
||||
|
||||
public void split(CommandEntity.Split command) throws Exception {
|
||||
Block.Split.Request request = Block.Split.Request
|
||||
.newBuilder()
|
||||
.setBlockId(command.getTarget())
|
||||
.setContextId(command.getContext())
|
||||
.setCursorPosition(command.getIndex())
|
||||
.build();
|
||||
|
||||
Timber.d("Splitting the target block with the following request:\n%s", request.toString());
|
||||
|
||||
service.blockSplit(request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,6 +219,17 @@ public class DefaultMiddlewareService implements MiddlewareService {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block.Split.Response blockSplit(Block.Split.Request request) throws Exception {
|
||||
byte[] encoded = Lib.blockSplit(request.toByteArray());
|
||||
Block.Split.Response response = Block.Split.Response.parseFrom(encoded);
|
||||
if (response.getError() != null && response.getError().getCode() != Block.Split.Response.Error.Code.NULL) {
|
||||
throw new Exception(response.getError().getDescription());
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockList.Duplicate.Response blockListDuplicate(BlockList.Duplicate.Request request) throws Exception {
|
||||
byte[] encoded = Lib.blockListDuplicate(request.toByteArray());
|
||||
|
|
|
@ -49,5 +49,7 @@ public interface MiddlewareService {
|
|||
|
||||
Block.Merge.Response blockMerge(Block.Merge.Request request) throws Exception;
|
||||
|
||||
Block.Split.Response blockSplit(Block.Split.Request request) throws Exception;
|
||||
|
||||
BlockList.Duplicate.Response blockListDuplicate(BlockList.Duplicate.Request request) throws Exception;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,344 @@
|
|||
package com.agileburo.anytype.presentation.page
|
||||
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState.Toolbar
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.presentation.common.StateReducer
|
||||
import com.agileburo.anytype.presentation.page.ControlPanelMachine.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.scan
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* State machine for control panels consisting of [Interactor], [ControlPanelState], [Event] and [Reducer]
|
||||
* [Interactor] reduces [Event] to the immutable [ControlPanelState] by applying [Reducer] fuction.
|
||||
* This [ControlPanelState] then will be rendered.
|
||||
*/
|
||||
sealed class ControlPanelMachine {
|
||||
|
||||
/**
|
||||
* @property scope coroutine scope (state machine runs inside this scope)
|
||||
*/
|
||||
class Interactor(
|
||||
private val scope: CoroutineScope
|
||||
) : ControlPanelMachine() {
|
||||
|
||||
private val reducer: Reducer = Reducer()
|
||||
val channel: Channel<Event> = Channel()
|
||||
private val events: Flow<Event> = channel.consumeAsFlow()
|
||||
|
||||
fun onEvent(event: Event) = scope.launch { channel.send(event) }
|
||||
|
||||
/**
|
||||
* @return a stream of immutable states, as processed by [Reducer].
|
||||
*/
|
||||
fun state(): Flow<ControlPanelState> =
|
||||
events.scan(ControlPanelState.init(), reducer.function)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events related to this state machine and to control panel logics.
|
||||
*/
|
||||
sealed class Event {
|
||||
|
||||
/**
|
||||
* Represents text selection changes events
|
||||
* @property selection text selection (end index and start index are inclusive)
|
||||
*/
|
||||
data class OnSelectionChanged(
|
||||
val selection: IntRange
|
||||
) : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a color option on [Toolbar.Markup] toolbar.
|
||||
*/
|
||||
object OnMarkupToolbarColorClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user toggled [Toolbar.AddBlock] toolbar button on [Toolbar.Block].
|
||||
*/
|
||||
object OnAddBlockToolbarToggleClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user toggled [Toolbar.TurnInto] toolbar button on [Toolbar.Block]
|
||||
*/
|
||||
object OnTurnIntoToolbarToggleClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected any of the options on [Toolbar.AddBlock] toolbar.
|
||||
*/
|
||||
object OnAddBlockToolbarOptionSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected any of the options on [Toolbar.TurnInto] toolbar.
|
||||
*/
|
||||
object OnTurnIntoToolbarOptionSelected : Event()
|
||||
|
||||
|
||||
/**
|
||||
* Represents an event when user toggled [Toolbar.Color] toolbar button on [Toolbar.Block]
|
||||
*/
|
||||
object OnColorToolbarToggleClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a markup text color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnMarkupTextColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a background color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnMarkupBackgroundColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a block text color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnBlockTextColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected an action toolbar on [Toolbar.Block]
|
||||
*/
|
||||
object OnActionToolbarClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user cleares the current focus by closing keyboard.
|
||||
*/
|
||||
object OnClearFocusClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when focus changes.
|
||||
* @property id id of the focused block
|
||||
*/
|
||||
data class OnFocusChanged(
|
||||
val id: String,
|
||||
val style: Block.Content.Text.Style
|
||||
) : Event()
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete reducer implementation that holds all the logic related to control panels.
|
||||
*/
|
||||
class Reducer : StateReducer<ControlPanelState, Event> {
|
||||
|
||||
override val function: suspend (ControlPanelState, Event) -> ControlPanelState
|
||||
get() = { state, event ->
|
||||
reduce(
|
||||
state,
|
||||
event
|
||||
).also { Timber.d("Reducing event:\n$event") }
|
||||
}
|
||||
|
||||
override suspend fun reduce(state: ControlPanelState, event: Event) = when (event) {
|
||||
is Event.OnSelectionChanged -> state.copy(
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
isVisible = event.selection.first != event.selection.last,
|
||||
selectedAction = if (event.selection.first != event.selection.last)
|
||||
state.markupToolbar.selectedAction
|
||||
else
|
||||
null
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null,
|
||||
isVisible = (event.selection.first == event.selection.last)
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = if (event.selection.first != event.selection.last)
|
||||
state.colorToolbar.copy()
|
||||
else
|
||||
state.colorToolbar.copy(isVisible = false)
|
||||
)
|
||||
is Event.OnMarkupToolbarColorClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = !state.colorToolbar.isVisible
|
||||
),
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
selectedAction = if (!state.colorToolbar.isVisible)
|
||||
Toolbar.Markup.Action.COLOR
|
||||
else
|
||||
null
|
||||
)
|
||||
)
|
||||
is Event.OnMarkupTextColorSelected -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnBlockTextColorSelected -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnAddBlockToolbarToggleClicked -> state.copy(
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = !state.addBlockToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (!state.addBlockToolbar.isVisible)
|
||||
Toolbar.Block.Action.ADD
|
||||
else
|
||||
null
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnTurnIntoToolbarToggleClicked -> state.copy(
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = !state.turnIntoToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (!state.turnIntoToolbar.isVisible)
|
||||
Toolbar.Block.Action.TURN_INTO
|
||||
else
|
||||
null
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnColorToolbarToggleClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = !state.colorToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (!state.colorToolbar.isVisible)
|
||||
Toolbar.Block.Action.COLOR
|
||||
else
|
||||
null
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnAddBlockToolbarOptionSelected -> state.copy(
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnMarkupBackgroundColorSelected -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnTurnIntoToolbarOptionSelected -> state.copy(
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnActionToolbarClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = !state.actionToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (state.actionToolbar.isVisible)
|
||||
null
|
||||
else Toolbar.Block.Action.BLOCK_ACTION
|
||||
)
|
||||
)
|
||||
is Event.OnClearFocusClicked -> ControlPanelState.init()
|
||||
is Event.OnFocusChanged -> {
|
||||
if (state.isNotVisible())
|
||||
state.copy(
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
isVisible = true,
|
||||
selectedAction = null
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
focus = ControlPanelState.Focus(
|
||||
id = event.id,
|
||||
type = ControlPanelState.Focus.Type.valueOf(
|
||||
value = event.style.name
|
||||
)
|
||||
)
|
||||
)
|
||||
else {
|
||||
state.copy(
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
),
|
||||
focus = ControlPanelState.Focus(
|
||||
id = event.id,
|
||||
type = ControlPanelState.Focus.Type.valueOf(
|
||||
value = event.style.name
|
||||
)
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ 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.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState.Toolbar
|
||||
import com.agileburo.anytype.core_utils.common.EventWrapper
|
||||
import com.agileburo.anytype.core_utils.ext.replace
|
||||
import com.agileburo.anytype.core_utils.ext.withLatestFrom
|
||||
|
@ -22,12 +21,10 @@ import com.agileburo.anytype.domain.event.model.Event
|
|||
import com.agileburo.anytype.domain.ext.*
|
||||
import com.agileburo.anytype.domain.page.ClosePage
|
||||
import com.agileburo.anytype.domain.page.OpenPage
|
||||
import com.agileburo.anytype.presentation.common.StateReducer
|
||||
import com.agileburo.anytype.presentation.mapper.toView
|
||||
import com.agileburo.anytype.presentation.navigation.AppNavigation
|
||||
import com.agileburo.anytype.presentation.navigation.SupportNavigation
|
||||
import com.agileburo.anytype.presentation.page.PageViewModel.ControlPanelMachine.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import com.agileburo.anytype.presentation.page.ControlPanelMachine.Interactor
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
@ -47,7 +44,8 @@ class PageViewModel(
|
|||
private val updateTextColor: UpdateTextColor,
|
||||
private val updateLinkMarks: UpdateLinkMarks,
|
||||
private val removeLinkMark: RemoveLinkMark,
|
||||
private val mergeBlocks: MergeBlocks
|
||||
private val mergeBlocks: MergeBlocks,
|
||||
private val splitBlock: SplitBlock
|
||||
) : ViewStateViewModel<PageViewModel.ViewState>(),
|
||||
SupportNavigation<EventWrapper<AppNavigation.Command>> {
|
||||
|
||||
|
@ -466,8 +464,23 @@ class PageViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onSplitLineEnterClicked(id: String) {
|
||||
// TODO
|
||||
fun onSplitLineEnterClicked(
|
||||
id: String,
|
||||
index: Int
|
||||
) {
|
||||
splitBlock.invoke(
|
||||
scope = viewModelScope,
|
||||
params = SplitBlock.Params(
|
||||
context = pageId,
|
||||
target = id,
|
||||
index = index
|
||||
)
|
||||
) { result ->
|
||||
result.either(
|
||||
fnL = { Timber.e(it, "Error while splitting the target block with id: $id") },
|
||||
fnR = { Timber.d("Succesfully split the target block with id: $id") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEndLineEnterClicked(
|
||||
|
@ -769,336 +782,6 @@ class PageViewModel(
|
|||
const val TEXT_CHANGES_DEBOUNCE_DURATION = 500L
|
||||
}
|
||||
|
||||
/**
|
||||
* State machine for control panels consisting of [Interactor], [ControlPanelState], [Event] and [Reducer]
|
||||
* [Interactor] reduces [Event] to the immutable [ControlPanelState] by applying [Reducer] fuction.
|
||||
* This [ControlPanelState] then will be rendered.
|
||||
*/
|
||||
sealed class ControlPanelMachine {
|
||||
|
||||
/**
|
||||
* @property scope coroutine scope (state machine runs inside this scope)
|
||||
*/
|
||||
class Interactor(
|
||||
private val scope: CoroutineScope
|
||||
) : ControlPanelMachine() {
|
||||
|
||||
private val reducer: Reducer = Reducer()
|
||||
val channel: Channel<Event> = Channel()
|
||||
private val events: Flow<Event> = channel.consumeAsFlow()
|
||||
|
||||
fun onEvent(event: Event) = scope.launch { channel.send(event) }
|
||||
|
||||
/**
|
||||
* @return a stream of immutable states, as processed by [Reducer].
|
||||
*/
|
||||
fun state(): Flow<ControlPanelState> =
|
||||
events.scan(ControlPanelState.init(), reducer.function)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events related to this state machine and to control panel logics.
|
||||
*/
|
||||
sealed class Event {
|
||||
|
||||
/**
|
||||
* Represents text selection changes events
|
||||
* @property selection text selection (end index and start index are inclusive)
|
||||
*/
|
||||
data class OnSelectionChanged(
|
||||
val selection: IntRange
|
||||
) : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a color option on [Toolbar.Markup] toolbar.
|
||||
*/
|
||||
object OnMarkupToolbarColorClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user toggled [Toolbar.AddBlock] toolbar button on [Toolbar.Block].
|
||||
*/
|
||||
object OnAddBlockToolbarToggleClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user toggled [Toolbar.TurnInto] toolbar button on [Toolbar.Block]
|
||||
*/
|
||||
object OnTurnIntoToolbarToggleClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected any of the options on [Toolbar.AddBlock] toolbar.
|
||||
*/
|
||||
object OnAddBlockToolbarOptionSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected any of the options on [Toolbar.TurnInto] toolbar.
|
||||
*/
|
||||
object OnTurnIntoToolbarOptionSelected : Event()
|
||||
|
||||
|
||||
/**
|
||||
* Represents an event when user toggled [Toolbar.Color] toolbar button on [Toolbar.Block]
|
||||
*/
|
||||
object OnColorToolbarToggleClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a markup text color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnMarkupTextColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a background color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnMarkupBackgroundColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected a block text color on [Toolbar.Color] toolbar.
|
||||
*/
|
||||
object OnBlockTextColorSelected : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user selected an action toolbar on [Toolbar.Block]
|
||||
*/
|
||||
object OnActionToolbarClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when user cleares the current focus by closing keyboard.
|
||||
*/
|
||||
object OnClearFocusClicked : Event()
|
||||
|
||||
/**
|
||||
* Represents an event when focus changes.
|
||||
* @property id id of the focused block
|
||||
*/
|
||||
data class OnFocusChanged(
|
||||
val id: String,
|
||||
val style: Block.Content.Text.Style
|
||||
) : Event()
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete reducer implementation that holds all the logic related to control panels.
|
||||
*/
|
||||
class Reducer : StateReducer<ControlPanelState, Event> {
|
||||
|
||||
override val function: suspend (ControlPanelState, Event) -> ControlPanelState
|
||||
get() = { state, event ->
|
||||
reduce(
|
||||
state,
|
||||
event
|
||||
).also { Timber.d("Reducing event:\n$event") }
|
||||
}
|
||||
|
||||
override suspend fun reduce(state: ControlPanelState, event: Event) = when (event) {
|
||||
is Event.OnSelectionChanged -> state.copy(
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
isVisible = event.selection.first != event.selection.last,
|
||||
selectedAction = if (event.selection.first != event.selection.last)
|
||||
state.markupToolbar.selectedAction
|
||||
else
|
||||
null
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null,
|
||||
isVisible = (event.selection.first == event.selection.last)
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = if (event.selection.first != event.selection.last)
|
||||
state.colorToolbar.copy()
|
||||
else
|
||||
state.colorToolbar.copy(isVisible = false)
|
||||
)
|
||||
is Event.OnMarkupToolbarColorClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = !state.colorToolbar.isVisible
|
||||
),
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
selectedAction = if (!state.colorToolbar.isVisible)
|
||||
Toolbar.Markup.Action.COLOR
|
||||
else
|
||||
null
|
||||
)
|
||||
)
|
||||
is Event.OnMarkupTextColorSelected -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnBlockTextColorSelected -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnAddBlockToolbarToggleClicked -> state.copy(
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = !state.addBlockToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (!state.addBlockToolbar.isVisible)
|
||||
Toolbar.Block.Action.ADD
|
||||
else
|
||||
null
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnTurnIntoToolbarToggleClicked -> state.copy(
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = !state.turnIntoToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (!state.turnIntoToolbar.isVisible)
|
||||
Toolbar.Block.Action.TURN_INTO
|
||||
else
|
||||
null
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnColorToolbarToggleClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = !state.colorToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (!state.colorToolbar.isVisible)
|
||||
Toolbar.Block.Action.COLOR
|
||||
else
|
||||
null
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
is Event.OnAddBlockToolbarOptionSelected -> state.copy(
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnMarkupBackgroundColorSelected -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
markupToolbar = state.markupToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnTurnIntoToolbarOptionSelected -> state.copy(
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
)
|
||||
)
|
||||
is Event.OnActionToolbarClicked -> state.copy(
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = !state.actionToolbar.isVisible
|
||||
),
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = if (state.actionToolbar.isVisible)
|
||||
null
|
||||
else Toolbar.Block.Action.BLOCK_ACTION
|
||||
)
|
||||
)
|
||||
is Event.OnClearFocusClicked -> ControlPanelState.init()
|
||||
is Event.OnFocusChanged -> {
|
||||
if (state.isNotVisible())
|
||||
state.copy(
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
isVisible = true,
|
||||
selectedAction = null
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
focus = ControlPanelState.Focus(
|
||||
id = event.id,
|
||||
type = ControlPanelState.Focus.Type.valueOf(
|
||||
value = event.style.name
|
||||
)
|
||||
)
|
||||
)
|
||||
else {
|
||||
state.copy(
|
||||
blockToolbar = state.blockToolbar.copy(
|
||||
selectedAction = null
|
||||
),
|
||||
focus = ControlPanelState.Focus(
|
||||
id = event.id,
|
||||
type = ControlPanelState.Focus.Type.valueOf(
|
||||
value = event.style.name
|
||||
)
|
||||
),
|
||||
addBlockToolbar = state.addBlockToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
actionToolbar = state.actionToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
turnIntoToolbar = state.turnIntoToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
colorToolbar = state.colorToolbar.copy(
|
||||
isVisible = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MarkupAction(
|
||||
val type: Markup.Type,
|
||||
val param: Any? = null
|
||||
|
@ -1113,5 +796,4 @@ class PageViewModel(
|
|||
markupActionChannel.cancel()
|
||||
controlPanelInteractor.channel.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -20,7 +20,8 @@ open class PageViewModelFactory(
|
|||
private val updateTextColor: UpdateTextColor,
|
||||
private val updateLinkMarks: UpdateLinkMarks,
|
||||
private val removeLinkMark: RemoveLinkMark,
|
||||
private val mergeBlocks: MergeBlocks
|
||||
private val mergeBlocks: MergeBlocks,
|
||||
private val splitBlock: SplitBlock
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -38,7 +39,8 @@ open class PageViewModelFactory(
|
|||
updateTextColor = updateTextColor,
|
||||
updateLinkMarks = updateLinkMarks,
|
||||
removeLinkMark = removeLinkMark,
|
||||
mergeBlocks = mergeBlocks
|
||||
mergeBlocks = mergeBlocks,
|
||||
splitBlock = splitBlock
|
||||
) as T
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package com.agileburo.anytype.presentation.page
|
|||
import MockDataFactory
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.presentation.page.PageViewModel.ControlPanelMachine
|
||||
import com.agileburo.anytype.presentation.util.CoroutinesTestRule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Rule
|
||||
|
|
|
@ -80,6 +80,9 @@ class PageViewModelTest {
|
|||
@Mock
|
||||
lateinit var mergeBlocks: MergeBlocks
|
||||
|
||||
@Mock
|
||||
lateinit var splitBlock: SplitBlock
|
||||
|
||||
private lateinit var vm: PageViewModel
|
||||
|
||||
@Before
|
||||
|
@ -2496,6 +2499,67 @@ class PageViewModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should proceed with splittig block on split-enter-key event`() {
|
||||
|
||||
// SETUP
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
val child = MockDataFactory.randomUuid()
|
||||
|
||||
val page = MockBlockFactory.makeOnePageWithOneTextBlock(
|
||||
root = root,
|
||||
child = child
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
vm.onBlockFocusChanged(
|
||||
id = child,
|
||||
hasFocus = true
|
||||
)
|
||||
|
||||
val index = MockDataFactory.randomInt()
|
||||
|
||||
vm.onSplitLineEnterClicked(
|
||||
id = child,
|
||||
index = index
|
||||
)
|
||||
|
||||
verify(splitBlock, times(1)).invoke(
|
||||
scope = any(),
|
||||
params = eq(
|
||||
SplitBlock.Params(
|
||||
context = root,
|
||||
target = child,
|
||||
index = index
|
||||
)
|
||||
),
|
||||
onResult = any()
|
||||
)
|
||||
}
|
||||
|
||||
private fun simulateNormalPageOpeningFlow() {
|
||||
|
||||
val root = MockDataFactory.randomUuid()
|
||||
|
@ -2560,7 +2624,8 @@ class PageViewModelTest {
|
|||
updateTextColor = updateTextColor,
|
||||
updateLinkMarks = updateLinkMark,
|
||||
removeLinkMark = removeLinkMark,
|
||||
mergeBlocks = mergeBlocks
|
||||
mergeBlocks = mergeBlocks,
|
||||
splitBlock = splitBlock
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue