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

Editor | Feature | Simple Tables (#2427)

* Editor | Feature | Simple tables, prototype (#2325)

* core model

* table view model

* view holders

* table view holders layouts

* table adapters

* add table block to block adapter

* map core table model to view

* stub table block

* fixes

* fixed row hight

* update middleware

* fix

* add createTable command to data and middleware modules

* add createTable use case

* add table row & table column layouts to block model

* update create table use case

* add table create to orchestrator

* add create table item to slash menu

* delete stubbing

* update create table usecase

* update proto

* add create table use case to tests

* fix use case

* add stubs for table blocks

* set default row, column size

* create table use case di

* set id to stub blocks

* table view

* map table block to view + test

* layout types: table row, table column

* table block model + mapping

* remove legacy

* rename table to table view holder

* table holders + row adapter

* tableviewholder setup

* add block test to table view

* table block layout

* add table item to slash menu

* test fix

* api update

* add restriction duplicate

* render table block with empty and text cells

* row view use diff cells

* tests on mapping table block to views

* table text cells + empty cells

* table row adapter diff update

* use paragraph block in table cells

* fix tests

* table rows mapping

* table row, cell holders

* table cell, row adapters

* layouts update

* code style

* add fill table row use case

* text + click listeners

* listener type, table row empty cell

* merge fixes

* add fill table row intent to editor

* set focus to cell text

* remember focus on emty cell clicked

* fix problem when horizontal layout has limited width

* prevent crash on text cell focus

* legacy

* add table view library

* table block listener

* table block adapter

* table block holders

* table block layouts

* integrate table blocl

* fix tests

* fix table view dep

* use list of cells instead rows

* update tests

* code style

* code style

* clear text in cells when cell is null

* legacy

* legacy

* revert local lib

* fix lib version

* pr fixes

* pr fixes

* pr fixes

* pr fix

* pr fixes

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple tables, part 2  (#2400)

* move text input widget into parent layout

* table cells adapter diff util + tests

* cells diffutil should return payload model

* set column header to 0

* table block update test

* create diff util in table block adapter

* do not show corner view

* update listeners in holders

* text cell layout update

* update cell container to frame layout

* on update empty to text cell, check mode

* clear empty cells on bind

* update diffUtil + tests

* added column header item

* set column header items

* create and bind column header items

* use recyler with grid layout

* update table cell mapping + model

* remove tableview lib from core-ui

* add row_id + colum_id to cells

* fix tests

* import

* update table cells diff util

* update cell adapter + holders

* update mw

* add height, row height + width to cell model

* fix test

* delete test

* table block design

* set support touch helper for cell holder

* update cells payload logic

* table holder fixes

* fix test

* pr fixes

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple tables, cells as text views (#2411)

* move text input widget into parent layout

* table cells adapter diff util + tests

* cells diffutil should return payload model

* set column header to 0

* table block update test

* create diff util in table block adapter

* do not show corner view

* update listeners in holders

* text cell layout update

* update cell container to frame layout

* on update empty to text cell, check mode

* clear empty cells on bind

* update diffUtil + tests

* added column header item

* set column header items

* create and bind column header items

* use recyler with grid layout

* update table cell mapping + model

* remove tableview lib from core-ui

* add row_id + colum_id to cells

* fix tests

* import

* update table cells diff util

* update cell adapter + holders

* update mw

* add height, row height + width to cell model

* fix test

* delete test

* table block design

* set support touch helper for cell holder

* update cells payload logic

* table holder fixes

* fix test

* pr fixes

* text cell design

* use grid as layout manager and add payloads for table block

* added background and selection for table block

* table block selection logic

* update table cells diff util

* click listeners in table cells + save table id

* table cells listeners

* refactoring table cell as text view

* fix tests

* fix merge conflict

* pr fixes

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple tables, create from slash filter (#2415)

* DROID-126
slash item simple table design

* DROID-126
update slash item model

* DROID-126
added max row and column number

* DROID-126
create table with entered rows and columns

* DROID-126
update table item with rows and columns

* DROID-126
tests

* DROID-126
pr fix

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple tables, edit cell text value (#2413)

* DROID-172
set text value di

* DROID-172
set block text fragment + view model

* DROID-172
added input action logic for text blockls

* DROID-172
input action listener fix

* DROID-172
clicks + commands on set block text value screen

* DROID-172
code style fix

* DROID-172
pr fix

* DROID-172
pr fix

* DROID-172
fix

* DROID-172
input action fixes

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple tables, cell/column/row menu, part 1 (#2417)

* DROID-131
icons

* DROID-131
simple table widget

* DROID-131
simple table widget adapters

* DROID-131
simpla table widget models + delegate

* DROID-131
include widget in editor view model

* DROID-131
add widget to editor fragment + view model logic

* DROID-131
fix tests

* DROID-131
pr fixes

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple tables, empty cell clicked (#2420)

* DROID-114
empty cell clicked

* DROID-114
doc

* DROID-114
open cell value modal after fill table row command

* DROID-114
pr fixes

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple Tables, cells selection state (#2425)

* DROID-131
dismiss listener

* DROID-131
add cell borders to diff util

* DROID-131
apply borders to selected cell

* DROID-131
update cell settings model + render

* DROID-131
update cell listeners

* DROID-131
apply and dismiss cells borders

* DROID-131
apply and dismiss cell borders in view model

* DROID-131
clicks

* DROID-131
update tests

* DROID-131 click params update

* DROID-131 table click in mode select

* DROID-131 naming

* DROID-131 pr fix

Co-authored-by: konstantiniiv <ki@anytype.io>

* Editor | Feature | Simple Table, design (#2429)

* DROID-180 fixes

* DROID-180 table vertical divider

* DROID-180 table horizontal divider

* DROID-180 update table block holder

* DROID-180 refactoring

* DROID-180 divider fixes

* DROID-180 add space cell

* DROID-180 update table cell diff util

* DROID-180 table block adapter + holders

* DROID-180 table block design fix

* DROID-180 add space item to table

* DROID-180 fix

* DROID-180 add space cell to tests

* DROID-180 add offset to horizontal item divider

* DROID-180 vertical divider update

* DROID-180 add isHeader to table row

* DROID-180 delete legacy

* DROID-180 update cell background + isheader logic

* DROID-180 fix

* DROID-180 design fix

* DROID-180 pr fix

* DROID-180 back to list adapter

Co-authored-by: konstantiniiv <ki@anytype.io>

* ci

* fix tests

* ci off

Co-authored-by: konstantiniiv <ki@anytype.io>
This commit is contained in:
Konstantin Ivanov 2022-07-26 10:52:54 +02:00 committed by GitHub
parent 234de6d854
commit 743bb29730
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 3964 additions and 45 deletions

View file

@ -72,6 +72,8 @@ import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.status.ThreadStatusChannel
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
@ -86,6 +88,7 @@ import com.anytypeio.anytype.presentation.editor.editor.InternalDetailModificati
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.presentation.editor.editor.Proxy
import com.anytypeio.anytype.presentation.editor.editor.pattern.DefaultPatternMatcher
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.editor.template.DefaultEditorTemplateDelegate
@ -228,6 +231,15 @@ open class EditorTestSetup {
@Mock
lateinit var objectTypesProvider: ObjectTypesProvider
@Mock
lateinit var createTable: CreateTable
@Mock
lateinit var fillTableRow: FillTableRow
@Mock
lateinit var simpleTableDelegate: SimpleTableDelegate
val root: String = "rootId123"
private val urlBuilder by lazy {
@ -368,7 +380,9 @@ open class EditorTestSetup {
turnIntoStyle = turnIntoStyle,
updateBlocksMark = updateBlocksMark,
setObjectType = setObjectType,
createBookmarkBlock = createBookmarkBlock
createBookmarkBlock = createBookmarkBlock,
createTable = createTable,
fillTableRow = fillTableRow
),
createNewDocument = createNewDocument,
interceptThreadStatus = interceptThreadStatus,
@ -388,7 +402,8 @@ open class EditorTestSetup {
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
editorTemplateDelegate = editorTemplateDelegate,
createNewObject = createNewObject
createNewObject = createNewObject,
simpleTablesDelegate = simpleTableDelegate
)
}

View file

@ -294,6 +294,13 @@ class ComponentManager(
.build()
}
val setTextBlockValueComponent = DependentComponentMap { ctx ->
editorComponent
.get(ctx)
.setBlockTextValueComponent()
.build()
}
val createBookmarkSubComponent = Component {
main
.createBookmarkBuilder()

View file

@ -75,6 +75,8 @@ import com.anytypeio.anytype.domain.relations.AddFileToObject
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.status.ThreadStatusChannel
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
@ -90,6 +92,8 @@ import com.anytypeio.anytype.presentation.editor.editor.Interactor
import com.anytypeio.anytype.presentation.editor.editor.InternalDetailModificationManager
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.presentation.editor.editor.pattern.DefaultPatternMatcher
import com.anytypeio.anytype.presentation.editor.editor.table.DefaultSimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.editor.template.DefaultEditorTemplateDelegate
@ -147,6 +151,8 @@ interface EditorSubComponent {
fun objectAppearancePreviewLayoutComponent() : ObjectAppearancePreviewLayoutSubComponent.Builder
fun objectAppearanceCoverComponent() : ObjectAppearanceCoverSubComponent.Builder
fun objectAppearanceChooseDescription() : ObjectAppearanceChooseDescriptionSubComponent.Builder
fun setBlockTextValueComponent(): SetBlockTextValueSubComponent.Builder
}
@ -205,7 +211,8 @@ object EditorSessionModule {
setDocCoverImage: SetDocCoverImage,
setDocImageIcon: SetDocumentImageIcon,
editorTemplateDelegate: EditorTemplateDelegate,
createNewObject: CreateNewObject
createNewObject: CreateNewObject,
simpleTableDelegate: SimpleTableDelegate
): EditorViewModelFactory = EditorViewModelFactory(
openPage = openPage,
closeObject = closePage,
@ -237,7 +244,8 @@ object EditorSessionModule {
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
editorTemplateDelegate = editorTemplateDelegate,
createNewObject = createNewObject
createNewObject = createNewObject,
simpleTablesDelegate = simpleTableDelegate
)
@JvmStatic
@ -264,6 +272,12 @@ object EditorSessionModule {
applyTemplate = applyTemplate
)
@JvmStatic
@Provides
@PerScreen
fun provideSimpleTableDelegate(
) : SimpleTableDelegate = DefaultSimpleTableDelegate()
@JvmStatic
@Provides
fun provideDefaultBlockViewRenderer(
@ -326,6 +340,8 @@ object EditorSessionModule {
setupBookmark: SetupBookmark,
createBookmarkBlock: CreateBookmarkBlock,
turnIntoDocument: TurnIntoDocument,
createTable: CreateTable,
fillTableRow: FillTableRow,
setObjectType: SetObjectType,
matcher: DefaultPatternMatcher,
move: Move,
@ -373,7 +389,9 @@ object EditorSessionModule {
updateFields = updateFields,
turnIntoStyle = turnInto,
updateBlocksMark = updateBlocksMark,
setObjectType = setObjectType
setObjectType = setObjectType,
createTable = createTable,
fillTableRow = fillTableRow
)
}
@ -751,6 +769,24 @@ object EditorUseCaseModule {
repo: BlockRepository
): SetLinkAppearance = SetLinkAppearance(repo)
@JvmStatic
@Provides
@PerScreen
fun provideCreateTableUseCase(
repo: BlockRepository
): CreateTable = CreateTable(
repo = repo
)
@JvmStatic
@Provides
@PerScreen
fun provideTableRowFill(
repo: BlockRepository
): FillTableRow = FillTableRow(
repo = repo
)
@JvmStatic
@Provides
@PerScreen

View file

@ -0,0 +1,39 @@
package com.anytypeio.anytype.di.feature
import com.anytypeio.anytype.core_utils.di.scope.PerDialog
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.objects.block.SetBlockTextValueViewModel
import com.anytypeio.anytype.ui.editor.modals.SetBlockTextValueFragment
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(modules = [SetBlockTextValueModule::class])
@PerDialog
interface SetBlockTextValueSubComponent {
@Subcomponent.Builder
interface Builder {
fun module(model: SetBlockTextValueModule): Builder
fun build(): SetBlockTextValueSubComponent
}
fun inject(fragment: SetBlockTextValueFragment)
}
@Module
object SetBlockTextValueModule {
@JvmStatic
@Provides
@PerDialog
fun provideViewModelFactory(
updateText: UpdateText,
storage: Editor.Storage
): SetBlockTextValueViewModel.Factory =
SetBlockTextValueViewModel.Factory(
storage = storage,
updateText = updateText
)
}

View file

@ -109,6 +109,7 @@ import com.anytypeio.anytype.presentation.editor.editor.control.ControlPanelStat
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.editor.sam.ScrollAndMoveTarget
import com.anytypeio.anytype.presentation.editor.editor.sam.ScrollAndMoveTargetDescriptor
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetViewState
import com.anytypeio.anytype.presentation.editor.markup.MarkupColorView
import com.anytypeio.anytype.presentation.editor.model.EditorFooter
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateViewState
@ -123,6 +124,7 @@ import com.anytypeio.anytype.ui.editor.modals.IconPickerFragmentBase
import com.anytypeio.anytype.ui.editor.modals.SelectProgrammingLanguageFragment
import com.anytypeio.anytype.ui.editor.modals.SelectProgrammingLanguageReceiver
import com.anytypeio.anytype.ui.editor.modals.SetLinkFragment
import com.anytypeio.anytype.ui.editor.modals.SetBlockTextValueFragment
import com.anytypeio.anytype.ui.editor.modals.TextBlockIconPickerFragment
import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuBaseFragment
import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuBaseFragment.DocumentMenuActionReceiver
@ -214,6 +216,9 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
binding.typeHasTemplateToolbar.id -> {
vm.onTypeHasTemplateToolbarHidden()
}
binding.simpleTableWidget.id -> {
vm.onHideSimpleTableWidget()
}
}
}
}
@ -472,6 +477,20 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
}
}
}
jobs += subscribe(vm.simpleTablesViewState) { state ->
val behavior = BottomSheetBehavior.from(binding.simpleTableWidget)
when (state) {
is SimpleTableWidgetViewState.Active -> {
binding.simpleTableWidget.onStateChanged(state = state.state)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.addBottomSheetCallback(onHideBottomSheetCallback)
}
SimpleTableWidgetViewState.Idle -> {
behavior.removeBottomSheetCallback(onHideBottomSheetCallback)
behavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}
}
vm.onStart(id = extractDocumentId())
super.onStart()
@ -1112,6 +1131,19 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
val margin = resources.getDimensionPixelSize(R.dimen.default_editor_item_offset)
lm.scrollToPositionWithOffset(command.pos, margin)
}
is Command.OpenSetBlockTextValueScreen -> {
val fr = SetBlockTextValueFragment.new(
ctx = command.ctx,
block = command.block,
table = command.table
).apply {
onDismissListener = {
vm.onSetBlockTextValueScreenDismiss()
hideKeyboard()
}
}
fr.show(childFragmentManager, null)
}
}
}
}
@ -1881,6 +1913,14 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
vm.onEnterSearchModeClicked()
}
override fun onSetTextBlockValue() {
vm.onSetTextBlockValue()
}
override fun onMentionClicked(target: Id) {
vm.onMentionClicked(target = target)
}
override fun onUndoRedoClicked() {
vm.onUndoRedoActionClicked()
}
@ -2073,4 +2113,6 @@ interface OnFragmentInteractionListener {
fun onSetObjectLink(id: Id)
fun onSetWebLink(uri: String)
fun onCreateObject(name: String)
fun onSetTextBlockValue()
fun onMentionClicked(target: Id)
}

View file

@ -0,0 +1,169 @@
package com.anytypeio.anytype.ui.editor.modals
import android.content.DialogInterface
import android.os.Bundle
import android.view.DragEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.features.editor.BlockAdapter
import com.anytypeio.anytype.core_ui.features.editor.DragAndDropAdapterDelegate
import com.anytypeio.anytype.core_ui.features.editor.marks
import com.anytypeio.anytype.core_ui.tools.ClipboardInterceptor
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.withParent
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetImeOffsetFragment
import com.anytypeio.anytype.databinding.FragmentSetBlockTextValueBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.ext.extractMarks
import com.anytypeio.anytype.presentation.objects.block.SetBlockTextValueViewModel
import com.anytypeio.anytype.ui.editor.OnFragmentInteractionListener
import java.util.*
import javax.inject.Inject
class SetBlockTextValueFragment :
BaseBottomSheetImeOffsetFragment<FragmentSetBlockTextValueBinding>(), ClipboardInterceptor,
View.OnDragListener {
private val vm: SetBlockTextValueViewModel by viewModels { factory }
var onDismissListener: (() -> Unit)? = null
@Inject
lateinit var factory: SetBlockTextValueViewModel.Factory
private val blockAdapter by lazy {
BlockAdapter(
restore = LinkedList(),
initialBlock = mutableListOf(),
onTextChanged = { _, _ -> },
onTitleBlockTextChanged = { _, _ -> },
onSelectionChanged = { _, _ -> },
onCheckboxClicked = {},
onTitleCheckboxClicked = {},
onFocusChanged = { _, _ -> },
onSplitLineEnterClicked = { id, editable, _ ->
vm.onKeyboardDoneKeyClicked(
ctx = ctx,
tableId = table,
targetId = id,
text = editable.toString(),
marks = editable.marks(),
markup = editable.extractMarks()
)
},
onSplitDescription = { _, _, _ -> },
onEmptyBlockBackspaceClicked = {},
onNonEmptyBlockBackspaceClicked = { _, _ -> },
onTextInputClicked = {},
onPageIconClicked = {},
onCoverClicked = {},
onTogglePlaceholderClicked = {},
onToggleClicked = {},
onTitleTextInputClicked = {},
onTextBlockTextChanged = {},
onClickListener = vm::onClickListener,
onMentionEvent = {},
onSlashEvent = {},
onBackPressedCallback = { false },
onKeyPressedEvent = {},
onDragAndDropTrigger = { _, _ -> false },
lifecycle = lifecycle,
dragAndDropSelector = DragAndDropAdapterDelegate(),
clipboardInterceptor = this,
onDragListener = this
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recycler.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = blockAdapter
}
}
override fun onStart() {
with(lifecycleScope) {
jobs += subscribe(vm.state) { render(it) }
jobs += subscribe(vm.toasts) { toast(it) }
}
vm.onStart(tableId = table, blockId = block)
super.onStart()
}
override fun onStop() {
vm.onStop()
super.onStop()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismissListener?.invoke()
}
private fun render(state: SetBlockTextValueViewModel.ViewState) {
when (state) {
SetBlockTextValueViewModel.ViewState.Exit -> {
withParent<OnFragmentInteractionListener> { onSetTextBlockValue() }
dismiss()
}
SetBlockTextValueViewModel.ViewState.Loading -> {
}
is SetBlockTextValueViewModel.ViewState.Success -> {
blockAdapter.updateWithDiffUtil(state.data)
}
is SetBlockTextValueViewModel.ViewState.OnMention -> {
withParent<OnFragmentInteractionListener> { onMentionClicked(state.targetId) }
dismiss()
}
}
}
override fun injectDependencies() {
componentManager().setTextBlockValueComponent.get(ctx).inject(this)
}
override fun releaseDependencies() {
componentManager().setTextBlockValueComponent.release(ctx)
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentSetBlockTextValueBinding {
return FragmentSetBlockTextValueBinding.inflate(inflater, container, false)
}
override fun onClipboardAction(action: ClipboardInterceptor.Action) {}
override fun onUrlPasted(url: Url) {}
override fun onDrag(v: View?, event: DragEvent?) = false
private val ctx: String get() = argString(CTX_KEY)
private val block: String get() = argString(BLOCK_ID_KEY)
private val table: String get() = argString(TABLE_ID_KEY)
companion object {
const val CTX_KEY = "arg.editor.block.text.value.ctx"
const val TABLE_ID_KEY = "arg.editor.block.text.value.table.id"
const val BLOCK_ID_KEY = "arg.editor.block.text.value.block.id"
const val DEFAULT_IME_ACTION = EditorInfo.IME_ACTION_DONE
fun new(ctx: Id, table: Id, block: Id) = SetBlockTextValueFragment().apply {
arguments = bundleOf(
CTX_KEY to ctx,
TABLE_ID_KEY to table,
BLOCK_ID_KEY to block
)
}
}
}

View file

@ -279,6 +279,18 @@
app:cardUseCompatPadding="true"
app:layout_behavior="@string/bottom_sheet_behavior"/>
<com.anytypeio.anytype.core_ui.widgets.toolbar.table.SimpleTableSettingWidget
android:id="@+id/simpleTableWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:behavior_skipCollapsed="true"
app:cardBackgroundColor="@color/background_secondary"
app:cardCornerRadius="16dp"
app:cardElevation="6dp"
app:cardUseCompatPadding="true"
app:layout_behavior="@string/bottom_sheet_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<View

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/sheet_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp"
android:src="@drawable/sheet_top" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="13dp"
android:layout_marginBottom="32dp" />
</LinearLayout>

View file

@ -18,6 +18,7 @@ buildscript {
ext.test_runner = 'androidx.test.runner.AndroidJUnitRunner'
repositories {
mavenLocal()
google()
mavenCentral()
}

View file

@ -343,6 +343,10 @@ data class Block(
data class Latex(val latex: String) : Content()
object TableOfContents : Content()
object Unsupported : Content()
object Table: Content()
data class TableRow(val isHeader: Boolean): Content()
object TableColumn: Content()
}
/**

View file

@ -45,6 +45,7 @@ import com.anytypeio.anytype.core_ui.databinding.ItemBlockRelationFileBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockRelationObjectBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockRelationPlaceholderBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockRelationTagBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTableBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTextBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleProfileBinding
@ -63,6 +64,7 @@ import com.anytypeio.anytype.core_ui.features.editor.holders.error.PictureError
import com.anytypeio.anytype.core_ui.features.editor.holders.error.VideoError
import com.anytypeio.anytype.core_ui.features.editor.holders.ext.setup
import com.anytypeio.anytype.core_ui.features.editor.holders.ext.setupPlaceholder
import com.anytypeio.anytype.core_ui.features.editor.holders.ext.toIMECode
import com.anytypeio.anytype.core_ui.features.editor.holders.media.Bookmark
import com.anytypeio.anytype.core_ui.features.editor.holders.media.File
import com.anytypeio.anytype.core_ui.features.editor.holders.media.Picture
@ -85,6 +87,7 @@ import com.anytypeio.anytype.core_ui.features.editor.holders.placeholders.Pictur
import com.anytypeio.anytype.core_ui.features.editor.holders.placeholders.VideoPlaceholder
import com.anytypeio.anytype.core_ui.features.editor.holders.relations.FeaturedRelationListViewHolder
import com.anytypeio.anytype.core_ui.features.editor.holders.relations.RelationViewHolder
import com.anytypeio.anytype.core_ui.features.table.holders.TableBlockHolder
import com.anytypeio.anytype.core_ui.features.editor.holders.text.Bulleted
import com.anytypeio.anytype.core_ui.features.editor.holders.text.Callout
import com.anytypeio.anytype.core_ui.features.editor.holders.text.Checkbox
@ -151,6 +154,7 @@ import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_RELATION_PLACEHOLDER
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_RELATION_STATUS
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_RELATION_TAGS
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TABLE
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TITLE
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TOC
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TODO_TITLE
@ -459,7 +463,7 @@ class BlockAdapter(
}
}
setOnEditorActionListener { v, actionId, _ ->
if (actionId == TextInputWidget.TEXT_INPUT_WIDGET_ACTION_GO) {
if (actionId == BlockView.InputAction.NewLine.toIMECode()) {
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
onSplitDescription(
@ -709,6 +713,10 @@ class BlockAdapter(
HOLDER_UNSUPPORTED -> Unsupported(
ItemBlockUnsupportedBinding.inflate(inflater, parent, false)
)
HOLDER_TABLE -> TableBlockHolder(
ItemBlockTableBinding.inflate(inflater, parent, false),
clickListener = onClickListener
)
else -> throw IllegalStateException("Unexpected view type: $viewType")
}
@ -1097,6 +1105,12 @@ class BlockAdapter(
item = blocks[position] as BlockView.TableOfContents
)
}
is TableBlockHolder -> {
holder.processChangePayload(
payloads = payloads.typeOf(),
item = blocks[position] as BlockView.Table
)
}
else -> throw IllegalStateException("Unexpected view holder: $holder")
}
checkIfDecorationChanged(holder, payloads.typeOf(), position)
@ -1521,6 +1535,9 @@ class BlockAdapter(
is Unsupported -> {
holder.bind(item = blocks[position] as BlockView.Unsupported)
}
is TableBlockHolder -> {
holder.bind(item = blocks[position] as BlockView.Table)
}
}
if (holder is Text) {

View file

@ -1,11 +1,13 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.ext
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.updatePadding
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.features.editor.BlockAdapter
import com.anytypeio.anytype.core_ui.features.editor.EditorTouchProcessor
import com.anytypeio.anytype.core_ui.features.editor.holders.relations.RelationViewHolder
import com.anytypeio.anytype.core_ui.features.table.holders.TableBlockHolder
import com.anytypeio.anytype.core_utils.ext.dimen
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
@ -56,4 +58,9 @@ fun RelationViewHolder.setupPlaceholder(adapter: BlockAdapter): RelationViewHold
}
}
return this
}
fun BlockView.InputAction.toIMECode(): Int = when (this) {
BlockView.InputAction.Done -> EditorInfo.IME_ACTION_DONE
BlockView.InputAction.NewLine -> EditorInfo.IME_ACTION_GO
}

View file

@ -30,6 +30,7 @@ abstract class Text(
) {
indentize(item)
select(item)
inputAction(item)
if (item.mode == BlockView.Mode.READ) {
enableReadMode()
@ -143,5 +144,9 @@ abstract class Text(
select(item)
}
fun inputAction(item: BlockView.TextBlockProps) {
content.setInputAction(item.inputAction)
}
override fun getDefaultTextColor(): Int = defTextColor
}

View file

@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemSlashWidgetStyleBinding
import com.anytypeio.anytype.core_utils.ext.gone
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem
class OtherMenuHolder(
@ -27,6 +28,23 @@ class OtherMenuHolder(
ivIcon.setImageResource(R.drawable.ic_slash_toc)
tvSubtitle.gone()
}
is SlashItem.Other.Table -> {
val rowCount = item.rowCount
val columnCount = item.columnCount
if (rowCount != null && columnCount != null) {
tvTitle.text = binding.root.resources.getString(
R.string.slash_widgth_other_simple_table_rows_columns_count,
rowCount,
columnCount
)
tvSubtitle.visible()
tvSubtitle.setText(R.string.slash_widget_other_simple_table_subtitle)
} else {
tvTitle.setText(R.string.slash_widget_other_simple_table)
tvSubtitle.gone()
}
ivIcon.setImageResource(R.drawable.ic_slash_simple_tables)
}
}
}
}

View file

@ -0,0 +1,117 @@
package com.anytypeio.anytype.core_ui.features.table
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTableRowItemBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTableSpaceBinding
import com.anytypeio.anytype.core_ui.features.table.holders.TableCellHolder
import com.anytypeio.anytype.core_utils.ext.typeOf
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
class TableBlockAdapter(
differ: TableCellsDiffUtil,
private val clickListener: (ListenerType) -> Unit
) : ListAdapter<BlockView.Table.Cell, TableCellHolder>(differ) {
private var tableBlockId = ""
fun setTableBlockId(id: Id) {
tableBlockId = id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TableCellHolder {
when (viewType) {
TYPE_CELL -> {
val binding = ItemBlockTableRowItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return TableCellHolder.TableTextCellHolder(
context = parent.context,
binding = binding
).apply {
textContent.setOnClickListener {
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
onCellClicked(getItem(pos))
}
}
editorTouchProcessor.onLongClick = {
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
clickListener(ListenerType.LongClick(tableBlockId))
}
}
}
}
TYPE_SPACE -> {
val binding = ItemBlockTableSpaceBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return TableCellHolder.TableSpaceHolder(binding)
}
else -> throw UnsupportedOperationException("wrong viewtype:$viewType")
}
}
private fun onCellClicked(item: BlockView.Table.Cell) {
when (item) {
is BlockView.Table.Cell.Empty -> clickListener(
ListenerType.TableEmptyCell(
cellId = item.getId(),
rowId = item.rowId,
tableId = tableBlockId
)
)
is BlockView.Table.Cell.Text ->
clickListener(
ListenerType.TableTextCell(
tableId = tableBlockId,
cellId = item.block.id
)
)
BlockView.Table.Cell.Space -> {}
}
}
override fun onBindViewHolder(holder: TableCellHolder, position: Int) {
if (holder is TableCellHolder.TableTextCellHolder) {
holder.bind(getItem(position))
}
}
override fun onBindViewHolder(
holder: TableCellHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
} else {
if (holder is TableCellHolder.TableTextCellHolder) {
holder.processChangePayload(
payloads = payloads.typeOf<TableCellsDiffUtil.Payload>().first(),
cell = getItem(position)
)
}
}
}
override fun getItemViewType(position: Int): Int = when (getItem(position)) {
is BlockView.Table.Cell.Empty -> TYPE_CELL
is BlockView.Table.Cell.Text -> TYPE_CELL
BlockView.Table.Cell.Space -> TYPE_SPACE
}
companion object {
const val TYPE_CELL = 1
const val TYPE_SPACE = 2
}
}

View file

@ -0,0 +1,122 @@
package com.anytypeio.anytype.core_ui.features.table
import androidx.recyclerview.widget.DiffUtil
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
object TableCellsDiffUtil : DiffUtil.ItemCallback<BlockView.Table.Cell>() {
override fun areItemsTheSame(
oldItem: BlockView.Table.Cell,
newItem: BlockView.Table.Cell
): Boolean {
if (oldItem is BlockView.Table.Cell.Empty && newItem is BlockView.Table.Cell.Empty) {
return oldItem.rowId == newItem.rowId && oldItem.columnId == newItem.columnId
}
if (oldItem is BlockView.Table.Cell.Empty && newItem is BlockView.Table.Cell.Text) {
return oldItem.rowId == newItem.rowId && oldItem.columnId == newItem.columnId
}
if (oldItem is BlockView.Table.Cell.Text && newItem is BlockView.Table.Cell.Text) {
return oldItem.rowId == newItem.rowId && oldItem.columnId == newItem.columnId
}
return false
}
override fun areContentsTheSame(
oldItem: BlockView.Table.Cell,
newItem: BlockView.Table.Cell
): Boolean {
if (oldItem is BlockView.Table.Cell.Empty && newItem is BlockView.Table.Cell.Empty) {
return oldItem.settings == newItem.settings
}
if (oldItem is BlockView.Table.Cell.Empty && newItem is BlockView.Table.Cell.Text) {
return false
}
if (oldItem is BlockView.Table.Cell.Text && newItem is BlockView.Table.Cell.Text) {
return oldItem.block == newItem.block && oldItem.settings == newItem.settings
}
return false
}
override fun getChangePayload(
oldItem: BlockView.Table.Cell,
newItem: BlockView.Table.Cell
): Any? {
val changes = mutableListOf<Int>()
var oldSettings: BlockView.Table.CellSettings? = null
var newSettings: BlockView.Table.CellSettings? = null
if (oldItem is BlockView.Table.Cell.Empty && newItem is BlockView.Table.Cell.Empty) {
oldSettings = oldItem.settings
newSettings = newItem.settings
}
if (oldItem is BlockView.Table.Cell.Empty && newItem is BlockView.Table.Cell.Text) {
oldSettings = oldItem.settings
newSettings = newItem.settings
}
if (oldItem is BlockView.Table.Cell.Text && newItem is BlockView.Table.Cell.Text) {
val oldBlock = oldItem.block
val newBlock = newItem.block
oldSettings = oldItem.settings
newSettings = newItem.settings
if (newBlock.text != oldBlock.text) {
changes.add(TEXT_CHANGED)
}
if (newBlock.color != oldBlock.color) {
changes.add(TEXT_COLOR_CHANGED)
}
if (newBlock.backgroundColor != oldBlock.backgroundColor) {
changes.add(BACKGROUND_COLOR_CHANGED)
}
if (newBlock.marks != oldBlock.marks) {
changes.add(MARKUP_CHANGED)
}
if (newBlock.alignment != oldBlock.alignment) {
changes.add(ALIGN_CHANGED)
}
}
if (oldSettings != null && newSettings != null) {
if (oldSettings.width != newSettings.width) {
changes.add(SETTING_WIDTH_CHANGED)
}
if (oldSettings.left != newSettings.left
|| oldSettings.top != newSettings.top
|| oldSettings.right != newSettings.right
|| oldSettings.bottom != newSettings.bottom
) {
changes.add(SETTING_BORDER_CHANGED)
}
if (oldSettings.isHeader != newSettings.isHeader) {
changes.add(BACKGROUND_COLOR_CHANGED)
}
}
return if (changes.isEmpty()) {
super.getChangePayload(oldItem, newItem)
} else {
Payload(changes)
}
}
data class Payload(
val changes: List<Int>
) {
val isBordersChanged: Boolean = changes.contains(SETTING_BORDER_CHANGED)
val isWidthChanged: Boolean = changes.contains(SETTING_WIDTH_CHANGED)
val isTextChanged = changes.contains(TEXT_CHANGED)
val isBackgroundChanged = changes.contains(BACKGROUND_COLOR_CHANGED)
val isTextColorChanged = changes.contains(TEXT_COLOR_CHANGED)
val isMarkupChanged = changes.contains(MARKUP_CHANGED)
val isAlignChanged = changes.contains(ALIGN_CHANGED)
}
const val TEXT_CHANGED = 1
const val TEXT_COLOR_CHANGED = 2
const val BACKGROUND_COLOR_CHANGED = 3
const val MARKUP_CHANGED = 4
const val ALIGN_CHANGED = 5
const val SETTING_WIDTH_CHANGED = 6
const val SETTING_BORDER_CHANGED = 7
}

View file

@ -0,0 +1,81 @@
package com.anytypeio.anytype.core_ui.features.table.holders
import android.widget.FrameLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTableBinding
import com.anytypeio.anytype.core_ui.extensions.drawable
import com.anytypeio.anytype.core_ui.extensions.setBlockBackgroundColor
import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil
import com.anytypeio.anytype.core_ui.features.editor.BlockViewHolder
import com.anytypeio.anytype.core_ui.features.table.TableBlockAdapter
import com.anytypeio.anytype.core_ui.features.table.TableCellsDiffUtil
import com.anytypeio.anytype.core_ui.layout.TableHorizontalItemDivider
import com.anytypeio.anytype.core_ui.layout.TableVerticalItemDivider
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
class TableBlockHolder(
binding: ItemBlockTableBinding,
clickListener: (ListenerType) -> Unit
) : BlockViewHolder(binding.root) {
val root: FrameLayout = binding.root
val recycler: RecyclerView = binding.recyclerTable
private val selected = binding.selected
private val tableAdapter = TableBlockAdapter(
differ = TableCellsDiffUtil,
clickListener = clickListener
)
private val lm = GridLayoutManager(itemView.context, 1, GridLayoutManager.HORIZONTAL, false)
private val mSpanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (recycler.adapter?.getItemViewType(position)) {
TableBlockAdapter.TYPE_CELL -> 1
else -> lm.spanCount
}
}
}
init {
val drawable = itemView.context.drawable(R.drawable.divider_dv_grid)
val verticalDecorator = TableVerticalItemDivider(drawable)
val horizontalDecorator = TableHorizontalItemDivider(drawable)
recycler.apply {
layoutManager = lm
lm.spanSizeLookup = mSpanSizeLookup
adapter = tableAdapter
addItemDecoration(verticalDecorator)
addItemDecoration(horizontalDecorator)
}
}
fun bind(item: BlockView.Table) {
selected.isSelected = item.isSelected
lm.spanCount = item.rowCount
tableAdapter.setTableBlockId(item.id)
tableAdapter.submitList(item.cells)
}
fun processChangePayload(
payloads: List<BlockViewDiffUtil.Payload>,
item: BlockView.Table
) {
payloads.forEach { payload ->
if (payload.changes.contains(BlockViewDiffUtil.SELECTION_CHANGED)) {
selected.isSelected = item.isSelected
}
if (payload.changes.contains(BlockViewDiffUtil.BACKGROUND_COLOR_CHANGED)) {
applyBackground(item.backgroundColor)
}
}
}
private fun applyBackground(background: String?) {
root.setBlockBackgroundColor(background)
}
}

View file

@ -0,0 +1,233 @@
package com.anytypeio.anytype.core_ui.features.table.holders
import android.content.Context
import android.view.Gravity
import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.toSpannable
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTableRowItemBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockTableSpaceBinding
import com.anytypeio.anytype.core_ui.extensions.resolveThemedTextColor
import com.anytypeio.anytype.core_ui.extensions.veryLight
import com.anytypeio.anytype.core_ui.features.editor.EditorTouchProcessor
import com.anytypeio.anytype.core_ui.features.editor.SupportCustomTouchProcessor
import com.anytypeio.anytype.core_ui.features.table.TableCellsDiffUtil
import com.anytypeio.anytype.core_utils.ext.invisible
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
import com.anytypeio.anytype.presentation.editor.editor.model.Alignment
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
sealed class TableCellHolder(view: View) : RecyclerView.ViewHolder(view) {
class TableTextCellHolder(context: Context, binding: ItemBlockTableRowItemBinding) :
TableCellHolder(binding.root), SupportCustomTouchProcessor {
val root: View = binding.root
val textContent: AppCompatTextView = binding.textContent
val selection: View = binding.selection
private val defTextColor: Int = itemView.resources.getColor(R.color.text_primary, null)
private val mentionIconSize =
itemView.resources.getDimensionPixelSize(R.dimen.mention_span_image_size_default)
private val mentionIconPadding =
itemView.resources.getDimensionPixelSize(R.dimen.mention_span_image_padding_default)
private val mentionCheckedIcon =
ContextCompat.getDrawable(context, R.drawable.ic_task_0_text_16)
private val mentionUncheckedIcon =
ContextCompat.getDrawable(context, R.drawable.ic_task_0_text_16)
private val mentionInitialsSize =
itemView.resources.getDimension(R.dimen.mention_span_initials_size_default)
override val editorTouchProcessor = EditorTouchProcessor(
fallback = { e -> itemView.onTouchEvent(e) })
init {
textContent.setOnTouchListener { v, e -> editorTouchProcessor.process(v, e) }
}
fun bind(
cell: BlockView.Table.Cell
) {
when (cell) {
is BlockView.Table.Cell.Empty -> {
setBorders(cell.settings)
textContent.text = null
setBackground(null, cell.settings)
}
is BlockView.Table.Cell.Text -> {
setBorders(cell.settings)
setBlockText(
text = cell.block.text,
markup = cell.block,
color = cell.block.color
)
setTextColor(cell.block.color)
setBackground(cell.block.backgroundColor, cell.settings)
setAlignment(cell.block.alignment)
}
}
}
fun processChangePayload(
payloads: TableCellsDiffUtil.Payload,
cell: BlockView.Table.Cell
) {
if (payloads.isBordersChanged) {
when (cell) {
is BlockView.Table.Cell.Empty -> setBorders(cell.settings)
is BlockView.Table.Cell.Text -> setBorders(cell.settings)
BlockView.Table.Cell.Space -> {}
}
}
if (cell is BlockView.Table.Cell.Text) {
processChangePayload(payloads, cell.block, cell.settings)
}
}
fun processChangePayload(
payloads: TableCellsDiffUtil.Payload,
block: BlockView.Text.Paragraph,
settings: BlockView.Table.CellSettings
) {
if (payloads.isTextChanged) {
setBlockText(
text = block.text,
markup = block,
color = block.color
)
}
if (payloads.isTextColorChanged) {
setTextColor(block.color)
}
if (payloads.isBackgroundChanged) {
setBackground(block.backgroundColor, settings)
}
if (payloads.isMarkupChanged) {
setBlockSpannableText(block, resolveTextBlockThemedColor(block.color))
}
if (payloads.isAlignChanged) {
setAlignment(block.alignment)
}
}
private fun setBlockText(
text: String,
markup: Markup,
color: String?
) {
when (markup.marks.isEmpty()) {
true -> textContent.text = text
false -> setBlockSpannableText(markup, resolveTextBlockThemedColor(color))
}
}
private fun setBlockSpannableText(
markup: Markup,
color: Int
) {
when (markup.marks.any { it is Markup.Mark.Mention || it is Markup.Mark.Object }) {
true -> setSpannableWithMention(markup, color)
false -> setSpannable(markup, color)
}
}
private fun setSpannable(markup: Markup, textColor: Int) {
textContent.setText(
markup.toSpannable(
textColor = textColor,
context = itemView.context
),
TextView.BufferType.SPANNABLE
)
}
private fun setSpannableWithMention(
markup: Markup,
textColor: Int
) {
textContent.setText(
markup.toSpannable(
textColor = textColor,
context = itemView.context,
mentionImageSize = mentionIconSize,
mentionImagePadding = mentionIconPadding,
mentionCheckedIcon = mentionCheckedIcon,
mentionUncheckedIcon = mentionUncheckedIcon,
mentionInitialsSize = mentionInitialsSize
),
TextView.BufferType.SPANNABLE
)
}
private fun setAlignment(alignment: Alignment?) {
if (alignment != null) {
textContent.gravity = when (alignment) {
Alignment.START -> Gravity.START
Alignment.CENTER -> Gravity.CENTER
Alignment.END -> Gravity.END
}
} else {
textContent.gravity = Gravity.START
}
}
private fun setTextColor(color: String?) {
textContent.setTextColor(resolveTextBlockThemedColor(color))
}
private fun setBackground(background: String?, settings: BlockView.Table.CellSettings) {
setTableCellBackgroundColor(background, root, settings.isHeader)
}
/**
* @param [color] color code, @see [ThemeColor]
*/
private fun setTableCellBackgroundColor(color: String?, view: View, isHeader: Boolean) {
if (!color.isNullOrEmpty()) {
val value = ThemeColor.values().find { value -> value.code == color }
if (value != null && value != ThemeColor.DEFAULT) {
view.setBackgroundColor(view.resources.veryLight(value, 0))
} else {
setTableCellHeaderOrEmptyBackground(view, isHeader)
}
} else {
setTableCellHeaderOrEmptyBackground(view, isHeader)
}
}
private fun setTableCellHeaderOrEmptyBackground(view: View, isHeader: Boolean) {
if (isHeader) {
root.setBackgroundColor(
itemView.resources.getColor(
R.color.table_row_header_background,
null
)
)
} else {
view.background = null
}
}
private fun resolveTextBlockThemedColor(color: String?): Int {
return itemView.context.resolveThemedTextColor(color, defTextColor)
}
private fun setBorders(settings: BlockView.Table.CellSettings) {
if (settings.isAllBordersApply()) {
selection.visible()
} else {
selection.invisible()
}
}
}
class TableSpaceHolder(binding: ItemBlockTableSpaceBinding) : TableCellHolder(binding.root)
}

View file

@ -0,0 +1,60 @@
package com.anytypeio.anytype.core_ui.layout
import android.graphics.drawable.Drawable
import androidx.recyclerview.widget.RecyclerView
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import kotlin.math.roundToInt
class TableHorizontalItemDivider(
private val drawable: Drawable
) : RecyclerView.ItemDecoration() {
override fun onDraw(
canvas: Canvas,
parent: RecyclerView,
state: RecyclerView.State
) {
canvas.save()
val top = 0
val bottom = parent.height
val childCount = parent.childCount
val itemCount = parent.adapter?.itemCount ?: 0
val rect = Rect()
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val position = parent.getChildLayoutPosition(child)
parent.layoutManager?.getDecoratedBoundsWithMargins(child, rect)
var right = rect.right + child.translationX.roundToInt()
var left = right - drawable.intrinsicWidth
if (position < itemCount - 1) {
drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas)
}
if (position == 0) {
right = child.left
left = right - drawable.intrinsicWidth
drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas)
}
}
canvas.restore()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildLayoutPosition(view)
outRect.right = drawable.intrinsicWidth
if (position == 0) {
outRect.left = drawable.intrinsicWidth
}
}
}

View file

@ -0,0 +1,60 @@
package com.anytypeio.anytype.core_ui.layout
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.roundToInt
class TableVerticalItemDivider(
private val drawable: Drawable
) : RecyclerView.ItemDecoration() {
override fun onDraw(
canvas: Canvas,
parent: RecyclerView,
state: RecyclerView.State
) {
canvas.save()
val spanCount = (parent.layoutManager as GridLayoutManager).spanCount
val childCount = parent.childCount
val itemCount = parent.adapter?.itemCount ?: 0
val rect = Rect()
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val position = parent.getChildLayoutPosition(child)
parent.getDecoratedBoundsWithMargins(child, rect)
var bottom = rect.bottom + child.translationY.roundToInt()
var top = bottom - drawable.intrinsicHeight
if (position < itemCount - 1) {
drawable.setBounds(rect.left, top, rect.right, bottom)
drawable.draw(canvas)
}
if (position.rem(spanCount) == 0 && position < itemCount - 1) {
bottom = child.top
top = bottom - drawable.intrinsicHeight
drawable.setBounds(rect.left, top, rect.right, bottom)
drawable.draw(canvas)
}
}
canvas.restore()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val spanCount = (parent.layoutManager as GridLayoutManager).spanCount
val itemCount = parent.adapter?.itemCount ?: 0
val position = parent.getChildLayoutPosition(view)
outRect.bottom = drawable.intrinsicHeight
if (position.rem(spanCount) == 0 && position < itemCount - 1) {
outRect.top = drawable.intrinsicHeight
}
}
}

View file

@ -21,6 +21,7 @@ import androidx.core.graphics.withTranslation
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.features.editor.EditorTouchProcessor
import com.anytypeio.anytype.core_ui.features.editor.holders.ext.toIMECode
import com.anytypeio.anytype.core_ui.tools.ClipboardInterceptor
import com.anytypeio.anytype.core_ui.tools.CustomBetterLinkMovementMethod
import com.anytypeio.anytype.core_ui.tools.DefaultTextWatcher
@ -31,6 +32,7 @@ import com.anytypeio.anytype.core_ui.widgets.text.highlight.HighlightAttributeRe
import com.anytypeio.anytype.core_ui.widgets.text.highlight.HighlightDrawer
import com.anytypeio.anytype.core_utils.ext.imm
import com.anytypeio.anytype.core_utils.ext.multilineIme
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import timber.log.Timber
class TextInputWidget : AppCompatEditText {
@ -42,7 +44,8 @@ class TextInputWidget : AppCompatEditText {
setOnLongClickListener { view -> view != null && !view.hasFocus() }
context.obtainStyledAttributes(attrs, R.styleable.TextInputWidget).apply {
ignoreDragAndDrop = getBoolean(R.styleable.TextInputWidget_ignoreDragAndDrop, false)
pasteAsPlainTextOnly = getBoolean(R.styleable.TextInputWidget_onlyPasteAsPlaneText, false)
pasteAsPlainTextOnly =
getBoolean(R.styleable.TextInputWidget_onlyPasteAsPlaneText, false)
recycle()
}
}
@ -85,12 +88,20 @@ class TextInputWidget : AppCompatEditText {
private var isSelectionWatcherBlocked = false
private var inputAction: BlockView.InputAction = DEFAULT_INPUT_WIDGET_ACTION
fun setInputAction(action: BlockView.InputAction) {
if (inputAction != action) {
inputAction = action
}
}
private fun setup() {
enableEditMode()
}
fun enableEditMode() {
multilineIme(action = TEXT_INPUT_WIDGET_ACTION_GO)
multilineIme(action = inputAction.toIMECode())
setTextIsSelectable(true)
}
@ -307,7 +318,7 @@ class TextInputWidget : AppCompatEditText {
onEnterClicked: (IntRange) -> Unit
) {
setOnEditorActionListener { v, actionId, _ ->
if (actionId == TEXT_INPUT_WIDGET_ACTION_GO) {
if (actionId == inputAction.toIMECode()) {
onEnterClicked.invoke(v.selectionStart..v.selectionEnd)
return@setOnEditorActionListener true
}
@ -336,6 +347,6 @@ class TextInputWidget : AppCompatEditText {
}
companion object {
const val TEXT_INPUT_WIDGET_ACTION_GO = EditorInfo.IME_ACTION_GO
val DEFAULT_INPUT_WIDGET_ACTION = BlockView.InputAction.NewLine
}
}

View file

@ -0,0 +1,76 @@
package com.anytypeio.anytype.core_ui.widgets.toolbar.table
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.databinding.ItemSimpleTableWidgetRecentBinding
class SimpleTableSettingAdapter(
private val cellAdapter: SimpleTableWidgetAdapter,
private val columnAdapter: SimpleTableWidgetAdapter,
private val rowAdapter: SimpleTableWidgetAdapter
) : RecyclerView.Adapter<SimpleTableSettingAdapter.VH>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemSimpleTableWidgetRecentBinding.inflate(inflater, parent, false)
return when (viewType) {
TYPE_CELL -> {
VH.Cell(binding).apply {
binding.recyclerCell.apply {
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
setHasFixedSize(true)
adapter = cellAdapter
}
}
}
TYPE_COLUMN -> {
VH.Column(binding).apply {
binding.recyclerCell.apply {
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
setHasFixedSize(true)
adapter = columnAdapter
}
}
}
TYPE_ROW -> {
VH.Row(binding).apply {
binding.recyclerCell.apply {
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
setHasFixedSize(true)
adapter = rowAdapter
}
}
}
else -> throw IllegalStateException("Unexpected view type: $viewType")
}
}
override fun onBindViewHolder(holder: VH, position: Int) {}
override fun getItemCount(): Int = DEFAULT_TABS_COUNT
override fun getItemViewType(position: Int): Int = when (position) {
0 -> TYPE_CELL
1 -> TYPE_COLUMN
2 -> TYPE_ROW
else -> throw IllegalStateException("Unexpected position: $position")
}
companion object {
const val DEFAULT_TABS_COUNT = 3
const val TYPE_CELL = 1
const val TYPE_COLUMN = 2
const val TYPE_ROW = 3
}
sealed class VH(view: View) : RecyclerView.ViewHolder(view) {
class Cell(val binding: ItemSimpleTableWidgetRecentBinding) : VH(binding.root)
class Column(val binding: ItemSimpleTableWidgetRecentBinding) : VH(binding.root)
class Row(val binding: ItemSimpleTableWidgetRecentBinding) : VH(binding.root)
}
}

View file

@ -0,0 +1,58 @@
package com.anytypeio.anytype.core_ui.widgets.toolbar.table
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.cardview.widget.CardView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.WidgetSimpleTableBinding
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetState
import com.google.android.material.tabs.TabLayoutMediator
class SimpleTableSettingWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : CardView(context, attrs, defStyleAttr) {
val binding = WidgetSimpleTableBinding.inflate(
LayoutInflater.from(context), this, true
)
private val cellAdapter = SimpleTableWidgetAdapter(items = listOf(),
onClick = { item -> })
private val columnAdapter = SimpleTableWidgetAdapter(items = listOf(),
onClick = { item -> })
private val rowAdapter = SimpleTableWidgetAdapter(items = listOf(),
onClick = { item -> })
private val pagerAdapter = SimpleTableSettingAdapter(
cellAdapter = cellAdapter,
columnAdapter = columnAdapter,
rowAdapter = rowAdapter
)
fun onStateChanged(state: SimpleTableWidgetState) {
when (state) {
is SimpleTableWidgetState.UpdateItems -> {
cellAdapter.update(state.cellItems)
columnAdapter.update(state.columnItems)
rowAdapter.update(state.rowItems)
}
SimpleTableWidgetState.Idle -> {}
}
}
init {
binding.viewpager.adapter = pagerAdapter
binding.viewpager.isUserInputEnabled = false
TabLayoutMediator(binding.tabsLayout, binding.viewpager) { tab, position ->
tab.text = when (position) {
0 -> context.getString(R.string.simple_tables_widget_tab_cell)
1 -> context.getString(R.string.simple_tables_widget_tab_column)
2 -> context.getString(R.string.simple_tables_widget_tab_row)
else -> throw IllegalStateException("Unexpected position: $position")
}
}.attach()
}
}

View file

@ -0,0 +1,119 @@
package com.anytypeio.anytype.core_ui.widgets.toolbar.table
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemSimpleTableActionBinding
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetItem
class SimpleTableWidgetAdapter(
private var items: List<SimpleTableWidgetItem>,
private val onClick: (SimpleTableWidgetItem) -> Unit
) :
RecyclerView.Adapter<SimpleTableWidgetAdapter.VH>() {
fun update(items: List<SimpleTableWidgetItem>) {
this.items = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(parent.context)
val holder = VH(
binding = ItemSimpleTableActionBinding.inflate(inflater, parent, false)
).apply {
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION)
onClick(items[pos])
}
return holder
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
class VH(binding: ItemSimpleTableActionBinding) :
RecyclerView.ViewHolder(binding.root) {
val icon = binding.icon
val title = binding.title
fun bind(item: SimpleTableWidgetItem) {
when (item) {
SimpleTableWidgetItem.Cell.ClearContents,
SimpleTableWidgetItem.Row.ClearContents,
SimpleTableWidgetItem.Column.ClearContents -> {
title.setText(R.string.simple_tables_widget_item_clear_contents)
icon.setImageResource(R.drawable.ic_slash_actions_clean_style)
}
SimpleTableWidgetItem.Cell.ClearStyle -> {
title.setText(R.string.simple_tables_widget_item_clear_style)
icon.setImageResource(R.drawable.ic_slash_actions_clean_style)
}
SimpleTableWidgetItem.Cell.Color,
SimpleTableWidgetItem.Column.Color,
SimpleTableWidgetItem.Row.Color -> {
title.setText(R.string.simple_tables_widget_item_clear_color)
icon.setImageResource(R.drawable.ic_style_toolbar_color)
}
SimpleTableWidgetItem.Cell.Style,
SimpleTableWidgetItem.Row.Style,
SimpleTableWidgetItem.Column.Style -> {
title.setText(R.string.simple_tables_widget_item_clear_style)
icon.setImageResource(R.drawable.ic_block_toolbar_block_style)
}
SimpleTableWidgetItem.Column.Delete,
SimpleTableWidgetItem.Row.Delete -> {
title.setText(R.string.toolbar_action_delete)
icon.setImageResource(R.drawable.ic_block_action_delete)
}
SimpleTableWidgetItem.Column.Duplicate,
SimpleTableWidgetItem.Row.Duplicate -> {
title.setText(R.string.toolbar_action_duplicate)
icon.setImageResource(R.drawable.ic_block_action_duplicate)
}
SimpleTableWidgetItem.Column.InsertLeft -> {
title.setText(R.string.simple_tables_widget_item_insert_left)
icon.setImageResource(R.drawable.ic_column_insert_left)
}
SimpleTableWidgetItem.Column.InsertRight -> {
title.setText(R.string.simple_tables_widget_item_insert_right)
icon.setImageResource(R.drawable.ic_column_insert_right)
}
SimpleTableWidgetItem.Column.MoveLeft -> {
title.setText(R.string.simple_tables_widget_item_move_left)
icon.setImageResource(R.drawable.ic_move_column_left)
}
SimpleTableWidgetItem.Column.MoveRight -> {
title.setText(R.string.simple_tables_widget_item_move_right)
icon.setImageResource(R.drawable.ic_move_column_right)
}
SimpleTableWidgetItem.Column.Sort,
SimpleTableWidgetItem.Row.Sort -> {
title.setText(R.string.sort)
icon.setImageResource(R.drawable.ic_action_sort)
}
SimpleTableWidgetItem.Row.InsertAbove -> {
title.setText(R.string.simple_tables_widget_item_insert_above)
icon.setImageResource(R.drawable.ic_add_row_above)
}
SimpleTableWidgetItem.Row.InsertBelow -> {
title.setText(R.string.simple_tables_widget_item_insert_below)
icon.setImageResource(R.drawable.ic_add_row_below)
}
SimpleTableWidgetItem.Row.MoveDown -> {
title.setText(R.string.simple_tables_widget_item_move_down)
icon.setImageResource(R.drawable.ic_move_row_down)
}
SimpleTableWidgetItem.Row.MoveUp -> {
title.setText(R.string.simple_tables_widget_item_move_up)
icon.setImageResource(R.drawable.ic_move_row_up)
}
}
}
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="@dimen/dp_2"
android:color="@color/amber_80" />
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_editor_table_cell_selected" android:state_selected="true" />
<item android:drawable="@drawable/bg_editor_table_cell" android:state_selected="false" />
</selector>

View file

@ -0,0 +1,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<stroke
android:width="2dp"
android:color="@color/amber_80" />
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M4.72,11.22C4.428,11.512 4.428,11.987 4.72,12.28C5.013,12.573 5.488,12.573 5.781,12.28L9.251,8.811V24.75H10.751V8.811L14.22,12.28C14.513,12.573 14.988,12.573 15.281,12.28C15.574,11.987 15.574,11.512 15.281,11.22L10.001,5.939L4.72,11.22Z"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M21.251,23.189V7.25H22.751V23.188L26.219,19.72C26.512,19.427 26.987,19.427 27.28,19.72C27.573,20.013 27.573,20.487 27.28,20.78L22,26.06L16.721,20.78C16.428,20.487 16.428,20.013 16.721,19.72C17.013,19.427 17.488,19.427 17.781,19.72L21.251,23.189Z"
android:fillColor="@color/glyph_active"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16.75,27.25C16.75,27.664 16.414,28 16,28C15.586,28 15.25,27.664 15.25,27.25L15.25,24.25L12.25,24.25C11.836,24.25 11.5,23.914 11.5,23.5C11.5,23.086 11.836,22.75 12.25,22.75L15.25,22.75L15.25,19.75C15.25,19.336 15.586,19 16,19C16.414,19 16.75,19.336 16.75,19.75L16.75,22.75L19.75,22.75C20.164,22.75 20.5,23.086 20.5,23.5C20.5,23.914 20.164,24.25 19.75,24.25L16.75,24.25L16.75,27.25ZM24,15.5L8,15.5C7.172,15.5 6.5,14.828 6.5,14L6.5,8C6.5,7.172 7.172,6.5 8,6.5L24,6.5C24.828,6.5 25.5,7.172 25.5,8L25.5,14C25.5,14.828 24.828,15.5 24,15.5ZM27,14C27,15.657 25.657,17 24,17L8,17C6.343,17 5,15.657 5,14L5,8C5,6.343 6.343,5 8,5L24,5C25.657,5 27,6.343 27,8L27,14Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M24,25.5L8,25.5C7.172,25.5 6.5,24.828 6.5,24L6.5,18C6.5,17.172 7.172,16.5 8,16.5L24,16.5C24.828,16.5 25.5,17.172 25.5,18L25.5,24C25.5,24.828 24.828,25.5 24,25.5ZM27,24C27,25.657 25.657,27 24,27L8,27C6.343,27 5,25.657 5,24L5,18C5,16.343 6.343,15 8,15L24,15C25.657,15 27,16.343 27,18L27,24ZM16.75,12.25C16.75,12.664 16.414,13 16,13C15.586,13 15.25,12.664 15.25,12.25L15.25,9.25L12.25,9.25C11.836,9.25 11.5,8.914 11.5,8.5C11.5,8.086 11.836,7.75 12.25,7.75L15.25,7.75L15.25,4.75C15.25,4.336 15.586,4 16,4C16.414,4 16.75,4.336 16.75,4.75L16.75,7.75L19.75,7.75C20.164,7.75 20.5,8.086 20.5,8.5C20.5,8.914 20.164,9.25 19.75,9.25L16.75,9.25L16.75,12.25Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="17dp"
android:viewportWidth="15"
android:viewportHeight="17">
<path
android:pathData="M7.5,2.5L7.5,2.5A1,1 0,0 1,8.5 3.5L8.5,3.5A1,1 0,0 1,7.5 4.5L7.5,4.5A1,1 0,0 1,6.5 3.5L6.5,3.5A1,1 0,0 1,7.5 2.5z"
android:fillColor="@color/gray"/>
<path
android:pathData="M7.5,7.5L7.5,7.5A1,1 0,0 1,8.5 8.5L8.5,8.5A1,1 0,0 1,7.5 9.5L7.5,9.5A1,1 0,0 1,6.5 8.5L6.5,8.5A1,1 0,0 1,7.5 7.5z"
android:fillColor="@color/gray"/>
<path
android:pathData="M7.5,12.5L7.5,12.5A1,1 0,0 1,8.5 13.5L8.5,13.5A1,1 0,0 1,7.5 14.5L7.5,14.5A1,1 0,0 1,6.5 13.5L6.5,13.5A1,1 0,0 1,7.5 12.5z"
android:fillColor="@color/gray"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M6.5,24L6.5,8C6.5,7.172 7.172,6.5 8,6.5L14,6.5C14.828,6.5 15.5,7.172 15.5,8L15.5,24C15.5,24.828 14.828,25.5 14,25.5L8,25.5C7.172,25.5 6.5,24.828 6.5,24ZM8,27C6.343,27 5,25.657 5,24L5,8C5,6.343 6.343,5 8,5L14,5C15.657,5 17,6.343 17,8L17,24C17,25.657 15.657,27 14,27L8,27ZM19.75,16.75C19.336,16.75 19,16.414 19,16C19,15.586 19.336,15.25 19.75,15.25L22.75,15.25L22.75,12.25C22.75,11.836 23.086,11.5 23.5,11.5C23.914,11.5 24.25,11.836 24.25,12.25L24.25,15.25L27.25,15.25C27.664,15.25 28,15.586 28,16C28,16.414 27.664,16.75 27.25,16.75L24.25,16.75L24.25,19.75C24.25,20.164 23.914,20.5 23.5,20.5C23.086,20.5 22.75,20.164 22.75,19.75L22.75,16.75L19.75,16.75Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M3.75,16.75C3.336,16.75 3,16.414 3,16C3,15.586 3.336,15.25 3.75,15.25L6.75,15.25L6.75,12.25C6.75,11.836 7.086,11.5 7.5,11.5C7.914,11.5 8.25,11.836 8.25,12.25L8.25,15.25L11.25,15.25C11.664,15.25 12,15.586 12,16C12,16.414 11.664,16.75 11.25,16.75L8.25,16.75L8.25,19.75C8.25,20.164 7.914,20.5 7.5,20.5C7.086,20.5 6.75,20.164 6.75,19.75L6.75,16.75L3.75,16.75ZM17,25.5C16.172,25.5 15.5,24.828 15.5,24L15.5,8C15.5,7.172 16.172,6.5 17,6.5L23,6.5C23.828,6.5 24.5,7.172 24.5,8L24.5,24C24.5,24.828 23.828,25.5 23,25.5L17,25.5ZM17,27C15.343,27 14,25.657 14,24L14,8C14,6.343 15.343,5 17,5L23,5C24.657,5 26,6.343 26,8L26,24C26,25.657 24.657,27 23,27L17,27Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M8.53,12.53C8.823,12.237 8.823,11.763 8.53,11.47C8.237,11.177 7.763,11.177 7.47,11.47L3.47,15.47L2.939,16L3.47,16.53L7.47,20.53C7.763,20.823 8.237,20.823 8.53,20.53C8.823,20.237 8.823,19.763 8.53,19.47L5.811,16.75L13,16.75L13,15.25L5.811,15.25L8.53,12.53ZM16.5,24L16.5,8C16.5,7.172 17.172,6.5 18,6.5L24,6.5C24.828,6.5 25.5,7.172 25.5,8L25.5,24C25.5,24.828 24.828,25.5 24,25.5L18,25.5C17.172,25.5 16.5,24.828 16.5,24ZM18,27C16.343,27 15,25.657 15,24L15,8C15,6.343 16.343,5 18,5L24,5C25.657,5 27,6.343 27,8L27,24C27,25.657 25.657,27 24,27L18,27Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M6.5,24L6.5,8C6.5,7.172 7.172,6.5 8,6.5L14,6.5C14.828,6.5 15.5,7.172 15.5,8L15.5,24C15.5,24.828 14.828,25.5 14,25.5L8,25.5C7.172,25.5 6.5,24.828 6.5,24ZM8,27C6.343,27 5,25.657 5,24L5,8C5,6.343 6.343,5 8,5L14,5C15.657,5 17,6.343 17,8L17,24C17,25.657 15.657,27 14,27L8,27ZM23.47,19.47C23.177,19.763 23.177,20.237 23.47,20.53C23.763,20.823 24.237,20.823 24.53,20.53L28.53,16.53L29.061,16L28.53,15.47L24.53,11.47C24.237,11.177 23.763,11.177 23.47,11.47C23.177,11.763 23.177,12.237 23.47,12.53L26.189,15.25L19,15.25L19,16.75L26.189,16.75L23.47,19.47Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M12.53,22.97C12.237,22.677 11.763,22.677 11.47,22.97C11.177,23.262 11.177,23.737 11.47,24.03L15.47,28.03L16,28.56L16.53,28.03L20.53,24.03C20.823,23.737 20.823,23.262 20.53,22.97C20.237,22.677 19.763,22.677 19.47,22.97L16.75,25.689L16.75,19L15.25,19L15.25,25.689L12.53,22.97ZM24,15.5L8,15.5C7.172,15.5 6.5,14.828 6.5,14L6.5,8C6.5,7.171 7.172,6.5 8,6.5L24,6.5C24.828,6.5 25.5,7.171 25.5,8L25.5,14C25.5,14.828 24.828,15.5 24,15.5ZM27,14C27,15.657 25.657,17 24,17L8,17C6.343,17 5,15.657 5,14L5,8C5,6.343 6.343,5 8,5L24,5C25.657,5 27,6.343 27,8L27,14Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M19.47,9.091C19.763,9.384 20.237,9.384 20.53,9.091C20.823,8.798 20.823,8.323 20.53,8.03L16.53,4.03L16,3.5L15.47,4.03L11.47,8.03C11.177,8.323 11.177,8.798 11.47,9.091C11.763,9.384 12.237,9.384 12.53,9.091L15.25,6.371V13.061H16.75V6.371L19.47,9.091ZM8,16.561H24C24.828,16.561 25.5,17.232 25.5,18.061V24.061C25.5,24.889 24.828,25.561 24,25.561H8C7.172,25.561 6.5,24.889 6.5,24.061V18.061C6.5,17.232 7.172,16.561 8,16.561ZM5,18.061C5,16.404 6.343,15.061 8,15.061H24C25.657,15.061 27,16.404 27,18.061V24.061C27,25.718 25.657,27.061 24,27.061H8C6.343,27.061 5,25.718 5,24.061V18.061Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2,3.5C1.448,3.5 1,3.948 1,4.5V8.5H8V3.5H2ZM9,8.5H15V3.5H9V8.5ZM23,8.5H16V3.5H22C22.552,3.5 23,3.948 23,4.5V8.5ZM24,9V8.5V4.5C24,3.395 23.104,2.5 22,2.5H16H15.5H15H9H8.5H8H2C0.895,2.5 0,3.395 0,4.5V8.5V9V9.5V14.5V15V15.5V19.5C0,20.604 0.895,21.5 2,21.5H8H8.5H9H15H15.5H16H22C23.104,21.5 24,20.604 24,19.5V15.5V15V14.5V9.5V9ZM9,20.5H15V15.5H9V20.5ZM9,14.5H15V9.5H9V14.5ZM8,20.5V15.5H1V19.5C1,20.052 1.448,20.5 2,20.5H8ZM1,14.5H8V9.5H1V14.5ZM16,15.5H23V19.5C23,20.052 22.552,20.5 22,20.5H16V15.5ZM23,9.5V14.5H16V9.5H23Z"
android:fillColor="@color/glyph_active"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
style="@style/DefaultTableBlockRootStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.core.widget.NestedScrollView
android:id="@+id/container"
style="@style/DefaultTableBlockContainerStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerTable"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
<View
android:id="@+id/selected"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/default_document_item_padding_end"
android:background="@drawable/item_block_multi_select_mode_selector"
tools:background="@drawable/item_block_multi_select_selected" />
</FrameLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/shape_primary"/>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="@dimen/item_block_table_cell_width"
android:layout_height="@dimen/item_block_table_cell_height">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textContent"
style="@style/BlockCellTextContentStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:maxLines="2"
android:singleLine="false"
tools:text="@string/default_text_placeholder" />
<View
android:id="@+id/selection"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_table_cell_board_all"
android:visibility="invisible" />
</FrameLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="140dp"
android:layout_height="match_parent"
android:minHeight="42dp" />

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/item_block_table_space_end_width"
android:layout_height="match_parent">
</View>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="52dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_10"
android:layout_marginEnd="@dimen/dp_10"
android:orientation="vertical">
<FrameLayout
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/rect_block_action_button_background">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center" />
</FrameLayout>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:fontFamily="@font/inter_regular"
android:gravity="center_horizontal"
android:textColor="@color/text_secondary"
android:textSize="11sp"
tools:text="Delete" />
</LinearLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:clipToPadding="false"
android:paddingBottom="8dp"
android:overScrollMode="never"
android:id="@+id/recyclerCell"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context="com.anytypeio.anytype.core_ui.widgets.toolbar.table.SimpleTableSettingWidget">
<View
android:id="@+id/dragger"
android:layout_width="48dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:background="@drawable/dragger" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="center"
app:tabBackground="@null"
app:tabMode="fixed"
app:tabIndicator="@null"
app:tabSelectedTextColor="@color/text_primary"
app:tabTextAppearance="@style/BlockTableWidgetTabsStyle"
app:tabTextColor="@color/text_tertiary" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:paddingTop="@dimen/dp_20"
android:overScrollMode="never" />
</LinearLayout>

View file

@ -179,5 +179,7 @@
<color name="palette_system_red">#F55522</color>
<color name="dashboard_tab_color">#CC0066C3</color>
<color name="amber_80">#FFC532</color>
<color name="table_row_header_background">#1A50491C</color>
</resources>

View file

@ -288,4 +288,9 @@
<dimen name="selection_left_right_offset">12dp</dimen>
<dimen name="divider_extra_space_bottom">2dp</dimen>
<dimen name="item_block_table_cell_width">140dp</dimen>
<dimen name="item_block_table_cell_height">63dp</dimen>
<dimen name="item_block_table_cell_padding_start">12dp</dimen>
<dimen name="item_block_table_cell_padding_top">9dp</dimen>
<dimen name="item_block_table_space_end_width">20dp</dimen>
</resources>

View file

@ -384,6 +384,9 @@
<string name="slash_widget_other_line">Line divider</string>
<string name="slash_widget_other_dots">Dots divider</string>
<string name="slash_widget_other_toc">Table of contents</string>
<string name="slash_widget_other_simple_table">Simple table</string>
<string name="slash_widgth_other_simple_table_rows_columns_count">Simple table %1$dx%2$d</string>
<string name="slash_widget_other_simple_table_subtitle">Create a simple table</string>
<string name="slash_widget_actions_delete">Delete</string>
<string name="slash_widget_actions_duplicate">Duplicate</string>
@ -525,4 +528,20 @@
<string name="item_relation_create_from_scratch_title">Type</string>
<string name="btn_restore">Restore</string>
<string name="simple_tables_widget_item_clear_contents">Clear contents</string>
<string name="simple_tables_widget_item_clear_color">Color</string>
<string name="simple_tables_widget_item_clear_style">Style</string>
<string name="simple_tables_widget_item_clear_clear_style">Clear style</string>
<string name="simple_tables_widget_item_insert_left">Insert left</string>
<string name="simple_tables_widget_item_insert_right">Insert right</string>
<string name="simple_tables_widget_item_move_left">Move left</string>
<string name="simple_tables_widget_item_move_right">Move right</string>
<string name="simple_tables_widget_item_insert_above">Insert above</string>
<string name="simple_tables_widget_item_insert_below">Insert below</string>
<string name="simple_tables_widget_item_move_up">Move up</string>
<string name="simple_tables_widget_item_move_down">Move down</string>
<string name="simple_tables_widget_tab_cell">Cell</string>
<string name="simple_tables_widget_tab_row">Row</string>
<string name="simple_tables_widget_tab_column">Column</string>
</resources>

View file

@ -983,4 +983,30 @@
<item name="android:textSize">15sp</item>
</style>
<!-- Editor, table block style, {root {container {recycler}} selected}-->
<style name="DefaultTableBlockRootStyle">
<item name="android:layout_marginTop">10dp</item>
<item name="android:layout_marginBottom">10dp</item>
<item name="android:paddingStart">@dimen/default_document_item_padding_start</item>
</style>
<style name="DefaultTableBlockContainerStyle">
<item name="android:paddingStart">@dimen/default_graphic_text_container_padding_start</item>
</style>
<style name="BlockCellTextContentStyle" parent="DefaultEditorTextStyle">
<item name="android:fontFamily">@font/inter_regular</item>
<item name="android:paddingTop">9dp</item>
<item name="android:paddingBottom">9dp</item>
<item name="android:paddingStart">@dimen/default_document_content_padding_start</item>
<item name="android:paddingEnd">@dimen/default_document_content_padding_end</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
</style>
<style name="BlockTableWidgetTabsStyle">
<item name="android:fontFamily">@font/inter_bold</item>
<item name="android:textSize">17sp</item>
</style>
</resources>

View file

@ -0,0 +1,225 @@
package com.anytypeio.anytype.core_ui.features.editor.table
import android.content.Context
import android.os.Build
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import com.anytypeio.anytype.core_models.StubParagraph
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.uitests.givenAdapter
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.TestFragment
import com.anytypeio.anytype.test_utils.utils.checkHasChildViewCount
import com.anytypeio.anytype.test_utils.utils.checkHasChildViewWithText
import com.anytypeio.anytype.test_utils.utils.checkIsDisplayed
import com.anytypeio.anytype.test_utils.utils.checkIsRecyclerSize
import com.anytypeio.anytype.test_utils.utils.onItemView
import com.anytypeio.anytype.test_utils.utils.rVMatcher
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
sdk = [Build.VERSION_CODES.P],
instrumentedPackages = ["androidx.loader.content"]
)
class TableBlockTest {
private val context: Context = ApplicationProvider.getApplicationContext()
lateinit var scenario: FragmentScenario<TestFragment>
@Before
fun setUp() {
context.setTheme(R.style.Theme_MaterialComponents)
scenario = launchFragmentInContainer()
}
@Test
fun `should render table block and update text in cell`() {
val rowId1 = "rowId1"
val columnId1 = "columnId1"
val columnId2 = "columnId2"
val columnId3 = "columnId3"
val columnId4 = "columnId4"
val oldText = "oldText"
val newText = "NewText"
val row1Block1 =
StubParagraph(id = "$rowId1-$columnId2", text = "a1")
val row1Block2 = StubParagraph(id = "$rowId1-$columnId3", text = oldText)
val cells = listOf(
BlockView.Table.Cell.Empty(
rowId = rowId1,
columnId = columnId1
),
BlockView.Table.Cell.Text(
block = BlockView.Text.Paragraph(
id = row1Block1.id,
text = row1Block1.content.asText().text
),
rowId = rowId1,
columnId = columnId2
),
BlockView.Table.Cell.Text(
block = BlockView.Text.Paragraph(
id = row1Block2.id,
text = row1Block2.content.asText().text
),
rowId = rowId1,
columnId = columnId3
),
BlockView.Table.Cell.Empty(
rowId = rowId1,
columnId = columnId4
)
)
val cellsNew = listOf(
BlockView.Table.Cell.Empty(
rowId = rowId1,
columnId = columnId1,
settings = BlockView.Table.CellSettings(
width = 140
)
),
BlockView.Table.Cell.Text(
block = BlockView.Text.Paragraph(
id = row1Block1.id,
text = row1Block1.content.asText().text
),
settings = BlockView.Table.CellSettings(
width = 140
),
rowId = rowId1,
columnId = columnId2
),
BlockView.Table.Cell.Text(
block = BlockView.Text.Paragraph(
id = row1Block2.id,
text = newText
),
settings = BlockView.Table.CellSettings(
width = 140
),
rowId = rowId1,
columnId = columnId3
),
BlockView.Table.Cell.Empty(
rowId = rowId1,
columnId = columnId4,
settings = BlockView.Table.CellSettings(
width = 140
)
)
)
val columns = listOf(
BlockView.Table.Column(id = columnId1, backgroundColor = null),
BlockView.Table.Column(id = columnId2, backgroundColor = null),
BlockView.Table.Column(id = columnId3, backgroundColor = null),
BlockView.Table.Column(id = columnId4, backgroundColor = null)
)
scenario.onFragment {
it.view?.updateLayoutParams {
width = 1200
}
val recycler = givenRecycler(it)
val tableId = MockDataFactory.randomUuid()
val views = listOf<BlockView>(
BlockView.Table(
id = tableId,
cells = cells,
columns = columns,
rowCount = 1,
isSelected = false
)
)
val adapter = givenAdapter(views)
recycler.adapter = adapter
com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher().apply {
onItemView(0, R.id.recyclerTable).checkIsDisplayed()
onItemView(0, R.id.recyclerTable).checkHasChildViewCount(4)
onItemView(0, R.id.recyclerTable).checkHasChildViewWithText(
0,
"",
R.id.textContent
).checkIsDisplayed()
onItemView(0, R.id.recyclerTable).checkHasChildViewWithText(
1,
row1Block1.content.asText().text,
R.id.textContent
).checkIsDisplayed()
onItemView(0, R.id.recyclerTable).checkHasChildViewWithText(
2,
row1Block2.content.asText().text,
R.id.textContent
).checkIsDisplayed()
}
val viewsUpdated = listOf<BlockView>(
BlockView.Table(
id = tableId,
cells = cellsNew,
columns = columns,
rowCount = 1,
isSelected = false
)
)
adapter.updateWithDiffUtil(viewsUpdated)
com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher().apply {
checkIsRecyclerSize(1)
onItemView(0, R.id.recyclerTable).checkIsDisplayed()
onItemView(0, R.id.recyclerTable).checkHasChildViewCount(4)
onItemView(0, R.id.recyclerTable).checkHasChildViewWithText(
0,
"",
R.id.textContent
).checkIsDisplayed()
onItemView(0, R.id.recyclerTable).checkHasChildViewWithText(
1,
row1Block1.content.asText().text,
R.id.textContent
).checkIsDisplayed()
onItemView(0, R.id.recyclerTable).checkHasChildViewWithText(
2,
newText,
R.id.textContent
).checkIsDisplayed()
}
}
}
private fun givenRecycler(it: Fragment): RecyclerView =
it.view!!.findViewById<RecyclerView>(com.anytypeio.anytype.test_utils.R.id.recycler).apply {
layoutManager = LinearLayoutManager(context)
}
}

View file

@ -31,6 +31,7 @@ object SlashConst {
const val SLASH_OTHER_LINE = "Line divider"
const val SLASH_OTHER_TOC = "Table of contents"
const val SLASH_OTHER_TOC_ABBREVIATION = "toc"
const val SLASH_OTHER_SIMPLE_TABLE = "Simple table"
const val SLASH_ALIGN_LEFT = "Left"
const val SLASH_ALIGN_CENTER = "Center"

View file

@ -75,4 +75,6 @@ fun String.isEndLineClick(range: IntRange): Boolean = range.first == length && r
inline fun <reified T> Fragment.withParent(action: T.() -> Unit) {
check(parentFragment is T) { "Parent is not ${T::class.java}. Please specify correct type" }
(parentFragment as T).action()
}
}
fun MatchResult?.parseMatchedInt(index: Int): Int? = this?.groups?.get(index)?.value?.toIntOrNull()

View file

@ -597,4 +597,21 @@ class BlockDataRepository(
override suspend fun duplicateObject(id: Id): Id {
return remote.duplicateObject(id)
}
override suspend fun createTable(
ctx: String,
target: String,
position: Position,
rowCount: Int,
columnCount: Int
): Payload = remote.createTable(
ctx = ctx,
target = target,
position = position,
rows = rowCount,
columns = columnCount
)
override suspend fun fillTableRow(ctx: String, targetIds: List<String>): Payload =
remote.fillTableRow(ctx, targetIds)
}

View file

@ -241,4 +241,14 @@ interface BlockDataStore {
suspend fun duplicateObject(id: Id): Id
suspend fun applyTemplate(ctx: Id, template: Id)
suspend fun createTable(
ctx: String,
target: String,
position: Position,
rows: Int,
columns: Int
): Payload
suspend fun fillTableRow(ctx: String, targetIds: List<String>): Payload
}

View file

@ -240,4 +240,14 @@ interface BlockRemote {
suspend fun duplicateObject(id: Id): Id
suspend fun applyTemplate(ctx: Id, template: Id)
suspend fun createTable(
ctx: String,
target: String,
position: Position,
rows: Int,
columns: Int
): Payload
suspend fun fillTableRow(ctx: String, targetIds: List<String>): Payload
}

View file

@ -513,4 +513,21 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
ctx = ctx,
template = template
)
override suspend fun createTable(
ctx: String,
target: String,
position: Position,
rowCount: Int,
columCount: Int
): Payload = remote.createTable(
ctx = ctx,
target = target,
position = position,
rows = rowCount,
columns = columCount
)
override suspend fun fillTableRow(ctx: String, targetIds: List<String>): Payload =
remote.fillTableRow(ctx, targetIds)
}

View file

@ -121,7 +121,7 @@ ext {
retrofit: "com.squareup.retrofit2:converter-gson:$retrofit_version",
okhttpLoggingInterceptor: "com.squareup.okhttp3:logging-interceptor:$okhttp_logging_interceptor_version",
timber: "com.jakewharton.timber:timber:$timber_version",
tableView: "com.evrencoskun.library:tableview:$table_view_version",
tableView: "com.github.evrencoskun:TableView:$table_view_version",
exoPlayerCore: "com.google.android.exoplayer:exoplayer-core:$exoplayer_version",
exoPlayerUi: "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version",
pickT: "com.github.HBiSoft:PickiT:$pickt_version",

View file

@ -301,4 +301,14 @@ interface BlockRepository {
suspend fun duplicateObject(id: Id): Id
suspend fun applyTemplate(ctx: Id, template: Id)
suspend fun createTable(
ctx: String,
target: String,
position: Position,
rowCount: Int,
columnCount: Int
): Payload
suspend fun fillTableRow(ctx: String, targetIds: List<String>): Payload
}

View file

@ -0,0 +1,39 @@
package com.anytypeio.anytype.domain.table
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.block.repo.BlockRepository
class CreateTable(
private val repo: BlockRepository
) : BaseUseCase<Payload, CreateTable.Params>() {
override suspend fun run(params: Params): Either<Throwable, Payload> = safe {
repo.createTable(
ctx = params.ctx,
target = params.target,
position = params.position,
rowCount = params.rowCount ?: DEFAULT_ROW_COUNT,
columnCount = params.columnCount ?: DEFAULT_COLUMN_COUNT
)
}
data class Params(
val ctx: Id,
val target: Id,
val position: Position,
val rowCount: Int?,
val columnCount: Int?
)
companion object {
const val DEFAULT_ROW_COUNT = 3
const val DEFAULT_COLUMN_COUNT = 3
const val DEFAULT_MAX_ROW_COUNT = 25
const val DEFAULT_MAX_COLUMN_COUNT = 25
}
}

View file

@ -0,0 +1,27 @@
package com.anytypeio.anytype.domain.table
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Either
import com.anytypeio.anytype.domain.block.repo.BlockRepository
class FillTableRow(
private val repo: BlockRepository
) : BaseUseCase<Payload, FillTableRow.Params>() {
override suspend fun run(params: Params): Either<Throwable, Payload> = safe {
repo.fillTableRow(
ctx = params.ctx,
targetIds = params.targetIds
)
}
/**
* @property [targetIds] the list of id rows that need to be filled in
*/
data class Params(
val ctx: Id,
val targetIds: List<Id>
)
}

View file

@ -549,4 +549,21 @@ class BlockMiddleware(
ctx = ctx,
template = template
)
override suspend fun createTable(
ctx: String,
target: String,
position: Position,
rowCount: Int,
columnCount: Int
): Payload = middleware.createTable(
ctx = ctx,
target = target,
position = position,
rowCount = rowCount,
columnCount = columnCount
)
override suspend fun fillTableRow(ctx: String, targetIds: List<String>): Payload =
middleware.fillTableRow(ctx, targetIds)
}

View file

@ -1646,6 +1646,39 @@ class Middleware(
if (BuildConfig.DEBUG) logResponse(response)
}
@Throws(Exception::class)
fun createTable(
ctx: String,
target: String,
position: Position,
rowCount: Int,
columnCount: Int
): Payload {
val request = Rpc.BlockTable.Create.Request(
contextId = ctx,
targetId = target,
position = position.toMiddlewareModel(),
rows = rowCount,
columns = columnCount
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.createTable(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.event.toPayload()
}
@Throws(Exception::class)
fun fillTableRow(ctx: String, targetIds: List<String>): Payload {
val request = Rpc.BlockTable.RowListFill.Request(
contextId = ctx,
blockIds = targetIds
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.blockTableRowListFill(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.event.toPayload()
}
private fun logRequest(any: Any) {
val message = "===> " + any::class.java.canonicalName + ":" + "\n" + any.toString()
Timber.d(message)

View file

@ -145,6 +145,33 @@ fun List<MBlock>.toCoreModels(
backgroundColor = block.backgroundColor.ifEmpty { null }
)
}
block.table != null -> {
Block(
id = block.id,
fields = block.toCoreModelsFields(),
children = block.childrenIds,
content = Block.Content.Table,
backgroundColor = block.backgroundColor
)
}
block.tableColumn != null -> {
Block(
id = block.id,
fields = block.toCoreModelsFields(),
children = block.childrenIds,
content = Block.Content.TableColumn,
backgroundColor = block.backgroundColor
)
}
block.tableRow != null -> {
Block(
id = block.id,
fields = block.toCoreModelsFields(),
children = block.childrenIds,
content = block.toCoreModelsTableRowBlock(),
backgroundColor = block.backgroundColor
)
}
else -> {
Block(
id = block.id,
@ -278,6 +305,13 @@ fun MBlock.toCoreModelsRelationBlock(): Block.Content.RelationBlock {
)
}
fun MBlock.toCoreModelsTableRowBlock(): Block.Content.TableRow {
val content = checkNotNull(tableRow)
return Block.Content.TableRow(
isHeader = content.isHeader
)
}
fun MBFileState.toCoreModels(): Block.Content.File.State = when (this) {
MBFileState.Empty -> Block.Content.File.State.EMPTY
MBFileState.Uploading -> Block.Content.File.State.UPLOADING

View file

@ -299,6 +299,16 @@ interface MiddlewareService {
//endregion
//region SIMPLE TABLE commands
@Throws(Exception::class)
fun createTable(request: Rpc.BlockTable.Create.Request) : Rpc.BlockTable.Create.Response
@Throws(Exception::class)
fun blockTableRowListFill(request: Rpc.BlockTable.RowListFill.Request): Rpc.BlockTable.RowListFill.Response
//endregion
//region DEBUG commands
@Throws(Exception::class)

View file

@ -1046,4 +1046,28 @@ class MiddlewareServiceImplementation : MiddlewareService {
return response
}
}
override fun createTable(request: Rpc.BlockTable.Create.Request): Rpc.BlockTable.Create.Response {
val encoded =
Service.blockTableCreate(Rpc.BlockTable.Create.Request.ADAPTER.encode(request))
val response = Rpc.BlockTable.Create.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.BlockTable.Create.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun blockTableRowListFill(request: Rpc.BlockTable.RowListFill.Request): Rpc.BlockTable.RowListFill.Response {
val encoded =
Service.blockTableRowListFill(Rpc.BlockTable.RowListFill.Request.ADAPTER.encode(request))
val response = Rpc.BlockTable.RowListFill.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.BlockTable.RowListFill.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
}

View file

@ -108,6 +108,8 @@ import com.anytypeio.anytype.presentation.editor.editor.ext.toReadMode
import com.anytypeio.anytype.presentation.editor.editor.ext.update
import com.anytypeio.anytype.presentation.editor.editor.ext.updateCursorAndEditMode
import com.anytypeio.anytype.presentation.editor.editor.ext.updateSelection
import com.anytypeio.anytype.presentation.editor.editor.ext.applyBordersToSelectedCells
import com.anytypeio.anytype.presentation.editor.editor.ext.removeBordersFromCells
import com.anytypeio.anytype.presentation.editor.editor.ext.updateTableOfContentsViews
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.markup
@ -140,6 +142,10 @@ import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleBackgrou
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleColorBackgroundToolbarState
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleOtherToolbarState
import com.anytypeio.anytype.presentation.editor.editor.styling.getStyleTextToolbarState
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetEvent
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetState
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableWidgetViewState
import com.anytypeio.anytype.presentation.editor.editor.toCoreModel
import com.anytypeio.anytype.presentation.editor.editor.updateText
import com.anytypeio.anytype.presentation.editor.model.EditorFooter
@ -237,6 +243,7 @@ class EditorViewModel(
private val setDocCoverImage: SetDocCoverImage,
private val setDocImageIcon: SetDocumentImageIcon,
private val templateDelegate: EditorTemplateDelegate,
private val simpleTableDelegate: SimpleTableDelegate,
private val createNewObject: CreateNewObject
) : ViewStateViewModel<ViewState>(),
PickerListener,
@ -246,6 +253,7 @@ class EditorViewModel(
ToggleStateHolder by renderer,
SelectionStateHolder by orchestrator.memory.selections,
EditorTemplateDelegate by templateDelegate,
SimpleTableDelegate by simpleTableDelegate,
StateReducer<List<Block>, Event> by reducer {
val actions = MutableStateFlow(ActionItemType.defaultSorting)
@ -268,6 +276,17 @@ class EditorViewModel(
}
}
val simpleTablesViewState = simpleTableDelegateState.map { state ->
when (state) {
is SimpleTableWidgetState.UpdateItems -> {
SimpleTableWidgetViewState.Active(
state = state
)
}
SimpleTableWidgetState.Idle -> SimpleTableWidgetViewState.Idle
}
}
val searchResultScrollPosition = MutableStateFlow(NO_SEARCH_RESULT_POSITION)
private val session = MutableStateFlow(Session.IDLE)
@ -1688,6 +1707,11 @@ class EditorViewModel(
is Content.Text -> {
excludedActions.add(ActionItemType.Download)
}
is Content.Table -> {
excludedActions.add(ActionItemType.Paste)
excludedActions.add(ActionItemType.Copy)
excludedActions.add(ActionItemType.Style)
}
else -> {
// do nothing
}
@ -1966,6 +1990,10 @@ class EditorViewModel(
viewModelScope.launch { controlPanelInteractor.onEvent(ControlPanelMachine.Event.SearchToolbar.OnEnterSearchMode) }
}
fun onSetTextBlockValue() {
viewModelScope.launch { refresh() }
}
fun onDocRelationsClicked() {
Timber.d("onDocRelationsClicked, ")
dispatch(
@ -2712,6 +2740,86 @@ class EditorViewModel(
}
}
private fun addSimpleTableBlock(item: SlashItem.Other.Table) {
val focused = blocks.first { it.id == orchestrator.stores.focus.current().id }
val content = focused.content
if (content is Content.Text && content.text.isEmpty()) {
viewModelScope.launch {
orchestrator.proxies.intents.send(
Intent.Table.CreateTable(
ctx = context,
target = focused.id,
position = Position.REPLACE,
rows = item.rowCount,
columns = item.columnCount
)
)
}
} else {
val position: Position
var target: Id = focused.id
if (focused.id == context) {
if (focused.children.isEmpty()) {
position = Position.INNER
} else {
position = Position.TOP
target = focused.children.first()
}
} else {
position = Position.BOTTOM
}
viewModelScope.launch {
orchestrator.proxies.intents.send(
Intent.Table.CreateTable(
ctx = context,
target = target,
position = position
)
)
}
}
}
private fun onTableRowEmptyCellClicked(cellId: Id, rowId: Id, tableId: Id) {
viewModelScope.launch {
orchestrator.stores.focus.update(
Editor.Focus(
id = cellId,
cursor = Editor.Cursor.Start
)
)
}
fillTableBlockRow(
cellId = cellId,
targetIds = listOf(rowId),
tableId = tableId
)
}
private fun fillTableBlockRow(cellId: Id, targetIds: List<Id>, tableId: Id) {
viewModelScope.launch {
orchestrator.proxies.intents.send(
Intent.Table.FillTableRow(
ctx = context,
targetIds = targetIds
)
)
}
dispatch(
Command.OpenSetBlockTextValueScreen(
ctx = context,
block = cellId,
table = tableId
)
)
}
fun onAddDividerBlockClicked(style: Content.Divider.Style) {
Timber.d("onAddDividerBlockClicked, style:[$style]")
addDividerBlock(style)
@ -3712,6 +3820,46 @@ class EditorViewModel(
is ListenerType.Callout.Icon -> {
dispatch(Command.OpenTextBlockIconPicker(clicked.blockId))
}
is ListenerType.TableEmptyCell -> {
when (mode) {
EditorMode.Edit -> {
proceedWithSelectingCell(
cellId = clicked.cellId,
tableId = clicked.tableId
)
onTableRowEmptyCellClicked(
cellId = clicked.cellId,
rowId = clicked.rowId,
tableId = clicked.tableId
)
}
EditorMode.Select -> onBlockMultiSelectClicked(target = clicked.tableId)
else -> Unit
}
}
is ListenerType.TableTextCell -> {
when (mode) {
EditorMode.Edit -> {
proceedWithSelectingCell(
cellId = clicked.cellId,
tableId = clicked.tableId
)
dispatch(
Command.OpenSetBlockTextValueScreen(
ctx = context,
block = clicked.cellId,
table = clicked.tableId
)
)
}
EditorMode.Select -> onBlockMultiSelectClicked(target = clicked.tableId)
else -> Unit
}
}
is ListenerType.TableEmptyCellMenu -> {}
is ListenerType.TableTextCellMenu -> {
onShowSimpleTableWidgetClicked(id = clicked.cellId)
}
}
}
@ -4341,6 +4489,12 @@ class EditorViewModel(
Command.OpenAddRelationScreen(ctx = context, target = targetId)
)
}
is SlashItem.Other.Table -> {
cutSlashFilter(targetId = targetId)
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
onHideKeyboardClicked()
addSimpleTableBlock(item)
}
}
}
@ -5290,7 +5444,7 @@ class EditorViewModel(
}
}
private fun onMentionClicked(target: String) {
fun onMentionClicked(target: String) {
proceedWithOpeningObjectByLayout(target)
}
@ -5738,6 +5892,42 @@ class EditorViewModel(
}
//endregion
//region SIMPLE TABLES
private fun onShowSimpleTableWidgetClicked(id: Id) {
viewModelScope.launch {
onSimpleTableEvent(SimpleTableWidgetEvent.onStart(id = id))
}
}
fun onHideSimpleTableWidget() {}
private fun proceedWithSelectingCell(cellId:Id, tableId: Id) {
clearSelections()
select(listOf(cellId))
val updated = views.applyBordersToSelectedCells(
tableId = tableId,
selection = currentSelection()
)
viewModelScope.launch {
orchestrator.stores.focus.update(Editor.Focus.empty())
orchestrator.stores.views.update(updated)
renderCommand.send(Unit)
}
}
fun onSetBlockTextValueScreenDismiss() {
clearSelections()
val updated = views.removeBordersFromCells()
viewModelScope.launch {
orchestrator.stores.views.update(updated)
renderCommand.send(Unit)
}
}
//endregion
}
private const val NO_POSITION = -1

View file

@ -33,6 +33,7 @@ import com.anytypeio.anytype.presentation.common.StateReducer
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.presentation.editor.editor.DetailModificationManager
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.template.EditorTemplateDelegate
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
@ -69,6 +70,7 @@ open class EditorViewModelFactory(
private val setDocCoverImage: SetDocCoverImage,
private val setDocImageIcon: SetDocumentImageIcon,
private val editorTemplateDelegate: EditorTemplateDelegate,
private val simpleTablesDelegate: SimpleTableDelegate,
private val createNewObject: CreateNewObject
) : ViewModelProvider.Factory {
@ -105,7 +107,8 @@ open class EditorViewModelFactory(
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject
createNewObject = createNewObject,
simpleTableDelegate = simpleTablesDelegate
) as T
}
}

View file

@ -140,4 +140,10 @@ sealed class Command {
) : Command()
data class ScrollToPosition(val pos: Int) : Command()
data class OpenSetBlockTextValueScreen(
val ctx: Id,
val table: Id,
val block: Id
) : Command()
}

View file

@ -211,4 +211,20 @@ sealed class Intent {
val style: Block.Content.Divider.Style
) : Divider()
}
sealed class Table : Intent() {
class CreateTable(
val ctx: Id,
val target: Id,
val position: Position,
val rows: Int? = null,
val columns: Int? = null
) : Table()
class FillTableRow(
val ctx: Id,
val targetIds: List<Id>
) : Table()
}
}

View file

@ -33,6 +33,8 @@ import com.anytypeio.anytype.domain.page.Redo
import com.anytypeio.anytype.domain.page.Undo
import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsChangeTextBlockStyleEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsCopyBlockEvent
@ -69,6 +71,8 @@ class Orchestrator(
private val createBookmarkBlock: CreateBookmarkBlock,
private val turnIntoDocument: TurnIntoDocument,
private val updateFields: UpdateFields,
private val createTable: CreateTable,
private val fillTableRow: FillTableRow,
private val move: Move,
private val copy: Copy,
private val paste: Paste,
@ -569,6 +573,31 @@ class Orchestrator(
}
)
}
is Intent.Table.CreateTable -> {
createTable(
params = CreateTable.Params(
ctx = intent.ctx,
target = intent.target,
position = intent.position,
rowCount = intent.rows,
columnCount = intent.columns
)
).process(
failure = defaultOnError,
success = { payload -> proxies.payloads.send(payload) }
)
}
is Intent.Table.FillTableRow -> {
fillTableRow(
params = FillTableRow.Params(
ctx = intent.ctx,
targetIds = intent.targetIds
)
).process(
failure = defaultOnError,
success = { payload -> proxies.payloads.send(payload) }
)
}
}
}
}

View file

@ -337,6 +337,9 @@ fun List<BlockView>.enterSAM(
is BlockView.TableOfContents -> view.copy(
isSelected = isSelected
)
is BlockView.Table -> view.copy(
isSelected = isSelected
)
else -> view.also { check(view !is BlockView.Permission) }
}
}
@ -892,6 +895,7 @@ fun BlockView.updateSelection(newSelection: Boolean) = when (this) {
is BlockView.Relation.Placeholder -> copy(isSelected = newSelection)
is BlockView.Latex -> copy(isSelected = newSelection)
is BlockView.TableOfContents -> copy(isSelected = newSelection)
is BlockView.Table -> copy(isSelected = newSelection)
else -> this.also {
if (this is BlockView.Selectable)
Timber.e("Error when change selection for Selectable BlockView $this")
@ -1058,4 +1062,57 @@ fun List<BlockView>.fillTableOfContents(): List<BlockView> {
fun BlockView.Text.isStyleClearable(): Boolean {
return this.isListBlock || this is BlockView.Text.Highlight
}
fun List<BlockView>.applyBordersToSelectedCells(
tableId: Id,
selection: Set<Id>
): List<BlockView> = map { view ->
if (view.id == tableId && view is BlockView.Table) {
val updatedCells = view.cells.map { cell ->
when (cell) {
is BlockView.Table.Cell.Empty -> {
if (selection.contains(cell.getId())) {
val settings = cell.settings.applyAllBorders()
cell.copy(settings = settings)
} else {
cell
}
}
is BlockView.Table.Cell.Text -> {
if (selection.contains(cell.getId())) {
val settings = cell.settings.applyAllBorders()
cell.copy(settings = settings)
} else {
cell
}
}
BlockView.Table.Cell.Space -> cell
}
}
view.copy(cells = updatedCells)
} else {
view
}
}
fun List<BlockView>.removeBordersFromCells(): List<BlockView> = map { view ->
if (view is BlockView.Table) {
val updatedCells = view.cells.map { cell ->
when (cell) {
is BlockView.Table.Cell.Empty -> {
val settings = cell.settings.removeAllBorders()
cell.copy(settings = settings)
}
is BlockView.Table.Cell.Text -> {
val settings = cell.settings.removeAllBorders()
cell.copy(settings = settings)
}
BlockView.Table.Cell.Space -> cell
}
}
view.copy(cells = updatedCells)
} else {
view
}
}

View file

@ -68,4 +68,8 @@ sealed interface ListenerType {
}
data class TableOfContentsItem(val target: Id, val item: Id) : ListenerType
data class TableEmptyCell(val cellId: Id, val rowId: Id, val tableId: Id) : ListenerType
data class TableTextCell(val cellId: Id, val tableId: Id) : ListenerType
data class TableEmptyCellMenu(val rowId: Id, val columnId: Id) : ListenerType
data class TableTextCellMenu(val cellId: Id, val rowId: Id, val tableId: Id) : ListenerType
}

View file

@ -48,6 +48,7 @@ import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_RELATION_PLACEHOLDER
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_RELATION_STATUS
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_RELATION_TAGS
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TABLE
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TITLE
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TOC
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_TODO_TITLE
@ -107,6 +108,19 @@ sealed class BlockView : ViewType {
val isSelected: Boolean
}
/**
* Views implementing this interface can change IME action on keyboard
* @property inputAction -
*/
interface SupportInputAction {
val inputAction: InputAction
}
sealed interface InputAction {
object NewLine : InputAction
object Done : InputAction
}
/**
* Views implementing this interface support alignment.
*/
@ -203,7 +217,8 @@ sealed class BlockView : ViewType {
Indentable,
Permission,
Alignable,
Selectable {
Selectable,
SupportInputAction {
val id: String
}
@ -267,7 +282,8 @@ sealed class BlockView : ViewType {
}
}
sealed class Text : BlockView(), TextBlockProps, Searchable, SupportGhostEditorSelection, Decoratable {
sealed class Text : BlockView(), TextBlockProps, Searchable, SupportGhostEditorSelection,
Decoratable {
// Dynamic properties (expected to be synchronised with framework widget)
@ -307,7 +323,8 @@ sealed class BlockView : ViewType {
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Text() {
override fun getViewType() = HOLDER_PARAGRAPH
override val body: String get() = text
@ -336,7 +353,8 @@ sealed class BlockView : ViewType {
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Header() {
override fun getViewType() = HOLDER_HEADER_ONE
override val body: String get() = text
@ -363,7 +381,8 @@ sealed class BlockView : ViewType {
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Header() {
override fun getViewType() = HOLDER_HEADER_TWO
override val body: String get() = text
@ -390,7 +409,8 @@ sealed class BlockView : ViewType {
override var cursor: Int? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Header() {
override fun getViewType() = HOLDER_HEADER_THREE
override val body: String get() = text
@ -417,7 +437,8 @@ sealed class BlockView : ViewType {
override val alignment: Alignment? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Text() {
override fun getViewType() = HOLDER_HIGHLIGHT
override val body: String get() = text
@ -438,6 +459,7 @@ sealed class BlockView : ViewType {
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList(),
val icon: ObjectIcon,
override var inputAction: InputAction = InputAction.NewLine
) : Text() {
override val alignment: Alignment? = null
override fun getViewType() = HOLDER_CALLOUT
@ -465,7 +487,8 @@ sealed class BlockView : ViewType {
override val alignment: Alignment? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Text(), Checkable {
override fun getViewType() = HOLDER_CHECKBOX
override val body: String get() = text
@ -492,7 +515,8 @@ sealed class BlockView : ViewType {
override val alignment: Alignment? = null,
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList()
override val decorations: List<Decoration> = emptyList(),
override var inputAction: InputAction = InputAction.NewLine
) : Text() {
override fun getViewType() = HOLDER_BULLET
override val body: String get() = text
@ -520,7 +544,8 @@ sealed class BlockView : ViewType {
override val searchFields: List<Searchable.Field> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
override val decorations: List<Decoration> = emptyList(),
val number: Int
val number: Int,
override var inputAction: InputAction = InputAction.NewLine
) : Text() {
override fun getViewType() = HOLDER_NUMBERED
override val body: String get() = text
@ -549,7 +574,8 @@ sealed class BlockView : ViewType {
override val decorations: List<Decoration> = emptyList(),
override val ghostEditorSelection: IntRange? = null,
val toggled: Boolean = false,
val isEmpty: Boolean = false
val isEmpty: Boolean = false,
override var inputAction: InputAction = InputAction.NewLine
) : Text() {
override fun getViewType() = HOLDER_TOGGLE
override val body: String get() = text
@ -1176,4 +1202,58 @@ sealed class BlockView : ViewType {
)
enum class Mode { READ, EDIT }
data class Table(
override val id: String,
override val isSelected: Boolean,
val backgroundColor: String? = null,
val columns: List<Column>,
val cells: List<Cell>,
val rowCount: Int
) : BlockView(), Selectable {
override fun getViewType(): Int = HOLDER_TABLE
data class Column(val id: String, val backgroundColor: String?)
sealed interface Cell {
data class Text(
val rowId: Id,
val columnId: Id,
val settings: CellSettings = CellSettings.empty(),
val block: BlockView.Text.Paragraph
) : Cell {
fun getId() = "$rowId-$columnId"
}
data class Empty(
val rowId: Id,
val columnId: Id,
val settings: CellSettings = CellSettings.empty(),
) : Cell {
fun getId() = "$rowId-$columnId"
}
object Space : Cell
}
data class CellSettings(
val width: Int = 0,
val isHeader: Boolean = false,
val top: Boolean = false,
val left: Boolean = false,
val right: Boolean = false,
val bottom: Boolean = false
) {
fun applyAllBorders() = this.copy(left = true, top = true, right = true, bottom = true)
fun removeAllBorders() =
this.copy(left = false, top = false, right = false, bottom = false)
fun isAllBordersApply() = left && top && right && bottom
companion object {
fun empty() = CellSettings()
}
}
}
}

View file

@ -61,4 +61,5 @@ object Types {
const val HOLDER_LATEX = 51
const val HOLDER_TOC = 54
const val HOLDER_CALLOUT = 55
const val HOLDER_TABLE = 56
}

View file

@ -2,9 +2,15 @@ package com.anytypeio.anytype.presentation.editor.editor.slash
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_utils.ext.parseMatchedInt
import com.anytypeio.anytype.domain.table.CreateTable.Companion.DEFAULT_COLUMN_COUNT
import com.anytypeio.anytype.domain.table.CreateTable.Companion.DEFAULT_MAX_COLUMN_COUNT
import com.anytypeio.anytype.domain.table.CreateTable.Companion.DEFAULT_MAX_ROW_COUNT
import com.anytypeio.anytype.domain.table.CreateTable.Companion.DEFAULT_ROW_COUNT
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
import com.anytypeio.anytype.presentation.editor.editor.model.UiBlock
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem.Other.Table.Companion.DEFAULT_PATTERN
fun List<ObjectType>.toSlashItemView(): List<SlashItem.ObjectType> = map { oType ->
SlashItem.ObjectType(
@ -94,7 +100,8 @@ object SlashExtensions {
fun getSlashWidgetOtherItems() = listOf(
SlashItem.Other.Line,
SlashItem.Other.Dots,
SlashItem.Other.TOC
SlashItem.Other.TOC,
SlashItem.Other.Table()
)
fun getSlashWidgetActionItems() = listOf(
@ -281,12 +288,32 @@ object SlashExtensions {
subheading: String
): List<SlashItem> {
val filtered = items.filter { item ->
searchBySubheadingOrName(
filter = filter,
subheading = subheading,
name = item.getSearchName(),
abbreviation = item.getAbbreviation()
)
if (item is SlashItem.Other.Table) {
val matchResults = DEFAULT_PATTERN.toRegex().findAll(filter)
if (matchResults.none()) {
searchBySubheadingOrName(
filter = filter,
subheading = subheading,
name = item.getSearchName(),
abbreviation = item.getAbbreviation()
)
} else {
val matchResult = matchResults.firstOrNull()
val rowCount = matchResult.parseMatchedInt(1) ?: DEFAULT_ROW_COUNT
val columnCount = matchResult.parseMatchedInt(2) ?: DEFAULT_COLUMN_COUNT
item.rowCount = 1.coerceAtLeast(DEFAULT_MAX_ROW_COUNT.coerceAtMost(rowCount))
item.columnCount =
1.coerceAtLeast(DEFAULT_MAX_COLUMN_COUNT.coerceAtMost(columnCount))
true
}
} else {
searchBySubheadingOrName(
filter = filter,
subheading = subheading,
name = item.getSearchName(),
abbreviation = item.getAbbreviation()
)
}
}
return updateWithSubheader(items = filtered)
}

View file

@ -332,6 +332,22 @@ sealed class SlashItem {
override fun getSearchName(): String = SlashConst.SLASH_OTHER_TOC
override fun getAbbreviation(): List<String> = listOf(SLASH_OTHER_TOC_ABBREVIATION)
}
/**
* Simple table
*/
data class Table(
var rowCount: Int? = null,
var columnCount: Int? = null
) : Other() {
override fun getSearchName(): String = SlashConst.SLASH_OTHER_SIMPLE_TABLE
override fun getAbbreviation(): List<String> = emptyList()
companion object {
const val DEFAULT_PATTERN = "table(\\d+)(?:[^\\d]{1}([\\d]+))?"
}
}
}
//endregion

View file

@ -0,0 +1,63 @@
package com.anytypeio.anytype.presentation.editor.editor.table
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.scan
import timber.log.Timber
interface SimpleTableDelegate {
val simpleTableDelegateState: Flow<SimpleTableWidgetState>
suspend fun onSimpleTableEvent(event: SimpleTableWidgetEvent)
}
class DefaultSimpleTableDelegate : SimpleTableDelegate {
private val events = MutableSharedFlow<SimpleTableWidgetEvent>(replay = 0)
override val simpleTableDelegateState =
events.scan(SimpleTableWidgetState.init()) { state, event ->
when (event) {
is SimpleTableWidgetEvent.onStart -> {
SimpleTableWidgetState.UpdateItems(
cellItems = listOf(
SimpleTableWidgetItem.Cell.ClearContents,
SimpleTableWidgetItem.Cell.Style,
SimpleTableWidgetItem.Cell.Color,
SimpleTableWidgetItem.Cell.ClearStyle
),
rowItems = listOf(
SimpleTableWidgetItem.Row.ClearContents,
SimpleTableWidgetItem.Row.Color,
SimpleTableWidgetItem.Row.Style,
SimpleTableWidgetItem.Row.Delete,
SimpleTableWidgetItem.Row.MoveUp,
SimpleTableWidgetItem.Row.MoveDown,
SimpleTableWidgetItem.Row.InsertAbove,
SimpleTableWidgetItem.Row.InsertBelow,
SimpleTableWidgetItem.Row.Duplicate,
SimpleTableWidgetItem.Row.Sort
),
columnItems = listOf(
SimpleTableWidgetItem.Column.ClearContents,
SimpleTableWidgetItem.Column.Color,
SimpleTableWidgetItem.Column.Style,
SimpleTableWidgetItem.Column.Delete,
SimpleTableWidgetItem.Column.InsertLeft,
SimpleTableWidgetItem.Column.InsertRight,
SimpleTableWidgetItem.Column.MoveLeft,
SimpleTableWidgetItem.Column.MoveRight,
SimpleTableWidgetItem.Column.Sort,
SimpleTableWidgetItem.Column.Duplicate
)
)
}
}
}.catch { e ->
Timber.e(e, "Error while processing simple table ")
}
override suspend fun onSimpleTableEvent(event: SimpleTableWidgetEvent) {
events.emit(event)
}
}

View file

@ -0,0 +1,7 @@
package com.anytypeio.anytype.presentation.editor.editor.table
import com.anytypeio.anytype.core_models.Id
sealed interface SimpleTableWidgetEvent {
data class onStart(val id: Id) : SimpleTableWidgetEvent
}

View file

@ -0,0 +1,37 @@
package com.anytypeio.anytype.presentation.editor.editor.table
sealed class SimpleTableWidgetItem {
sealed class Cell : SimpleTableWidgetItem() {
object ClearContents : Cell()
object Color : Cell()
object Style : Cell()
object ClearStyle : Cell()
}
sealed class Column : SimpleTableWidgetItem() {
object InsertLeft : Column()
object InsertRight : Column()
object MoveLeft : Column()
object MoveRight : Column()
object Duplicate : Column()
object Delete : Column()
object ClearContents : Column()
object Sort : Column()
object Color : Column()
object Style : Column()
}
sealed class Row : SimpleTableWidgetItem() {
object InsertAbove : Row()
object InsertBelow : Row()
object MoveUp : Row()
object MoveDown : Row()
object Duplicate : Row()
object Delete : Row()
object ClearContents : Row()
object Sort : Row()
object Color : Row()
object Style : Row()
}
}

View file

@ -0,0 +1,24 @@
package com.anytypeio.anytype.presentation.editor.editor.table
sealed class SimpleTableWidgetState {
object Idle : SimpleTableWidgetState()
data class UpdateItems(
val cellItems: List<SimpleTableWidgetItem>,
val columnItems: List<SimpleTableWidgetItem>,
val rowItems: List<SimpleTableWidgetItem>
) : SimpleTableWidgetState() {
companion object {
fun empty() = UpdateItems(
cellItems = emptyList(),
columnItems = emptyList(),
rowItems = emptyList()
)
}
}
companion object {
fun init(): SimpleTableWidgetState = Idle
}
}

View file

@ -0,0 +1,6 @@
package com.anytypeio.anytype.presentation.editor.editor.table
sealed class SimpleTableWidgetViewState {
object Idle : SimpleTableWidgetViewState()
data class Active(val state: SimpleTableWidgetState) : SimpleTableWidgetViewState()
}

View file

@ -741,6 +741,21 @@ class DefaultBlockViewRenderer @Inject constructor(
)
)
}
is Content.Table -> {
isPreviousBlockMedia = false
mCounter = 0
result.add(
table(
mode = mode,
block = block,
focus = focus,
indent = indent,
details = details,
selection = selection,
blocks = this
)
)
}
}
}
@ -942,7 +957,7 @@ class DefaultBlockViewRenderer @Inject constructor(
)
}
private fun bulleted(
fun bulleted(
mode: EditorMode,
block: Block,
content: Content.Text,
@ -1746,6 +1761,147 @@ class DefaultBlockViewRenderer @Inject constructor(
)
}
private fun table(
mode: EditorMode,
block: Block,
focus: Focus,
indent: Int,
details: Block.Details,
selection: Set<Id>,
blocks: Map<String, List<Block>>
): BlockView.Table {
var cells: List<BlockView.Table.Cell> = emptyList()
var columns: List<BlockView.Table.Column> = emptyList()
var rowCount = 0
blocks.getValue(block.id).forEach { container ->
val containerContent = container.content
if (containerContent !is Content.Layout) return@forEach
if (containerContent.type == Content.Layout.Type.TABLE_COLUMN) {
columns = blocks.getValue(container.id).map { tableColumn(it) }
}
if (containerContent.type == Content.Layout.Type.TABLE_ROW) {
val rows = blocks.getValue(container.id)
rowCount = rows.size
cells = tableCells(
mode = mode,
focus = focus,
indent = indent,
details = details,
selection = selection,
rows = rows,
columns = columns,
blocks = blocks
)
}
}
return BlockView.Table(
id = block.id,
columns = columns,
cells = cells,
rowCount = rowCount,
isSelected = checkIfSelected(
mode = mode,
block = block,
selection = selection
),
backgroundColor = block.backgroundColor
)
}
private fun tableCells(
blocks: Map<String, List<Block>>,
rows: List<Block>,
columns: List<BlockView.Table.Column>,
mode: EditorMode,
focus: Focus,
indent: Int,
details: Block.Details,
selection: Set<Id>
): List<BlockView.Table.Cell> {
val cells = mutableListOf<BlockView.Table.Cell>()
columns.map { column ->
rows.forEach { row ->
val isHeader = (row.content as? Content.TableRow)?.isHeader ?: false
val cellId = "${row.id}-${column.id}"
val rowsChildren = blocks.getValue(row.id)
val block = rowsChildren.firstOrNull { it.id == cellId }
if (block != null) {
val content = block.content
check(content is Content.Text)
{ Timber.e("Table row block content should be Text") }
if (content.style == Content.Text.Style.P) {
cells.add(
BlockView.Table.Cell.Text(
rowId = row.id,
columnId = column.id,
settings = buildCellSettings(
cellId = cellId,
selection = selection,
isHeader = isHeader
),
block = paragraph(
mode = mode,
block = block,
content = content,
focus = focus,
indent = indent,
details = details,
selection = selection,
schema = emptyList()
)
)
)
} else {
Timber.w("Block should be paragraph")
}
} else {
cells.add(
BlockView.Table.Cell.Empty(
rowId = row.id,
columnId = column.id,
settings = buildCellSettings(
cellId = cellId,
selection = selection,
isHeader = isHeader
)
)
)
}
}
}
cells.add(BlockView.Table.Cell.Space)
return cells
}
private fun buildCellSettings(
cellId: Id,
selection: Set<Id>,
isHeader: Boolean
): BlockView.Table.CellSettings {
return if (selection.contains(cellId)) {
BlockView.Table.CellSettings(
left = true,
top = true,
right = true,
bottom = true,
isHeader = isHeader
)
} else {
BlockView.Table.CellSettings(
isHeader = isHeader
)
}
}
private fun tableColumn(block: Block): BlockView.Table.Column {
return BlockView.Table.Column(
id = block.id,
backgroundColor = block.backgroundColor
)
}
private fun relation(
ctx: Id,
block: Block,

View file

@ -0,0 +1,145 @@
package com.anytypeio.anytype.presentation.objects.block
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.editor.updateText
import com.anytypeio.anytype.presentation.editor.model.TextUpdate
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import timber.log.Timber
class SetBlockTextValueViewModel(
private val updateText: UpdateText,
private val storage: Editor.Storage
) : BaseViewModel() {
private val doc: List<BlockView> get() = storage.views.current()
val state = MutableStateFlow<ViewState>(ViewState.Loading)
private val jobs = mutableListOf<Job>()
fun onStart(tableId: Id, blockId: Id) {
jobs += viewModelScope.launch {
storage.views.stream().mapNotNull { views ->
val table = views.firstOrNull { it.id == tableId }
if (table != null && table is BlockView.Table) {
val block = table.cells.firstOrNull { cell ->
when (cell) {
is BlockView.Table.Cell.Empty -> cell.getId() == blockId
is BlockView.Table.Cell.Text -> cell.getId() == blockId
BlockView.Table.Cell.Space -> false
}
}
if (block is BlockView.Table.Cell.Text) {
block.block.copy(inputAction = BlockView.InputAction.Done)
} else {
null
}
} else {
null
}
}.collectLatest { block ->
state.value = ViewState.Success(data = listOf(block))
}
}
}
fun onStop() {
jobs.apply {
forEach { it.cancel() }
clear()
}
}
fun onKeyboardDoneKeyClicked(
ctx: Id,
tableId: String,
targetId: String,
text: String,
marks: List<Markup.Mark>,
markup: List<Block.Content.Text.Mark>
) {
viewModelScope.launch {
storage.views.update(doc.map { view ->
if (view.id == tableId && view is BlockView.Table) {
val updated = view.cells.map { it ->
if (it is BlockView.Table.Cell.Text && it.block.id == targetId) {
it.copy(block = it.block.copy(text = text, marks = marks))
} else {
it
}
}
view.copy(cells = updated)
} else {
view
}
})
}
val update = TextUpdate.Default(target = targetId, text = text, markup = markup)
val updated = storage.document.get().map { block ->
if (block.id == update.target) {
block.updateText(update)
} else
block
}
storage.document.update(updated)
viewModelScope.launch {
updateText(
UpdateText.Params(
context = ctx,
target = targetId,
text = text,
marks = markup
)
).process(
failure = { e ->
Timber.e(e, "Error while updating block text value")
_toasts.emit("Error while updating block text value ${e.localizedMessage}")
},
success = { state.value = ViewState.Exit }
)
}
}
fun onClickListener(clicked: ListenerType) {
if (clicked is ListenerType.Mention) {
state.value = ViewState.OnMention(clicked.target)
}
}
class Factory(
private val updateText: UpdateText,
private val storage: Editor.Storage
) :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SetBlockTextValueViewModel(
updateText = updateText,
storage = storage
) as T
}
}
sealed class ViewState {
data class Success(val data: List<BlockView>) : ViewState()
data class OnMention(val targetId: String) : ViewState()
object Exit : ViewState()
object Loading : ViewState()
}
}

View file

@ -70,6 +70,8 @@ import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.presentation.BuildConfig
@ -93,6 +95,8 @@ import com.anytypeio.anytype.presentation.editor.editor.pattern.DefaultPatternMa
import com.anytypeio.anytype.presentation.editor.editor.slash.SlashItem
import com.anytypeio.anytype.presentation.editor.editor.styling.StyleToolbarState
import com.anytypeio.anytype.presentation.editor.editor.styling.StylingEvent
import com.anytypeio.anytype.presentation.editor.editor.table.DefaultSimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.editor.template.DefaultEditorTemplateDelegate
@ -308,11 +312,19 @@ open class EditorViewModelTest {
@Mock
lateinit var applyTemplate: ApplyTemplate
@Mock
lateinit var fillTableRow: FillTableRow
private lateinit var editorTemplateDelegate: EditorTemplateDelegate
private lateinit var simpleTableDelegate: SimpleTableDelegate
@Mock
lateinit var createNewObject: CreateNewObject
@Mock
lateinit var createTable: CreateTable
private lateinit var updateDetail: UpdateDetail
lateinit var vm: EditorViewModel
@ -354,6 +366,7 @@ open class EditorViewModelTest {
getTemplates = getTemplates,
applyTemplate = applyTemplate
)
simpleTableDelegate = DefaultSimpleTableDelegate()
}
@Test
@ -3925,6 +3938,8 @@ open class EditorViewModelTest {
turnIntoStyle = turnIntoStyle,
updateBlocksMark = updateBlocksMark,
setObjectType = setObjectType,
createTable = createTable,
fillTableRow = fillTableRow
),
dispatcher = Dispatcher.Default(),
detailModificationManager = InternalDetailModificationManager(storage.details),
@ -3940,7 +3955,8 @@ open class EditorViewModelTest {
setDocImageIcon = setDocImageIcon,
delegator = delegator,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject
createNewObject = createNewObject,
simpleTableDelegate = simpleTableDelegate
)
}

View file

@ -62,6 +62,8 @@ import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
@ -72,6 +74,8 @@ import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.editor.pattern.DefaultPatternMatcher
import com.anytypeio.anytype.presentation.editor.editor.table.DefaultSimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.selection.SelectionStateHolder
import com.anytypeio.anytype.presentation.editor.template.EditorTemplateDelegate
@ -252,6 +256,14 @@ open class EditorPresentationTestSetup {
@Mock
lateinit var getTemplates: GetTemplates
@Mock
lateinit var createTable: CreateTable
@Mock
lateinit var fillTableRow: FillTableRow
lateinit var simpleTableDelegate: SimpleTableDelegate
protected val builder: UrlBuilder get() = UrlBuilder(gateway)
private lateinit var updateDetail: UpdateDetail
@ -274,6 +286,7 @@ open class EditorPresentationTestSetup {
setDocCoverImage = SetDocCoverImage(repo)
setDocImageIcon = SetDocumentImageIcon(repo)
downloadUnsplashImage = DownloadUnsplashImage(unsplashRepo)
simpleTableDelegate = DefaultSimpleTableDelegate()
orchestrator = Orchestrator(
createBlock = createBlock,
@ -312,7 +325,9 @@ open class EditorPresentationTestSetup {
setRelationKey = setRelationKey,
turnIntoStyle = turnIntoStyle,
updateBlocksMark = updateBlocksMark,
setObjectType = setObjectType
setObjectType = setObjectType,
createTable = createTable,
fillTableRow = fillTableRow
)
return EditorViewModel(
@ -350,7 +365,8 @@ open class EditorPresentationTestSetup {
setDocImageIcon = setDocImageIcon,
downloadUnsplashImage = downloadUnsplashImage,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject
createNewObject = createNewObject,
simpleTableDelegate = simpleTableDelegate
)
}

View file

@ -601,7 +601,8 @@ class EditorSlashWidgetClicksTest: EditorPresentationTestSetup() {
SlashItem.Subheader.OtherWithBack,
SlashItem.Other.Line,
SlashItem.Other.Dots,
SlashItem.Other.TOC
SlashItem.Other.TOC,
SlashItem.Other.Table()
)
val expected = SlashWidgetState.UpdateItems(

View file

@ -4,6 +4,7 @@ import android.os.Build
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_HEADER_TWO
import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_NUMBERED
@ -1243,7 +1244,8 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
SlashItem.Subheader.Other,
SlashItem.Other.Line,
SlashItem.Other.Dots,
SlashItem.Other.TOC
SlashItem.Other.TOC,
SlashItem.Other.Table()
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@ -1551,7 +1553,7 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
}
@Test
fun `should return table of contents on table filter`() {
fun `should return table of contents and simple table on table filter`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
@ -1582,8 +1584,225 @@ class EditorSlashWidgetFilterTest : EditorPresentationTestSetup() {
val expectedItems = listOf(
SlashItem.Subheader.Other,
SlashItem.Other.TOC
SlashItem.Other.TOC,
SlashItem.Other.Table()
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@Test
fun `slash item should have 9 rows and 7 columns`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
val fields = Block.Fields.empty()
val customDetails = Block.Details(mapOf(root to fields))
stubInterceptEvents()
stubGetObjectTypes(listOf())
stubOpenDocument(doc, customDetails)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(a.id, true)
onSlashTextWatcherEvent(SlashEvent.Start(100, 0))
}
// TESTING
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/", HOLDER_NUMBERED))
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/table9x7", HOLDER_NUMBERED))
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.widgetState as SlashWidgetState.UpdateItems
val expectedItems = listOf(
SlashItem.Subheader.Other,
SlashItem.Other.Table(
rowCount = 9,
columnCount = 7
)
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@Test
fun `slash item should have 19 rows and default max columns`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
val fields = Block.Fields.empty()
val customDetails = Block.Details(mapOf(root to fields))
stubInterceptEvents()
stubGetObjectTypes(listOf())
stubOpenDocument(doc, customDetails)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(a.id, true)
onSlashTextWatcherEvent(SlashEvent.Start(100, 0))
}
// TESTING
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/", HOLDER_NUMBERED))
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/table19x78t", HOLDER_NUMBERED))
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.widgetState as SlashWidgetState.UpdateItems
val expectedItems = listOf(
SlashItem.Subheader.Other,
SlashItem.Other.Table(
rowCount = 19,
columnCount = 25
)
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@Test
fun `slash item should have default max rows and default max columns`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
val fields = Block.Fields.empty()
val customDetails = Block.Details(mapOf(root to fields))
stubInterceptEvents()
stubGetObjectTypes(listOf())
stubOpenDocument(doc, customDetails)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(a.id, true)
onSlashTextWatcherEvent(SlashEvent.Start(100, 0))
}
// TESTING
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/", HOLDER_NUMBERED))
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/table199x78", HOLDER_NUMBERED))
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.widgetState as SlashWidgetState.UpdateItems
val expectedItems = listOf(
SlashItem.Subheader.Other,
SlashItem.Other.Table(
rowCount = CreateTable.DEFAULT_MAX_ROW_COUNT,
columnCount = CreateTable.DEFAULT_MAX_COLUMN_COUNT
)
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@Test
fun `slash item should have 5 rows and default min columns`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
val fields = Block.Fields.empty()
val customDetails = Block.Details(mapOf(root to fields))
stubInterceptEvents()
stubGetObjectTypes(listOf())
stubOpenDocument(doc, customDetails)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(a.id, true)
onSlashTextWatcherEvent(SlashEvent.Start(100, 0))
}
// TESTING
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/", HOLDER_NUMBERED))
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/table5", HOLDER_NUMBERED))
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.widgetState as SlashWidgetState.UpdateItems
val expectedItems = listOf(
SlashItem.Subheader.Other,
SlashItem.Other.Table(
rowCount = 5,
columnCount = CreateTable.DEFAULT_COLUMN_COUNT
)
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@Test
fun `slash item should have default max rows and default min columns`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
val fields = Block.Fields.empty()
val customDetails = Block.Details(mapOf(root to fields))
stubInterceptEvents()
stubGetObjectTypes(listOf())
stubOpenDocument(doc, customDetails)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(a.id, true)
onSlashTextWatcherEvent(SlashEvent.Start(100, 0))
}
// TESTING
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/", HOLDER_NUMBERED))
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/table26", HOLDER_NUMBERED))
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.widgetState as SlashWidgetState.UpdateItems
val expectedItems = listOf(
SlashItem.Subheader.Other,
SlashItem.Other.Table(
rowCount = CreateTable.DEFAULT_MAX_ROW_COUNT,
columnCount = CreateTable.DEFAULT_COLUMN_COUNT
)
)
assertEquals(expected = expectedItems, actual = command.otherItems)
}
@Test
fun `slash item tables should not be present`() {
// SETUP
val doc = MockTypicalDocumentFactory.page(root)
val a = MockTypicalDocumentFactory.a
val fields = Block.Fields.empty()
val customDetails = Block.Details(mapOf(root to fields))
stubInterceptEvents()
stubGetObjectTypes(listOf())
stubOpenDocument(doc, customDetails)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(a.id, true)
onSlashTextWatcherEvent(SlashEvent.Start(100, 0))
}
// TESTING
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/", HOLDER_NUMBERED))
vm.onSlashTextWatcherEvent(SlashEvent.Filter("/tablee5x8", HOLDER_NUMBERED))
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.widgetState as SlashWidgetState.UpdateItems
val expectedItems = emptyList<SlashItem>()
assertEquals(expected = expectedItems, actual = command.otherItems)
}
}

View file

@ -0,0 +1,586 @@
package com.anytypeio.anytype.presentation.editor.editor.table
import android.util.Log
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.StubBulleted
import com.anytypeio.anytype.core_models.StubHeader
import com.anytypeio.anytype.core_models.StubLayoutColumns
import com.anytypeio.anytype.core_models.StubLayoutRows
import com.anytypeio.anytype.core_models.StubNumbered
import com.anytypeio.anytype.core_models.StubParagraph
import com.anytypeio.anytype.core_models.StubTable
import com.anytypeio.anytype.core_models.StubTableColumn
import com.anytypeio.anytype.core_models.StubTableRow
import com.anytypeio.anytype.core_models.StubTitle
import com.anytypeio.anytype.core_models.ext.asMap
import com.anytypeio.anytype.core_models.ext.content
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.domain.config.Gateway
import com.anytypeio.anytype.domain.editor.Editor
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.render.BlockViewRenderer
import com.anytypeio.anytype.presentation.editor.render.DefaultBlockViewRenderer
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.TXT
import kotlinx.coroutines.runBlocking
import net.lachlanmckee.timberjunit.TimberTestRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
class TableBlockRendererTest {
class BlockViewRenderWrapper(
private val blocks: Map<Id, List<Block>>,
private val renderer: BlockViewRenderer,
private val restrictions: List<ObjectRestriction> = emptyList()
) : BlockViewRenderer by renderer {
suspend fun render(
root: Block,
anchor: Id,
focus: Editor.Focus,
indent: Int,
details: Block.Details
): List<BlockView> = blocks.render(
root = root,
anchor = anchor,
focus = focus,
indent = indent,
details = details,
relations = emptyList(),
restrictions = restrictions,
selection = emptySet(),
objectTypes = listOf()
)
}
@get:Rule
val timberTestRule: TimberTestRule = TimberTestRule.builder()
.minPriority(Log.DEBUG)
.showThread(true)
.showTimestamp(false)
.onlyLogWhenTestFails(true)
.build()
@Mock
lateinit var toggleStateHolder: ToggleStateHolder
@Mock
lateinit var gateway: Gateway
@Mock
lateinit var coverImageHashProvider: CoverImageHashProvider
private lateinit var renderer: DefaultBlockViewRenderer
private lateinit var wrapper: BlockViewRenderWrapper
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
renderer = DefaultBlockViewRenderer(
urlBuilder = UrlBuilder(gateway),
toggleStateHolder = toggleStateHolder,
coverImageHashProvider = coverImageHashProvider
)
}
@Test
fun `should return table block with columns, rows and text cells`() {
val blocksUpper = mutableListOf<Block>()
val blocksDown = mutableListOf<Block>()
for (i in 1..5) {
val blockU = StubBulleted()
blocksUpper.add(blockU)
val blockD = StubNumbered()
blocksDown.add(blockD)
}
val rowsSize = 6
val columnsSize = 4
val mapRows = mutableMapOf<String, List<Block>>()
val rows = mutableListOf<Block>()
val columns = mutableListOf<Block>()
for (i in 1..rowsSize) {
val rowId = "rowId$i"
val cells = mutableListOf<Block>()
for (j in 1..columnsSize) {
val columnId = "columnId$j"
val cellId = "$rowId-$columnId"
val p = StubParagraph(id = cellId)
cells.add(p)
}
val row = StubTableRow(id = rowId, children = cells.map { it.id })
rows.add(row)
mapRows[row.id] = cells
}
val layoutRow = StubLayoutRows(children = rows.map { it.id })
for (j in 1..columnsSize) {
columns.add(StubTableColumn(id = "columnId$j"))
}
val layoutColumn = StubLayoutColumns(children = columns.map { it.id })
val table = StubTable(children = listOf(layoutColumn.id, layoutRow.id))
val title = StubTitle()
val header = StubHeader(children = listOf(title.id))
val page = Block(
id = "page",
children = listOf(header.id) + blocksUpper.map { it.id } + listOf(table.id) + blocksDown.map { it.id },
fields = Block.Fields.empty(),
content = Block.Content.Smart()
)
val l = mutableListOf<Block>()
columns.forEach { l.add(it) }
l.add(layoutRow)
layoutRow.children.forEach { child ->
val row = rows.first { it.id == child }
l.add(row)
val cells = mapRows[row.id]
cells?.forEach { l.add(it) }
}
val blocks =
listOf(page, header) + blocksUpper + listOf(table, title, layoutColumn) + l + blocksDown
/**
* Should be 25 blocks + 6 rows + 4 columns + layoutRow + layoutColumn + table + title + header + 10 text blocks + page
*/
assertEquals(50, blocks.size)
val details = mapOf(page.id to Block.Fields.empty())
val map = blocks.asMap()
wrapper = BlockViewRenderWrapper(
blocks = map,
renderer = renderer
)
val result = runBlocking {
wrapper.render(
root = page,
anchor = page.id,
focus = Editor.Focus.empty(),
indent = 0,
details = Block.Details(details)
)
}
val cells = mutableListOf<BlockView.Table.Cell>()
columns.forEachIndexed { columnIndex, column ->
rows.forEachIndexed { rowIndex, row ->
val cell = mapRows[row.id]?.get(columnIndex)!!
val p = BlockView.Text.Paragraph(
id = cell.id,
text = cell.content.asText().text
)
cells.add(
BlockView.Table.Cell.Text(
block = p,
rowId = row.id,
columnId = column.id
)
)
}
}
cells.add(BlockView.Table.Cell.Space)
val columnViews = mutableListOf<BlockView.Table.Column>()
columns.forEach { column ->
columnViews.add(
BlockView.Table.Column(
id = column.id,
backgroundColor = column.backgroundColor
)
)
}
val expected = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<Block.Content.Text>().text,
image = null
)
) + blocksUpper.map { block: Block ->
BlockView.Text.Bulleted(
id = block.id,
text = block.content<TXT>().text
)
} + listOf(
BlockView.Table(
id = table.id,
cells = cells,
columns = columnViews,
rowCount = rowsSize,
isSelected = false
)
) + blocksDown.mapIndexed { idx, block ->
BlockView.Text.Numbered(
id = block.id,
text = block.content<TXT>().text,
number = idx.inc()
)
}
assertEquals(expected = expected, actual = result)
}
@Test
fun `should return table block with columns, rows and empty cells`() {
val blocksUpper = mutableListOf<Block>()
val blocksDown = mutableListOf<Block>()
for (i in 1..5) {
val blockU = StubBulleted()
blocksUpper.add(blockU)
val blockD = StubNumbered()
blocksDown.add(blockD)
}
val rowsSize = 6
val columnsSize = 4
val rows = mutableListOf<Block>()
val columns = mutableListOf<Block>()
for (i in 1..rowsSize) {
val rowId = "rowId$i"
rows.add(StubTableRow(id = rowId))
}
val layoutRow = StubLayoutRows(children = rows.map { it.id })
for (j in 1..columnsSize) {
columns.add(StubTableColumn(id = "columnId$j"))
}
val layoutColumn = StubLayoutColumns(children = columns.map { it.id })
val table = StubTable(children = listOf(layoutColumn.id, layoutRow.id))
val title = StubTitle()
val header = StubHeader(children = listOf(title.id))
val page = Block(
id = "page",
children = listOf(header.id) + blocksUpper.map { it.id } + listOf(table.id) + blocksDown.map { it.id },
fields = Block.Fields.empty(),
content = Block.Content.Smart()
)
val l = mutableListOf<Block>()
columns.forEach { l.add(it) }
l.add(layoutRow)
layoutRow.children.forEach { child ->
val row = rows.first { it.id == child }
l.add(row)
}
val blocks =
listOf(page, header) + blocksUpper + listOf(table, title, layoutColumn) + l + blocksDown
/**
* 6 rows + 4 columns + layoutRow + layoutColumn + table + title + header + 10 text blocks + page
*/
assertEquals(26, blocks.size)
val details = mapOf(page.id to Block.Fields.empty())
val map = blocks.asMap()
wrapper = BlockViewRenderWrapper(
blocks = map,
renderer = renderer
)
val result = runBlocking {
wrapper.render(
root = page,
anchor = page.id,
focus = Editor.Focus.empty(),
indent = 0,
details = Block.Details(details)
)
}
val cells = mutableListOf<BlockView.Table.Cell>()
columns.forEachIndexed { columnIndex, column ->
rows.forEachIndexed { rowIndex, row ->
cells.add(
BlockView.Table.Cell.Empty(
rowId = row.id,
columnId = column.id
)
)
}
}
cells.add(BlockView.Table.Cell.Space)
val columnViews = mutableListOf<BlockView.Table.Column>()
columns.forEach { column ->
columnViews.add(
BlockView.Table.Column(
id = column.id,
backgroundColor = column.backgroundColor
)
)
}
val expected = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<Block.Content.Text>().text,
image = null
)
) + blocksUpper.map { block: Block ->
BlockView.Text.Bulleted(
id = block.id,
text = block.content<TXT>().text
)
} + listOf(
BlockView.Table(
id = table.id,
cells = cells,
columns = columnViews,
rowCount = rowsSize,
isSelected = false
)
) + blocksDown.mapIndexed { idx, block ->
BlockView.Text.Numbered(
id = block.id,
text = block.content<TXT>().text,
number = idx.inc()
)
}
assertEquals(expected = expected, actual = result)
}
@Test
fun `should return table block with columns, rows and empty plus text cells`() {
val blocksUpper = mutableListOf<Block>()
val blocksDown = mutableListOf<Block>()
for (i in 1..5) {
val blockU = StubBulleted()
blocksUpper.add(blockU)
val blockD = StubNumbered()
blocksDown.add(blockD)
}
val rowsSize = 3
val columnsSize = 4
val mapRows = mutableMapOf<String, List<Block>>()
val rows = mutableListOf<Block>()
val columns = mutableListOf<Block>()
val rowId1 = "rowId1"
val rowId2 = "rowId2"
val rowId3 = "rowId3"
val columnId1 = "columnId1"
val columnId2 = "columnId2"
val columnId3 = "columnId3"
val columnId4 = "columnId4"
val row1Block1 = StubParagraph(id = "$rowId1-$columnId2")
val row1Block2 = StubParagraph(id = "$rowId1-$columnId4")
val row2Block1 = StubParagraph(id = "$rowId2-$columnId1")
val row2Block2 = StubParagraph(id = "$rowId2-$columnId2")
val row2Block3 = StubParagraph(id = "$rowId2-$columnId4")
rows.apply {
add(StubTableRow(rowId1, listOf(row1Block1.id, row1Block2.id)))
add(StubTableRow(rowId2, listOf(row2Block1.id, row2Block2.id, row2Block3.id)))
add(StubTableRow(rowId3))
mapRows[rowId1] = listOf(row1Block1, row1Block2)
mapRows[rowId2] = listOf(row2Block1, row2Block2, row2Block3)
}
val layoutRow = StubLayoutRows(children = rows.map { it.id })
for (j in 1..columnsSize) {
columns.add(StubTableColumn(id = "columnId$j"))
}
val layoutColumn = StubLayoutColumns(children = columns.map { it.id })
val table = StubTable(children = listOf(layoutColumn.id, layoutRow.id))
val title = StubTitle()
val header = StubHeader(children = listOf(title.id))
val page = Block(
id = "page",
children = listOf(header.id) + blocksUpper.map { it.id } + listOf(table.id) + blocksDown.map { it.id },
fields = Block.Fields.empty(),
content = Block.Content.Smart()
)
val l = mutableListOf<Block>()
columns.forEach { l.add(it) }
l.add(layoutRow)
layoutRow.children.forEach { child ->
val row = rows.first { it.id == child }
l.add(row)
val cells = mapRows[row.id]
cells?.forEach { l.add(it) }
}
val blocks =
listOf(page, header) + blocksUpper + listOf(table, title, layoutColumn) + l + blocksDown
assertEquals(28, blocks.size)
val details = mapOf(page.id to Block.Fields.empty())
val map = blocks.asMap()
wrapper = BlockViewRenderWrapper(
blocks = map,
renderer = renderer
)
val result = runBlocking {
wrapper.render(
root = page,
anchor = page.id,
focus = Editor.Focus.empty(),
indent = 0,
details = Block.Details(details)
)
}
val cells =
listOf(
BlockView.Table.Cell.Empty(
rowId = rowId1,
columnId = columnId1
),
BlockView.Table.Cell.Text(
rowId = rowId2,
columnId = columnId1,
block = BlockView.Text.Paragraph(
id = row2Block1.id,
text = row2Block1.content.asText().text
)
),
BlockView.Table.Cell.Empty(
rowId = rowId3,
columnId = columnId1
), //column1
BlockView.Table.Cell.Text(
rowId = rowId1,
columnId = columnId2,
block = BlockView.Text.Paragraph(
id = row1Block1.id,
text = row1Block1.content.asText().text
)
),
BlockView.Table.Cell.Text(
rowId = rowId2,
columnId = columnId2,
block = BlockView.Text.Paragraph(
id = row2Block2.id,
text = row2Block2.content.asText().text
)
),
BlockView.Table.Cell.Empty(
rowId = rowId3,
columnId = columnId2
),//column2
BlockView.Table.Cell.Empty(
rowId = rowId1,
columnId = columnId3
),
BlockView.Table.Cell.Empty(
rowId = rowId2,
columnId = columnId3
),
BlockView.Table.Cell.Empty(
rowId = rowId3,
columnId = columnId3
),//column3
BlockView.Table.Cell.Text(
rowId = rowId1,
columnId = columnId4,
block = BlockView.Text.Paragraph(
id = row1Block2.id,
text = row1Block2.content.asText().text
)
),
BlockView.Table.Cell.Text(
rowId = rowId2,
columnId = columnId4,
block = BlockView.Text.Paragraph(
id = row2Block3.id,
text = row2Block3.content.asText().text
)
),
BlockView.Table.Cell.Empty(
rowId = rowId3,
columnId = columnId4
),
BlockView.Table.Cell.Space
)
val columnViews = mutableListOf<BlockView.Table.Column>()
columns.forEach { column ->
columnViews.add(
BlockView.Table.Column(
id = column.id,
backgroundColor = column.backgroundColor
)
)
}
val expected = listOf(
BlockView.Title.Basic(
id = title.id,
isFocused = false,
text = title.content<Block.Content.Text>().text,
image = null
)
) + blocksUpper.map { block: Block ->
BlockView.Text.Bulleted(
id = block.id,
text = block.content<TXT>().text
)
} + listOf(
BlockView.Table(
id = table.id,
cells = cells,
columns = columnViews,
rowCount = rowsSize,
isSelected = false
)
) + blocksDown.mapIndexed { idx, block ->
BlockView.Text.Numbered(
id = block.id,
text = block.content<TXT>().text,
number = idx.inc()
)
}
assertEquals(expected = expected, actual = result)
}
}

View file

@ -4,9 +4,10 @@ import com.anytypeio.anytype.presentation.MockBlockContentFactory.StubTextConten
import com.anytypeio.anytype.test_utils.MockDataFactory
fun StubHeader(
id: Id = MockDataFactory.randomUuid(),
children: List<Id> = emptyList()
): Block = Block(
id = MockDataFactory.randomUuid(),
id = id,
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
@ -15,9 +16,10 @@ fun StubHeader(
)
fun StubTitle(
id: Id = MockDataFactory.randomUuid(),
text: String = MockDataFactory.randomString()
): Block = Block(
id = MockDataFactory.randomUuid(),
id = id,
content = StubTextContent(
text = text,
style = Block.Content.Text.Style.TITLE
@ -107,12 +109,13 @@ fun StubFile(
)
fun StubBulleted(
id: Id = MockDataFactory.randomUuid(),
text: String = MockDataFactory.randomString(),
children: List<Id> = emptyList(),
marks: List<Block.Content.Text.Mark> = emptyList(),
isChecked: Boolean = MockDataFactory.randomBoolean()
): Block = Block(
id = MockDataFactory.randomUuid(),
id = id,
content = StubTextContent(
text = text,
style = Block.Content.Text.Style.BULLET,
@ -139,11 +142,12 @@ fun StubToggle(
)
fun StubNumbered(
id: Id = MockDataFactory.randomUuid(),
text: String = MockDataFactory.randomString(),
children: List<Id> = emptyList(),
marks: List<Block.Content.Text.Mark> = emptyList()
): Block = Block(
id = MockDataFactory.randomUuid(),
id = id,
content = StubTextContent(
text = text,
style = Block.Content.Text.Style.NUMBERED,
@ -230,4 +234,58 @@ fun StubSmartBlock(
children = children,
fields = Block.Fields.empty(),
content = Block.Content.Smart()
)
fun StubTable(
id: Id = MockDataFactory.randomUuid(),
children: List<Id> = emptyList(),
background: String? = null
): Block = Block(
id = id,
content = Block.Content.Table,
children = children,
fields = Block.Fields.empty(),
backgroundColor = background
)
fun StubLayoutRows(
id: Id = MockDataFactory.randomUuid(),
children: List<Id> = emptyList(),
): Block = Block(
id = id,
content = Block.Content.Layout(type = Block.Content.Layout.Type.TABLE_ROW),
children = children,
fields = Block.Fields.empty(),
)
fun StubLayoutColumns(
id: Id = MockDataFactory.randomUuid(),
children: List<Id> = emptyList(),
): Block = Block(
id = id,
content = Block.Content.Layout(type = Block.Content.Layout.Type.TABLE_COLUMN),
children = children,
fields = Block.Fields.empty(),
)
fun StubTableRow(
id: Id = MockDataFactory.randomUuid(),
children: List<Id> = emptyList(),
): Block = Block(
id = id,
content = Block.Content.TableRow(false),
children = children,
fields = Block.Fields.empty(),
)
fun StubTableColumn(
id: Id = MockDataFactory.randomUuid(),
children: List<Id> = emptyList(),
background: String? = null
): Block = Block(
id = id,
content = Block.Content.TableColumn,
children = children,
fields = Block.Fields.empty(),
backgroundColor = background
)