Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper password text input field #3062

Closed
sebkur opened this issue Apr 16, 2023 · 5 comments
Closed

Proper password text input field #3062

sebkur opened this issue Apr 16, 2023 · 5 comments
Labels
desktop enhancement New feature or request

Comments

@sebkur
Copy link
Contributor

sebkur commented Apr 16, 2023

I think its common for password fields to have copying and cutting text out of them disabled. Pasting should still be possible for people to paste passwords e.g. from password managers, however it's not common to be able to cut or copy text out of them using keyboard shortcuts (Ctrl-C, Ctrl-X, Command-C, Command-X) or by selecting the text and right clicking with the mouse, then selecting "Cut" or "Copy" from there.

We have a password field in our app that employs PasswordVisualTransformation to make characters typed unreadable. While not strictly necessary for this example, it also has a trailing button to toggle visibility of the password, i.e. toggle the password visual transformation.

Now to disable the ability to cut and copy text from the field, we came up with the solution to provide a modified clipboard manager using CompositionLocalProvider. It effectively makes it impossible to copy anything to the clipboard, however, the UX is not optimal. It's still possible to Ctrl-X cut the text into nirvana. Also, when disabling the password transformation using the trailing button, the text can be selected and right clicked, revealing the usual context menu with "copy" and "cut" actions. While they don't put anything into the clipboard either, it would be better if those buttons were not there in the first place.

I found a few hacky solutions on the web, but they seem to only work on Android, not on desktop:

For reference, here's example code with the custom clipboard manager in place:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication

fun main() {
    singleWindowApplication(title = "Password test") {
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text("Enter a password below:")

            var password by remember { mutableStateOf("") }
            var isPasswordVisible by remember { mutableStateOf(false) }
            val clipboardManager: ClipboardManager = LocalClipboardManager.current

            CompositionLocalProvider(
                LocalClipboardManager provides object : ClipboardManager {
                    override fun getText() = clipboardManager.getText() // allow pasting text from clipboard
                    override fun setText(annotatedString: AnnotatedString) =
                        Unit // don't allow copying text into clipboard
                }) {
                OutlinedTextField(
                    value = password,
                    onValueChange = { p -> password = p },
                    visualTransformation = if (!isPasswordVisible) PasswordVisualTransformation() else VisualTransformation.None,
                    trailingIcon = {
                        ShowHidePasswordIcon(
                            isVisible = isPasswordVisible,
                            toggleIsVisible = {
                                isPasswordVisible = !isPasswordVisible
                            },
                        )
                    },
                )
            }
        }
    }
}

@Composable
private fun ShowHidePasswordIcon(
    isVisible: Boolean,
    toggleIsVisible: () -> Unit,
) = IconButton(
    onClick = toggleIsVisible
) {
    Icon(if (isVisible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, null)
}
@sebkur sebkur added enhancement New feature or request submitted labels Apr 16, 2023
@sebkur
Copy link
Contributor Author

sebkur commented Apr 16, 2023

While it's relatively simple to create a password field from OutlinedTextField using PasswordVisualTransformation it does not seem to be so easy to modify these kind of things mentioned above. I'm wondering if the way to go is try to modify the normal text field behavior or would it be better if there was a library Composable for a proper password text field?

@sebkur
Copy link
Contributor Author

sebkur commented Apr 16, 2023

For comparison, a Swing JPasswordField does implement this behavior:

import java.awt.Dimension
import javax.swing.BoxLayout
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JPasswordField

fun main() {
    val frame = JFrame("Password test")
    frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE

    val panel = JPanel()
    panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS)
    frame.contentPane = panel

    val text = JLabel("Enter a password below:")
    val password = JPasswordField()
    password.maximumSize = Dimension(Integer.MAX_VALUE, password.minimumSize.height)

    panel.add(text)
    panel.add(password)

    frame.size = Dimension(800, 600)
    frame.isVisible = true
}

@sebkur
Copy link
Contributor Author

sebkur commented Apr 18, 2023

My colleague pointed me to the tutorial on modifying the context menu. I thought this might help in improving the password text field behavior, however I have some problems with it:

  • I found a way to add new items to the context menu, but have not found a way to remove some of them (i.e. copy and cut)
  • It still does not disable cut and copy shortcuts (Ctrl+C, Ctrl+X)

I discovered something interesting though: CoreTextField already has some special logic for the case when the PasswordVisualTransformation is applied to it: https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt#L417

@tangshimin
Copy link

disables the copy and ContextMenu

var text by remember { mutableStateOf("input password") }
CustomTextMenuProvider {
    OutlinedTextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("disable copy example") }
    )
}


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CustomTextMenuProvider(content: @Composable () -> Unit) {
    CompositionLocalProvider(
        LocalTextContextMenu provides object : TextContextMenu {
            @Composable
            override fun Area(
                textManager: TextContextMenu.TextManager,
                state: ContextMenuState,
                content: @Composable () -> Unit
            )  {
                val localization = LocalLocalization.current
                val items = {
                    listOfNotNull(
                        textManager.paste?.let {
                            ContextMenuItem(localization.paste, it)
                        },
                    )
                }

                ContextMenuArea(items, state, content = content)
            }
        },
        LocalClipboardManager provides object :  ClipboardManager {
            // paste
            override fun getText(): AnnotatedString? {
                return AnnotatedString(Toolkit.getDefaultToolkit().systemClipboard.getContents(null).toString())
            }
            // copy
            override fun setText(text: AnnotatedString) {}
        },
        content = content
    )
}

@okushnikov
Copy link
Collaborator

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
desktop enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants