mirror of
https://github.com/qingjian-team/qingjian.git
synced 2026-11-06 08:13:51 +00:00
- 整句候选翻译译的是猜出来的候选,猜错就白发;改成用户在面板的框里自己打一句、停键后再译,整句只发确认过的内容 - 翻译组排在最前,每种目标语言算一个分类([ ] 换),打拼音时按 ↑ 直接落在这组;没在打字时 [shortcut] translate_panel(Ctrl+Alt+Y / ⌃⌥Y)打开 - Core panel/translate:原文带光标,停键 800 ms、两个字以上才发,内容没变不重发,没翻出来 2 秒后自动重试一次、再失败等回车;译文按原文 + 语言缓存在内存(换提供方清空),不落盘 - 译文放进应用的 preedit:Windows 译文靠轮询送到,DLL 记着写进文档的组句文字,变了就补一次编辑会话;行内不显示拼音的设置下也照样放 - 框的排版抽成 bamboo-slip 的 TextFlow,与设置程序的 TextField 共用;面板不是 Ui 搭的,布局后按各字位置给出 PanelHit::Caret - [predict] sentence_translation 不再认(旧配置里写着不报错),两个设置程序的开关与文档一并去掉;生词本整句翻译保留 - Windows dispatch/composed/mod.rs 超了 800 行,按联想、导航、出帧拆开 - 初音主题当前分组胶囊改纯色
234 lines
9.5 KiB
Rust
234 lines
9.5 KiB
Rust
//! 按消息类型分派:会话开关、按键、轮询、失焦上屏、选区 / 光标矩形 / 中英模式的通知。
|
||
|
||
use qingjian_platform::protocol::{
|
||
ClientMessage, Frame, KeyEvent, KeyOutcome, PROTOCOL_VERSION, SESSION_OPENED_SINCE,
|
||
ServerMessage, SessionId,
|
||
};
|
||
|
||
use std::time::Instant;
|
||
|
||
use super::Router;
|
||
use super::key::Effect;
|
||
use super::session::SessionInfo;
|
||
|
||
impl Router {
|
||
pub(super) fn dispatch(&mut self, message: ClientMessage) -> Option<ServerMessage> {
|
||
match message {
|
||
ClientMessage::OpenSession {
|
||
session,
|
||
app,
|
||
protocol,
|
||
} => {
|
||
tracing::debug!(?session, app, protocol, "会话打开");
|
||
if protocol != PROTOCOL_VERSION {
|
||
tracing::warn!(
|
||
?session,
|
||
app,
|
||
dll = protocol,
|
||
server = PROTOCOL_VERSION,
|
||
"DLL 与 Server 的协议版本不同(应用还没重启、用着旧 DLL?),照常服务"
|
||
);
|
||
}
|
||
// 同一会话重开(DLL 断线重连):从干净状态起。
|
||
if self.focused == Some(session) {
|
||
self.reset_composition();
|
||
self.focused = None;
|
||
}
|
||
self.sessions.insert(
|
||
session,
|
||
SessionInfo {
|
||
app,
|
||
private: false,
|
||
english: None,
|
||
vietnamese: None,
|
||
protocol,
|
||
},
|
||
);
|
||
// 按键行为设置回一次,让 DLL 不必自己读配置文件。**只回给会读这条回包的 DLL**:
|
||
// 更老的 DLL 的 `open` 是只写不读,多回一条会被它当成下一次 `Poll` 的应答而报错,
|
||
// 那条连接就废了(老 DLL 在没重启的应用里还会活很久)。它们从 `ModeSync` 那一拍
|
||
// 也能拿到同一份(新字段它直接忽略),只是慢一拍。
|
||
(protocol >= SESSION_OPENED_SINCE).then(|| ServerMessage::SessionOpened {
|
||
session,
|
||
input: self.input_settings(session),
|
||
})
|
||
}
|
||
ClientMessage::Key { session, event } => Some(self.handle_key(session, event)),
|
||
ClientMessage::Poll { session } => Some(self.handle_poll(session)),
|
||
ClientMessage::Commit { session } => {
|
||
let text = self.commit_raw_for(session);
|
||
tracing::debug!(?session, ?text, "焦点离开,结束组句");
|
||
Some(ServerMessage::Committed { session, text })
|
||
}
|
||
ClientMessage::Surrounding { session, text } => {
|
||
tracing::trace!(?session, chars = text.chars().count(), "收到光标前文");
|
||
self.set_surrounding(session, text);
|
||
None
|
||
}
|
||
ClientMessage::Privacy { session, private } => {
|
||
tracing::debug!(?session, private, "输入框私密状态");
|
||
self.set_privacy(session, private);
|
||
None
|
||
}
|
||
ClientMessage::Selection {
|
||
session,
|
||
request,
|
||
text,
|
||
rect,
|
||
} => Some(self.handle_selection(session, request, text, rect)),
|
||
ClientMessage::PositionCandidates { session, rect } => {
|
||
self.position_candidates(session, rect);
|
||
None
|
||
}
|
||
ClientMessage::HideCandidates { session } => {
|
||
// 组句在 DLL 侧结束(应用终止组句 / 翻译评审失焦):只收窗口;缓冲留给下一键的 Commit 清。
|
||
if self.focused == Some(session) {
|
||
self.end_translation();
|
||
self.cancel_prediction();
|
||
self.hide_candidate_window();
|
||
}
|
||
None
|
||
}
|
||
ClientMessage::ModeChanged {
|
||
session,
|
||
english,
|
||
vietnamese,
|
||
} => {
|
||
tracing::debug!(?session, english, vietnamese, "输入模式");
|
||
self.handle_mode_changed(session, english, vietnamese);
|
||
None
|
||
}
|
||
ClientMessage::SyncMode { session } => {
|
||
self.activate_mode(session);
|
||
self.handle_ime_active();
|
||
Some(ServerMessage::ModeSync {
|
||
session,
|
||
english: Some(self.english),
|
||
vietnamese: Some(self.vietnamese_mode),
|
||
input: self.input_settings(session),
|
||
indicator: self.indicator_state(),
|
||
})
|
||
}
|
||
ClientMessage::ImeSwitched { session } => {
|
||
tracing::debug!(?session, "切成了别的输入法");
|
||
self.handle_ime_switched();
|
||
None
|
||
}
|
||
ClientMessage::Indicator { session, command } => {
|
||
tracing::debug!(?session, ?command, "任务栏图标菜单");
|
||
self.activate_mode(session);
|
||
self.handle_indicator(command);
|
||
None
|
||
}
|
||
ClientMessage::CloseSession { session } => {
|
||
self.sessions.remove(&session);
|
||
if self.mode_session == Some(session) {
|
||
self.mode_session = None;
|
||
}
|
||
if self.focused == Some(session) {
|
||
self.reset_composition();
|
||
self.focused = None;
|
||
}
|
||
self.flush_learning();
|
||
tracing::debug!(?session, "会话关闭");
|
||
None
|
||
}
|
||
}
|
||
}
|
||
|
||
fn handle_key(&mut self, session: SessionId, event: KeyEvent) -> ServerMessage {
|
||
self.ensure_focus(session);
|
||
self.notice = None;
|
||
self.candidates.close_menu();
|
||
if self.translation.is_some() {
|
||
return self.handle_translation_review(session, &event);
|
||
}
|
||
if self.engine.composition().is_empty()
|
||
&& !self.panel_open()
|
||
&& self.matches_translate_panel_combo(&event)
|
||
&& self.open_translate_panel()
|
||
{
|
||
tracing::debug!(?session, "translate panel opened by shortcut");
|
||
let shown = self.self_drawn_frame();
|
||
self.reconcile_candidates(&shown);
|
||
return ServerMessage::KeyResult {
|
||
session,
|
||
outcome: KeyOutcome::Consumed,
|
||
commit: None,
|
||
frame: self.current_frame(),
|
||
english: None,
|
||
};
|
||
}
|
||
if self.config.cloud_enabled
|
||
&& self.engine.composition().is_empty()
|
||
&& !self.panel_open()
|
||
&& self.engine.prediction_enabled()
|
||
&& self.matches_translate_combo(&event)
|
||
{
|
||
self.selection_seq += 1;
|
||
self.pending_selection = Some(self.selection_seq);
|
||
tracing::debug!(
|
||
?session,
|
||
request = self.selection_seq,
|
||
"翻译选中文字:请 DLL 读选区"
|
||
);
|
||
return ServerMessage::RequestSelection {
|
||
session,
|
||
request: self.selection_seq,
|
||
};
|
||
}
|
||
let (commit, outcome) = match self.apply_key(&event) {
|
||
Effect::Changed(commit) => {
|
||
if !event.modifiers.vietnamese_mode {
|
||
self.recompose();
|
||
}
|
||
(commit, KeyOutcome::Consumed)
|
||
}
|
||
Effect::Navigated => (None, KeyOutcome::Consumed),
|
||
Effect::Passthrough => (None, KeyOutcome::Passthrough),
|
||
};
|
||
// 点击上屏的字还没等到轮询就来了键:接在这次结果前面,免得丢
|
||
let commit = match (self.take_clicked(session), commit) {
|
||
(Some(clicked), commit) => Some(clicked + commit.as_deref().unwrap_or_default()),
|
||
(None, commit) => commit,
|
||
};
|
||
// 选词没选完一段拼音时 Core 把词留在组句里、上屏文本是空串:不让 DLL 结束组句
|
||
let commit = commit.filter(|text| !text.is_empty());
|
||
self.poll_prediction();
|
||
self.engine.tick_panel_translation(Instant::now());
|
||
// 自绘窗吃未降级的帧;发给 DLL 的那份按老协议降级(见 composed 的 current_frame)
|
||
let shown = self.self_drawn_frame();
|
||
self.reconcile_candidates(&shown);
|
||
ServerMessage::KeyResult {
|
||
session,
|
||
outcome,
|
||
commit,
|
||
frame: self.current_frame(),
|
||
english: None,
|
||
}
|
||
}
|
||
|
||
/// 云联想轮询:聚焦会话拉一次异步结果回最新一帧,否则回空帧。释义兜底与本地整句模型也借这个节拍收。
|
||
fn handle_poll(&mut self, session: SessionId) -> ServerMessage {
|
||
self.tick();
|
||
let learned = self.engine.poll_glosses();
|
||
if learned > 0 {
|
||
tracing::info!(learned, "释义兜底写入个人释义表");
|
||
}
|
||
let frame = if self.focused == Some(session) {
|
||
self.poll_prediction();
|
||
self.engine.tick_panel_translation(Instant::now());
|
||
let shown = self.self_drawn_frame();
|
||
self.reconcile_candidates(&shown);
|
||
self.current_frame()
|
||
} else {
|
||
Frame::default()
|
||
};
|
||
ServerMessage::Update {
|
||
session,
|
||
frame,
|
||
commit: self.take_clicked(session),
|
||
}
|
||
}
|
||
}
|