diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt index 85ee64e6a5..39c7748893 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/editor/base/EditorTestSetup.kt @@ -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 ) } diff --git a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt index c525d93d49..456f210114 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt @@ -294,6 +294,13 @@ class ComponentManager( .build() } + val setTextBlockValueComponent = DependentComponentMap { ctx -> + editorComponent + .get(ctx) + .setBlockTextValueComponent() + .build() + } + val createBookmarkSubComponent = Component { main .createBookmarkBuilder() diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/EditorDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/EditorDI.kt index 1975a93333..cf45584151 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/EditorDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/EditorDI.kt @@ -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 diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/SetBlockTextValueDi.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/SetBlockTextValueDi.kt new file mode 100644 index 0000000000..e25039f556 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/SetBlockTextValueDi.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt index c0b0433f7b..627ac4fc42 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt @@ -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(R.layout.f binding.typeHasTemplateToolbar.id -> { vm.onTypeHasTemplateToolbarHidden() } + binding.simpleTableWidget.id -> { + vm.onHideSimpleTableWidget() + } } } } @@ -472,6 +477,20 @@ open class EditorFragment : NavigationFragment(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(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(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) } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/modals/SetBlockTextValueFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/modals/SetBlockTextValueFragment.kt new file mode 100644 index 0000000000..25c2773d8e --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/modals/SetBlockTextValueFragment.kt @@ -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(), 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 { onSetTextBlockValue() } + dismiss() + } + SetBlockTextValueViewModel.ViewState.Loading -> { + } + is SetBlockTextValueViewModel.ViewState.Success -> { + blockAdapter.updateWithDiffUtil(state.data) + } + is SetBlockTextValueViewModel.ViewState.OnMention -> { + withParent { 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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_editor.xml b/app/src/main/res/layout/fragment_editor.xml index 443dd37881..41d74c3f6d 100644 --- a/app/src/main/res/layout/fragment_editor.xml +++ b/app/src/main/res/layout/fragment_editor.xml @@ -279,6 +279,18 @@ app:cardUseCompatPadding="true" app:layout_behavior="@string/bottom_sheet_behavior"/> + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index adcfe4281b..160f0305b0 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ buildscript { ext.test_runner = 'androidx.test.runner.AndroidJUnitRunner' repositories { + mavenLocal() google() mavenCentral() } diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt index b81fd8bd28..517088d56d 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt @@ -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() } /** diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt index c46f41562f..d3d8ce4ab2 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt @@ -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) { diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/ext/EditorHolderExtensions.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/ext/EditorHolderExtensions.kt index 8889ad3423..51e08aa6e4 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/ext/EditorHolderExtensions.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/ext/EditorHolderExtensions.kt @@ -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 } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Text.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Text.kt index e4b25953db..4042449001 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Text.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Text.kt @@ -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 } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/slash/holders/OtherMenuHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/slash/holders/OtherMenuHolder.kt index a4a15c64b1..a4363f0248 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/slash/holders/OtherMenuHolder.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/slash/holders/OtherMenuHolder.kt @@ -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) + } } } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/TableBlockAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/TableBlockAdapter.kt new file mode 100644 index 0000000000..98291ba1db --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/TableBlockAdapter.kt @@ -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(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 + ) { + if (payloads.isEmpty()) { + onBindViewHolder(holder, position) + } else { + if (holder is TableCellHolder.TableTextCellHolder) { + holder.processChangePayload( + payloads = payloads.typeOf().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 + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/TableCellsDiffUtil.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/TableCellsDiffUtil.kt new file mode 100644 index 0000000000..63833264fc --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/TableCellsDiffUtil.kt @@ -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() { + + 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() + 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 + ) { + 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 +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/holders/TableBlockHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/holders/TableBlockHolder.kt new file mode 100644 index 0000000000..3bf3b21a50 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/holders/TableBlockHolder.kt @@ -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, + 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) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/holders/TableTextCellHolder.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/holders/TableTextCellHolder.kt new file mode 100644 index 0000000000..df86049132 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/table/holders/TableTextCellHolder.kt @@ -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) +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/TableHorizontalItemDivider.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/TableHorizontalItemDivider.kt new file mode 100644 index 0000000000..bb311aff88 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/TableHorizontalItemDivider.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/TableVerticalItemDivider.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/TableVerticalItemDivider.kt new file mode 100644 index 0000000000..3785fd6071 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/layout/TableVerticalItemDivider.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/TextInputWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/TextInputWidget.kt index 74a63b19bc..84ccbc02b7 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/TextInputWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/text/TextInputWidget.kt @@ -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 } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableSettingAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableSettingAdapter.kt new file mode 100644 index 0000000000..197249800a --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableSettingAdapter.kt @@ -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() { + + 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) + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableSettingWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableSettingWidget.kt new file mode 100644 index 0000000000..58b5733503 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableSettingWidget.kt @@ -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() + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableWidgetAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableWidgetAdapter.kt new file mode 100644 index 0000000000..191c5aaf87 --- /dev/null +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/toolbar/table/SimpleTableWidgetAdapter.kt @@ -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, + private val onClick: (SimpleTableWidgetItem) -> Unit +) : + RecyclerView.Adapter() { + + fun update(items: List) { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/bg_editor_table_cell.xml b/core-ui/src/main/res/drawable/bg_editor_table_cell.xml new file mode 100644 index 0000000000..f156edc838 --- /dev/null +++ b/core-ui/src/main/res/drawable/bg_editor_table_cell.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/bg_editor_table_cell_selected.xml b/core-ui/src/main/res/drawable/bg_editor_table_cell_selected.xml new file mode 100644 index 0000000000..1a85862a2e --- /dev/null +++ b/core-ui/src/main/res/drawable/bg_editor_table_cell_selected.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/bg_editor_table_cell_selector.xml b/core-ui/src/main/res/drawable/bg_editor_table_cell_selector.xml new file mode 100644 index 0000000000..c83dcc1b9e --- /dev/null +++ b/core-ui/src/main/res/drawable/bg_editor_table_cell_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/bg_table_cell_board_all.xml b/core-ui/src/main/res/drawable/bg_table_cell_board_all.xml new file mode 100644 index 0000000000..67ad00ab12 --- /dev/null +++ b/core-ui/src/main/res/drawable/bg_table_cell_board_all.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/ic_action_sort.xml b/core-ui/src/main/res/drawable/ic_action_sort.xml new file mode 100644 index 0000000000..14ebd74d69 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_action_sort.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core-ui/src/main/res/drawable/ic_add_row_above.xml b/core-ui/src/main/res/drawable/ic_add_row_above.xml new file mode 100644 index 0000000000..d6c315b848 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_add_row_above.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_add_row_below.xml b/core-ui/src/main/res/drawable/ic_add_row_below.xml new file mode 100644 index 0000000000..3cd9ac7350 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_add_row_below.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_cell_menu.xml b/core-ui/src/main/res/drawable/ic_cell_menu.xml new file mode 100644 index 0000000000..758c1fc222 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_cell_menu.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core-ui/src/main/res/drawable/ic_column_insert_left.xml b/core-ui/src/main/res/drawable/ic_column_insert_left.xml new file mode 100644 index 0000000000..43f5331903 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_column_insert_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_column_insert_right.xml b/core-ui/src/main/res/drawable/ic_column_insert_right.xml new file mode 100644 index 0000000000..f4a7359e63 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_column_insert_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_move_column_left.xml b/core-ui/src/main/res/drawable/ic_move_column_left.xml new file mode 100644 index 0000000000..55c304d4fd --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_move_column_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_move_column_right.xml b/core-ui/src/main/res/drawable/ic_move_column_right.xml new file mode 100644 index 0000000000..d643b6f79d --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_move_column_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_move_row_down.xml b/core-ui/src/main/res/drawable/ic_move_row_down.xml new file mode 100644 index 0000000000..8e999837a7 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_move_row_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_move_row_up.xml b/core-ui/src/main/res/drawable/ic_move_row_up.xml new file mode 100644 index 0000000000..d5034d980e --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_move_row_up.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_slash_simple_tables.xml b/core-ui/src/main/res/drawable/ic_slash_simple_tables.xml new file mode 100644 index 0000000000..0cb6b2a2b4 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_slash_simple_tables.xml @@ -0,0 +1,10 @@ + + + diff --git a/core-ui/src/main/res/layout/item_block_table.xml b/core-ui/src/main/res/layout/item_block_table.xml new file mode 100644 index 0000000000..b3fcfdc45e --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_table.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_table_column_header.xml b/core-ui/src/main/res/layout/item_block_table_column_header.xml new file mode 100644 index 0000000000..78458b5ddd --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_table_column_header.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_table_row_item.xml b/core-ui/src/main/res/layout/item_block_table_row_item.xml new file mode 100644 index 0000000000..7eadc7b3af --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_table_row_item.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_table_row_item_empty.xml b/core-ui/src/main/res/layout/item_block_table_row_item_empty.xml new file mode 100644 index 0000000000..740f9c6d6a --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_table_row_item_empty.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_block_table_space.xml b/core-ui/src/main/res/layout/item_block_table_space.xml new file mode 100644 index 0000000000..35df0898c5 --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_table_space.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_simple_table_action.xml b/core-ui/src/main/res/layout/item_simple_table_action.xml new file mode 100644 index 0000000000..980c89dcc5 --- /dev/null +++ b/core-ui/src/main/res/layout/item_simple_table_action.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/item_simple_table_widget_recent.xml b/core-ui/src/main/res/layout/item_simple_table_widget_recent.xml new file mode 100644 index 0000000000..cdc649b1ca --- /dev/null +++ b/core-ui/src/main/res/layout/item_simple_table_widget_recent.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/core-ui/src/main/res/layout/widget_simple_table.xml b/core-ui/src/main/res/layout/widget_simple_table.xml new file mode 100644 index 0000000000..8a9977e3d9 --- /dev/null +++ b/core-ui/src/main/res/layout/widget_simple_table.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/main/res/values/colors.xml b/core-ui/src/main/res/values/colors.xml index c1968e52fe..7b9415a2e4 100644 --- a/core-ui/src/main/res/values/colors.xml +++ b/core-ui/src/main/res/values/colors.xml @@ -179,5 +179,7 @@ #F55522 #CC0066C3 + #FFC532 + #1A50491C diff --git a/core-ui/src/main/res/values/dimens.xml b/core-ui/src/main/res/values/dimens.xml index 72cc2061b8..ad4f94bf49 100644 --- a/core-ui/src/main/res/values/dimens.xml +++ b/core-ui/src/main/res/values/dimens.xml @@ -288,4 +288,9 @@ 12dp 2dp + 140dp + 63dp + 12dp + 9dp + 20dp \ No newline at end of file diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index 1b9068ccfd..ef7b6a079e 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -384,6 +384,9 @@ Line divider Dots divider Table of contents + Simple table + Simple table %1$dx%2$d + Create a simple table Delete Duplicate @@ -525,4 +528,20 @@ Type Restore + Clear contents + Color + Style + Clear style + Insert left + Insert right + Move left + Move right + Insert above + Insert below + Move up + Move down + Cell + Row + Column + diff --git a/core-ui/src/main/res/values/styles.xml b/core-ui/src/main/res/values/styles.xml index b16cfe508b..0efc5370b8 100644 --- a/core-ui/src/main/res/values/styles.xml +++ b/core-ui/src/main/res/values/styles.xml @@ -983,4 +983,30 @@ 15sp + + + + + + + + + \ No newline at end of file diff --git a/core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/editor/table/TableBlockTest.kt b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/editor/table/TableBlockTest.kt new file mode 100644 index 0000000000..ed85ac38db --- /dev/null +++ b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/features/editor/table/TableBlockTest.kt @@ -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 + + @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.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.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(com.anytypeio.anytype.test_utils.R.id.recycler).apply { + layoutManager = LinearLayoutManager(context) + } +} \ No newline at end of file diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/const/SlashConst.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/const/SlashConst.kt index e8a1400096..ee20b439a3 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/const/SlashConst.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/const/SlashConst.kt @@ -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" diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt index 549953c960..9206b90ee6 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt @@ -75,4 +75,6 @@ fun String.isEndLineClick(range: IntRange): Boolean = range.first == length && r inline fun Fragment.withParent(action: T.() -> Unit) { check(parentFragment is T) { "Parent is not ${T::class.java}. Please specify correct type" } (parentFragment as T).action() -} \ No newline at end of file +} + +fun MatchResult?.parseMatchedInt(index: Int): Int? = this?.groups?.get(index)?.value?.toIntOrNull() \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index dfe47dfeea..dfeadc7af5 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -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): Payload = + remote.fillTableRow(ctx, targetIds) } \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataStore.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataStore.kt index 28c31a008d..fc30f03f6a 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataStore.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataStore.kt @@ -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): Payload } \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index c4d0c8813e..7fa3596129 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -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): Payload } \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemoteDataStore.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemoteDataStore.kt index 6ca9f63dc2..23c95de5a1 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemoteDataStore.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemoteDataStore.kt @@ -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): Payload = + remote.fillTableRow(ctx, targetIds) } \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 56772491ad..9a37f02eca 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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", diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index fadd809e32..eca95aba9a 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -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): Payload } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/table/CreateTable.kt b/domain/src/main/java/com/anytypeio/anytype/domain/table/CreateTable.kt new file mode 100644 index 0000000000..545db08242 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/table/CreateTable.kt @@ -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() { + + override suspend fun run(params: Params): Either = 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 + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/table/FillTableRow.kt b/domain/src/main/java/com/anytypeio/anytype/domain/table/FillTableRow.kt new file mode 100644 index 0000000000..af1af6bc06 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/table/FillTableRow.kt @@ -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() { + + override suspend fun run(params: Params): Either = 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 + ) +} \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt index e165e77e33..51348086f1 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt @@ -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): Payload = + middleware.fillTableRow(ctx, targetIds) } \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index 9964189f10..c80b843865 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -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): 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) diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt index e8d5c3c9c1..a47aa55add 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt @@ -145,6 +145,33 @@ fun List.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 diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt index 6e83049e7c..0a934b97da 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt @@ -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) diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt index af7fa05ecd..8a8ddb0505 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt @@ -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 + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt index 854e0d6776..35ba6b00b4 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt @@ -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(), PickerListener, @@ -246,6 +253,7 @@ class EditorViewModel( ToggleStateHolder by renderer, SelectionStateHolder by orchestrator.memory.selections, EditorTemplateDelegate by templateDelegate, + SimpleTableDelegate by simpleTableDelegate, StateReducer, 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, 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 diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModelFactory.kt index 2bc992117d..3b6a3297dc 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModelFactory.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModelFactory.kt @@ -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 } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Command.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Command.kt index a3926dbbb6..901abc310b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Command.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Command.kt @@ -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() } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Intent.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Intent.kt index f67d4e7e82..4b5f9bd499 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Intent.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Intent.kt @@ -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 + ) : Table() + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Orchestrator.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Orchestrator.kt index beae4aec67..02fa01e3e4 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Orchestrator.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/Orchestrator.kt @@ -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) } + ) + } } } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt index dae92d1fcf..fcd7527c9c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/ext/BlockViewExt.kt @@ -337,6 +337,9 @@ fun List.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.fillTableOfContents(): List { fun BlockView.Text.isStyleClearable(): Boolean { return this.isListBlock || this is BlockView.Text.Highlight +} + +fun List.applyBordersToSelectedCells( + tableId: Id, + selection: Set +): List = 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.removeBordersFromCells(): List = 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 + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt index e7cb273049..fd1591f64a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/listener/ListenerType.kt @@ -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 } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt index 89cef83a46..61f2f0e081 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt @@ -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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, - override val decorations: List = emptyList() + override val decorations: List = 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 = emptyList(), override val ghostEditorSelection: IntRange? = null, override val decorations: List = 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 = 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, + val cells: List, + 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() + } + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt index de0200953f..092214d98b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt @@ -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 } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashExtensions.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashExtensions.kt index 0ac7b70aa5..1ebead4049 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashExtensions.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashExtensions.kt @@ -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.toSlashItemView(): List = 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 { 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) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashItem.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashItem.kt index 4a3490d82e..756a49e600 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashItem.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/slash/SlashItem.kt @@ -332,6 +332,22 @@ sealed class SlashItem { override fun getSearchName(): String = SlashConst.SLASH_OTHER_TOC override fun getAbbreviation(): List = 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 = emptyList() + + companion object { + const val DEFAULT_PATTERN = "table(\\d+)(?:[^\\d]{1}([\\d]+))?" + } + } + } //endregion diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableDelegate.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableDelegate.kt new file mode 100644 index 0000000000..17aa79fd7b --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableDelegate.kt @@ -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 + suspend fun onSimpleTableEvent(event: SimpleTableWidgetEvent) +} + +class DefaultSimpleTableDelegate : SimpleTableDelegate { + + private val events = MutableSharedFlow(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) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetEvent.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetEvent.kt new file mode 100644 index 0000000000..ea49fe61b2 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetEvent.kt @@ -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 +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetItem.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetItem.kt new file mode 100644 index 0000000000..c3be0cc0ce --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetItem.kt @@ -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() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetState.kt new file mode 100644 index 0000000000..1d33bde0ef --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetState.kt @@ -0,0 +1,24 @@ +package com.anytypeio.anytype.presentation.editor.editor.table + +sealed class SimpleTableWidgetState { + + object Idle : SimpleTableWidgetState() + + data class UpdateItems( + val cellItems: List, + val columnItems: List, + val rowItems: List + ) : SimpleTableWidgetState() { + companion object { + fun empty() = UpdateItems( + cellItems = emptyList(), + columnItems = emptyList(), + rowItems = emptyList() + ) + } + } + + companion object { + fun init(): SimpleTableWidgetState = Idle + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetViewState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetViewState.kt new file mode 100644 index 0000000000..a02e3c7a80 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/table/SimpleTableWidgetViewState.kt @@ -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() +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt index ae3fa22ffe..dd556febfa 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt @@ -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, + blocks: Map> + ): BlockView.Table { + + var cells: List = emptyList() + var columns: List = 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>, + rows: List, + columns: List, + mode: EditorMode, + focus: Focus, + indent: Int, + details: Block.Details, + selection: Set + ): List { + val cells = mutableListOf() + 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, + 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, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/block/SetBlockTextValueViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/block/SetBlockTextValueViewModel.kt new file mode 100644 index 0000000000..7ef3b264a6 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/block/SetBlockTextValueViewModel.kt @@ -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 get() = storage.views.current() + val state = MutableStateFlow(ViewState.Loading) + private val jobs = mutableListOf() + + 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: List + ) { + 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 create(modelClass: Class): T { + return SetBlockTextValueViewModel( + updateText = updateText, + storage = storage + ) as T + } + } + + sealed class ViewState { + data class Success(val data: List) : ViewState() + data class OnMention(val targetId: String) : ViewState() + object Exit : ViewState() + object Loading : ViewState() + } +} \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/EditorViewModelTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/EditorViewModelTest.kt index d473e0329c..575095560e 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/EditorViewModelTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/EditorViewModelTest.kt @@ -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 ) } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorPresentationTestSetup.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorPresentationTestSetup.kt index 34988bcd97..e28f5e5cbd 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorPresentationTestSetup.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorPresentationTestSetup.kt @@ -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 ) } diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetClicksTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetClicksTest.kt index 6a5614363a..a8f5ece80c 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetClicksTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetClicksTest.kt @@ -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( diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetFilterTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetFilterTest.kt index a358175701..b2c39cc159 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetFilterTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/EditorSlashWidgetFilterTest.kt @@ -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() + assertEquals(expected = expectedItems, actual = command.otherItems) + } } \ No newline at end of file diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/table/TableBlockRendererTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/table/TableBlockRendererTest.kt new file mode 100644 index 0000000000..e9ecc484f5 --- /dev/null +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/editor/editor/table/TableBlockRendererTest.kt @@ -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>, + private val renderer: BlockViewRenderer, + private val restrictions: List = emptyList() + ) : BlockViewRenderer by renderer { + suspend fun render( + root: Block, + anchor: Id, + focus: Editor.Focus, + indent: Int, + details: Block.Details + ): List = 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() + val blocksDown = mutableListOf() + + 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>() + val rows = mutableListOf() + val columns = mutableListOf() + + for (i in 1..rowsSize) { + val rowId = "rowId$i" + val cells = mutableListOf() + 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() + 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() + 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() + + 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().text, + image = null + ) + ) + blocksUpper.map { block: Block -> + BlockView.Text.Bulleted( + id = block.id, + text = block.content().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().text, + number = idx.inc() + ) + } + + assertEquals(expected = expected, actual = result) + } + + @Test + fun `should return table block with columns, rows and empty cells`() { + + val blocksUpper = mutableListOf() + val blocksDown = mutableListOf() + + 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() + val columns = mutableListOf() + + 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() + 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() + 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() + + 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().text, + image = null + ) + ) + blocksUpper.map { block: Block -> + BlockView.Text.Bulleted( + id = block.id, + text = block.content().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().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() + val blocksDown = mutableListOf() + + 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>() + val rows = mutableListOf() + val columns = mutableListOf() + + 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() + 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() + + 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().text, + image = null + ) + ) + blocksUpper.map { block: Block -> + BlockView.Text.Bulleted( + id = block.id, + text = block.content().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().text, + number = idx.inc() + ) + } + + assertEquals(expected = expected, actual = result) + } +} \ No newline at end of file diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt index 43cd9e8964..9d714343b9 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt @@ -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 = 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 = emptyList(), marks: List = 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 = emptyList(), marks: List = 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 = 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 = 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 = 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 = emptyList(), +): Block = Block( + id = id, + content = Block.Content.TableRow(false), + children = children, + fields = Block.Fields.empty(), +) + +fun StubTableColumn( + id: Id = MockDataFactory.randomUuid(), + children: List = emptyList(), + background: String? = null +): Block = Block( + id = id, + content = Block.Content.TableColumn, + children = children, + fields = Block.Fields.empty(), + backgroundColor = background ) \ No newline at end of file