diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt index 5f1080a6b..07cff6f59 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf @@ -70,7 +69,6 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -105,10 +103,10 @@ import com.hippo.ehviewer.ui.jumpToReaderByPage import com.hippo.ehviewer.ui.main.GalleryCommentCard import com.hippo.ehviewer.ui.main.NavigationIcon import com.hippo.ehviewer.ui.openBrowser +import com.hippo.ehviewer.ui.tools.addBBCodeTextContextMenuItems import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel import com.hippo.ehviewer.ui.tools.awaitSelectAction import com.hippo.ehviewer.ui.tools.normalizeSpan -import com.hippo.ehviewer.ui.tools.rememberBBCodeTextToolbar import com.hippo.ehviewer.ui.tools.showNoButton import com.hippo.ehviewer.ui.tools.toBBCode import com.hippo.ehviewer.ui.tools.updateSpan @@ -119,7 +117,6 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.system.logcat -import kotlin.collections.forEach import kotlin.math.roundToInt import moe.tarsin.coroutines.runSuspendCatching import moe.tarsin.navigate @@ -479,18 +476,15 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat }, ) { val color = MaterialTheme.colorScheme.onPrimaryContainer - val toolbar = rememberBBCodeTextToolbar(userCommentBackField) - CompositionLocalProvider(LocalTextToolbar provides toolbar) { - BasicTextField( - value = userComment, - onValueChange = { textFieldValue -> - userComment = textFieldValue.updateSpan(userComment) - }, - modifier = Modifier.weight(1f).padding(keylineMargin), - textStyle = MaterialTheme.typography.bodyLarge.merge(color = color), - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - ) - } + BasicTextField( + value = userComment, + onValueChange = { textFieldValue -> + userComment = textFieldValue.updateSpan(userComment) + }, + modifier = Modifier.weight(1f).padding(keylineMargin).addBBCodeTextContextMenuItems(userCommentBackField), + textStyle = MaterialTheme.typography.bodyLarge.merge(color = color), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + ) IconButton( onClick = { launchIO { sendComment() } }, shapes = IconButtonDefaults.shapes(), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextContextMenu.kt similarity index 52% rename from app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt rename to app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextContextMenu.kt index 9c3740737..0b847912f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextToolbar.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/BBCodeTextContextMenu.kt @@ -1,18 +1,17 @@ package com.hippo.ehviewer.ui.tools -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem +import android.content.Context +import androidx.annotation.StringRes +import androidx.compose.foundation.text.contextmenu.builder.item +import androidx.compose.foundation.text.contextmenu.data.TextContextMenuKeys +import androidx.compose.foundation.text.contextmenu.modifier.appendTextContextMenuComponents +import androidx.compose.foundation.text.contextmenu.modifier.filterTextContextMenuComponents import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.TextToolbar -import androidx.compose.ui.platform.TextToolbarStatus +import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange @@ -25,8 +24,7 @@ import androidx.compose.ui.text.input.getTextAfterSelection import androidx.compose.ui.text.input.getTextBeforeSelection import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle -import com.hippo.ehviewer.R -import com.hippo.ehviewer.ui.MainActivity +import com.ehviewer.core.i18n.R import com.hippo.ehviewer.util.toRangeSet import io.github.petertrr.diffutils.diffInline import io.github.petertrr.diffutils.patch.ChangeDelta @@ -36,8 +34,7 @@ import io.github.petertrr.diffutils.patch.InsertDelta import kotlinx.coroutines.delay import kotlinx.coroutines.launch import moe.tarsin.kt.unreachable - -typealias PlatformRect = android.graphics.Rect +import moe.tarsin.string fun TextFieldValue.updateSpan(origin: TextFieldValue): TextFieldValue { val oriSpan = origin.annotatedString.spanStyles @@ -141,118 +138,81 @@ fun AnnotatedString.toBBCode() = buildString { } @Composable -context(activity: MainActivity) -fun rememberBBCodeTextToolbar(textFieldValue: MutableState): TextToolbar { +context(_: Context) +fun Modifier.addBBCodeTextContextMenuItems(textFieldValue: MutableState): Modifier { var tfv by textFieldValue - val view = LocalView.current val coroutineScope = rememberCoroutineScope() - val toolbar = remember { - object : TextToolbar { - private var actionMode: ActionMode? = null - private val callback = object : TextActionModeCallback() { - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - super.onCreateActionMode(mode, menu) - activity.menuInflater.inflate(R.menu.context_comment, menu) - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - super.onPrepareActionMode(mode, menu) - if (tfv.selection.collapsed) { - menu.removeGroup(R.id.bbcode_group) + return appendTextContextMenuComponents { + if (tfv.selection.collapsed) return@appendTextContextMenuComponents + BBCodeFormat.entries.forEach { item -> + item(key = item, label = string(item.id)) { + val capturedTfv = tfv + val start = capturedTfv.selection.min + val end = capturedTfv.selection.max + val annotatedString = buildAnnotatedString { + val len = capturedTfv.text.length + fun addStyle(style: SpanStyle) { + append(capturedTfv.annotatedString) + addStyle(style, start, end) } - return true - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - val handled = super.onActionItemClicked(mode, item) - if (handled) { - return true - } - val capturedTfv = tfv - val start = capturedTfv.selection.min - val end = capturedTfv.selection.max - val annotatedString = buildAnnotatedString { - val len = capturedTfv.text.length - fun addStyle(style: SpanStyle) { - append(capturedTfv.annotatedString) - addStyle(style, start, end) + fun addTextDecoration(decoration: TextDecoration) { + append(capturedTfv.getTextBeforeSelection(len)) + val ans = capturedTfv.getSelectedText() + val spans = ans.spanStyles.filter { + it.item.textDecoration == null || it.item.textDecoration == decoration } - fun addTextDecoration(decoration: TextDecoration) { + withStyle(SpanStyle(textDecoration = decoration)) { + append(AnnotatedString(ans.text, spans)) + } + append(capturedTfv.getTextAfterSelection(len)) + } + when (item) { + BBCodeFormat.Bold -> addStyle(SpanStyle(fontWeight = FontWeight.Bold)) + BBCodeFormat.Italic -> addStyle(SpanStyle(fontStyle = FontStyle.Italic)) + BBCodeFormat.Underline -> addTextDecoration(TextDecoration.Underline) + BBCodeFormat.Strikethrough -> addTextDecoration(TextDecoration.LineThrough) + BBCodeFormat.Url -> append(capturedTfv.annotatedString) + BBCodeFormat.Clear -> { append(capturedTfv.getTextBeforeSelection(len)) - val ans = capturedTfv.getSelectedText() - val spans = ans.spanStyles.filter { - it.item.textDecoration == null || it.item.textDecoration == decoration - } - withStyle(SpanStyle(textDecoration = decoration)) { - append(AnnotatedString(ans.text, spans)) - } + append(capturedTfv.getSelectedText().text) append(capturedTfv.getTextAfterSelection(len)) } - when (item.itemId) { - R.id.action_bold -> addStyle(SpanStyle(fontWeight = FontWeight.Bold)) - R.id.action_italic -> addStyle(SpanStyle(fontStyle = FontStyle.Italic)) - R.id.action_underline -> addTextDecoration(TextDecoration.Underline) - R.id.action_strikethrough -> addTextDecoration(TextDecoration.LineThrough) - R.id.action_url -> append(capturedTfv.annotatedString) - R.id.action_clear -> { - append(capturedTfv.getTextBeforeSelection(len)) - append(capturedTfv.getSelectedText().text) - append(capturedTfv.getTextAfterSelection(len)) - } - } } - tfv = TextFieldValue( - annotatedString = buildAnnotatedString { - append(annotatedString) - // Hacky: Trigger recomposition to hide text toolbar - append(Char.MIN_VALUE) - }, - selection = TextRange(end), - ) - coroutineScope.launch { - // Hacky: Let TextField recompose first - delay(100) - tfv = tfv.copy(annotatedString = annotatedString) - } - - mode.finish() - return true } - - override fun onDestroyActionMode(mode: ActionMode) { - actionMode = null - } - } - - override var status = TextToolbarStatus.Hidden - - override fun hide() { - status = TextToolbarStatus.Hidden - actionMode?.finish() - actionMode = null - } - - override fun showMenu( - rect: Rect, - onCopyRequested: (() -> Unit)?, - onPasteRequested: (() -> Unit)?, - onCutRequested: (() -> Unit)?, - onSelectAllRequested: (() -> Unit)?, - ) { - callback.rect = rect - callback.onCopyRequested = onCopyRequested - callback.onCutRequested = onCutRequested - callback.onPasteRequested = onPasteRequested - callback.onSelectAllRequested = onSelectAllRequested - if (actionMode == null) { - status = TextToolbarStatus.Shown - actionMode = view.startActionMode(callback, ActionMode.TYPE_FLOATING) - } else { - actionMode?.invalidate() + tfv = TextFieldValue( + annotatedString = buildAnnotatedString { + append(annotatedString) + // Hacky: Trigger recomposition to hide text toolbar + append(Char.MIN_VALUE) + }, + selection = TextRange(end), + ) + coroutineScope.launch { + // Hacky: Let TextField recompose first + delay(100) + tfv = tfv.copy(annotatedString = annotatedString) } + close() } } + }.filterTextContextMenuComponents { + when (it.key) { + TextContextMenuKeys.CutKey, + TextContextMenuKeys.CopyKey, + TextContextMenuKeys.PasteKey, + TextContextMenuKeys.SelectAllKey, + is BBCodeFormat, + -> true + else -> false + } } - return toolbar +} + +private enum class BBCodeFormat(@StringRes val id: Int) { + Bold(R.string.format_bold), + Italic(R.string.format_italic), + Underline(R.string.format_underline), + Strikethrough(R.string.format_strikethrough), + Url(R.string.format_url), + Clear(R.string.format_plain), } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/TextActionModeCallback.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/TextActionModeCallback.kt deleted file mode 100644 index c77f0bf46..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/TextActionModeCallback.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.ehviewer.ui.tools - -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.compose.ui.geometry.Rect - -open class TextActionModeCallback( - var rect: Rect = Rect.Zero, - var onCopyRequested: (() -> Unit)? = null, - var onPasteRequested: (() -> Unit)? = null, - var onCutRequested: (() -> Unit)? = null, - var onSelectAllRequested: (() -> Unit)? = null, -) : ActionMode.Callback2() { - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - onCopyRequested?.let { - addMenuItem(menu, MenuItemOption.Copy) - } - onPasteRequested?.let { - addMenuItem(menu, MenuItemOption.Paste) - } - onCutRequested?.let { - addMenuItem(menu, MenuItemOption.Cut) - } - onSelectAllRequested?.let { - addMenuItem(menu, MenuItemOption.SelectAll) - } - return true - } - - // this method is called to populate new menu items when the actionMode was invalidated - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - updateMenuItems(menu) - // should return true so that new menu items are populated - return true - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - MenuItemOption.Copy.id -> onCopyRequested?.invoke() - MenuItemOption.Paste.id -> onPasteRequested?.invoke() - MenuItemOption.Cut.id -> onCutRequested?.invoke() - MenuItemOption.SelectAll.id -> onSelectAllRequested?.invoke() - else -> return false - } - mode.finish() - return true - } - - override fun onDestroyActionMode(mode: ActionMode) = Unit - - override fun onGetContentRect(mode: ActionMode, view: View, outRect: PlatformRect) { - outRect.set(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt()) - } - - private fun updateMenuItems(menu: Menu) { - addOrRemoveMenuItem(menu, MenuItemOption.Copy, onCopyRequested) - addOrRemoveMenuItem(menu, MenuItemOption.Paste, onPasteRequested) - addOrRemoveMenuItem(menu, MenuItemOption.Cut, onCutRequested) - addOrRemoveMenuItem(menu, MenuItemOption.SelectAll, onSelectAllRequested) - } - - private fun addMenuItem(menu: Menu, item: MenuItemOption) { - menu.add(0, item.id, item.order, item.titleResource) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) - } - - private fun addOrRemoveMenuItem( - menu: Menu, - item: MenuItemOption, - callback: (() -> Unit)?, - ) { - when { - callback != null && menu.findItem(item.id) == null -> addMenuItem(menu, item) - callback == null && menu.findItem(item.id) != null -> menu.removeItem(item.id) - } - } -} - -enum class MenuItemOption(val id: Int) { - Copy(0), - Paste(1), - Cut(2), - SelectAll(3), - ; - - val titleResource: Int - get() = when (this) { - Copy -> android.R.string.copy - Paste -> android.R.string.paste - Cut -> android.R.string.cut - SelectAll -> android.R.string.selectAll - } - - /** - * This item will be shown before all items that have order greater than this value. - */ - val order = id -}