Skip to content

Commit

Permalink
Extract clipboard monitoring from clipboard-master
Browse files Browse the repository at this point in the history
  • Loading branch information
DoumanAsh committed Feb 5, 2024
1 parent 973219a commit 1ef2b82
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,15 @@ default-target = "x86_64-pc-windows-msvc"
[target.'cfg(windows)'.dependencies]
error-code = "3"

[target.'cfg(windows)'.dependencies.windows-win]
version = "3"
optional = true

[features]
std = ["error-code/std"]
# Enables clipboard monitoring code
monitor = ["windows-win"]

[[test]]
name = "monitor"
required-features = ["monitor"]
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//!# Features
//!
//! - `std` - Enables usage of `std`, including `std::error::Error` trait.
//! - `monitor` - Enables code related to clipboard monitoring.
//!
//!# Clipboard
//!
Expand Down Expand Up @@ -91,6 +92,10 @@ mod sys;
pub mod types;
pub mod formats;
pub mod raw;
#[cfg(feature = "monitor")]
pub mod monitor;
#[cfg(feature = "monitor")]
pub use monitor::Monitor;
pub(crate) mod utils;

pub use raw::{get_owner, empty, seq_num, size, is_format_avail, register_format, count_formats, EnumFormats};
Expand Down
173 changes: 173 additions & 0 deletions src/monitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//! Clipboard monitoring utility

use error_code::ErrorCode;
use windows_win::{
raw,
Window,
Messages
};

use windows_win::sys::{
HWND,
AddClipboardFormatListener,
RemoveClipboardFormatListener,
PostMessageW,
WM_CLIPBOARDUPDATE,
};


const CLOSE_PARAM: isize = -1;

///Shutdown channel
///
///On drop requests shutdown to gracefully close clipboard listener as soon as possible.
///
///This is silently ignored, if there is no thread awaiting
pub struct Shutdown {
window: HWND,
}

unsafe impl Send for Shutdown {}

impl Drop for Shutdown {
#[inline(always)]
fn drop(&mut self) {
unsafe {
PostMessageW(self.window, WM_CLIPBOARDUPDATE, 0, CLOSE_PARAM)
};
}
}

///Clipboard listener guard.
///
///On drop unsubscribes window from listening on clipboard changes
struct ClipboardListener(HWND);

impl ClipboardListener {
#[inline]
///Subscribes window to clipboard changes.
pub fn new(window: &Window) -> Result<Self, ErrorCode> {
let window = window.inner();
unsafe {
if AddClipboardFormatListener(window) != 1 {
Err(ErrorCode::last_system())
} else {
Ok(ClipboardListener(window))
}
}
}
}

impl Drop for ClipboardListener {
#[inline]
fn drop(&mut self) {
unsafe {
RemoveClipboardFormatListener(self.0);
}
}
}

///Clipboard monitor
///
///This is implemented via dummy message-only window.
///
///This approach definitely works for console applications,
///but it is not tested on windowed application
///
///Due to nature of implementation, it is not safe to move it into different thread.
///
///If needed, user should create monitor and pass Shutdown handle to the separate thread.
///
///Once created, messages will start accumulating immediately
///
///Therefore you generally should start listening for messages once you created instance
///
///`Monitor` implements `Iterator` by continuously calling `Monitor::recv` and returning the same result
///This `Iterator` is never ending, even when you perform shutdown.
pub struct Monitor {
_listener: ClipboardListener,
window: Window,
}

impl Monitor {
#[inline(always)]
///Creates new instance
pub fn new() -> Result<Self, ErrorCode> {
let window = Window::from_builder(raw::window::Builder::new().class_name("STATIC").parent_message())?;
let _listener = ClipboardListener::new(&window)?;

Ok(Self {
_listener,
window
})
}

#[inline(always)]
fn iter(&self) -> Messages {
let mut msg = Messages::new();
msg.window(Some(self.window.inner()))
.low(Some(WM_CLIPBOARDUPDATE))
.high(Some(WM_CLIPBOARDUPDATE));
msg
}

#[inline(always)]
///Creates shutdown channel.
pub fn shutdown_channel(&self) -> Shutdown {
Shutdown {
window: self.window.inner()
}
}

///Blocks waiting for new clipboard message
///
///Returns `Ok(true)` if event received.
///
///If `Shutdown` request detected, then return `Ok(false)`
pub fn recv(&mut self) -> Result<bool, ErrorCode> {
for msg in self.iter() {
let msg = msg?;
match msg.id() {
WM_CLIPBOARDUPDATE => return Ok(msg.inner().lParam != CLOSE_PARAM),
_ => unreachable!(),
}
}

unreachable!();
}

///Attempts to check if there is any clipboard update event
///
///Returns `Ok(true)` if event received, otherwise return `Ok(false)`
///
///If `Shutdown` request detected, it is skipped
pub fn try_recv(&mut self) -> Result<bool, ErrorCode> {
let mut iter = self.iter();
iter.non_blocking();
while let Some(msg) = iter.next() {
let msg = msg?;
match msg.id() {
WM_CLIPBOARDUPDATE => {
//Skip shutdown requests
if msg.inner().lParam == CLOSE_PARAM {
continue;
}

return Ok(true);
}
_ => unreachable!(),
}
}

Ok(false)
}
}

impl Iterator for Monitor {
type Item = Result<bool, ErrorCode>;

#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
Some(self.recv())
}
}
3 changes: 1 addition & 2 deletions src/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +502,7 @@ pub fn get_bitmap(out: &mut alloc::vec::Vec<u8>) -> SysResult<usize> {
let out_before = out.len();

let dc = crate::utils::Scope(unsafe { GetDC(ptr::null_mut()) }, free_dc);
let mut buffer = alloc::vec::Vec::new();
buffer.resize(img_size, 0u8);
let mut buffer = alloc::vec![0; img_size];

if unsafe { GetDIBits(dc.0, clipboard_data.as_ptr() as _, 0, bitmap.bmHeight as _, buffer.as_mut_ptr() as _, header_storage.get() as _, DIB_RGB_COLORS) } == 0 {
return Err(ErrorCode::last_system());
Expand Down
29 changes: 29 additions & 0 deletions tests/monitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use clipboard_win::{Clipboard, Monitor, set_clipboard_string};

#[test]
fn should_get_clipboard_event() {
let mut monitor = Monitor::new().expect("create monitor");
let result = monitor.try_recv().expect("Success");
assert!(!result);

let _clip = Clipboard::new_attempts(10).expect("Open clipboard");
set_clipboard_string("test").expect("Success");
let result = monitor.try_recv().expect("Success");
assert!(result);
let result = monitor.try_recv().expect("Success");
assert!(!result);

monitor.shutdown_channel();
set_clipboard_string("test").expect("Success");
let result = monitor.try_recv().expect("Success");
assert!(result);
let result = monitor.try_recv().expect("Success");
assert!(!result);

set_clipboard_string("test").expect("Success");
let result = monitor.recv().expect("Success");
assert!(result);
monitor.shutdown_channel();
let result = monitor.recv().expect("Success");
assert!(!result);
}

0 comments on commit 1ef2b82

Please sign in to comment.