anytype-kotlin-wild/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt

2178 lines
84 KiB
Kotlin

package com.anytypeio.anytype.ui.editor
import android.animation.ObjectAnimator
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.Intent
import android.graphics.Point
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
import android.view.animation.OvershootInterpolator
import android.widget.PopupMenu
import android.widget.TextView
import androidx.activity.addCallback
import androidx.annotation.RequiresApi
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.ChangeBounds
import androidx.transition.Fade
import androidx.transition.Slide
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_ui.extensions.addTextFromSelectedStart
import com.anytypeio.anytype.core_ui.extensions.color
import com.anytypeio.anytype.core_ui.extensions.cursorYBottomCoordinate
import com.anytypeio.anytype.core_ui.extensions.getLabelText
import com.anytypeio.anytype.core_ui.extensions.getToastMsg
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.scrollandmove.DefaultScrollAndMoveTargetDescriptor
import com.anytypeio.anytype.core_ui.features.editor.scrollandmove.ScrollAndMoveStateListener
import com.anytypeio.anytype.core_ui.features.editor.scrollandmove.ScrollAndMoveTargetHighlighter
import com.anytypeio.anytype.core_ui.menu.ObjectTypePopupMenu
import com.anytypeio.anytype.core_ui.reactive.clicks
import com.anytypeio.anytype.core_ui.reactive.longClicks
import com.anytypeio.anytype.core_ui.tools.ClipboardInterceptor
import com.anytypeio.anytype.core_ui.tools.EditorHeaderOverlayDetector
import com.anytypeio.anytype.core_ui.tools.LastItemBottomOffsetDecorator
import com.anytypeio.anytype.core_ui.tools.MarkupColorToolbarFooter
import com.anytypeio.anytype.core_ui.tools.MentionFooterItemDecorator
import com.anytypeio.anytype.core_ui.tools.NoteHeaderItemDecorator
import com.anytypeio.anytype.core_ui.tools.OutsideClickDetector
import com.anytypeio.anytype.core_ui.tools.SlashWidgetFooterItemDecorator
import com.anytypeio.anytype.core_ui.tools.StyleToolbarItemDecorator
import com.anytypeio.anytype.core_ui.widgets.FeaturedRelationGroupWidget
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_ui.widgets.toolbar.BlockToolbarWidget
import com.anytypeio.anytype.core_ui.widgets.toolbar.ChooseTypeHorizontalWidget
import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.core_utils.const.FileConstants.REQUEST_PROFILE_IMAGE_CODE
import com.anytypeio.anytype.core_utils.ext.PopupExtensions.calculateRectInWindow
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.cancel
import com.anytypeio.anytype.core_utils.ext.clipboard
import com.anytypeio.anytype.core_utils.ext.containsItemDecoration
import com.anytypeio.anytype.core_utils.ext.dimen
import com.anytypeio.anytype.core_utils.ext.drawable
import com.anytypeio.anytype.core_utils.ext.focusAndShowKeyboard
import com.anytypeio.anytype.core_utils.ext.gone
import com.anytypeio.anytype.core_utils.ext.hide
import com.anytypeio.anytype.core_utils.ext.hideKeyboard
import com.anytypeio.anytype.core_utils.ext.hideSoftInput
import com.anytypeio.anytype.core_utils.ext.invisible
import com.anytypeio.anytype.core_utils.ext.lastDecorator
import com.anytypeio.anytype.core_utils.ext.safeNavigate
import com.anytypeio.anytype.core_utils.ext.screen
import com.anytypeio.anytype.core_utils.ext.show
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.syncTranslationWithImeVisibility
import com.anytypeio.anytype.core_utils.ext.throttleFirst
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ui.showActionableSnackBar
import com.anytypeio.anytype.databinding.FragmentEditorBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.ext.extractMarks
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.presentation.editor.EditorViewModelFactory
import com.anytypeio.anytype.presentation.editor.Snack
import com.anytypeio.anytype.presentation.editor.editor.Command
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.ViewState
import com.anytypeio.anytype.presentation.editor.editor.control.ControlPanelState
import com.anytypeio.anytype.presentation.editor.editor.control.ControlPanelState.Toolbar.Main
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.markup.MarkupColorView
import com.anytypeio.anytype.presentation.editor.model.EditorFooter
import com.anytypeio.anytype.presentation.editor.template.SelectTemplateViewState
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.presentation.sync.SyncStatusView
import com.anytypeio.anytype.ui.alert.AlertUpdateAppFragment
import com.anytypeio.anytype.ui.base.NavigationFragment
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.editor.cover.SelectCoverObjectFragment
import com.anytypeio.anytype.ui.editor.gallery.FullScreenPictureFragment
import com.anytypeio.anytype.ui.editor.layout.ObjectLayoutFragment
import com.anytypeio.anytype.ui.editor.modals.CreateBookmarkFragment
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.SetBlockTextValueFragment
import com.anytypeio.anytype.ui.editor.modals.TextBlockIconPickerFragment
import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuBaseFragment.DocumentMenuActionReceiver
import com.anytypeio.anytype.ui.editor.sheets.ObjectMenuFragment
import com.anytypeio.anytype.ui.linking.LinkToObjectFragment
import com.anytypeio.anytype.ui.linking.LinkToObjectOrWebPagesFragment
import com.anytypeio.anytype.ui.linking.OnLinkToAction
import com.anytypeio.anytype.ui.moving.MoveToFragment
import com.anytypeio.anytype.ui.moving.OnMoveToAction
import com.anytypeio.anytype.ui.objects.appearance.ObjectAppearanceSettingFragment
import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment
import com.anytypeio.anytype.ui.objects.types.pickers.OnObjectSelectTypeAction
import com.anytypeio.anytype.ui.relations.ObjectRelationListFragment
import com.anytypeio.anytype.ui.relations.RelationAddToObjectBlockFragment
import com.anytypeio.anytype.ui.relations.RelationDateValueFragment
import com.anytypeio.anytype.ui.relations.RelationTextValueFragment
import com.anytypeio.anytype.ui.relations.RelationValueFragment
import com.anytypeio.anytype.ui.relations.value.TagStatusFragment
import com.anytypeio.anytype.ui.spaces.SelectSpaceFragment
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.ARG_TEMPLATE_ID
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import javax.inject.Inject
import kotlin.math.abs
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.fragment_editor),
OnFragmentInteractionListener,
SelectProgrammingLanguageReceiver,
RelationTextValueFragment.TextValueEditReceiver,
RelationDateValueFragment.DateValueEditReceiver,
DocumentMenuActionReceiver,
ClipboardInterceptor,
OnMoveToAction,
OnLinkToAction,
OnObjectSelectTypeAction {
private val keyboardDelayJobs = mutableListOf<Job>()
protected val ctx get() = arg<Id>(ID_KEY)
private val screen: Point by lazy { screen() }
private val scrollAndMoveStateChannel = Channel<Int>()
init {
processScrollAndMoveStateChanges()
}
private val scrollAndMoveTargetDescriptor: ScrollAndMoveTargetDescriptor by lazy {
DefaultScrollAndMoveTargetDescriptor()
}
private val onHideBottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
when (bottomSheet.id) {
binding.styleToolbarOther.id -> {
vm.onCloseBlockStyleExtraToolbarClicked()
}
binding.styleToolbarMain.id -> {
vm.onCloseBlockStyleToolbarClicked()
}
binding.styleToolbarColors.id -> {
vm.onCloseBlockStyleColorToolbarClicked()
}
binding.blockActionToolbar.id -> {
vm.onBlockActionPanelHidden()
}
binding.undoRedoToolbar.id -> {
vm.onUndoRedoToolbarIsHidden()
}
binding.styleToolbarBackground.id -> {
vm.onCloseBlockStyleBackgroundToolbarClicked()
}
binding.simpleTableWidget.id -> {
vm.onHideSimpleTableWidget()
}
}
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
}
private val scrollAndMoveTopMargin by lazy {
0
}
private val scrollAndMoveStateListener by lazy {
ScrollAndMoveStateListener {
lifecycleScope.launch {
scrollAndMoveStateChannel.send(it)
}
}
}
private val scrollAndMoveTargetHighlighter by lazy {
ScrollAndMoveTargetHighlighter(
targeted = drawable(R.drawable.scroll_and_move_rectangle),
disabled = drawable(R.drawable.scroll_and_move_disabled),
line = drawable(R.drawable.scroll_and_move_line),
screen = screen,
padding = dimen(R.dimen.scroll_and_move_start_end_padding),
indentation = dimen(R.dimen.indent),
descriptor = scrollAndMoveTargetDescriptor
)
}
private val defaultBottomOffsetDecorator by lazy {
LastItemBottomOffsetDecorator(
dimen(R.dimen.dp_48)
)
}
private val footerMentionDecorator by lazy { MentionFooterItemDecorator(screen) }
private val noteHeaderDecorator by lazy {
NoteHeaderItemDecorator(offset = dimen(R.dimen.default_note_title_offset))
}
private val markupColorToolbarFooter by lazy { MarkupColorToolbarFooter(screen) }
private val slashWidgetFooter by lazy { SlashWidgetFooterItemDecorator(screen) }
private val styleToolbarFooter by lazy { StyleToolbarItemDecorator(screen) }
private val actionToolbarFooter by lazy { StyleToolbarItemDecorator(screen) }
protected val vm by viewModels<EditorViewModel> { factory }
private val blockAdapter by lazy {
BlockAdapter(
restore = vm.restore,
initialBlock = mutableListOf(),
onTextChanged = { id, editable ->
vm.onTextChanged(
id = id,
text = editable.toString(),
marks = editable.extractMarks()
)
},
onTextBlockTextChanged = vm::onTextBlockTextChanged,
onDescriptionChanged = vm::onDescriptionBlockTextChanged,
onTitleBlockTextChanged = vm::onTitleBlockTextChanged,
onSelectionChanged = vm::onSelectionChanged,
onCheckboxClicked = vm::onCheckboxClicked,
onTitleCheckboxClicked = vm::onTitleCheckboxClicked,
onFocusChanged = vm::onBlockFocusChanged,
onSplitLineEnterClicked = { id, editable, range ->
vm.onEnterKeyClicked(
target = id,
text = editable.toString(),
marks = editable.extractMarks(),
range = range
)
},
onSplitDescription = { id, editable, range ->
vm.onSplitObjectDescription(
target = id,
text = editable.toString(),
range = range
)
},
onEmptyBlockBackspaceClicked = vm::onEmptyBlockBackspaceClicked,
onNonEmptyBlockBackspaceClicked = { id, editable ->
vm.onNonEmptyBlockBackspaceClicked(
id = id,
text = editable.toString(),
marks = editable.extractMarks()
)
},
onTextInputClicked = vm::onTextInputClicked,
onPageIconClicked = vm::onPageIconClicked,
onCoverClicked = vm::onAddCoverClicked,
onTogglePlaceholderClicked = vm::onTogglePlaceholderClicked,
onToggleClicked = vm::onToggleClicked,
onTitleTextInputClicked = vm::onTitleTextInputClicked,
onClickListener = vm::onClickListener,
clipboardInterceptor = this,
onMentionEvent = vm::onMentionEvent,
onSlashEvent = vm::onSlashTextWatcherEvent,
onBackPressedCallback = { vm.onBackPressedCallback() },
onKeyPressedEvent = vm::onKeyPressedEvent,
onDragAndDropTrigger = { vh: RecyclerView.ViewHolder, event: MotionEvent? ->
dndDelegate.handleDragAndDropTrigger(vh, event)
},
onDragListener = dndDelegate.dndListener,
lifecycle = lifecycle,
dragAndDropSelector = DragAndDropAdapterDelegate(),
onCellSelectionChanged = vm::onCellSelectionChanged
)
}
private fun searchScrollAndMoveTarget() {
binding.recycler.findFocus().let { child ->
if (child is TextInputWidget) child.text
}
val centerX = screen.x / 2f
val centerY = (binding.targeter.y + (binding.targeter.height / 2f)) - scrollAndMoveTopMargin
var target: View? = binding.recycler.findChildViewUnder(centerX, centerY)
if (target == null) {
target = binding.recycler.findChildViewUnder(centerX, centerY - 5)
if (target == null) {
target = binding.recycler.findChildViewUnder(centerX, centerY + 5)
}
}
if (target == null) {
scrollAndMoveTargetDescriptor.clear()
} else {
val position = binding.recycler.getChildAdapterPosition(target)
val top = target.top
val height = target.height
val view = blockAdapter.views[position]
val indent = if (view is BlockView.Indentable) view.indent else 0
val ratio = if (centerY < top) {
val delta = top - centerY
delta / height
} else {
val delta = centerY - top
delta / height
}
scrollAndMoveTargetDescriptor.update(
target = ScrollAndMoveTarget(
position = position,
ratio = ratio,
indent = indent
)
)
}
}
val titleVisibilityDetector by lazy {
EditorHeaderOverlayDetector(
threshold = dimen(R.dimen.default_toolbar_height),
thresholdPadding = dimen(R.dimen.dp_8)
) { isHeaderOverlaid ->
if (isHeaderOverlaid) {
binding.topToolbar.setBackgroundColor(0)
binding.topToolbar.statusText.animate().alpha(1f)
.setDuration(DEFAULT_TOOLBAR_ANIM_DURATION)
.start()
binding.topToolbar.container.animate().alpha(0f)
.setDuration(DEFAULT_TOOLBAR_ANIM_DURATION)
.start()
if (blockAdapter.views.isNotEmpty()) {
val firstView = blockAdapter.views.first()
if (firstView is BlockView.Title && firstView.hasCover) {
binding.topToolbar.setStyle(overCover = true)
} else {
binding.topToolbar.setStyle(overCover = false)
}
}
} else {
binding.topToolbar.setBackgroundColor(requireContext().color(R.color.defaultCanvasColor))
binding.topToolbar.statusText.animate().alpha(0f)
.setDuration(DEFAULT_TOOLBAR_ANIM_DURATION)
.start()
binding.topToolbar.container.animate().alpha(1f)
.setDuration(DEFAULT_TOOLBAR_ANIM_DURATION)
.start()
binding.topToolbar.setStyle(overCover = false)
}
}
}
private val pickerDelegate = PickerDelegate.Impl(this) { actions ->
when (actions) {
PickerDelegate.Actions.OnCancelCopyFileToCacheDir -> {
vm.onCancelCopyFileToCacheDir()
}
is PickerDelegate.Actions.OnPickedDocImageFromDevice -> {
vm.onPickedDocImageFromDevice(actions.ctx, actions.filePath)
}
is PickerDelegate.Actions.OnProceedWithFilePath -> {
vm.onProceedWithFilePath(filePath = actions.filePath)
}
is PickerDelegate.Actions.OnStartCopyFileToCacheDir -> {
vm.onStartCopyFileToCacheDir(actions.uri)
}
}
}
private val dndDelegate = DragAndDropDelegate()
@Inject
lateinit var factory: EditorViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pickerDelegate.initPicker(ctx)
setupOnBackPressedDispatcher()
getEditorSettings()
}
override fun onStart() {
with(lifecycleScope) {
jobs += subscribe(vm.toasts) { toast(it) }
jobs += subscribe(vm.snacks) { snack ->
when (snack) {
is Snack.UndoRedo -> {
Snackbar.make(requireView(), snack.message, Snackbar.LENGTH_SHORT).apply {
anchorView = binding.undoRedoToolbar
}.show()
}
}
}
jobs += subscribe(vm.footers) { footer ->
when (footer) {
EditorFooter.None -> {
if (binding.recycler.containsItemDecoration(noteHeaderDecorator)) {
binding.recycler.removeItemDecoration(noteHeaderDecorator)
}
}
EditorFooter.Note -> {
if (!binding.recycler.containsItemDecoration(noteHeaderDecorator)) {
binding.recycler.addItemDecoration(noteHeaderDecorator)
}
}
}
}
jobs += subscribe(vm.copyFileStatus) { command ->
pickerDelegate.onCopyFileCommand(command)
}
jobs += subscribe(vm.selectTemplateViewState) { state ->
when (state) {
is SelectTemplateViewState.Active -> {
binding.topToolbar.showTemplates()
binding.topToolbar.setTemplates(count = state.count)
}
SelectTemplateViewState.Idle -> {
binding.topToolbar.hideTemplates()
}
}
}
}
vm.onStart(id = extractDocumentId(), saveAsLastOpened = saveAsLastOpened())
super.onStart()
}
override fun onStop() {
vm.onStop()
pickerDelegate.onStop()
super.onStop()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(CURRENT_MEDIA_UPLOAD_KEY, vm.currentMediaUploadDescription)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if (savedInstanceState != null) {
vm.onRestoreSavedState(savedInstanceState.getParcelable(CURRENT_MEDIA_UPLOAD_KEY))
}
}
private fun setupOnBackPressedDispatcher() =
requireActivity()
.onBackPressedDispatcher
.addCallback(this) {
vm.onSystemBackPressed(childFragmentManager.backStackEntryCount > 0)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupWindowInsetAnimation()
dndDelegate.init(blockAdapter, vm, this)
binding.recycler.addOnItemTouchListener(
OutsideClickDetector(vm::onOutsideClicked)
)
observeSelectingTemplate()
binding.recycler.apply {
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
itemAnimator = null
adapter = blockAdapter
addOnScrollListener(titleVisibilityDetector)
addItemDecoration(defaultBottomOffsetDecorator)
}
binding.toolbar.apply {
blockActionsClick()
.onEach { vm.onBlockToolbarBlockActionsClicked() }
.launchIn(lifecycleScope)
openSlashWidgetClicks()
.onEach { vm.onStartSlashWidgetClicked() }
.launchIn(lifecycleScope)
hideKeyboardClicks()
.onEach { vm.onHideKeyboardClicked() }
.launchIn(lifecycleScope)
changeStyleClicks()
.onEach { vm.onBlockToolbarStyleClicked() }
.launchIn(lifecycleScope)
mentionClicks()
.onEach { vm.onStartMentionWidgetClicked() }
.launchIn(lifecycleScope)
}
binding.scrollAndMoveBottomAction
.apply
.clicks()
.throttleFirst()
.onEach {
vm.onApplyScrollAndMoveClicked()
onApplyScrollAndMoveClicked()
}
.launchIn(lifecycleScope)
binding.scrollAndMoveBottomAction
.cancel
.clicks()
.throttleFirst()
.onEach { vm.onExitScrollAndMoveClicked() }
.launchIn(lifecycleScope)
binding.multiSelectTopToolbar
.doneButton
.clicks()
.throttleFirst()
.onEach { vm.onExitMultiSelectModeClicked() }
.launchIn(lifecycleScope)
binding.bottomToolbar
.homeClicks()
.onEach { vm.onHomeButtonClicked() }
.launchIn(lifecycleScope)
binding.bottomToolbar
.backClicks()
.onEach { vm.onBackButtonPressed() }
.launchIn(lifecycleScope)
binding.bottomToolbar
.profileClicks()
.onEach {
findNavController().navigate(
R.id.selectSpaceScreen,
args = SelectSpaceFragment.args(exitHomeWhenSpaceIsSelected = true)
)
}
.launchIn(lifecycleScope)
binding.bottomToolbar
.searchClicks()
.onEach { vm.onPageSearchClicked() }
.launchIn(lifecycleScope)
binding.bottomToolbar
.addDocClicks()
.onEach { vm.onAddNewDocumentClicked() }
.launchIn(lifecycleScope)
binding
.bottomToolbar
.binding
.btnAddDoc
.longClicks(withHaptic = true)
.onEach {
val dialog = SelectObjectTypeFragment().apply {
onTypeSelected = {
vm.onAddNewDocumentClicked(it)
}
}
dialog.show(childFragmentManager, "editor-create-object-of-type-dialog")
}
.launchIn(lifecycleScope)
binding.topToolbar.menu
.clicks()
.throttleFirst()
.onEach { vm.onDocumentMenuClicked() }
.launchIn(lifecycleScope)
binding.markupToolbar
.highlightClicks()
.onEach { vm.onMarkupHighlightToggleClicked() }
.launchIn(lifecycleScope)
binding.markupToolbar
.colorClicks()
.onEach { vm.onMarkupColorToggleClicked() }
.launchIn(lifecycleScope)
binding.markupToolbar
.linkClicks()
.onEach { vm.onEditLinkClicked() }
.launchIn(lifecycleScope)
binding.markupToolbar
.markup()
.onEach { type -> vm.onStyleToolbarMarkupAction(type, null) }
.launchIn(lifecycleScope)
binding.blockActionToolbar.actionListener = { action -> vm.onMultiSelectAction(action) }
binding.markupColorToolbar.onColorClickedListener = { color ->
if (color is MarkupColorView.Text) {
vm.onStyleToolbarMarkupAction(
type = Markup.Type.TEXT_COLOR,
param = color.code
)
} else {
vm.onStyleToolbarMarkupAction(
type = Markup.Type.BACKGROUND_COLOR,
param = color.code
)
}
}
binding.simpleTableWidget.setListener {
vm.onSimpleTableWidgetItemClicked(it)
}
binding.undoRedoToolbar.undo.clicks()
.throttleFirst()
.onEach {
vm.onActionUndoClicked()
}.launchIn(lifecycleScope)
binding.undoRedoToolbar.redo.clicks()
.throttleFirst()
.onEach {
vm.onActionRedoClicked()
}.launchIn(lifecycleScope)
lifecycleScope.subscribe(binding.styleToolbarMain.styles) {
vm.onUpdateTextBlockStyle(it)
}
lifecycleScope.subscribe(binding.styleToolbarMain.other) {
vm.onBlockStyleToolbarOtherClicked()
}
lifecycleScope.subscribe(binding.styleToolbarMain.colors) {
vm.onBlockStyleToolbarColorClicked()
}
lifecycleScope.subscribe(binding.styleToolbarColors.events) {
vm.onStylingToolbarEvent(it)
}
lifecycleScope.subscribe(binding.styleToolbarOther.actions) {
vm.onStylingToolbarEvent(it)
}
lifecycleScope.subscribe(binding.styleToolbarBackground.actions) {
vm.onStylingToolbarEvent(it)
}
binding.mentionSuggesterToolbar.setupClicks(
mentionClick = vm::onMentionSuggestClick,
newPageClick = vm::onAddMentionNewPageClicked
)
lifecycleScope.launch {
binding.slashWidget.clickEvents.collect { item ->
vm.onSlashItemClicked(item)
}
}
binding.topToolbar.templates.clicks()
.throttleFirst()
.onEach { vm.onTemplatesToolbarClicked() }
.launchIn(lifecycleScope)
lifecycleScope.launch {
binding.searchToolbar.events().collect { vm.onSearchToolbarEvent(it) }
}
binding.objectNotExist.root.findViewById<TextView>(R.id.btnToDashboard).clicks()
.throttleFirst()
.onEach { vm.onHomeButtonClicked() }
.launchIn(lifecycleScope)
binding.chooseTypeWidget.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ChooseTypeHorizontalWidget(
state = vm.typesWidgetState.collectAsStateWithLifecycle().value,
onTypeClicked = vm::onTypesWidgetItemClicked
)
}
}
BottomSheetBehavior.from(binding.styleToolbarMain).state = BottomSheetBehavior.STATE_HIDDEN
BottomSheetBehavior.from(binding.styleToolbarOther).state = BottomSheetBehavior.STATE_HIDDEN
BottomSheetBehavior.from(binding.styleToolbarColors).state =
BottomSheetBehavior.STATE_HIDDEN
BottomSheetBehavior.from(binding.blockActionToolbar).state =
BottomSheetBehavior.STATE_HIDDEN
BottomSheetBehavior.from(binding.undoRedoToolbar).state = BottomSheetBehavior.STATE_HIDDEN
BottomSheetBehavior.from(binding.styleToolbarBackground).state =
BottomSheetBehavior.STATE_HIDDEN
BottomSheetBehavior.from(binding.simpleTableWidget).state =
BottomSheetBehavior.STATE_HIDDEN
}
open fun setupWindowInsetAnimation() {
if (BuildConfig.USE_NEW_WINDOW_INSET_API && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
binding.toolbar.syncTranslationWithImeVisibility(
dispatchMode = DISPATCH_MODE_STOP
)
binding.chooseTypeWidget.syncTranslationWithImeVisibility(
dispatchMode = DISPATCH_MODE_STOP
)
}
}
private fun onApplyScrollAndMoveClicked() {
scrollAndMoveTargetDescriptor.current()?.let { target ->
vm.onApplyScrollAndMove(
target = blockAdapter.views[target.position].id,
ratio = target.ratio
)
}
}
override fun onAddBookmarkUrlClicked(target: String, url: String) {
vm.onAddBookmarkUrl(target = target, url = url)
}
override fun onSetBlockWebLink(blockId: Id, link: Id) {
vm.onAddWebLinkToBlock(blockId = blockId, link = link)
}
override fun onSetBlockObjectLink(blockId: Id, objectId: Id) {
vm.onAddObjectLinkToBlock(blockId = blockId, objectId = objectId)
}
override fun onRemoveMarkupLinkClicked(blockId: String, range: IntRange) {
vm.onUnlinkPressed(blockId = blockId, range = range)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
vm.state.observe(viewLifecycleOwner) { render(it) }
vm.navigation.observe(viewLifecycleOwner, navObserver)
vm.controlPanelViewState.observe(viewLifecycleOwner) { render(it) }
vm.commands.observe(viewLifecycleOwner) { execute(it) }
vm.searchResultScrollPosition
.filter { it != EditorViewModel.NO_SEARCH_RESULT_POSITION }
.onEach {
(binding.recycler.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(it, dimen(R.dimen.default_toolbar_height))
}
.launchIn(lifecycleScope)
vm.syncStatus.onEach { status -> bindSyncStatus(status) }.launchIn(lifecycleScope)
vm.isSyncStatusVisible.onEach { isSyncStatusVisible ->
if (isSyncStatusVisible)
binding.topToolbar.findViewById<ViewGroup>(R.id.statusContainer).visible()
else
binding.topToolbar.findViewById<ViewGroup>(R.id.statusContainer).invisible()
}.launchIn(lifecycleScope)
vm.isUndoEnabled.onEach {
// TODO
}.launchIn(lifecycleScope)
vm.isRedoEnabled.onEach {
// TODO
}.launchIn(lifecycleScope)
with(lifecycleScope) {
launch {
vm.actions.collectLatest {
binding.blockActionToolbar.bind(it)
delay(DEFAULT_DELAY_BLOCK_ACTION_TOOLBAR)
handler.post {
if (hasBinding) {
binding.blockActionToolbar.scrollToPosition(0, smooth = true)
}
}
}
}
subscribe(vm.isUndoRedoToolbarIsVisible) { isVisible ->
if (hasBinding) {
val behavior = BottomSheetBehavior.from(binding.undoRedoToolbar)
if (isVisible) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.addBottomSheetCallback(onHideBottomSheetCallback)
} else {
behavior.removeBottomSheetCallback(onHideBottomSheetCallback)
behavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}
subscribe(vm.icon) { icon ->
if (hasBinding) binding.bottomToolbar.bind(icon)
}
}
}
private fun bindSyncStatus(status: SyncStatusView?) {
binding.topToolbar.status.bind(status)
if (status == null) {
binding.topToolbar.hideStatusContainer()
} else {
binding.topToolbar.showStatusContainer()
}
val tvStatus = binding.topToolbar.statusText
binding.topToolbar.statusContainer.setOnClickListener {
toast(status.getToastMsg(requireContext()))
}
tvStatus.text = status?.getLabelText(requireContext())
}
override fun onDestroyView() {
pickerDelegate.clearPickit()
super.onDestroyView()
}
override fun onDestroy() {
pickerDelegate.deleteTemporaryFile()
pickerDelegate.clearOnCopyFile()
super.onDestroy()
}
override fun onSetRelationKeyClicked(blockId: Id, key: Id) {
vm.onSetRelationKeyClicked(blockId = blockId, key = key)
}
private fun execute(event: EventWrapper<Command>) {
event.getContentIfNotHandled()?.let { command ->
when (command) {
is Command.OpenDocumentImagePicker -> {
pickerDelegate.openFilePicker(command.mimeType, REQUEST_PROFILE_IMAGE_CODE)
}
is Command.OpenTextBlockIconPicker -> {
TextBlockIconPickerFragment.new(
context = ctx, blockId = command.block
).showChildFragment()
}
Command.OpenDocumentEmojiIconPicker -> {
hideSoftInput()
findNavController().safeNavigate(
R.id.pageScreen,
R.id.action_pageScreen_to_objectIconPickerScreen,
bundleOf(
IconPickerFragmentBase.ARG_CONTEXT_ID_KEY to ctx,
)
)
}
is Command.OpenBookmarkSetter -> {
CreateBookmarkFragment.newInstance(
target = command.target,
url = command.url
).showChildFragment()
}
is Command.OpenGallery -> {
pickerDelegate.openFilePicker(command.mimeType, null)
}
is Command.PopBackStack -> {
childFragmentManager.popBackStack()
}
is Command.CloseKeyboard -> {
hideSoftInput()
}
is Command.ScrollToActionMenu -> {
proceedWithScrollingToActionMenu(command)
}
is Command.Browse -> {
try {
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(command.url)
}.let {
startActivity(it)
}
} catch (e: Throwable) {
toast("Couldn't parse url: ${command.url}")
}
}
is Command.OpenDocumentMenu -> {
hideKeyboard()
val fr = ObjectMenuFragment.new(
ctx = ctx,
isArchived = command.isArchived,
isFavorite = command.isFavorite,
isLocked = command.isLocked,
fromName = getFrom(),
isTemplate = command.isTemplate
)
if (!fr.isAdded) {
fr.showChildFragment()
} else {
Timber.d("Ignoring, fragment already added.")
}
}
is Command.OpenCoverGallery -> {
findNavController().safeNavigate(
R.id.pageScreen,
R.id.action_pageScreen_to_objectCoverScreen,
bundleOf(SelectCoverObjectFragment.CTX_KEY to command.ctx)
)
}
is Command.OpenObjectLayout -> {
val fr = ObjectLayoutFragment.new(command.ctx).apply {
onDismissListener = { vm.onLayoutDialogDismissed() }
}
fr.showChildFragment()
}
is Command.OpenFullScreenImage -> {
val screen = FullScreenPictureFragment.new(command.target, command.url).apply {
enterTransition = Fade()
exitTransition = Fade()
}
childFragmentManager
.beginTransaction()
.add(R.id.root, screen)
.addToBackStack(null)
.commit()
}
is Command.AlertDialog -> {
if (childFragmentManager.findFragmentByTag(TAG_ALERT) == null) {
AlertUpdateAppFragment().apply {
onCancel = { navigation().exit() }
}.showChildFragment(TAG_ALERT)
} else {
// Do nothing
}
}
is Command.ClearSearchInput -> {
binding.searchToolbar.clear()
}
is Command.Dialog.SelectLanguage -> {
SelectProgrammingLanguageFragment.new(command.target)
.showChildFragment()
}
is Command.OpenObjectRelationScreen.RelationAdd -> {
hideKeyboard()
ObjectRelationListFragment
.new(
ctx = command.ctx,
target = command.target,
mode = ObjectRelationListFragment.MODE_ADD
)
.showChildFragment()
}
is Command.OpenObjectRelationScreen.RelationList -> {
hideKeyboard()
findNavController().safeNavigate(
R.id.pageScreen,
R.id.objectRelationListScreen,
bundleOf(
ObjectRelationListFragment.ARG_CTX to command.ctx,
ObjectRelationListFragment.ARG_TARGET to command.target,
ObjectRelationListFragment.ARG_LOCKED to command.isLocked,
ObjectRelationListFragment.ARG_MODE to ObjectRelationListFragment.MODE_LIST,
)
)
}
is Command.OpenObjectRelationScreen.Value.Default -> {
hideKeyboard()
val fr = RelationValueFragment.new(
ctx = command.ctx,
target = command.target,
relationKey = command.relationKey,
targetObjectTypes = command.targetObjectTypes,
isLocked = command.isLocked
)
fr.showChildFragment()
}
is Command.OpenObjectRelationScreen.Value.Text -> {
hideKeyboard()
val fr = RelationTextValueFragment.new(
ctx = command.ctx,
relationKey = command.relationKey,
objectId = command.target,
isLocked = command.isLocked
)
fr.showChildFragment()
}
is Command.OpenObjectRelationScreen.Value.Date -> {
hideKeyboard()
val fr = RelationDateValueFragment.new(
ctx = command.ctx,
objectId = command.target,
relationKey = command.relationKey
)
fr.showChildFragment()
}
Command.AddSlashWidgetTriggerToFocusedBlock -> {
binding.recycler.addTextFromSelectedStart(text = "/")
}
is Command.OpenObjectSelectTypeScreen -> {
hideKeyboard()
val dialog = SelectObjectTypeFragment.newInstance(
excludedTypeKeys = command.excludedTypes,
onTypeSelected = vm::onObjectTypeChanged
)
dialog.show(childFragmentManager, null)
}
is Command.OpenMoveToScreen -> {
jobs += lifecycleScope.launch {
hideSoftInput()
delay(DEFAULT_ANIM_DURATION)
val fr = MoveToFragment.new(
ctx = ctx,
blocks = command.blocks,
restorePosition = command.restorePosition,
restoreBlock = command.restoreBlock
)
fr.showChildFragment()
}
}
is Command.OpenObjectSnackbar -> {
binding.root.showActionableSnackBar(
from = command.fromText,
to = command.toText,
icon = command.icon,
middleString = R.string.snack_move_to
) {
if (command.isDataView) {
vm.proceedWithOpeningDataViewObject(command.id)
} else {
vm.proceedWithOpeningObject(command.id)
}
}
}
is Command.OpenLinkToScreen -> {
jobs += lifecycleScope.launch {
hideSoftInput()
delay(DEFAULT_ANIM_DURATION)
val fr = LinkToObjectFragment.new(
target = command.target,
position = command.position,
ignore = vm.context
)
fr.showChildFragment()
}
}
is Command.AddMentionWidgetTriggerToFocusedBlock -> {
binding.recycler.addTextFromSelectedStart(text = "@")
}
is Command.OpenAddRelationScreen -> {
hideSoftInput()
val fr = RelationAddToObjectBlockFragment.newInstance(
ctx = command.ctx,
target = command.target
)
fr.showChildFragment()
}
is Command.OpenLinkToObjectOrWebScreen -> {
hideSoftInput()
val fr = LinkToObjectOrWebPagesFragment.newInstance(
ctx = command.ctx,
blockId = command.target,
rangeStart = command.range.first,
rangeEnd = command.range.last,
isWholeBlockMarkup = command.isWholeBlockMarkup
)
fr.showChildFragment()
}
is Command.ShowKeyboard -> {
binding.recycler.findFocus()?.focusAndShowKeyboard()
}
is Command.OpenFileByDefaultApp -> {
vm.startSharingFile(command.id) { uri ->
openFileByDefaultApp(uri)
}
}
is Command.SaveTextToSystemClipboard -> {
val clipData = ClipData.newPlainText("Uri", command.text)
clipboard().setPrimaryClip(clipData)
}
is Command.OpenObjectAppearanceSettingScreen -> {
val fr = ObjectAppearanceSettingFragment.new(
ctx = command.ctx,
block = command.block
)
fr.showChildFragment()
}
is Command.ScrollToPosition -> {
val lm = binding.recycler.layoutManager as LinearLayoutManager
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.showChildFragment()
}
is Command.OpenObjectTypeMenu -> openObjectTypeMenu(command)
}
}
}
private fun openObjectTypeMenu(command: Command.OpenObjectTypeMenu) {
val layoutManager = binding.recycler.layoutManager as LinearLayoutManager
val featuredGroupWidget = findFeaturedGroupWidget(layoutManager)
featuredGroupWidget?.getObjectTypeView()?.let { anchor ->
createObjectTypeMenu(anchor, command).show()
}
}
private fun findFeaturedGroupWidget(layoutManager: LinearLayoutManager): FeaturedRelationGroupWidget? {
return (0 until layoutManager.childCount)
.map { layoutManager.getChildAt(it) }
.find { it is FeaturedRelationGroupWidget } as? FeaturedRelationGroupWidget
}
private fun createObjectTypeMenu(anchor: View, command: Command.OpenObjectTypeMenu): PopupMenu {
val themeWrapper = ContextThemeWrapper(context, R.style.DefaultPopupMenuStyle)
return ObjectTypePopupMenu(
context = themeWrapper,
anchor = anchor,
items = command.items,
onChangeTypeClicked = vm::onChangeObjectTypeClicked,
onOpenSetClicked = vm::proceedWithOpeningDataViewObject,
onCreateSetClicked = vm::onCreateNewSetForType
)
}
private fun getFrom() = (blockAdapter.views
.firstOrNull { it is BlockView.TextSupport } as? BlockView.TextSupport)
?.text
private fun openFileByDefaultApp(uri: Uri) {
try {
val intent = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, requireContext().contentResolver.getType(uri))
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
} catch (e: Exception) {
if (e is ActivityNotFoundException) {
toast("No application found to open the selected file")
} else {
toast("Could not open file: ${e.message}")
}
Timber.e(e, "Error while opening file")
}
}
private fun proceedWithScrollingToActionMenu(command: Command.ScrollToActionMenu): Unit {
val lastSelected =
(vm.state.value as ViewState.Success).blocks.indexOfLast { it.id == command.target }
if (lastSelected != -1) {
val lm = binding.recycler.layoutManager as LinearLayoutManager
val targetView = lm.findViewByPosition(lastSelected)
if (targetView != null) {
val behavior = BottomSheetBehavior.from(binding.blockActionToolbar)
val toolbarTop: Float = if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
binding.blockActionToolbar.y - binding.blockActionToolbar.measuredHeight
} else {
binding.blockActionToolbar.y
}
val targetBottom = targetView.y + targetView.measuredHeight
val delta = toolbarTop - targetBottom
if (delta < 0) binding.recycler.smoothScrollBy(0, abs(delta.toInt()))
}
}
}
private fun render(state: ViewState) {
when (state) {
is ViewState.Success -> {
blockAdapter.updateWithDiffUtil(state.blocks)
binding.recycler.visible()
binding.recycler.invalidateItemDecorations()
val isLocked = vm.mode is Editor.Mode.Locked
binding.topToolbar.setIsLocked(isLocked)
resetDocumentTitle(state)
binding.loadingContainer.root.gone()
}
ViewState.Loading -> {}
ViewState.NotExist -> {
binding.recycler.gone()
binding.loadingContainer.root.gone()
binding.objectNotExist.root.visible()
}
}
}
open fun resetDocumentTitle(state: ViewState.Success) {
val title = state.blocks.firstOrNull { view ->
view is BlockView.Title.Basic || view is BlockView.Title.Profile || view is BlockView.Title.Todo
}
if (title != null) {
when (title) {
is BlockView.Title.Basic -> {
resetTopToolbarTitle(
text = title.text,
emoji = title.emoji,
image = title.image,
)
if (title.hasCover) {
val mng = binding.recycler.layoutManager as LinearLayoutManager
val pos = mng.findFirstVisibleItemPosition()
if (pos == -1 || pos == 0) {
binding.topToolbar.setStyle(overCover = true)
}
} else {
binding.topToolbar.setStyle(overCover = false)
}
}
is BlockView.Title.Profile -> {
resetTopToolbarTitle(
text = title.text,
emoji = null,
image = title.image,
)
if (title.hasCover) {
val mng = binding.recycler.layoutManager as LinearLayoutManager
val pos = mng.findFirstVisibleItemPosition()
if (pos == -1 || pos == 0) {
binding.topToolbar.setStyle(overCover = true)
}
} else {
binding.topToolbar.setStyle(overCover = false)
}
}
is BlockView.Title.Todo -> {
resetTopToolbarTitle(
text = title.text,
emoji = null,
image = title.image,
)
if (title.hasCover) {
val mng = binding.recycler.layoutManager as LinearLayoutManager
val pos = mng.findFirstVisibleItemPosition()
if (pos == -1 || pos == 0) {
binding.topToolbar.setStyle(overCover = true)
}
} else {
binding.topToolbar.setStyle(overCover = false)
}
}
else -> {
}
}
}
}
private fun resetTopToolbarTitle(text: String?, emoji: String?, image: String?) {
binding.topToolbar.title.text = text
val iconView = binding.topToolbar.icon
when {
text.isNullOrBlank() -> {
iconView.setIcon(ObjectIcon.None)
iconView.gone()
}
!emoji.isNullOrBlank() -> {
iconView.setIcon(ObjectIcon.Basic.Emoji(emoji))
iconView.visible()
}
!image.isNullOrBlank() -> {
iconView.setIcon(ObjectIcon.Basic.Image(image))
iconView.visible()
}
else -> {
iconView.setIcon(ObjectIcon.None)
iconView.gone()
}
}
}
open fun render(state: ControlPanelState) {
keyboardDelayJobs.cancel()
val insets = ViewCompat.getRootWindowInsets(binding.root)
if (state.navigationToolbar.isVisible) {
binding.placeholder.requestFocus()
binding.placeholder.hideKeyboard()
binding.bottomToolbar.visible()
} else {
binding.bottomToolbar.gone()
}
if (state.mainToolbar.isVisible) {
binding.toolbar.visible()
binding.toolbar.state = when (state.mainToolbar.targetBlockType) {
Main.TargetBlockType.Any -> BlockToolbarWidget.State.Any
Main.TargetBlockType.Title -> BlockToolbarWidget.State.Title
Main.TargetBlockType.Cell -> BlockToolbarWidget.State.Cell
Main.TargetBlockType.Description -> BlockToolbarWidget.State.Description
}
} else {
binding.toolbar.invisible()
}
setMainMarkupToolbarState(state)
state.multiSelect.apply {
val behavior = BottomSheetBehavior.from(binding.blockActionToolbar)
if (isVisible) {
binding.multiSelectTopToolbar.apply {
setBlockSelectionText(count)
visible()
}
binding.recycler.apply {
if (itemAnimator == null) itemAnimator = DefaultItemAnimator()
}
proceedWithHidingSoftInput()
binding.topToolbar.invisible()
if (!state.multiSelect.isScrollAndMoveEnabled) {
if (!binding.recycler.containsItemDecoration(actionToolbarFooter)) {
binding.recycler.addItemDecoration(actionToolbarFooter)
}
if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
keyboardDelayJobs += lifecycleScope.launch {
binding.blockActionToolbar.scrollToPosition(0)
delayKeyboardHide(insets)
behavior.apply {
setState(BottomSheetBehavior.STATE_EXPANDED)
addBottomSheetCallback(onHideBottomSheetCallback)
}
showSelectButton()
}
}
} else {
behavior.removeBottomSheetCallback(onHideBottomSheetCallback)
}
} else {
binding.recycler.apply { itemAnimator = null }
behavior.apply {
setState(BottomSheetBehavior.STATE_HIDDEN)
removeBottomSheetCallback(onHideBottomSheetCallback)
}
if (!state.simpleTableWidget.isVisible) hideSelectButton()
binding.recycler.removeItemDecoration(actionToolbarFooter)
}
if (isScrollAndMoveEnabled)
enterScrollAndMove()
else
exitScrollAndMove()
}
state.styleTextToolbar.apply {
val behavior = BottomSheetBehavior.from(binding.styleToolbarMain)
if (isVisible) {
binding.styleToolbarMain.setSelectedStyle(this.state)
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
keyboardDelayJobs += lifecycleScope.launch {
if (binding.recycler.lastDecorator() == defaultBottomOffsetDecorator) {
binding.recycler.addItemDecoration(styleToolbarFooter)
}
proceedWithHidingSoftInput()
delayKeyboardHide(insets)
behavior.apply {
setState(BottomSheetBehavior.STATE_EXPANDED)
addBottomSheetCallback(onHideBottomSheetCallback)
}
}
}
} else {
if (!state.styleColorBackgroundToolbar.isVisible && !state.styleExtraToolbar.isVisible) {
binding.recycler.removeItemDecoration(styleToolbarFooter)
}
behavior.apply {
removeBottomSheetCallback(onHideBottomSheetCallback)
setState(BottomSheetBehavior.STATE_HIDDEN)
}
}
}
state.styleExtraToolbar.apply {
if (isVisible) {
binding.styleToolbarOther.setProperties(state.styleExtraToolbar.state)
BottomSheetBehavior.from(binding.styleToolbarOther).apply {
setState(BottomSheetBehavior.STATE_EXPANDED)
addBottomSheetCallback(onHideBottomSheetCallback)
}
} else {
BottomSheetBehavior.from(binding.styleToolbarOther).apply {
removeBottomSheetCallback(onHideBottomSheetCallback)
setState(BottomSheetBehavior.STATE_HIDDEN)
}
}
}
state.styleColorBackgroundToolbar.apply {
val behavior = BottomSheetBehavior.from(binding.styleToolbarColors)
if (isVisible) {
binding.styleToolbarColors.update(state.styleColorBackgroundToolbar.state)
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
keyboardDelayJobs += lifecycleScope.launch {
proceedWithHidingSoftInput()
delayKeyboardHide(insets)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.addBottomSheetCallback(onHideBottomSheetCallback)
}
}
} else {
behavior.apply {
removeBottomSheetCallback(onHideBottomSheetCallback)
setState(BottomSheetBehavior.STATE_HIDDEN)
}
}
}
state.styleBackgroundToolbar.apply {
val behavior = BottomSheetBehavior.from(binding.styleToolbarBackground)
if (isVisible) {
state.styleBackgroundToolbar.state.let {
binding.styleToolbarBackground.update(it)
}
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
keyboardDelayJobs += lifecycleScope.launch {
if (binding.recycler.lastDecorator() == defaultBottomOffsetDecorator) {
binding.recycler.addItemDecoration(styleToolbarFooter)
}
proceedWithHidingSoftInput()
delayKeyboardHide(insets)
behavior.apply {
setState(BottomSheetBehavior.STATE_EXPANDED)
addBottomSheetCallback(onHideBottomSheetCallback)
}
}
}
} else {
BottomSheetBehavior.from(binding.styleToolbarBackground).apply {
setState(BottomSheetBehavior.STATE_HIDDEN)
addBottomSheetCallback(onHideBottomSheetCallback)
}
}
}
state.mentionToolbar.apply {
if (isVisible) {
if (!binding.mentionSuggesterToolbar.isVisible) {
showMentionToolbar(this)
}
if (updateList) {
binding.mentionSuggesterToolbar.addItems(mentions)
}
mentionFilter?.let {
binding.mentionSuggesterToolbar.updateFilter(it)
}
} else {
binding.mentionSuggesterToolbar.invisible()
binding.mentionSuggesterToolbar.clear()
binding.recycler.removeItemDecoration(footerMentionDecorator)
}
}
state.slashWidget.apply {
TransitionManager.endTransitions(binding.sheet)
if (isVisible) {
if (!binding.slashWidget.isVisible) {
binding.slashWidget.scrollToTop()
showSlashWidget(this)
}
widgetState?.let {
binding.slashWidget.onStateChanged(it)
}
} else {
if (binding.slashWidget.isVisible) {
binding.slashWidget.gone()
}
binding.recycler.removeItemDecoration(slashWidgetFooter)
}
}
state.searchToolbar.apply {
if (isVisible) {
binding.searchToolbar.visible()
binding.searchToolbar.focus()
} else {
binding.searchToolbar.gone()
}
}
state.simpleTableWidget.apply {
val behavior = BottomSheetBehavior.from(binding.simpleTableWidget)
if (isVisible) {
binding.multiSelectTopToolbar.apply {
setTableSelectionText(
count = state.simpleTableWidget.selectedCount,
tab = state.simpleTableWidget.tab
)
visible()
}
binding.simpleTableWidget.onStateChanged(
items = state.simpleTableWidget.items,
tab = state.simpleTableWidget.tab
)
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
keyboardDelayJobs += lifecycleScope.launch {
if (binding.recycler.lastDecorator() == defaultBottomOffsetDecorator) {
binding.recycler.addItemDecoration(styleToolbarFooter)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
hidingSoftInput()
delayKeyboardHide(insets)
} else {
proceedWithHidingSoftInput()
delayKeyboardHide(insets)
}
behavior.apply {
setState(BottomSheetBehavior.STATE_EXPANDED)
addBottomSheetCallback(onHideBottomSheetCallback)
}
showSelectButton()
}
}
} else {
if (behavior.state == BottomSheetBehavior.STATE_EXPANDED) {
behavior.removeBottomSheetCallback(onHideBottomSheetCallback)
behavior.state = BottomSheetBehavior.STATE_HIDDEN
}
if (!state.multiSelect.isVisible) hideSelectButton()
}
}
}
private fun applySlideTransition(transTarget: View, transDuration: Long, transRoot: ViewGroup) {
val transitionSet = TransitionSet().apply {
addTransition(Slide())
duration = transDuration
interpolator = DecelerateInterpolator(DECELERATE_INTERPOLATOR_FACTOR)
ordering = TransitionSet.ORDERING_TOGETHER
addTarget(transTarget)
}
TransitionManager.endTransitions(transRoot)
TransitionManager.beginDelayedTransition(
transRoot,
transitionSet
)
}
@RequiresApi(Build.VERSION_CODES.R)
private fun hidingSoftInput() {
ViewCompat.getWindowInsetsController(requireView())?.hide(WindowInsetsCompat.Type.ime())
}
private fun proceedWithHidingSoftInput() {
// TODO enable when switching to API 30
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// val controller = root.windowInsetsController
// if (controller != null) {
// controller.hide(WindowInsetsCompat.Type.ime())
// } else {
// hideSoftInput()
// }
// } else {
// hideSoftInput()
// }
hideSoftInput()
}
private suspend fun delayKeyboardHide(insets: WindowInsetsCompat?) {
if (insets != null) {
if (insets.isVisible(WindowInsetsCompat.Type.ime())) {
delay(DELAY_HIDE_KEYBOARD)
}
} else {
delay(DELAY_HIDE_KEYBOARD)
}
}
private fun hideBlockActionPanel() {
BottomSheetBehavior.from(binding.blockActionToolbar).apply {
setState(BottomSheetBehavior.STATE_HIDDEN)
}
}
private fun setMainMarkupToolbarState(state: ControlPanelState) {
if (state.markupMainToolbar.isVisible) {
binding.markupToolbar.setProps(
props = state.markupMainToolbar.style,
supportedTypes = state.markupMainToolbar.supportedTypes,
isBackgroundColorSelected = state.markupMainToolbar.isBackgroundColorSelected,
isTextColorSelected = state.markupMainToolbar.isTextColorSelected
)
binding.markupToolbar.visible()
if (state.markupColorToolbar.isVisible) {
if (state.markupMainToolbar.isTextColorSelected) {
binding.markupColorToolbar.setTextColor(
state.markupMainToolbar.style?.markupTextColor
?: state.markupMainToolbar.style?.blockTextColor
?: ThemeColor.DEFAULT.code
)
}
if (state.markupMainToolbar.isBackgroundColorSelected) {
binding.markupColorToolbar.setBackgroundColor(
state.markupMainToolbar.style?.markupHighlightColor
?: state.markupMainToolbar.style?.blockBackroundColor
?: ThemeColor.DEFAULT.code
)
}
if (binding.markupColorToolbar.translationY > 0) {
binding.recycler.addItemDecoration(markupColorToolbarFooter)
}
showMarkupColorToolbarWithAnimation()
} else {
if (binding.markupColorToolbar.translationY == 0f) {
binding.recycler.removeItemDecoration(markupColorToolbarFooter)
hideMarkupColorToolbarWithAnimation()
}
}
} else {
binding.markupToolbar.invisible()
if (binding.markupColorToolbar.translationY == 0f) {
binding.markupColorToolbar.translationY = dimen(R.dimen.dp_104).toFloat()
}
}
}
private fun showMarkupColorToolbarWithAnimation() {
val focus = binding.recycler.findFocus()
if (focus != null && focus is TextInputWidget) {
val cursorCoord = focus.cursorYBottomCoordinate()
val parentBottom = calculateRectInWindow(binding.recycler).bottom
val toolbarHeight = binding.markupToolbar.height + binding.markupColorToolbar.height
val minPosY = parentBottom - toolbarHeight
if (minPosY <= cursorCoord) {
val scrollY = (parentBottom - minPosY) - (parentBottom - cursorCoord)
Timber.d("New scroll y: $scrollY")
handler.post {
binding.recycler.smoothScrollBy(0, scrollY)
}
}
binding.markupColorToolbar
.animate()
.translationY(0f)
.setDuration(DEFAULT_ANIM_DURATION)
.start()
}
}
private fun hideMarkupColorToolbarWithAnimation() {
binding.markupColorToolbar
.animate()
.translationY(dimen(R.dimen.dp_104).toFloat())
.setDuration(DEFAULT_ANIM_DURATION)
.start()
}
private fun showMentionToolbar(state: ControlPanelState.Toolbar.MentionToolbar) {
state.cursorCoordinate?.let { cursorCoordinate ->
val parentBottom = calculateRectInWindow(binding.recycler).bottom
val toolbarHeight = binding.mentionSuggesterToolbar.getMentionSuggesterWidgetMinHeight()
val minPosY = parentBottom - toolbarHeight
if (minPosY <= cursorCoordinate) {
val scrollY = (parentBottom - minPosY) - (parentBottom - cursorCoordinate)
binding.recycler.addItemDecoration(footerMentionDecorator)
handler.post {
binding.recycler.smoothScrollBy(0, scrollY)
}
}
binding.mentionSuggesterToolbar.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = toolbarHeight
}
val set = ConstraintSet().apply {
clone(binding.sheet)
setVisibility(R.id.mentionSuggesterToolbar, View.VISIBLE)
connect(
R.id.mentionSuggesterToolbar,
ConstraintSet.BOTTOM,
R.id.sheet,
ConstraintSet.BOTTOM
)
}
val transitionSet = TransitionSet().apply {
addTransition(ChangeBounds())
duration = SHOW_MENTION_TRANSITION_DURATION
interpolator = LinearInterpolator()
ordering = TransitionSet.ORDERING_TOGETHER
}
TransitionManager.beginDelayedTransition(binding.sheet, transitionSet)
set.applyTo(binding.sheet)
}
}
private fun showSlashWidget(state: ControlPanelState.Toolbar.SlashWidget) {
state.cursorCoordinate?.let { cursorCoordinate ->
val parentBottom = calculateRectInWindow(binding.recycler).bottom
val toolbarHeight = binding.slashWidget.getWidgetMinHeight()
val minPosY = parentBottom - toolbarHeight
if (minPosY <= cursorCoordinate) {
val scrollY = (parentBottom - minPosY) - (parentBottom - cursorCoordinate)
binding.recycler.addItemDecoration(slashWidgetFooter)
handler.post {
binding.recycler.smoothScrollBy(
0,
scrollY,
DecelerateInterpolator(DECELERATE_INTERPOLATOR_FACTOR),
SLASH_SHOW_ANIM_DURATION.toInt()
)
}
}
binding.slashWidget.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = toolbarHeight
}
val set = ConstraintSet().apply {
clone(binding.sheet)
setVisibility(R.id.slashWidget, View.VISIBLE)
connect(
R.id.slashWidget,
ConstraintSet.BOTTOM,
R.id.sheet,
ConstraintSet.BOTTOM
)
}
applySlideTransition(binding.slashWidget, SLASH_SHOW_ANIM_DURATION, binding.sheet)
set.applyTo(binding.sheet)
}
}
private fun enterScrollAndMove() {
if (binding.recycler.lastDecorator() !is ScrollAndMoveTargetHighlighter) {
binding.recycler.addItemDecoration(scrollAndMoveTargetHighlighter)
showTargeterWithAnimation()
binding.recycler.addOnScrollListener(scrollAndMoveStateListener)
binding.multiSelectTopToolbar.invisible()
showTopScrollAndMoveToolbar()
binding.scrollAndMoveBottomAction.show()
hideBlockActionPanel()
lifecycleScope.launch {
delay(300)
searchScrollAndMoveTarget()
binding.recycler.invalidate()
}
} else {
Timber.d("Skipping enter scroll-and-move")
}
}
private fun showTargeterWithAnimation() {
binding.targeter.translationY = -binding.targeter.y
ObjectAnimator.ofFloat(
binding.targeter,
TARGETER_ANIMATION_PROPERTY,
0f
).apply {
duration = 300
doOnStart { binding.targeter.visible() }
interpolator = OvershootInterpolator()
start()
}
}
private fun exitScrollAndMove() {
binding.recycler.apply {
removeItemDecoration(scrollAndMoveTargetHighlighter)
removeOnScrollListener(scrollAndMoveStateListener)
}
hideTopScrollAndMoveToolbar()
binding.scrollAndMoveBottomAction.hide()
binding.targeter.invisible()
scrollAndMoveTargetDescriptor.clear()
}
private fun hideSelectButton() {
if (binding.multiSelectTopToolbar.translationY >= 0) {
ObjectAnimator.ofFloat(
binding.multiSelectTopToolbar,
SELECT_BUTTON_ANIMATION_PROPERTY,
-requireContext().dimen(R.dimen.dp_48)
).apply {
duration = SELECT_BUTTON_HIDE_ANIMATION_DURATION
interpolator = DecelerateInterpolator()
doOnEnd { if (hasBinding) binding.topToolbar.visible() }
start()
}
}
}
private fun showSelectButton() {
if (binding.multiSelectTopToolbar.translationY < 0) {
ObjectAnimator.ofFloat(
binding.multiSelectTopToolbar,
SELECT_BUTTON_ANIMATION_PROPERTY,
0f
).apply {
duration = SELECT_BUTTON_SHOW_ANIMATION_DURATION
interpolator = DecelerateInterpolator()
start()
}
}
}
private fun hideTopScrollAndMoveToolbar() {
ObjectAnimator.ofFloat(
binding.scrollAndMoveHint,
SELECT_BUTTON_ANIMATION_PROPERTY,
-requireContext().dimen(R.dimen.dp_48)
).apply {
duration = SELECT_BUTTON_HIDE_ANIMATION_DURATION
interpolator = DecelerateInterpolator()
start()
}
}
private fun showTopScrollAndMoveToolbar() {
ObjectAnimator.ofFloat(
binding.scrollAndMoveHint,
SELECT_BUTTON_ANIMATION_PROPERTY,
0f
).apply {
duration = SELECT_BUTTON_SHOW_ANIMATION_DURATION
interpolator = DecelerateInterpolator()
start()
}
}
override fun onClipboardAction(action: ClipboardInterceptor.Action) {
when (action) {
is ClipboardInterceptor.Action.Copy -> vm.onCopy(action.selection)
is ClipboardInterceptor.Action.Paste -> vm.onPaste(action.selection)
}
}
override fun onBookmarkPasted(url: Url) {
vm.onBookmarkPasted(url)
}
override fun onLinkPasted(url: Url) {
vm.proceedToAddUriToTextAsLink(url)
}
private fun hideKeyboard() {
Timber.d("Hiding keyboard")
hideSoftInput()
}
private fun extractDocumentId(): String {
return requireArguments()
.getString(ID_KEY)
?: throw IllegalStateException("Document id missing")
}
open fun saveAsLastOpened(): Boolean {
return true
}
private fun processScrollAndMoveStateChanges() {
lifecycleScope.launch {
scrollAndMoveStateChannel
.consumeAsFlow()
.mapLatest { searchScrollAndMoveTarget() }
.debounce(SAM_DEBOUNCE)
.collect { binding.recycler.invalidate() }
}
}
override fun injectDependencies() {
componentManager().editorComponent.get(extractDocumentId()).inject(this)
}
override fun releaseDependencies() {
componentManager().editorComponent.release(extractDocumentId())
}
private fun getEditorSettings() {
}
override fun onExitToDesktopClicked() {
vm.navigateToDesktop()
}
override fun onLanguageSelected(target: Id, key: String) {
Timber.d("key: $key")
vm.onSelectProgrammingLanguageClicked(target, key)
}
override fun onMoveToBinSuccess() {
vm.onMovedToBin()
}
override fun onSearchOnPageClicked() {
vm.onEnterSearchModeClicked()
}
override fun onSetTextBlockValue() {
vm.onSetTextBlockValue()
}
override fun onMentionClicked(target: Id) {
vm.onMentionClicked(target = target)
}
override fun onUndoRedoClicked() {
vm.onUndoRedoActionClicked()
}
override fun onDocRelationsClicked() {
vm.onDocRelationsClicked()
}
override fun onAddCoverClicked() {
vm.onAddCoverClicked()
}
override fun onSetIconClicked() {
findNavController().safeNavigate(
R.id.pageScreen,
R.id.objectIconPickerScreen,
bundleOf(
IconPickerFragmentBase.ARG_CONTEXT_ID_KEY to ctx,
)
)
}
override fun onLayoutClicked() {
vm.onLayoutClicked()
}
override fun onTextValueChanged(ctx: Id, text: String, objectId: Id, relationKey: Key) {
vm.onRelationTextValueChanged(
ctx = ctx,
value = text,
relationKey = relationKey
)
}
override fun onNumberValueChanged(ctx: Id, number: Double?, objectId: Id, relationKey: Key) {
vm.onRelationTextValueChanged(
ctx = ctx,
value = number,
relationKey = relationKey
)
}
override fun onDateValueChanged(
ctx: Id,
timeInSeconds: Number?,
objectId: Id,
relationKey: Key
) {
vm.onRelationTextValueChanged(
ctx = ctx,
relationKey = relationKey,
value = timeInSeconds
)
}
override fun onMoveTo(
target: Id,
blocks: List<Id>,
text: String,
icon: ObjectIcon,
isDataView: Boolean
) {
vm.proceedWithMoveToAction(
target = target,
text = text,
icon = icon,
blocks = blocks,
isDataView = isDataView
)
}
override fun onMoveToClose(blocks: List<Id>, restorePosition: Int?, restoreBlock: Id?) {
vm.proceedWithMoveToExit(
blocks = blocks,
restorePosition = restorePosition,
restoreBlock = restoreBlock
)
}
override fun onLinkTo(
link: Id,
target: Id,
isBookmark: Boolean
) {
vm.proceedWithLinkToAction(
link = link,
target = target,
isBookmark = isBookmark
)
}
override fun onLinkToClose(block: Id, position: Int?) {
vm.proceedWithLinkToExit(
block = block,
position = position
)
}
override fun onSetObjectLink(id: Id) {
vm.proceedToAddObjectToTextAsLink(id)
}
override fun onSetWebLink(link: String) {
vm.proceedToAddUriToTextAsLink(link)
}
override fun onCopyLink(link: String) {
vm.onCopyLinkClicked(link)
}
override fun onCreateObject(name: String) {
vm.proceedToCreateObjectAndAddToTextAsLink(name)
}
override fun onProceedWithUpdateType(objType: ObjectWrapper.Type) {
vm.onObjectTypeChanged(objType)
}
override fun onAddRelationToTarget(target: Id, relationKey: Key) {
vm.proceedWithAddingRelationToTarget(
target = target,
relationKey = relationKey
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
pickerDelegate.resolveActivityResult(requestCode, resultCode, data)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
open fun observeSelectingTemplate() {
val navController = findNavController()
val navBackStackEntry = navController.getBackStackEntry(R.id.pageScreen)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(ARG_TEMPLATE_ID)
) {
val resultTemplateId = navBackStackEntry.savedStateHandle.get<String>(ARG_TEMPLATE_ID)
if (resultTemplateId != null) {
navBackStackEntry.savedStateHandle.remove<String>(ARG_TEMPLATE_ID)
vm.onProceedWithApplyingTemplateByObjectId(template = resultTemplateId)
}
}
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
//------------ End of Anytype Custom Context Menu ------------
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentEditorBinding = FragmentEditorBinding.inflate(
inflater, container, false
)
companion object {
fun newInstance(id: String): EditorFragment = EditorFragment().apply {
arguments = bundleOf(ID_KEY to id)
}
const val ID_KEY = "id"
const val DEFAULT_ANIM_DURATION = 150L
const val DEFAULT_DELAY_BLOCK_ACTION_TOOLBAR = 100L
const val DEFAULT_TOOLBAR_ANIM_DURATION = 150L
const val SLASH_SHOW_ANIM_DURATION = 400L
const val SLASH_HIDE_ANIM_DURATION = 1200L
const val DECELERATE_INTERPOLATOR_FACTOR = 2.5f
const val SHOW_MENTION_TRANSITION_DURATION = 150L
const val SELECT_BUTTON_SHOW_ANIMATION_DURATION = 200L
const val SELECT_BUTTON_HIDE_ANIMATION_DURATION = 200L
const val SELECT_BUTTON_ANIMATION_PROPERTY = "translationY"
const val TARGETER_ANIMATION_PROPERTY = "translationY"
const val SAM_DEBOUNCE = 100L
const val DELAY_HIDE_KEYBOARD = 300L
const val TAG_ALERT = "tag.alert"
const val TAG_LINK = "tag.link"
const val EMPTY_TEXT = ""
const val DRAG_AND_DROP_LABEL = "Anytype's editor drag-and-drop."
private const val CURRENT_MEDIA_UPLOAD_KEY = "currentMediaUploadDescription"
}
}
interface OnFragmentInteractionListener {
fun onSetBlockWebLink(blockId: String, link: String)
fun onSetBlockObjectLink(blockId: Id, objectId: Id)
fun onRemoveMarkupLinkClicked(blockId: String, range: IntRange)
fun onAddBookmarkUrlClicked(target: String, url: String)
fun onExitToDesktopClicked()
fun onSetRelationKeyClicked(blockId: Id, key: Id)
fun onSetObjectLink(objectId: Id)
fun onSetWebLink(link: String)
fun onCreateObject(name: String)
fun onSetTextBlockValue()
fun onMentionClicked(target: Id)
fun onCopyLink(link: String)
fun onAddRelationToTarget(target: Id, relationKey: Key)
}