feat(core): add clipboard writeText and readText APIs (#2035)

This commit is contained in:
Lucas Fernandes Nogueira
2021-06-21 13:32:22 -03:00
committed by GitHub
parent 3280c4aa91
commit 285bf64bf9
13 changed files with 265 additions and 35 deletions

View File

@@ -0,0 +1,8 @@
---
"api": patch
"tauri": patch
"tauri-runtime": patch
"tauri-runtime-wry": patch
---
Adds `clipboard` APIs (write and read text).

View File

@@ -13,8 +13,8 @@ use tauri_runtime::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
DetachedWindow, PendingWindow, WindowEvent,
},
Dispatch, Error, GlobalShortcutManager, Icon, Params, Result, RunEvent, RunIteration, Runtime,
RuntimeHandle, UserAttentionType,
ClipboardManager, Dispatch, Error, GlobalShortcutManager, Icon, Params, Result, RunEvent,
RunIteration, Runtime, RuntimeHandle, UserAttentionType,
};
#[cfg(feature = "menu")]
@@ -33,6 +33,7 @@ use uuid::Uuid;
use wry::{
application::{
accelerator::{Accelerator, AcceleratorId},
clipboard::Clipboard,
dpi::{
LogicalPosition as WryLogicalPosition, LogicalSize as WryLogicalSize,
PhysicalPosition as WryPhysicalPosition, PhysicalSize as WryPhysicalSize,
@@ -97,7 +98,7 @@ macro_rules! dispatcher_getter {
}};
}
macro_rules! shortcut_getter {
macro_rules! getter {
($self: ident, $rx: expr, $message: expr) => {{
if current_thread().id() == $self.context.main_thread_id
&& !$self.context.is_event_loop_running.load(Ordering::Relaxed)
@@ -113,22 +114,22 @@ macro_rules! shortcut_getter {
}};
}
#[derive(Debug, Clone)]
struct GlobalShortcutWrapper(GlobalShortcut);
unsafe impl Send for GlobalShortcutWrapper {}
#[derive(Clone)]
struct GlobalShortcutManagerContext {
struct EventLoopContext {
main_thread_id: ThreadId,
is_event_loop_running: Arc<AtomicBool>,
proxy: EventLoopProxy<Message>,
}
#[derive(Debug, Clone)]
struct GlobalShortcutWrapper(GlobalShortcut);
unsafe impl Send for GlobalShortcutWrapper {}
/// Wrapper around [`WryShortcutManager`].
#[derive(Clone)]
pub struct GlobalShortcutManagerHandle {
context: GlobalShortcutManagerContext,
context: EventLoopContext,
shortcuts: HashMap<String, (AcceleratorId, GlobalShortcutWrapper)>,
listeners: GlobalShortcutListeners,
}
@@ -136,7 +137,7 @@ pub struct GlobalShortcutManagerHandle {
impl GlobalShortcutManager for GlobalShortcutManagerHandle {
fn is_registered(&self, accelerator: &str) -> Result<bool> {
let (tx, rx) = channel();
Ok(shortcut_getter!(
Ok(getter!(
self,
rx,
Message::GlobalShortcut(GlobalShortcutMessage::IsRegistered(
@@ -150,7 +151,7 @@ impl GlobalShortcutManager for GlobalShortcutManagerHandle {
let wry_accelerator: Accelerator = accelerator.parse().expect("invalid accelerator");
let id = wry_accelerator.clone().id();
let (tx, rx) = channel();
let shortcut = shortcut_getter!(
let shortcut = getter!(
self,
rx,
Message::GlobalShortcut(GlobalShortcutMessage::Register(wry_accelerator, tx))
@@ -164,7 +165,7 @@ impl GlobalShortcutManager for GlobalShortcutManagerHandle {
fn unregister_all(&mut self) -> Result<()> {
let (tx, rx) = channel();
shortcut_getter!(
getter!(
self,
rx,
Message::GlobalShortcut(GlobalShortcutMessage::UnregisterAll(tx))
@@ -177,13 +178,10 @@ impl GlobalShortcutManager for GlobalShortcutManagerHandle {
fn unregister(&mut self, accelerator: &str) -> Result<()> {
if let Some((accelerator_id, shortcut)) = self.shortcuts.remove(accelerator) {
let (tx, rx) = channel();
shortcut_getter!(
getter!(
self,
rx,
Message::GlobalShortcut(GlobalShortcutMessage::Unregister(
GlobalShortcutWrapper(shortcut.0),
tx
))
Message::GlobalShortcut(GlobalShortcutMessage::Unregister(shortcut, tx))
)?;
self.listeners.lock().unwrap().remove(&accelerator_id);
}
@@ -191,6 +189,32 @@ impl GlobalShortcutManager for GlobalShortcutManagerHandle {
}
}
#[derive(Clone)]
pub struct ClipboardManagerWrapper {
context: EventLoopContext,
}
impl ClipboardManager for ClipboardManagerWrapper {
fn read_text(&self) -> Result<Option<String>> {
let (tx, rx) = channel();
Ok(getter!(
self,
rx,
Message::Clipboard(ClipboardMessage::ReadText(tx))
))
}
fn write_text<T: Into<String>>(&mut self, text: T) -> Result<()> {
let (tx, rx) = channel();
getter!(
self,
rx,
Message::Clipboard(ClipboardMessage::WriteText(text.into(), tx))
);
Ok(())
}
}
/// Wrapper around a [`wry::application::window::Icon`] that can be created from an [`Icon`].
pub struct WryIcon(WindowIcon);
@@ -650,6 +674,12 @@ pub(crate) enum GlobalShortcutMessage {
UnregisterAll(Sender<Result<()>>),
}
#[derive(Clone)]
pub(crate) enum ClipboardMessage {
WriteText(String, Sender<()>),
ReadText(Sender<Option<String>>),
}
#[derive(Clone)]
pub(crate) enum Message {
Task(MainTask),
@@ -659,6 +689,7 @@ pub(crate) enum Message {
Tray(TrayMessage),
CreateWebview(Arc<Mutex<Option<CreateWebviewHandler>>>, Sender<WindowId>),
GlobalShortcut(GlobalShortcutMessage),
Clipboard(ClipboardMessage),
}
#[derive(Clone)]
@@ -1099,6 +1130,8 @@ pub struct Wry {
main_thread_id: ThreadId,
global_shortcut_manager: Arc<Mutex<WryShortcutManager>>,
global_shortcut_manager_handle: GlobalShortcutManagerHandle,
clipboard_manager: Arc<Mutex<Clipboard>>,
clipboard_manager_handle: ClipboardManagerWrapper,
is_event_loop_running: Arc<AtomicBool>,
event_loop: EventLoop<Message>,
webviews: Arc<Mutex<HashMap<WindowId, WebviewWrapper>>>,
@@ -1159,28 +1192,39 @@ impl Runtime for Wry {
type Dispatcher = WryDispatcher;
type Handle = WryHandle;
type GlobalShortcutManager = GlobalShortcutManagerHandle;
type ClipboardManager = ClipboardManagerWrapper;
#[cfg(feature = "system-tray")]
type TrayHandler = SystemTrayHandle;
fn new() -> Result<Self> {
let event_loop = EventLoop::<Message>::with_user_event();
let global_shortcut_manager = WryShortcutManager::new(&event_loop);
let global_shortcut_listeners = GlobalShortcutListeners::default();
let proxy = event_loop.create_proxy();
let main_thread_id = current_thread().id();
let is_event_loop_running = Arc::new(AtomicBool::default());
let event_loop_context = EventLoopContext {
main_thread_id,
is_event_loop_running: is_event_loop_running.clone(),
proxy,
};
let global_shortcut_manager = WryShortcutManager::new(&event_loop);
let global_shortcut_listeners = GlobalShortcutListeners::default();
let clipboard_manager = Clipboard::new();
let clipboard_manager_handle = ClipboardManagerWrapper {
context: event_loop_context.clone(),
};
Ok(Self {
main_thread_id,
global_shortcut_manager: Arc::new(Mutex::new(global_shortcut_manager)),
global_shortcut_manager_handle: GlobalShortcutManagerHandle {
context: GlobalShortcutManagerContext {
main_thread_id,
is_event_loop_running: is_event_loop_running.clone(),
proxy,
},
context: event_loop_context,
shortcuts: Default::default(),
listeners: global_shortcut_listeners,
},
clipboard_manager: Arc::new(Mutex::new(clipboard_manager)),
clipboard_manager_handle,
is_event_loop_running,
event_loop,
webviews: Default::default(),
@@ -1209,6 +1253,10 @@ impl Runtime for Wry {
self.global_shortcut_manager_handle.clone()
}
fn clipboard_manager(&self) -> Self::ClipboardManager {
self.clipboard_manager_handle.clone()
}
fn create_window<P: Params<Runtime = Self>>(
&self,
pending: PendingWindow<P>,
@@ -1298,6 +1346,7 @@ impl Runtime for Wry {
let tray_context = self.tray_context.clone();
let global_shortcut_manager = self.global_shortcut_manager.clone();
let global_shortcut_manager_handle = self.global_shortcut_manager_handle.clone();
let clipboard_manager = self.clipboard_manager.clone();
let mut iteration = RunIteration::default();
@@ -1318,6 +1367,7 @@ impl Runtime for Wry {
window_event_listeners: window_event_listeners.clone(),
global_shortcut_manager: global_shortcut_manager.clone(),
global_shortcut_manager_handle: global_shortcut_manager_handle.clone(),
clipboard_manager: clipboard_manager.clone(),
#[cfg(feature = "menu")]
menu_event_listeners: menu_event_listeners.clone(),
#[cfg(feature = "system-tray")]
@@ -1340,6 +1390,7 @@ impl Runtime for Wry {
let tray_context = self.tray_context;
let global_shortcut_manager = self.global_shortcut_manager.clone();
let global_shortcut_manager_handle = self.global_shortcut_manager_handle.clone();
let clipboard_manager = self.clipboard_manager.clone();
self.event_loop.run(move |event, event_loop, control_flow| {
handle_event_loop(
@@ -1352,6 +1403,7 @@ impl Runtime for Wry {
window_event_listeners: window_event_listeners.clone(),
global_shortcut_manager: global_shortcut_manager.clone(),
global_shortcut_manager_handle: global_shortcut_manager_handle.clone(),
clipboard_manager: clipboard_manager.clone(),
#[cfg(feature = "menu")]
menu_event_listeners: menu_event_listeners.clone(),
#[cfg(feature = "system-tray")]
@@ -1368,6 +1420,7 @@ struct EventLoopIterationContext<'a> {
window_event_listeners: WindowEventListeners,
global_shortcut_manager: Arc<Mutex<WryShortcutManager>>,
global_shortcut_manager_handle: GlobalShortcutManagerHandle,
clipboard_manager: Arc<Mutex<Clipboard>>,
#[cfg(feature = "menu")]
menu_event_listeners: MenuEventListeners,
#[cfg(feature = "system-tray")]
@@ -1386,6 +1439,7 @@ fn handle_event_loop(
window_event_listeners,
global_shortcut_manager,
global_shortcut_manager_handle,
clipboard_manager,
#[cfg(feature = "menu")]
menu_event_listeners,
#[cfg(feature = "system-tray")]
@@ -1689,6 +1743,15 @@ fn handle_event_loop(
)
.unwrap(),
},
Message::Clipboard(message) => match message {
ClipboardMessage::WriteText(text, tx) => {
clipboard_manager.lock().unwrap().write_text(text);
tx.send(()).unwrap();
}
ClipboardMessage::ReadText(tx) => tx
.send(clipboard_manager.lock().unwrap().read_text())
.unwrap(),
},
},
_ => (),
}

View File

@@ -270,6 +270,25 @@ pub trait GlobalShortcutManager {
fn unregister(&mut self, accelerator: &str) -> crate::Result<()>;
}
/// Clipboard manager.
pub trait ClipboardManager {
/// Writes the text into the clipboard as plain text.
///
/// # Panics
///
/// Panics if the app is not running yet, usually when called on the `tauri::Builder#setup` closure.
/// You can spawn a task to use the API using the `tauri::async_runtime` to prevent the panic.
fn write_text<T: Into<String>>(&mut self, text: T) -> Result<()>;
/// Read the content in the clipboard as plain text.
///
/// # Panics
///
/// Panics if the app is not running yet, usually when called on the `tauri::Builder#setup` closure.
/// You can spawn a task to use the API using the `tauri::async_runtime` to prevent the panic.
fn read_text(&self) -> Result<Option<String>>;
}
/// The webview runtime interface.
pub trait Runtime: Sized + 'static {
/// The message dispatcher.
@@ -278,6 +297,8 @@ pub trait Runtime: Sized + 'static {
type Handle: RuntimeHandle<Runtime = Self>;
/// The global shortcut manager type.
type GlobalShortcutManager: GlobalShortcutManager + Clone + Send;
/// The clipboard manager type.
type ClipboardManager: ClipboardManager + Clone + Send;
/// The tray handler type.
#[cfg(feature = "system-tray")]
type TrayHandler: menu::TrayHandle + Clone + Send;
@@ -291,6 +312,9 @@ pub trait Runtime: Sized + 'static {
/// Gets the global shortcut manager.
fn global_shortcut_manager(&self) -> Self::GlobalShortcutManager;
/// Gets the clipboard manager.
fn clipboard_manager(&self) -> Self::ClipboardManager;
/// Create a new webview window.
fn create_window<P: Params<Runtime = Self>>(
&self,

View File

@@ -114,6 +114,7 @@ crate::manager::default_args! {
runtime_handle: <P::Runtime as Runtime>::Handle,
manager: WindowManager<P>,
global_shortcut_manager: <P::Runtime as Runtime>::GlobalShortcutManager,
clipboard_manager: <P::Runtime as Runtime>::ClipboardManager,
#[cfg(feature = "system-tray")]
tray_handle: Option<tray::SystemTrayHandle<P>>,
}
@@ -125,6 +126,7 @@ impl<P: Params> Clone for AppHandle<P> {
runtime_handle: self.runtime_handle.clone(),
manager: self.manager.clone(),
global_shortcut_manager: self.global_shortcut_manager.clone(),
clipboard_manager: self.clipboard_manager.clone(),
#[cfg(feature = "system-tray")]
tray_handle: self.tray_handle.clone(),
}
@@ -163,6 +165,7 @@ crate::manager::default_args! {
runtime: Option<P::Runtime>,
manager: WindowManager<P>,
global_shortcut_manager: <P::Runtime as Runtime>::GlobalShortcutManager,
clipboard_manager: <P::Runtime as Runtime>::ClipboardManager,
#[cfg(feature = "system-tray")]
tray_handle: Option<tray::SystemTrayHandle<P>>,
handle: AppHandle<P>,
@@ -220,11 +223,6 @@ macro_rules! shared_app_impl {
.expect("tray not configured; use the `Builder#system_tray` API first.")
}
/// Gets a copy of the global shortcut manager instance.
pub fn global_shortcut_manager(&self) -> <P::Runtime as Runtime>::GlobalShortcutManager {
self.global_shortcut_manager.clone()
}
/// The path resolver for the application.
pub fn path_resolver(&self) -> PathResolver {
PathResolver {
@@ -233,6 +231,16 @@ macro_rules! shared_app_impl {
}
}
/// Gets a copy of the global shortcut manager instance.
pub fn global_shortcut_manager(&self) -> <P::Runtime as Runtime>::GlobalShortcutManager {
self.global_shortcut_manager.clone()
}
/// Gets a copy of the clipboard manager instance.
pub fn clipboard_manager(&self) -> <P::Runtime as Runtime>::ClipboardManager {
self.clipboard_manager.clone()
}
/// Gets the app's configuration, defined on the `tauri.conf.json` file.
pub fn config(&self) -> Arc<Config> {
self.manager.config()
@@ -717,17 +725,20 @@ where
let runtime = R::new()?;
let runtime_handle = runtime.handle();
let global_shortcut_manager = runtime.global_shortcut_manager();
let clipboard_manager = runtime.clipboard_manager();
let mut app = App {
runtime: Some(runtime),
manager: manager.clone(),
global_shortcut_manager: global_shortcut_manager.clone(),
clipboard_manager: clipboard_manager.clone(),
#[cfg(feature = "system-tray")]
tray_handle: None,
handle: AppHandle {
runtime_handle,
manager,
global_shortcut_manager,
clipboard_manager,
#[cfg(feature = "system-tray")]
tray_handle: None,
},

View File

@@ -14,6 +14,7 @@ use std::sync::Arc;
mod app;
mod cli;
mod clipboard;
mod dialog;
mod event;
#[allow(unused_imports)]
@@ -54,6 +55,7 @@ enum Module {
Notification(notification::Cmd),
Http(http::Cmd),
GlobalShortcut(global_shortcut::Cmd),
Clipboard(clipboard::Cmd),
}
impl Module {
@@ -152,6 +154,12 @@ impl Module {
.and_then(|r| r.json)
.map_err(InvokeError::from)
}),
Self::Clipboard(cmd) => resolver.respond_async(async move {
cmd
.run(window)
.and_then(|r| r.json)
.map_err(InvokeError::from)
}),
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::InvokeResponse;
use crate::{
runtime::{ClipboardManager, Params},
window::Window,
};
use serde::Deserialize;
/// The API descriptor.
#[derive(Deserialize)]
#[serde(tag = "cmd", content = "data", rename_all = "camelCase")]
pub enum Cmd {
/// Write a text string to the clipboard.
WriteText(String),
/// Read clipboard content as text.
ReadText,
}
impl Cmd {
pub fn run<P: Params>(self, window: Window<P>) -> crate::Result<InvokeResponse> {
let mut clipboard = window.app_handle.clipboard_manager();
match self {
Self::WriteText(text) => Ok(clipboard.write_text(text)?.into()),
Self::ReadText => Ok(clipboard.read_text()?.into()),
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,7 @@
import Shortcuts from "./components/Shortcuts.svelte";
import Shell from "./components/Shell.svelte";
import Updater from "./components/Updater.svelte";
import Clipboard from "./components/Clipboard.svelte";
const MENU_TOGGLE_HOTKEY = 'ctrl+b';
@@ -69,7 +70,11 @@
{
label: "Updater",
component: Updater,
},
},
{
label: "Clipboard",
component: Clipboard,
}
];
let selected = views[0];

View File

@@ -0,0 +1,36 @@
<script>
import {
writeText,
readText
} from "@tauri-apps/api/clipboard";
export let onMessage;
let text = "clipboard message";
function write() {
writeText(text)
.then(() => {
onMessage('Wrote to the clipboard');
})
.catch(onMessage);
}
function read() {
readText()
.then((contents) => {
onMessage(`Clipboard contents: ${contents}`);
})
.catch(onMessage);
}
</script>
<div>
<div>
<input
placeholder="Text to write to the clipboard"
bind:value={text}
/>
<button type="button" on:click={write}>Write</button>
</div>
<button type="button" on:click={read}>Read</button>
</div>

View File

@@ -28,7 +28,8 @@ export default [
cli: './src/cli.ts',
notification: './src/notification.ts',
globalShortcut: './src/globalShortcut.ts',
process: './src/process.ts'
process: './src/process.ts',
clipboard: './src/clipboard.ts'
},
treeshake: true,
perf: true,

View File

@@ -0,0 +1,43 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
/**
* Read and write to the system clipboard.
*
* This package is also accessible with `window.__TAURI__.clipboard` when `tauri.conf.json > build > withGlobalTauri` is set to true.
* @packageDocumentation
*/
import { invokeTauriCommand } from './helpers/tauri'
/**
* Writes a plain text to the clipboard.
*
* @returns A promise indicating the success or failure of the operation.
*/
async function writeText(text: string): Promise<void> {
return invokeTauriCommand({
__tauriModule: 'Clipboard',
message: {
cmd: 'writeText',
data: text
}
})
}
/**
* Gets the clipboard content as plain text.
*
* @returns A promise resolving to the clipboard content as plain text.
*/
async function readText(): Promise<string | null> {
return invokeTauriCommand({
__tauriModule: 'Clipboard',
message: {
cmd: 'readText'
}
})
}
export { writeText, readText }

View File

@@ -19,6 +19,7 @@ type TauriModule =
| 'Http'
| 'GlobalShortcut'
| 'Process'
| 'Clipboard'
interface TauriCommand {
__tauriModule: TauriModule