翻译模块
This commit is contained in:
@@ -12,6 +12,7 @@ use specta::Type;
|
||||
use super::monitor::start_monitor;
|
||||
use super::reader::{write_dib, write_files, write_text};
|
||||
use super::storage::Storage;
|
||||
use super::suppress::SuppressState;
|
||||
use windows_sys::Win32::System::DataExchange::GetClipboardSequenceNumber;
|
||||
|
||||
/// 剪贴板设置(持久化到 clipboard/settings.json)
|
||||
@@ -56,8 +57,9 @@ impl Default for ClipboardSettings {
|
||||
pub struct ClipboardManager {
|
||||
storage: Arc<Storage>,
|
||||
settings: Arc<Mutex<ClipboardSettings>>,
|
||||
/// 本应用 copy_back 写入后的剪贴板序列号,用于跳过自身写入产生的记录
|
||||
suppress: Arc<Mutex<Option<u32>>>,
|
||||
/// 写入抑制状态。由本模块持有(谁启动监听谁负责),但**对其他模块开放**:
|
||||
/// 划词取词会连续改三次剪贴板,同样需要屏蔽,见 `super::suppress`。
|
||||
suppress: Arc<SuppressState>,
|
||||
monitor_stop: Arc<AtomicBool>,
|
||||
monitor_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
settings_path: PathBuf,
|
||||
@@ -75,7 +77,7 @@ impl ClipboardManager {
|
||||
};
|
||||
let settings_path = clip_dir.join("settings.json");
|
||||
let settings = Arc::new(Mutex::new(load_settings(&settings_path)));
|
||||
let suppress = Arc::new(Mutex::new(None));
|
||||
let suppress = Arc::new(SuppressState::new());
|
||||
let monitor_stop = Arc::new(AtomicBool::new(true));
|
||||
Self {
|
||||
storage,
|
||||
@@ -167,7 +169,7 @@ impl ClipboardManager {
|
||||
// 绑定到写入完成后的剪贴板序列号:仅跳过本次写入产生的记录,
|
||||
// 用户后续复制(序列号不同)不会被误吞。
|
||||
let seq = unsafe { GetClipboardSequenceNumber() };
|
||||
*self.suppress.lock().unwrap_or_else(|e| e.into_inner()) = Some(seq);
|
||||
self.suppress.mark_seq(seq);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("写回剪贴板失败".into())
|
||||
@@ -177,6 +179,20 @@ impl ClipboardManager {
|
||||
pub fn storage(&self) -> &Arc<Storage> {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
/// 写入抑制状态(供划词取词等会改剪贴板的其他模块共享)
|
||||
pub fn suppress(&self) -> Arc<SuppressState> {
|
||||
self.suppress.clone()
|
||||
}
|
||||
|
||||
/// 把**当前**剪贴板序列号登记为「应跳过」。
|
||||
///
|
||||
/// 给其他模块写完剪贴板后调用:它们自己拿不到「写入后」的序列号,
|
||||
/// 但知道「刚刚写完」这件事。把 win32 调用留在这个模块里,
|
||||
/// 别处就不必重复引入 DataExchange。
|
||||
pub fn suppress_current_sequence(&self) {
|
||||
self.suppress.mark_seq(unsafe { GetClipboardSequenceNumber() });
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ClipboardManager {
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod monitor;
|
||||
pub mod popup;
|
||||
pub mod reader;
|
||||
pub mod storage;
|
||||
pub mod suppress;
|
||||
|
||||
pub use commands::{
|
||||
clipboard_clear, clipboard_copy_back, clipboard_count, clipboard_delete, clipboard_get_history,
|
||||
|
||||
@@ -12,15 +12,17 @@ use tauri::{AppHandle, Emitter};
|
||||
|
||||
use super::reader::{read_clipboard, ClipData};
|
||||
use super::storage::{NewItem, Storage};
|
||||
use super::suppress::SuppressState;
|
||||
use windows_sys::Win32::System::DataExchange::GetClipboardSequenceNumber;
|
||||
|
||||
/// 启动监听线程,返回 JoinHandle。
|
||||
/// `suppress` 记录本应用 copy_back 写入后的剪贴板序列号,用于跳过自身写入产生的记录。
|
||||
/// `suppress` 记录本应用写入剪贴板产生的序列号、以及取词等流程的屏蔽窗口,
|
||||
/// 用于跳过自身写入产生的记录。
|
||||
pub fn start_monitor(
|
||||
storage: Arc<Storage>,
|
||||
app: AppHandle,
|
||||
settings: Arc<Mutex<super::manager::ClipboardSettings>>,
|
||||
suppress: Arc<Mutex<Option<u32>>>,
|
||||
suppress: Arc<SuppressState>,
|
||||
stop: Arc<AtomicBool>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || loop {
|
||||
@@ -41,13 +43,10 @@ pub fn start_monitor(
|
||||
if seq == last {
|
||||
continue;
|
||||
}
|
||||
// 序列号变化:仅当变化来自本应用 copy_back(序列号精确匹配)时跳过,
|
||||
// 避免旧布尔标志在用户后续复制时被误吞。
|
||||
if let Ok(mut s) = suppress.lock() {
|
||||
if *s == Some(seq) {
|
||||
*s = None;
|
||||
continue;
|
||||
}
|
||||
// 序列号变化,但可能来自本应用:精确记账(copy_back)或屏蔽窗口内(取词流程)。
|
||||
// 判定放在读剪贴板之前,避免为一次注定要丢弃的变化做无谓的读取与解码。
|
||||
if suppress.should_skip(seq) {
|
||||
continue;
|
||||
}
|
||||
let (rec_text, rec_image, rec_files, max_items, max_image_kb, dedup) = {
|
||||
let s = settings.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
//! 剪贴板写入抑制。
|
||||
//!
|
||||
//! 存在的理由:剪贴板监听线程会把**任何**序列号变化录进历史。而应用自身也会写剪贴板
|
||||
//! (copy_back 写回、划词取词时模拟 Ctrl+C 与随后的还原),这些都不该出现在用户的
|
||||
//! 历史里。抑制状态因此必须能被**多个模块**访问,而不是某个模块的私有字段。
|
||||
//!
|
||||
//! 两种机制并存,因为要解决的问题不同:
|
||||
//!
|
||||
//! - **精确抑制(seq)**:只跳过「本应用刚写入的那一次变化」。写入者是我们自己时,
|
||||
//! 写入后的序列号可以立刻读到,于是能精确记账一次;用户随后的复制是另一个序列号,
|
||||
//! 不会被误吞。`copy_back` 用这条。
|
||||
//!
|
||||
//! - **时间窗抑制(burst)**:屏蔽一个区间内的**所有**变化。划词取词要连续动三次剪贴板
|
||||
//! (Ctrl+C 覆盖 → 我们读走 → 还原原文),而监听线程是 250ms 轮询:按序列号逐个记账
|
||||
//! 存在竞态——监听恰好落在我们两次操作之间时,选区文本就被录进历史了。
|
||||
//! 因此取词期间必须整体屏蔽,读完并还原之后再解除。
|
||||
//!
|
||||
//! burst 用**计数**而非布尔:取词流程内部可能再触发一次写入,用布尔会在内层先结束时
|
||||
//! 提前解除屏蔽。配对由 [`SuppressState::burst`] 返回的 RAII 守卫保证,提前 return 也安全。
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SuppressState {
|
||||
/// 待跳过的序列号(消费一次即清空)
|
||||
seq: Mutex<Option<u32>>,
|
||||
/// 屏蔽窗口嵌套计数
|
||||
burst: AtomicUsize,
|
||||
}
|
||||
|
||||
/// 屏蔽窗口守卫:析构时自动解除,保证与 `begin` 严格配对。
|
||||
pub struct BurstGuard<'a>(&'a SuppressState);
|
||||
|
||||
impl Drop for BurstGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.end_burst();
|
||||
}
|
||||
}
|
||||
|
||||
impl SuppressState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 记账:跳过 `seq` 这一次变化(消费一次)。
|
||||
/// 只记一个序列号即可——本应用的写入是串行的,不会同时积压多次。
|
||||
pub fn mark_seq(&self, seq: u32) {
|
||||
if let Ok(mut guard) = self.seq.lock() {
|
||||
*guard = Some(seq);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始屏蔽窗口。返回的守卫析构时自动结束,**不要**手动配对 end。
|
||||
pub fn burst(&self) -> BurstGuard<'_> {
|
||||
self.burst.fetch_add(1, Ordering::SeqCst);
|
||||
BurstGuard(self)
|
||||
}
|
||||
|
||||
fn end_burst(&self) {
|
||||
// saturating:异常路径下的多余 end 不应让计数下溢,否则会永久屏蔽
|
||||
let _ = self
|
||||
.burst
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| {
|
||||
Some(v.saturating_sub(1))
|
||||
});
|
||||
}
|
||||
|
||||
/// 当前是否处于屏蔽窗口内(供调用方在取词前做提示,不参与判定)
|
||||
pub fn in_burst(&self) -> bool {
|
||||
self.burst.load(Ordering::SeqCst) > 0
|
||||
}
|
||||
|
||||
/// 监听线程询问:这次变化是否应当跳过。
|
||||
/// 命中序列号时**消费**该记账(下次同序列号不再跳过),避免误吞用户后续的复制。
|
||||
pub fn should_skip(&self, seq: u32) -> bool {
|
||||
if self.in_burst() {
|
||||
return true;
|
||||
}
|
||||
let mut guard = match self.seq.lock() {
|
||||
Ok(g) => g,
|
||||
Err(e) => e.into_inner(),
|
||||
};
|
||||
if *guard == Some(seq) {
|
||||
*guard = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seq_suppression_is_consumed_once() {
|
||||
let s = SuppressState::new();
|
||||
s.mark_seq(7);
|
||||
assert!(s.should_skip(7), "记账的那次应被跳过");
|
||||
assert!(!s.should_skip(7), "同序列号不应被反复跳过");
|
||||
assert!(!s.should_skip(8), "其他序列号不受影响");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burst_suppresses_everything_until_dropped() {
|
||||
let s = SuppressState::new();
|
||||
{
|
||||
let _guard = s.burst();
|
||||
assert!(s.should_skip(1));
|
||||
assert!(s.should_skip(2));
|
||||
assert!(s.in_burst());
|
||||
}
|
||||
assert!(!s.in_burst());
|
||||
assert!(!s.should_skip(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burst_is_reentrant() {
|
||||
let s = SuppressState::new();
|
||||
let outer = s.burst();
|
||||
{
|
||||
let _inner = s.burst();
|
||||
}
|
||||
// 内层结束不应提前解除外层
|
||||
assert!(s.in_burst());
|
||||
assert!(s.should_skip(9));
|
||||
drop(outer);
|
||||
assert!(!s.in_burst());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_end_does_not_underflow() {
|
||||
let s = SuppressState::new();
|
||||
s.end_burst();
|
||||
s.end_burst();
|
||||
// 下溢会让计数变成极大值从而永久屏蔽,这里确保不会
|
||||
assert!(!s.in_burst());
|
||||
assert!(!s.should_skip(4));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user