diff --git a/internal/common/enums.rs b/internal/common/enums.rs index b27e2a75621..9b398c2431b 100644 --- a/internal/common/enums.rs +++ b/internal/common/enums.rs @@ -345,7 +345,8 @@ macro_rules! for_each_enums { enum InputType { /// The default value. This will render all characters normally Text, - /// This will render all characters with a character that defaults to "*" + /// This will render all characters with a character that defaults to "*". + /// The text can't be copied or cut to the clipboard. Password, /// This will only accept and render number characters (0-9) Number, diff --git a/internal/compiler/widgets/common/lineedit-base.slint b/internal/compiler/widgets/common/lineedit-base.slint index 5f4bba3af24..a64a3c01ce3 100644 --- a/internal/compiler/widgets/common/lineedit-base.slint +++ b/internal/compiler/widgets/common/lineedit-base.slint @@ -91,7 +91,7 @@ export component LineEditBase inherits Rectangle { Menu { MenuItem { title: @tr("Cut"); - enabled: !root.read-only && root.enabled; + enabled: !root.read-only && root.enabled && root.input-type != InputType.password; activated => { text-input.cut(); } @@ -99,7 +99,7 @@ export component LineEditBase inherits Rectangle { MenuItem { title: @tr("Copy"); - enabled: !root.text.is-empty; + enabled: !root.text.is-empty && root.input-type != InputType.password; activated => { text-input.copy(); } diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index 41153470861..f4948e11042 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -1098,7 +1098,7 @@ impl Item for TextInput { self.paste(window_adapter, self_rc); return KeyEventResult::EventAccepted; } - StandardShortcut::Cut if !self.read_only() => { + StandardShortcut::Cut if !self.read_only() && !self.is_password() => { self.cut(window_adapter, self_rc); return KeyEventResult::EventAccepted; } @@ -2085,6 +2085,9 @@ impl TextInput { window_adapter: &Rc, clipboard: Clipboard, ) { + if self.is_password() { + return; + } let (anchor, cursor) = self.selection_anchor_and_cursor(); if anchor == cursor { return; diff --git a/tests/cases/widgets/lineedit_context_menu.slint b/tests/cases/widgets/lineedit_context_menu.slint new file mode 100644 index 00000000000..73fbae264bc --- /dev/null +++ b/tests/cases/widgets/lineedit_context_menu.slint @@ -0,0 +1,88 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// cSpell: ignore hunter +import { LineEdit } from "std-widgets.slint"; + +export component TestCase inherits Window { + width: 400px; + height: 400px; + + VerticalLayout { + alignment: start; + + plain := LineEdit { + text: "plain"; + } + + secret := LineEdit { + input-type: password; + text: "hunter2"; + } + + target := LineEdit { + text: ""; + } + } + + in-out property plain-text <=> plain.text; + in-out property secret-text <=> secret.text; + in-out property target-text <=> target.text; + + public function select-all-plain() { plain.focus(); plain.select-all(); } + public function select-all-secret() { secret.focus(); secret.select-all(); } + public function focus-target() { target.focus(); } +} + +/* +```rust +use slint::platform::PointerEventButton; +use slint_testing::ElementHandle; + +let instance = TestCase::new().unwrap(); +slint_testing::mock_elapsed_time(500); + +let open_menu = |id: &str| { + let field = ElementHandle::find_by_element_id(&instance, id).next().unwrap(); + field.mock_single_click(PointerEventButton::Right); + slint_testing::mock_elapsed_time(500); +}; +let click_entry = |title: &str| { + let entry = ElementHandle::find_by_accessible_label(&instance, title) + .next() + .unwrap_or_else(|| panic!("no '{title}' entry in the context menu")); + entry.mock_single_click(PointerEventButton::Left); + slint_testing::mock_elapsed_time(50); +}; + +// A plain LineEdit cuts through the context menu. This also proves the clicks below +// would activate an enabled entry. +instance.invoke_select_all_plain(); +open_menu("TestCase::plain"); +click_entry("Cut"); +assert_eq!(instance.get_plain_text(), "", "Cut must remove the selection of a plain LineEdit"); + +// Paste puts it back, so the clipboard now holds "plain". +instance.invoke_focus_target(); +open_menu("TestCase::target"); +click_entry("Paste"); +assert_eq!(instance.get_target_text(), "plain", "Paste must insert the clipboard content"); + +// Cut is disabled on a password field: the selection survives. +instance.invoke_select_all_secret(); +open_menu("TestCase::secret"); +click_entry("Cut"); +assert_eq!(instance.get_secret_text(), "hunter2", "Cut must not empty a password LineEdit"); + +// Copy is disabled too, so the clipboard still holds "plain" and not the password. +click_entry("Copy"); +slint_testing::send_keyboard_string_sequence(&instance, &slint::SharedString::from(slint::platform::Key::Escape)); +slint_testing::mock_elapsed_time(50); + +instance.set_target_text("".into()); +instance.invoke_focus_target(); +open_menu("TestCase::target"); +click_entry("Paste"); +assert_eq!(instance.get_target_text(), "plain", "Copy must not put a password on the clipboard"); +``` +*/