Migrate to the new context menu APIs
This commit is contained in:
1 parent
e1252d0554
commit
f174ccfc04
3 files changed
+83
-245
No files matched your search
@@ -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(),
|
||||
|
||||
+73
-113
@@ -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<TextFieldValue>): TextToolbar {
|
||||
context(_: Context)
|
||||
fun Modifier.addBBCodeTextContextMenuItems(textFieldValue: MutableState<TextFieldValue>): 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),
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user