Skip to content

Commit

Permalink
Handle standard text keyboard shortcuts without menu
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cmyr committed Mar 17, 2021
1 parent c6142e6 commit 8cec823
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImeInvalidation> =
Selector::new("druid-builtin.invalidate-ime");
Expand Down
8 changes: 8 additions & 0 deletions druid/src/text/input_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ impl<T> TextComponent<T> {
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
Expand Down
46 changes: 43 additions & 3 deletions druid/src/widget/textbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -328,6 +329,35 @@ impl<T: TextStorage + EditableText> TextBox<T> {
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<Command> {
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<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
Expand Down Expand Up @@ -363,6 +393,12 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
}
_ => (),
},
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();
Expand All @@ -383,13 +419,17 @@ impl<T: TextStorage + EditableText> Widget<T> for TextBox<T> {
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, "");
Expand Down

0 comments on commit 8cec823

Please sign in to comment.