diff --git a/docs/astro/src/content/docs/reference/std-widgets/views/textedit.mdx b/docs/astro/src/content/docs/reference/std-widgets/views/textedit.mdx index 6e4e73e6b92..8072fa5514c 100644 --- a/docs/astro/src/content/docs/reference/std-widgets/views/textedit.mdx +++ b/docs/astro/src/content/docs/reference/std-widgets/views/textedit.mdx @@ -156,6 +156,12 @@ Copies the selected text to the clipboard and removes it from the editable area. ### paste() Pastes the text content of the clipboard at the cursor position. This function takes effect regardless of the `read-only` and `enabled` properties. +### undo() +Undoes the last text operation. This function takes effect regardless of the `read-only` and `enabled` properties. + +### redo() +Redoes the last undone text operation. This function takes effect regardless of the `read-only` and `enabled` properties. + ## Callbacks ### edited(string) diff --git a/internal/compiler/widgets/common/textedit-base.slint b/internal/compiler/widgets/common/textedit-base.slint index e8acddb96ed..bae6f57e5bc 100644 --- a/internal/compiler/widgets/common/textedit-base.slint +++ b/internal/compiler/widgets/common/textedit-base.slint @@ -59,11 +59,35 @@ export component TextEditBase inherits Rectangle { text-input.paste(); } + public function undo() { + text-input.undo(); + } + + public function redo() { + text-input.redo(); + } + forward-focus: text-input; ContextMenuArea { enabled: root.enabled; Menu { + if !root.read-only && root.enabled: MenuItem { + title: @tr("Undo"); + activated => { + text-input.undo(); + } + } + + if !root.read-only && root.enabled: MenuItem { + title: @tr("Redo"); + activated => { + text-input.redo(); + } + } + + if !root.read-only && root.enabled: MenuSeparator { } + MenuItem { title: @tr("Cut"); enabled: !root.read-only && root.enabled; diff --git a/internal/compiler/widgets/cosmic/textedit.slint b/internal/compiler/widgets/cosmic/textedit.slint index 52094e9510b..b97222abdef 100644 --- a/internal/compiler/widgets/cosmic/textedit.slint +++ b/internal/compiler/widgets/cosmic/textedit.slint @@ -63,6 +63,14 @@ export component TextEdit { base.paste(); } + public function undo() { + base.undo(); + } + + public function redo() { + base.redo(); + } + forward-focus: base; horizontal-stretch: 1; vertical-stretch: 1; diff --git a/internal/compiler/widgets/cupertino/textedit.slint b/internal/compiler/widgets/cupertino/textedit.slint index bb8c425c99b..462ef9d7eb9 100644 --- a/internal/compiler/widgets/cupertino/textedit.slint +++ b/internal/compiler/widgets/cupertino/textedit.slint @@ -117,6 +117,14 @@ export component TextEdit { text-input.paste(); } + public function undo() { + text-input.undo(); + } + + public function redo() { + text-input.redo(); + } + forward-focus: text-input; horizontal-stretch: 1; vertical-stretch: 1; @@ -146,21 +154,35 @@ export component TextEdit { } ContextMenuArea { + enabled: root.enabled; Menu { + if !root.read-only && root.enabled: MenuItem { + title: @tr("Undo"); + activated => { text-input.undo(); } + } + if !root.read-only && root.enabled: MenuItem { + title: @tr("Redo"); + activated => { text-input.redo(); } + } + if !root.read-only && root.enabled: MenuSeparator { } MenuItem { title: @tr("Cut"); + enabled: !root.read-only && root.enabled; activated => { text-input.cut(); } } MenuItem { title: @tr("Copy"); + enabled: !root.text.is-empty; activated => { text-input.copy(); } } MenuItem { title: @tr("Paste"); + enabled: !root.read-only && root.enabled; activated => { text-input.paste(); } } MenuItem { title: @tr("Select All"); + enabled: !root.text.is-empty; activated => { text-input.select-all(); } } } diff --git a/internal/compiler/widgets/fluent/textedit.slint b/internal/compiler/widgets/fluent/textedit.slint index 2347e38c8fe..1674f431918 100644 --- a/internal/compiler/widgets/fluent/textedit.slint +++ b/internal/compiler/widgets/fluent/textedit.slint @@ -63,6 +63,14 @@ export component TextEdit { base.paste(); } + public function undo() { + base.undo(); + } + + public function redo() { + base.redo(); + } + forward-focus: base; horizontal-stretch: 1; vertical-stretch: 1; diff --git a/internal/compiler/widgets/material/textedit.slint b/internal/compiler/widgets/material/textedit.slint index 8e3e32c7b03..b90789aa484 100644 --- a/internal/compiler/widgets/material/textedit.slint +++ b/internal/compiler/widgets/material/textedit.slint @@ -63,6 +63,14 @@ export component TextEdit { base.paste(); } + public function undo() { + base.undo(); + } + + public function redo() { + base.redo(); + } + forward-focus: base; horizontal-stretch: 1; vertical-stretch: 1; diff --git a/internal/compiler/widgets/qt/textedit.slint b/internal/compiler/widgets/qt/textedit.slint index 40174e1f353..d7b2b60b488 100644 --- a/internal/compiler/widgets/qt/textedit.slint +++ b/internal/compiler/widgets/qt/textedit.slint @@ -62,6 +62,14 @@ export component TextEdit { base.paste(); } + public function undo() { + base.undo(); + } + + public function redo() { + base.redo(); + } + forward-focus: base; horizontal-stretch: 1; vertical-stretch: 1; diff --git a/tests/cases/widgets/textedit.slint b/tests/cases/widgets/textedit.slint index 9a17133ace6..634e5625a1a 100644 --- a/tests/cases/widgets/textedit.slint +++ b/tests/cases/widgets/textedit.slint @@ -31,6 +31,12 @@ export component TestCase inherits Window { public function select_all() { edit.select_all(); } + public function undo() { + edit.undo(); + } + public function redo() { + edit.redo(); + } } /* @@ -41,6 +47,18 @@ use std::cell::RefCell; use std::rc::Rc; use slint::SharedString; +// Opens the context menu with the menu key and activates the `n`th entry, +// counting from 1 and skipping separators the way the menu's arrow keys do. +// Activating a disabled entry leaves the menu open, so this closes it. +fn menu_activate(instance: &TestCase, n: usize) { + slint_testing::send_keyboard_string_sequence(instance, &SharedString::from(slint::platform::Key::Menu)); + for _ in 0..n { + slint_testing::send_keyboard_string_sequence(instance, &SharedString::from(slint::platform::Key::DownArrow)); + } + slint_testing::send_keyboard_string_sequence(instance, "\n"); + slint_testing::send_keyboard_string_sequence(instance, &SharedString::from(slint::platform::Key::Escape)); +} + fn ctrl_key(instance: &TestCase, key: &str) { slint_testing::send_keyboard_char(instance, slint::private_unstable_api::re_exports::Key::Control.into(), true); slint_testing::send_keyboard_string_sequence(instance, key); @@ -99,11 +117,8 @@ slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(slin slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(slint::platform::Key::UpArrow)); slint_testing::send_keyboard_string_sequence(&instance, "\n"); assert_eq!(instance.get_text(), "Hellođź‘‹"); -// copy -slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(slint::platform::Key::Menu)); -slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(slint::platform::Key::DownArrow)); -slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(slint::platform::Key::DownArrow)); -slint_testing::send_keyboard_string_sequence(&instance, "\n"); +// copy: Undo, Redo and a separator come first, so Copy is the fourth entry the arrow keys reach +menu_activate(&instance, 4); assert_eq!(instance.get_text(), "Hellođź‘‹"); slint_testing::send_keyboard_string_sequence(&instance, "Xxx"); @@ -143,6 +158,55 @@ assert_eq!(instance.get_font_italic(), true); instance.invoke_select_all(); instance.invoke_cut(); assert_eq!(instance.get_text(), ""); + +// undo and redo, through the public functions +instance.set_enabled(true); +slint_testing::send_keyboard_string_sequence(&instance, "abc"); +assert_eq!(instance.get_text(), "abc"); +instance.invoke_undo(); +assert_eq!(instance.get_text(), ""); +instance.invoke_redo(); +assert_eq!(instance.get_text(), "abc"); + +// and through the first two entries of the context menu +menu_activate(&instance, 1); +assert_eq!(instance.get_text(), ""); +menu_activate(&instance, 2); +assert_eq!(instance.get_text(), "abc"); + +// neither the menu nor Ctrl+Z reaches a read-only or a disabled TextEdit +instance.invoke_select_all(); +instance.invoke_cut(); +assert_eq!(instance.get_text(), ""); +// the menu above took the keyboard focus away from the TextEdit, so click it back +slint_testing::send_mouse_click(&instance, 25., 25.); +instance.set_read_only(true); +ctrl_key(&instance, "z"); +assert_eq!(instance.get_text(), ""); +// read-only hides Undo and Redo, so the first entry is Cut, which read-only disables +menu_activate(&instance, 1); +assert_eq!(instance.get_text(), ""); +instance.set_read_only(false); +slint_testing::send_mouse_click(&instance, 25., 25.); +instance.set_enabled(false); +ctrl_key(&instance, "z"); +assert_eq!(instance.get_text(), ""); +instance.set_enabled(true); + +// with neither in the way, the same shortcut undoes the cut +ctrl_key(&instance, "z"); +assert_eq!(instance.get_text(), "abc"); + +// with Undo and Redo hidden, a read-only menu lists Cut, Copy, Paste and Select All, +// so its fourth entry selects the text instead of copying it +slint_testing::send_mouse_click(&instance, 25., 25.); +// the undo above left the whole text selected, so collapse the selection first +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(slint::platform::Key::Home)); +instance.set_read_only(true); +menu_activate(&instance, 4); +instance.invoke_cut(); +assert_eq!(instance.get_text(), ""); +instance.set_read_only(false); ``` */