From 8cec823c7ca1c9bedca090becef3e4977b3e8ca8 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 17 Mar 2021 17:12:28 -0400 Subject: [PATCH] Handle standard text keyboard shortcuts without menu The textbox will now receive copy/cut/paste/undo/redo/select-all without a menu being present. Several of these (select-all, undo/redo) do not currently have implementations, but we will at least get the commands. - progress on #1652 - closes #1030 --- CHANGELOG.md | 2 ++ druid/src/command.rs | 3 ++ druid/src/text/input_component.rs | 8 ++++++ druid/src/widget/textbox.rs | 46 +++++++++++++++++++++++++++++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af373a3ffb..e3a6d038fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ You can find its changes [documented below](#070---2021-01-01). ### Fixed - `Notification`s will not be delivered to the widget that sends them ([#1640] by [@cmyr]) +- `TextBox` can handle standard keyboard shortcuts without needing menus ([#1660] by [@cmyr]) - Fixed docs of derived Lens ([#1523] by [@Maan2003]) @@ -644,6 +645,7 @@ Last release without a changelog :( [#1640]: https://github.com/linebender/druid/pull/1640 [#1641]: https://github.com/linebender/druid/pull/1641 [#1647]: https://github.com/linebender/druid/pull/1647 +[#1660]: https://github.com/linebender/druid/pull/1660 [Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master [0.7.0]: https://github.com/linebender/druid/compare/v0.6.0...v0.7.0 diff --git a/druid/src/command.rs b/druid/src/command.rs index d66f58825c..b3b61aac80 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -325,6 +325,9 @@ pub mod sys { /// Redo. pub const REDO: Selector = Selector::new("druid-builtin.menu-redo"); + /// Select all. + pub const SELECT_ALL: Selector = Selector::new("druid-builtin.menu-select-all"); + /// Text input state has changed, and we need to notify the platform. pub(crate) const INVALIDATE_IME: Selector = Selector::new("druid-builtin.invalidate-ime"); diff --git a/druid/src/text/input_component.rs b/druid/src/text/input_component.rs index a402bd1d97..651cd9e8d0 100644 --- a/druid/src/text/input_component.rs +++ b/druid/src/text/input_component.rs @@ -202,6 +202,14 @@ impl TextComponent { self.lock.get() == ImeLock::None } + /// Returns `true` if the IME is actively composing (or the text is locked.) + /// + /// When text is composing, you should avoid doing things like modifying the + /// selection or copy/pasting text. + pub fn is_composing(&self) -> bool { + self.can_read() && self.borrow().composition_range.is_some() + } + /// Attempt to mutably borrow the inner [`EditSession`]. /// /// # Panics diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index f7e752120b..da30cdee30 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -23,7 +23,8 @@ use crate::text::{EditableText, Selection, TextComponent, TextLayout, TextStorag use crate::widget::prelude::*; use crate::widget::{Padding, Scroll, WidgetWrapper}; use crate::{ - theme, Color, FontDescriptor, KeyOrValue, Point, Rect, TextAlignment, TimerToken, Vec2, + theme, Color, Command, FontDescriptor, HotKey, KeyEvent, KeyOrValue, Point, Rect, SysMods, + TextAlignment, TimerToken, Vec2, }; const TEXTBOX_INSETS: Insets = Insets::new(4.0, 2.0, 4.0, 2.0); @@ -328,6 +329,35 @@ impl TextBox { self.inner.wrapped_mut().scroll_to(rect + SCROLL_TO_INSETS); } } + + /// These commands may be supplied by menus; but if they aren't, we + /// inject them again, here. + fn fallback_do_builtin_command( + &mut self, + ctx: &mut EventCtx, + key: &KeyEvent, + ) -> Option { + use crate::commands as sys; + let our_id = ctx.widget_id(); + match key { + key if HotKey::new(SysMods::Cmd, "c").matches(key) => Some(sys::COPY.to(our_id)), + key if HotKey::new(SysMods::Cmd, "x").matches(key) => Some(sys::CUT.to(our_id)), + // we have to send paste to the window, in order to get it converted into the `Paste` + // event + key if HotKey::new(SysMods::Cmd, "v").matches(key) => { + Some(sys::PASTE.to(ctx.window_id())) + } + key if HotKey::new(SysMods::Cmd, "z").matches(key) => Some(sys::UNDO.to(our_id)), + key if HotKey::new(SysMods::CmdShift, "Z").matches(key) && !cfg!(windows) => { + Some(sys::REDO.to(our_id)) + } + key if HotKey::new(SysMods::Cmd, "y").matches(key) && cfg!(windows) => { + Some(sys::REDO.to(our_id)) + } + key if HotKey::new(SysMods::Cmd, "a").matches(key) => Some(sys::SELECT_ALL.to(our_id)), + _ => None, + } + } } impl Widget for TextBox { @@ -363,6 +393,12 @@ impl Widget for TextBox { } _ => (), }, + Event::KeyDown(key) if !self.text().is_composing() => { + if let Some(cmd) = self.fallback_do_builtin_command(ctx, key) { + ctx.submit_command(cmd); + ctx.set_handled(); + } + } Event::MouseDown(mouse) if self.text().can_write() => { if !mouse.focus { ctx.request_focus(); @@ -383,13 +419,17 @@ impl Widget for TextBox { self.reset_cursor_blink(ctx.request_timer(CURSOR_BLINK_DURATION)); } Event::Command(ref cmd) - if self.text().can_read() && ctx.is_focused() && cmd.is(crate::commands::COPY) => + if !self.text().is_composing() + && ctx.is_focused() + && cmd.is(crate::commands::COPY) => { self.text().borrow().set_clipboard(); ctx.set_handled(); } Event::Command(cmd) - if self.text().can_write() && ctx.is_focused() && cmd.is(crate::commands::CUT) => + if !self.text().is_composing() + && ctx.is_focused() + && cmd.is(crate::commands::CUT) => { if self.text().borrow().set_clipboard() { let inval = self.text_mut().borrow_mut().insert_text(data, "");