From 684d7f2d9a83ce93f954d82cc699be4168a00f06 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Wed, 15 Feb 2023 14:27:23 +0400 Subject: [PATCH] Paste text in calculator --- .../feature/calculator/CalculatorScreen.kt | 3 +- .../feature/calculator/CalculatorViewModel.kt | 2 + .../FloatingTextActionModeCallback.kt | 60 ++++++++++++++ .../calculator/components/InputTextField.kt | 19 ++++- .../components/UnittoActionModeCallback.kt | 81 +++++++++++++++++++ .../UnittoPrimaryTextActionModeCallback.kt | 43 ++++++++++ .../components/UnittoTextToolbar.kt | 80 ++++++++++++++++++ 7 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt index 514f2786..70c82148 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt @@ -79,7 +79,8 @@ private fun CalculatorScreen( text = uiState.input, selection = TextRange(uiState.selection.first, uiState.selection.last) ), - onCursorChange = onCursorChange + onCursorChange = onCursorChange, + pasteCallback = addSymbol ) AnimatedVisibility(visible = uiState.output.isNotEmpty()) { Text( diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt index 1f1daf40..36ae7a27 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt @@ -121,6 +121,8 @@ internal class CalculatorViewModel @Inject constructor( } fun onCursorChange(selection: IntRange) { + // When we paste, selection is set to the length of the pasted text (start and end) + if (selection.first > _input.value.length) return _selection.update { selection } } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt new file mode 100644 index 00000000..ab6711d8 --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt @@ -0,0 +1,60 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.calculator.components + +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.annotation.RequiresApi + +@RequiresApi(23) +internal class FloatingTextActionModeCallback( + private val callback: UnittoActionModeCallback +) : ActionMode.Callback2() { + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + return callback.onActionItemClicked(mode, item) + } + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return callback.onCreateActionMode(mode, menu) + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return callback.onPrepareActionMode() + } + + override fun onDestroyActionMode(mode: ActionMode?) { + callback.onDestroyActionMode() + } + + override fun onGetContentRect( + mode: ActionMode?, + view: View?, + outRect: android.graphics.Rect? + ) { + val rect = callback.rect + outRect?.set( + rect.left.toInt(), + rect.top.toInt(), + rect.right.toInt(), + rect.bottom.toInt() + ) + } +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt index b49360f3..aa722c2d 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt @@ -23,7 +23,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalTextInputService +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge @@ -32,16 +35,24 @@ import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge internal fun InputTextField( modifier: Modifier, value: TextFieldValue, - onCursorChange: (IntRange) -> Unit + onCursorChange: (IntRange) -> Unit, + pasteCallback: (String) -> Unit ) { + val clipboardManager = LocalClipboardManager.current CompositionLocalProvider( - // FIXME Can't paste if this is null - LocalTextInputService provides null + LocalTextInputService provides null, + LocalTextToolbar provides UnittoTextToolbar( + view = LocalView.current, + pasteCallback = { pasteCallback(clipboardManager.getText()?.text ?: "") } + ) ) { BasicTextField( modifier = modifier, + singleLine = true, value = value, - onValueChange = { onCursorChange(it.selection.start..it.selection.end) }, + onValueChange = { + onCursorChange(it.selection.start..it.selection.end) + }, textStyle = NumbersTextStyleDisplayLarge.copy( textAlign = TextAlign.End, color = MaterialTheme.colorScheme.onBackground diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt new file mode 100644 index 00000000..984469b8 --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt @@ -0,0 +1,81 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.calculator.components + +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import androidx.compose.ui.geometry.Rect + +private const val MENU_ITEM_COPY = 0 +private const val MENU_ITEM_PASTE = 1 +private const val MENU_ITEM_CUT = 2 +private const val MENU_ITEM_SELECT_ALL = 3 + +internal class UnittoActionModeCallback( + var rect: Rect = Rect.Zero, + var onCopyRequested: (() -> Unit)? = null, + var onPasteRequested: (() -> Unit)? = null, + var onCutRequested: (() -> Unit)? = null, + var onSelectAllRequested: (() -> Unit)? = null +) { + fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + requireNotNull(menu) + requireNotNull(mode) + + onCopyRequested?.let { + menu.add(0, MENU_ITEM_COPY, 0, android.R.string.copy) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + + onPasteRequested?.let { + menu.add(0, MENU_ITEM_PASTE, 1, android.R.string.paste) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + + onCutRequested?.let { + menu.add(0, MENU_ITEM_CUT, 2, android.R.string.cut) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + + onSelectAllRequested?.let { + menu.add(0, MENU_ITEM_SELECT_ALL, 3, android.R.string.selectAll) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + return true + } + + fun onPrepareActionMode(): Boolean { + return false + } + + fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + when (item!!.itemId) { + MENU_ITEM_COPY -> onCopyRequested?.invoke() + MENU_ITEM_PASTE -> onPasteRequested?.invoke() + MENU_ITEM_CUT -> onCutRequested?.invoke() + MENU_ITEM_SELECT_ALL -> onSelectAllRequested?.invoke() + else -> return false + } + mode?.finish() + return true + } + + fun onDestroyActionMode() {} +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt new file mode 100644 index 00000000..21750f3a --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt @@ -0,0 +1,43 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.calculator.components + +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem + +internal class UnittoPrimaryTextActionModeCallback( + private val callback: UnittoActionModeCallback +) : ActionMode.Callback { + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + return callback.onActionItemClicked(mode, item) + } + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return callback.onCreateActionMode(mode, menu) + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + return callback.onPrepareActionMode() + } + + override fun onDestroyActionMode(mode: ActionMode?) { + callback.onDestroyActionMode() + } +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt new file mode 100644 index 00000000..a11b996a --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt @@ -0,0 +1,80 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.calculator.components + +import android.os.Build +import android.view.ActionMode +import android.view.View +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.TextToolbar +import androidx.compose.ui.platform.TextToolbarStatus + +internal class UnittoTextToolbar( + private val view: View, + private val pasteCallback: () -> Unit +) : TextToolbar { + + private var actionMode: ActionMode? = null + private val textActionModeCallback: UnittoActionModeCallback = UnittoActionModeCallback() + override var status: TextToolbarStatus = TextToolbarStatus.Hidden + private set + + override fun showMenu( + rect: Rect, + onCopyRequested: (() -> Unit)?, + onPasteRequested: (() -> Unit)?, + onCutRequested: (() -> Unit)?, + onSelectAllRequested: (() -> Unit)? + ) { + textActionModeCallback.rect = rect + textActionModeCallback.onCopyRequested = onCopyRequested + textActionModeCallback.onCutRequested = onCutRequested + textActionModeCallback.onPasteRequested = { pasteCallback(); onPasteRequested?.invoke() } + textActionModeCallback.onSelectAllRequested = onSelectAllRequested + if (actionMode == null) { + status = TextToolbarStatus.Shown + actionMode = startActionMode(view, textActionModeCallback) + } else { + actionMode?.invalidate() + } + } + + override fun hide() { + status = TextToolbarStatus.Hidden + actionMode?.finish() + actionMode = null + } +} + +private fun startActionMode( + view: View, + textActionModeCallback: UnittoActionModeCallback +): ActionMode { + return if (Build.VERSION.SDK_INT >= 23) { + view.startActionMode( + FloatingTextActionModeCallback(textActionModeCallback), + ActionMode.TYPE_FLOATING + ) + } else { + // Old devices use toolbar instead of a floating menu + view.startActionMode( + UnittoPrimaryTextActionModeCallback(textActionModeCallback) + ) + } +}