Restore common actions in comment field
This commit is contained in:
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 ->
|
||||
|
||||
+82
-76
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user