Restore common actions in comment field

This commit is contained in:
FooIbar committed 2023-11-21 22:00:18 +08:00
1 parent bb2d500067
commit 1a1d444b20
4 files changed
+227 -107

No files matched your search

@@ -80,7 +80,6 @@ import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
@@ -113,7 +112,6 @@ import com.hippo.ehviewer.ui.main.GalleryCommentCard
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.NoopClipboardManager
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
import com.hippo.ehviewer.ui.tools.normalizeSpan
import com.hippo.ehviewer.ui.tools.rememberBBCodeTextToolbar
@@ -462,10 +460,7 @@ fun GalleryCommentsScreen(galleryDetail: GalleryDetail, navigator: NavController
) {
val color = MaterialTheme.colorScheme.onPrimaryContainer
val toolbar = rememberBBCodeTextToolbar(userCommentBackField)
CompositionLocalProvider(
LocalTextToolbar provides toolbar,
LocalClipboardManager provides NoopClipboardManager,
) {
CompositionLocalProvider(LocalTextToolbar provides toolbar) {
BasicTextField(
value = userComment,
onValueChange = { textFieldValue ->
@@ -8,7 +8,6 @@ import android.text.style.UnderlineSpan
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -17,7 +16,6 @@ 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.ClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.TextToolbar
@@ -48,11 +46,6 @@ import moe.tarsin.kt.unreachable
typealias PlatformRect = android.graphics.Rect
object NoopClipboardManager : ClipboardManager {
override fun getText() = null
override fun setText(annotatedString: AnnotatedString) = Unit
}
fun TextFieldValue.updateSpan(origin: TextFieldValue): TextFieldValue {
val oriSpan = origin.annotatedString.spanStyles
val annoStr = origin.annotatedString
@@ -182,6 +175,82 @@ fun rememberBBCodeTextToolbar(textFieldValue: MutableState<TextFieldValue>): Tex
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 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
}
withStyle(SpanStyle(textDecoration = decoration)) {
append(AnnotatedString(ans.text, spans))
}
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() {
@@ -197,77 +266,14 @@ fun rememberBBCodeTextToolbar(textFieldValue: MutableState<TextFieldValue>): Tex
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(
object : ActionMode.Callback2() {
override fun onCreateActionMode(p0: ActionMode, p1: Menu): Boolean {
activity.menuInflater.inflate(R.menu.context_comment, p1)
return true
}
override fun onPrepareActionMode(p0: ActionMode, p1: Menu): Boolean {
return true
}
override fun onActionItemClicked(p0: ActionMode, p1: MenuItem): Boolean {
val capturedTfv = tfv
coroutineScope.launch {
// Hacky: Let TextField recompose first
delay(100)
// Reversed range is not supported by AnnotatedString
val start = capturedTfv.selection.start.coerceAtMost(capturedTfv.selection.end)
val end = capturedTfv.selection.start.coerceAtLeast(capturedTfv.selection.end)
tfv = TextFieldValue(
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
}
withStyle(SpanStyle(textDecoration = decoration)) {
append(AnnotatedString(ans.text, spans))
}
append(capturedTfv.getTextAfterSelection(len))
}
when (p1.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))
}
}
},
selection = TextRange(end),
)
}
// Hacky: Notify BasicTextField clear state
onCopyRequested?.invoke()
return true
}
override fun onDestroyActionMode(p0: ActionMode) {
actionMode = null
}
override fun onGetContentRect(mode: ActionMode, view: View, outRect: PlatformRect) {
outRect.set(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt())
}
},
ActionMode.TYPE_FLOATING,
)
actionMode = view.startActionMode(callback, ActionMode.TYPE_FLOATING)
} else {
actionMode?.invalidate()
}
@@ -0,0 +1,116 @@
/*
* 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) {}
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
}
+28 -25
View File
@@ -1,29 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_bold"
android:title="@string/format_bold"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_italic"
android:title="@string/format_italic"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_underline"
android:title="@string/format_underline"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_strikethrough"
android:title="@string/format_strikethrough"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_url"
android:title="@string/format_url"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_clear"
android:title="@string/format_plain"
app:showAsAction="ifRoom" />
<group
android:id="@+id/bbcode_group"
android:menuCategory="secondary">
<item
android:id="@+id/action_bold"
android:title="@string/format_bold"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_italic"
android:title="@string/format_italic"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_underline"
android:title="@string/format_underline"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_strikethrough"
android:title="@string/format_strikethrough"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_url"
android:title="@string/format_url"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_clear"
android:title="@string/format_plain"
app:showAsAction="ifRoom" />
</group>
</menu>