Skip to content

Commit

Permalink
Add primary keyboard support on unix (selection copy, middle click pa…
Browse files Browse the repository at this point in the history
…ste)

Closes #2146
  • Loading branch information
ogoffart committed Jan 31, 2023
1 parent 32d2ba7 commit f34b1bd
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 66 deletions.
26 changes: 18 additions & 8 deletions internal/backends/qt/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,26 +222,36 @@ impl i_slint_core::platform::Platform for Backend {
}

#[cfg(not(no_qt))]
fn set_clipboard_text(&self, _text: &str) {
fn set_clipboard_text(&self, _text: &str, _clipboard: i_slint_core::platform::Clipboard) {
use cpp::cpp;
let is_selection: bool = match _clipboard {
i_slint_core::platform::Clipboard::DefaultClipboard => false,
i_slint_core::platform::Clipboard::SelectionClipboard => true,
_ => return,
};
let text: qttypes::QString = _text.into();
cpp! {unsafe [text as "QString"] {
cpp! {unsafe [text as "QString", is_selection as "bool"] {
ensure_initialized();
QGuiApplication::clipboard()->setText(text);
QGuiApplication::clipboard()->setText(text, is_selection ? QClipboard::Selection : QClipboard::Clipboard);
} }
}

#[cfg(not(no_qt))]
fn clipboard_text(&self) -> Option<String> {
fn clipboard_text(&self, _clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
use cpp::cpp;
let has_text = cpp! {unsafe [] -> bool as "bool" {
let is_selection: bool = match _clipboard {
i_slint_core::platform::Clipboard::DefaultClipboard => false,
i_slint_core::platform::Clipboard::SelectionClipboard => true,
_ => return None,
};
let has_text = cpp! {unsafe [is_selection as "bool"] -> bool as "bool" {
ensure_initialized();
return QGuiApplication::clipboard()->mimeData()->hasText();
return QGuiApplication::clipboard()->mimeData(is_selection ? QClipboard::Selection : QClipboard::Clipboard)->hasText();
} };
if has_text {
return Some(
cpp! { unsafe [] -> qttypes::QString as "QString" {
return QGuiApplication::clipboard()->text();
cpp! { unsafe [is_selection as "bool"] -> qttypes::QString as "QString" {
return QGuiApplication::clipboard()->text(is_selection ? QClipboard::Selection : QClipboard::Clipboard);
}}
.into(),
);
Expand Down
18 changes: 12 additions & 6 deletions internal/backends/testing/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ impl i_slint_core::platform::Platform for TestingBackend {
core::time::Duration::from_millis(i_slint_core::animations::current_tick().0)
}

fn set_clipboard_text(&self, text: &str) {
*self.clipboard.lock().unwrap() = Some(text.into());
}

fn clipboard_text(&self) -> Option<String> {
self.clipboard.lock().unwrap().clone()
fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
*self.clipboard.lock().unwrap() = Some(text.into());
}
}

fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
if clipboard == i_slint_core::platform::Clipboard::DefaultClipboard {
self.clipboard.lock().unwrap().clone()
} else {
None
}
}
}

Expand Down
108 changes: 72 additions & 36 deletions internal/backends/winit/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ pub trait WinitWindow: WindowAdapter {
fn take_pending_redraw(&self) -> bool;
}

/// The Default, and the selection clippoard
type ClipboardPair = (Box<dyn copypasta::ClipboardProvider>, Box<dyn copypasta::ClipboardProvider>);

struct NotRunningEventLoop {
clipboard: RefCell<Box<dyn copypasta::ClipboardProvider>>,
clipboard: RefCell<ClipboardPair>,
instance: winit::event_loop::EventLoop<CustomEvent>,
event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>,
}
Expand All @@ -69,13 +72,16 @@ impl NotRunningEventLoop {
struct RunningEventLoop<'a> {
event_loop_target: &'a winit::event_loop::EventLoopWindowTarget<CustomEvent>,
event_loop_proxy: &'a winit::event_loop::EventLoopProxy<CustomEvent>,
clipboard: &'a RefCell<Box<dyn copypasta::ClipboardProvider>>,
clipboard: &'a RefCell<ClipboardPair>,
}

pub(crate) trait EventLoopInterface {
fn event_loop_target(&self) -> &winit::event_loop::EventLoopWindowTarget<CustomEvent>;
fn event_loop_proxy(&self) -> &winit::event_loop::EventLoopProxy<CustomEvent>;
fn clipboard(&self) -> RefMut<'_, dyn ClipboardProvider>;
fn clipboard(
&self,
_: i_slint_core::platform::Clipboard,
) -> Option<RefMut<'_, dyn ClipboardProvider>>;
}

impl EventLoopInterface for NotRunningEventLoop {
Expand All @@ -87,8 +93,19 @@ impl EventLoopInterface for NotRunningEventLoop {
&self.event_loop_proxy
}

fn clipboard(&self) -> RefMut<'_, dyn ClipboardProvider> {
RefMut::map(self.clipboard.borrow_mut(), |clipboard_box| clipboard_box.as_mut())
fn clipboard(
&self,
clipboard: i_slint_core::platform::Clipboard,
) -> Option<RefMut<'_, dyn ClipboardProvider>> {
match clipboard {
corelib::platform::Clipboard::DefaultClipboard => {
Some(RefMut::map(self.clipboard.borrow_mut(), |p| p.0.as_mut()))
}
corelib::platform::Clipboard::SelectionClipboard => {
Some(RefMut::map(self.clipboard.borrow_mut(), |p| p.1.as_mut()))
}
_ => None,
}
}
}

Expand All @@ -101,8 +118,19 @@ impl<'a> EventLoopInterface for RunningEventLoop<'a> {
self.event_loop_proxy
}

fn clipboard(&self) -> RefMut<'_, dyn ClipboardProvider> {
RefMut::map(self.clipboard.borrow_mut(), |clipboard_box| clipboard_box.as_mut())
fn clipboard(
&self,
clipboard: i_slint_core::platform::Clipboard,
) -> Option<RefMut<'_, dyn ClipboardProvider>> {
match clipboard {
corelib::platform::Clipboard::DefaultClipboard => {
Some(RefMut::map(self.clipboard.borrow_mut(), |p| p.0.as_mut()))
}
corelib::platform::Clipboard::SelectionClipboard => {
Some(RefMut::map(self.clipboard.borrow_mut(), |p| p.1.as_mut()))
}
_ => None,
}
}
}

Expand Down Expand Up @@ -671,45 +699,53 @@ fn winit_key_code_to_string(virtual_keycode: winit::event::VirtualKeyCode) -> Op
})
}

fn create_clipboard<T>(
_event_loop: &winit::event_loop::EventLoopWindowTarget<T>,
) -> Box<dyn copypasta::ClipboardProvider> {
#[allow(unused_mut)]
let mut clipboard: Option<Box<dyn copypasta::ClipboardProvider>> = None;

cfg_if::cfg_if! {
if #[cfg(all(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
), feature = "wayland", not(feature = "x11")))] {
type DefaultClipboard = copypasta::nop_clipboard::NopClipboardContext;
} else {
type DefaultClipboard = copypasta::ClipboardContext;
}
}

fn create_clipboard<T>(_event_loop: &winit::event_loop::EventLoopWindowTarget<T>) -> ClipboardPair {
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
if cfg!(feature = "wayland") {
use winit::platform::unix::EventLoopWindowTargetExtUnix;
{
#[cfg(feature = "wayland")]
if let Some(wayland_display) = _event_loop.wayland_display() {
clipboard = unsafe {
Some(Box::new(
copypasta::wayland_clipboard::create_clipboards_from_external(wayland_display)
.1,
))
if let Some(wayland_display) =
winit::platform::unix::EventLoopWindowTargetExtUnix::wayland_display(_event_loop)
{
let clipboard = unsafe {
copypasta::wayland_clipboard::create_clipboards_from_external(wayland_display)
};
return (Box::new(clipboard.1), Box::new(clipboard.0));
};
#[cfg(feature = "x11")]
{
let prim = copypasta::x11_clipboard::X11ClipboardContext::<
copypasta::x11_clipboard::Primary,
>::new()
.map_or(
Box::new(copypasta::nop_clipboard::NopClipboardContext)
as Box<dyn copypasta::ClipboardProvider>,
|x| Box::new(x) as Box<dyn copypasta::ClipboardProvider>,
);
let sec = copypasta::x11_clipboard::X11ClipboardContext::<
copypasta::x11_clipboard::Clipboard,
>::new()
.map_or(
Box::new(copypasta::nop_clipboard::NopClipboardContext)
as Box<dyn copypasta::ClipboardProvider>,
|x| Box::new(x) as Box<dyn copypasta::ClipboardProvider>,
);
return (sec, prim);
}
}

clipboard.unwrap_or_else(|| Box::new(DefaultClipboard::new().unwrap()))
#[allow(unreachable_code)]
(
copypasta::ClipboardContext::new().map_or(
Box::new(copypasta::nop_clipboard::NopClipboardContext)
as Box<dyn copypasta::ClipboardProvider>,
|x| Box::new(x) as Box<dyn copypasta::ClipboardProvider>,
),
Box::new(copypasta::nop_clipboard::NopClipboardContext),
)
}
8 changes: 4 additions & 4 deletions internal/backends/winit/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,15 @@ impl i_slint_core::platform::Platform for Backend {
Some(Box::new(Proxy))
}

fn set_clipboard_text(&self, text: &str) {
fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
crate::event_loop::with_window_target(|event_loop_target| {
event_loop_target.clipboard().set_contents(text.into()).ok()
event_loop_target.clipboard(clipboard)?.set_contents(text.into()).ok()
});
}

fn clipboard_text(&self) -> Option<String> {
fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
crate::event_loop::with_window_target(|event_loop_target| {
event_loop_target.clipboard().get_contents().ok()
event_loop_target.clipboard(clipboard)?.get_contents().ok()
})
}
}
36 changes: 27 additions & 9 deletions internal/core/items/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::input::{
use crate::item_rendering::{CachedRenderingData, ItemRenderer};
use crate::layout::{LayoutInfo, Orientation};
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor};
use crate::platform::Clipboard;
#[cfg(feature = "rtti")]
use crate::rtti::*;
use crate::window::{WindowAdapter, WindowInner};
Expand Down Expand Up @@ -344,8 +345,20 @@ impl Item for TextInput {
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
}
}
MouseEvent::Pressed { position, button: PointerEventButton::Middle, .. } => {
let clicked_offset =
window_adapter.renderer().text_input_byte_offset_for_position(self, position)
as i32;
self.as_ref().anchor_position_byte_offset.set(clicked_offset);
self.set_cursor_position(clicked_offset, true, window_adapter, self_rc);
self.paste(window_adapter, self_rc, Clipboard::SelectionClipboard);
if !self.has_focus() {
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
}
}
MouseEvent::Released { button: PointerEventButton::Left, .. } => {
self.as_ref().pressed.set(false)
self.as_ref().pressed.set(false);
self.copy(Clipboard::SelectionClipboard);
}
MouseEvent::Exit => {
window_adapter.set_mouse_cursor(super::MouseCursor::Default);
Expand Down Expand Up @@ -457,15 +470,15 @@ impl Item for TextInput {
return KeyEventResult::EventAccepted;
}
StandardShortcut::Copy => {
self.copy();
self.copy(Clipboard::DefaultClipboard);
return KeyEventResult::EventAccepted;
}
StandardShortcut::Paste if !self.read_only() => {
self.paste(window_adapter, self_rc);
self.paste(window_adapter, self_rc, Clipboard::DefaultClipboard);
return KeyEventResult::EventAccepted;
}
StandardShortcut::Cut if !self.read_only() => {
self.copy();
self.copy(Clipboard::DefaultClipboard);
self.delete_selection(window_adapter, self_rc);
return KeyEventResult::EventAccepted;
}
Expand Down Expand Up @@ -969,22 +982,27 @@ impl TextInput {
);
}

fn copy(self: Pin<&Self>) {
fn copy(self: Pin<&Self>, clipboard: Clipboard) {
let (anchor, cursor) = self.selection_anchor_and_cursor();
if anchor == cursor {
return;
}
let text = self.text();
crate::platform::PLATFORM_INSTANCE.with(|p| {
if let Some(backend) = p.get() {
backend.set_clipboard_text(&text[anchor..cursor]);
backend.set_clipboard_text(&text[anchor..cursor], clipboard);
}
});
}

fn paste(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
if let Some(text) =
crate::platform::PLATFORM_INSTANCE.with(|p| p.get().and_then(|p| p.clipboard_text()))
fn paste(
self: Pin<&Self>,
window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc,
clipboard: Clipboard,
) {
if let Some(text) = crate::platform::PLATFORM_INSTANCE
.with(|p| p.get().and_then(|p| p.clipboard_text(clipboard)))
{
self.insert(&text, window_adapter, self_rc);
}
Expand Down
26 changes: 23 additions & 3 deletions internal/core/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ pub trait Platform {
core::time::Duration::from_millis(500)
}

/// Sends the given text into the system clipboard
fn set_clipboard_text(&self, _text: &str) {}
/// Sends the given text into the system clipboard.
///
/// If the platform doesn't support the specified clipboard, this function should do nothing
fn set_clipboard_text(&self, _text: &str, _clipboard: Clipboard) {}

/// Returns a copy of text stored in the system clipboard, if any.
fn clipboard_text(&self) -> Option<String> {
///
/// If the platform doesn't support the specified clipboard, the function should return None
fn clipboard_text(&self, _clipboard: Clipboard) -> Option<String> {
None
}

Expand All @@ -87,6 +92,21 @@ pub trait Platform {
}
}

/// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`]
#[non_exhaustive]
#[derive(PartialEq, Clone, Default)]
pub enum Clipboard {
/// This is the default clipboard used for text action for Ctrl+V, Ctrl+C.
/// Corresponds to the secondary clipboard on X11.
#[default]
DefaultClipboard,

/// This is the clipboard that is used when text is selected
/// Corresponds to the primary clipboard on X11.
/// The Platform implementation should do nothing if copy on select is not supported on that platform.
SelectionClipboard,
}

/// Trait that is returned by the [`Platform::new_event_loop_proxy`]
///
/// This are the implementation details for the function that may need to
Expand Down

0 comments on commit f34b1bd

Please sign in to comment.