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)
+ )
+ }
+}