Skip to content

Commit

Permalink
Add clipboard support
Browse files Browse the repository at this point in the history
This patch adds support for Copy/Paste operations inside text fields, by
allowing a long-press in the text field to pop up an option menu with
the Copy/Paste options. The Ctrl+Shift+C/V and XF86_Copy/Paste keys are
also supported.

Additionally pasting into engine text fields has been enabled through
IME, by adding `Paste` as a new option menu entry.

Closes #68.
  • Loading branch information
chrisduerr committed Jan 9, 2025
1 parent 4bfb62c commit a2b1e95
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 89 deletions.
3 changes: 3 additions & 0 deletions src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ pub trait Engine {
/// Set preedit text at the current cursor position.
fn set_preedit_string(&mut self, text: String, cursor_begin: i32, cursor_end: i32);

/// Paste clipboard text.
fn paste(&mut self, text: String);

/// Clear engine focus.
fn clear_focus(&mut self);

Expand Down
37 changes: 25 additions & 12 deletions src/engine/webkit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ use crate::engine::webkit::platform::WebKitDisplay;
use crate::engine::{Engine, EngineId, Group, GroupId, BG};
use crate::storage::cookie_whitelist::CookieWhitelist;
use crate::ui::overlay::option_menu::{Anchor, OptionMenuId, OptionMenuItem, OptionMenuPosition};
use crate::window::TextInputChange;
use crate::{KeyboardFocus, Position, Size, State};
use crate::window::{TextInputChange, WindowHandler};
use crate::{KeyboardFocus, PasteTarget, Position, Size, State};

mod input_method_context;
mod platform;
Expand Down Expand Up @@ -95,9 +95,6 @@ trait WebKitHandler {
/// Open URI in a new window.
fn open_in_window(&mut self, uri: String);

/// Write text to the system clipboard.
fn set_clipboard(&mut self, text: String);

/// Add host to the cookie whitelist.
fn add_cookie_exception(&mut self, host: String);

Expand Down Expand Up @@ -238,8 +235,7 @@ impl WebKitHandler for State {
(Position::new(x, y + height).into(), Some(width as u32))
},
None => {
let position = webkit_engine.last_input_position;
let position = Position::new(position.x.round() as i32, position.y.round() as i32);
let position = webkit_engine.last_input_position.i32_round();
let menu_position = OptionMenuPosition::new(position, Anchor::BottomRight);
(menu_position, None)
},
Expand Down Expand Up @@ -316,10 +312,6 @@ impl WebKitHandler for State {
}
}

fn set_clipboard(&mut self, text: String) {
self.set_clipboard(text);
}

fn add_cookie_exception(&mut self, host: String) {
self.storage.cookie_whitelist.add(&host);
}
Expand Down Expand Up @@ -753,6 +745,10 @@ impl Engine for WebKitEngine {
self.webkit_display.input_method_context().emit_by_name::<()>("preedit-finished", &[]);
}

fn paste(&mut self, text: String) {
self.commit_string(text);
}

fn clear_focus(&mut self) {
self.set_focused(false);
}
Expand Down Expand Up @@ -1068,6 +1064,10 @@ impl ContextMenu {
n_items += 3;
}

if self.context.contains(HitTestResultContext::EDITABLE) {
n_items += 1;
}

n_items
}

Expand Down Expand Up @@ -1103,7 +1103,15 @@ impl ContextMenu {
2 => return Some(ContextMenuItem::OpenInNewTab),
_ => (),
}
// index -= 3;
index -= 3;
}

if self.context.contains(HitTestResultContext::EDITABLE) {
match index {
0 => return Some(ContextMenuItem::Paste),
_ => (),
}
// index -= 1;
}

None
Expand Down Expand Up @@ -1138,6 +1146,9 @@ impl ContextMenu {
engine.queue.set_clipboard(uri);
}
},
Some(ContextMenuItem::Paste) => {
engine.queue.request_paste(PasteTarget::Browser(engine.id()))
},
None => (),
}
}
Expand Down Expand Up @@ -1165,6 +1176,7 @@ enum ContextMenuItem {
OpenInNewTab,
OpenInNewWindow,
CopyLink,
Paste,
}

impl ContextMenuItem {
Expand All @@ -1176,6 +1188,7 @@ impl ContextMenuItem {
Self::OpenInNewTab => "Open in New Tab",
Self::OpenInNewWindow => "Open in New Window",
Self::CopyLink => "Copy Link",
Self::Paste => "Paste",
}
}
}
Expand Down
40 changes: 38 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::io::Read;
use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign};
use std::os::fd::{AsFd, AsRawFd};
use std::ptr::NonNull;
Expand All @@ -26,7 +27,7 @@ use smithay_client_toolkit::reexports::client::{
ConnectError, Connection, EventQueue, QueueHandle,
};
use smithay_client_toolkit::seat::keyboard::{Keysym, Modifiers, RepeatInfo};
use tracing::info;
use tracing::{info, warn};
use tracing_subscriber::{EnvFilter, FmtSubscriber};

use crate::engine::webkit::{WebKitError, WebKitState};
Expand All @@ -35,7 +36,7 @@ use crate::storage::history::History;
use crate::storage::Storage;
use crate::wayland::protocols::{KeyRepeat, ProtocolStates, TextInput};
use crate::wayland::WaylandDispatch;
use crate::window::{KeyboardFocus, Window, WindowId};
use crate::window::{KeyboardFocus, PasteTarget, Window, WindowHandler, WindowId};

mod engine;
mod storage;
Expand Down Expand Up @@ -292,6 +293,35 @@ impl State {

self.clipboard.text = text;
}

/// Request clipboard paste.
fn request_paste(&mut self, target: PasteTarget) {
// Get available Wayland text selection.
let selection_offer = match self.protocol_states.data_device.data().selection_offer() {
Some(selection_offer) => selection_offer,
None => return,
};
let mut pipe = match selection_offer.receive("text/plain".into()) {
Ok(pipe) => pipe,
Err(err) => {
warn!("Clipboard paste failed: {err}");
return;
},
};

// Asynchronously write paste text to the window.
let mut queue = self.queue.clone();
source::unix_fd_add_local(pipe.as_raw_fd(), IOCondition::IN, move |_, _| {
// Read available text from pipe.
let mut text = String::new();
pipe.read_to_string(&mut text).unwrap();

// Forward text to the paste target.
queue.paste(target, text);

ControlFlow::Break
});
}
}

/// Key status tracking for WlKeyboard.
Expand Down Expand Up @@ -416,6 +446,12 @@ impl<T> Position<T> {
}
}

impl Position<f64> {
fn i32_round(&self) -> Position {
Position::new(self.x.round() as i32, self.y.round() as i32)
}
}

impl<T> From<(T, T)> for Position<T> {
fn from((x, y): (T, T)) -> Self {
Self { x, y }
Expand Down
Loading

0 comments on commit a2b1e95

Please sign in to comment.