From e0aac55044f17a06059c46ac1b872ce388b139be Mon Sep 17 00:00:00 2001 From: Laurent Montel Date: Mon, 14 Sep 2026 15:17:57 +0200 Subject: [PATCH] TextEdit was missing undo/redo support. Add forwarding functions to the existing TextInput behaviour, and add context menu entries if lineedit is not enable/read-only. --- .../reference/std-widgets/views/textedit.mdx | 2 + .../widgets/common/textedit-base.slint | 24 ++++++ .../compiler/widgets/cosmic/textedit.slint | 8 ++ .../compiler/widgets/cupertino/textedit.slint | 22 ++++++ .../compiler/widgets/fluent/textedit.slint | 8 ++ .../compiler/widgets/material/textedit.slint | 8 ++ internal/compiler/widgets/qt/textedit.slint | 8 ++ tests/cases/widgets/textedit.slint | 74 +++++++++++++++++-- 8 files changed, 149 insertions(+), 5 deletions(-) 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 250e790211c..4bf09eccefd 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 @@ -140,6 +140,8 @@ The height of the visible area of the text edit (not including the scrollbar) - **`copy()`** Copies the selected text to the clipboard. - **`cut()`** Copies the selected text to the clipboard and removes it from the editable area. This function takes effect regardless of the `read-only` and `enabled` properties. - **`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 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); ``` */