diff --git a/rust_indicator/Cargo.toml b/rust_indicator/Cargo.toml index c51dd8c..ecb7ba4 100644 --- a/rust_indicator/Cargo.toml +++ b/rust_indicator/Cargo.toml @@ -8,6 +8,7 @@ windows = { version = "0.58", features = [ "Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Accessibility", "Win32_Graphics_Gdi", "Win32_Graphics_GdiPlus", diff --git a/rust_indicator/src/config.rs b/rust_indicator/src/config.rs index 5955b96..2f5f309 100644 --- a/rust_indicator/src/config.rs +++ b/rust_indicator/src/config.rs @@ -15,10 +15,12 @@ pub struct Config { pub poll_track_interval_ms: u64, pub tray_enable: bool, + pub debug_console: bool, pub caret_enable: bool, pub caret_color_cn: u32, pub caret_color_en: u32, + pub caret_color_en_upper: u32, pub caret_size: i32, pub caret_offset_x: i32, pub caret_offset_y: i32, @@ -27,6 +29,7 @@ pub struct Config { pub mouse_enable: bool, pub mouse_color_cn: u32, pub mouse_color_en: u32, + pub mouse_color_en_upper: u32, pub mouse_size: i32, pub mouse_offset_x: i32, pub mouse_offset_y: i32, @@ -40,16 +43,19 @@ impl Default for Config { poll_state_interval_ms: 100, poll_track_interval_ms: 10, tray_enable: true, + debug_console: false, caret_enable: true, - caret_color_cn: parse_color("#FF7800A0"), - caret_color_en: parse_color("#0078FF30"), + caret_color_cn: parse_color("#FF0000A0"), + caret_color_en: parse_color("#0000FFA0"), + caret_color_en_upper: parse_color("#008000A0"), caret_size: 8, caret_offset_x: 0, caret_offset_y: 0, caret_show_en: true, mouse_enable: true, - mouse_color_cn: parse_color("#FF7800A0"), - mouse_color_en: parse_color("#0078FF30"), + mouse_color_cn: parse_color("#FF0000A0"), + mouse_color_en: parse_color("#0000FFA0"), + mouse_color_en_upper: parse_color("#008000A0"), mouse_size: 8, mouse_offset_x: 2, mouse_offset_y: 18, @@ -121,13 +127,20 @@ fn load_config() -> Config { if let Some(v) = get("poll", "state_interval_ms") { if let Ok(n) = v.parse() { config.poll_state_interval_ms = n; } } if let Some(v) = get("poll", "track_interval_ms") { if let Ok(n) = v.parse() { config.poll_track_interval_ms = n; } } - if let Some(v) = get("tray", "enable") { + if let Some(v) = get("tray", "enable") { match v.as_str() { "true" => config.tray_enable = true, "false" => config.tray_enable = false, _ => {} // 保持默认值 } } + if let Some(v) = get("tray", "debug_console") { + match v.as_str() { + "true" => config.debug_console = true, + "false" => config.debug_console = false, + _ => {} + } + } if let Some(v) = get("caret", "enable") { match v.as_str() { @@ -138,6 +151,7 @@ fn load_config() -> Config { } if let Some(v) = get("caret", "color_cn") { config.caret_color_cn = v.parse_color(); } if let Some(v) = get("caret", "color_en") { config.caret_color_en = v.parse_color(); } + if let Some(v) = get("caret", "color_en_upper") { config.caret_color_en_upper = v.parse_color(); } if let Some(v) = get("caret", "size") { if let Ok(n) = v.parse() { config.caret_size = n; } } if let Some(v) = get("caret", "offset_x") { if let Ok(n) = v.parse() { config.caret_offset_x = n; } } if let Some(v) = get("caret", "offset_y") { if let Ok(n) = v.parse() { config.caret_offset_y = n; } } @@ -158,6 +172,7 @@ fn load_config() -> Config { } if let Some(v) = get("mouse", "color_cn") { config.mouse_color_cn = v.parse_color(); } if let Some(v) = get("mouse", "color_en") { config.mouse_color_en = v.parse_color(); } + if let Some(v) = get("mouse", "color_en_upper") { config.mouse_color_en_upper = v.parse_color(); } if let Some(v) = get("mouse", "size") { if let Ok(n) = v.parse() { config.mouse_size = n; } } if let Some(v) = get("mouse", "offset_x") { if let Ok(n) = v.parse() { config.mouse_offset_x = n; } } if let Some(v) = get("mouse", "offset_y") { if let Ok(n) = v.parse() { config.mouse_offset_y = n; } } @@ -188,11 +203,15 @@ track_interval_ms = 10 # 位置追踪间隔 (ms) [tray] enable = true # 是否显示托盘图标 (false 时完全后台运行,只能通过任务管理器结束) +debug_console = false # 是否显示调试控制台 (用于开发调试) [caret] +# 颜色格式:#RRGGBBAA +# RR=红, GG=绿, BB=蓝, AA=透明度(00=透明, FF=不透明, A0=63%不透明) enable = true # 是否启用文本光标提示 -color_cn = "#FF7800A0" # 中文状态颜色 (#RRGGBBAA) -color_en = "#0078FF30" # 英文状态颜色 +color_cn = "#FF0000A0" # 中文状态颜色(红) +color_en = "#0000FFA0" # 英文小写状态颜色(蓝) +color_en_upper = "#008000A0" # 英文大写状态颜色(绿) size = 8 # 提示球大小 offset_x = 0 offset_y = 0 @@ -200,8 +219,9 @@ show_en = true # 英文状态下是否显示 [mouse] enable = true # 是否开启鼠标提示 -color_cn = "#FF7800A0" # 中文状态颜色 -color_en = "#0078FF30" # 英文状态颜色 +color_cn = "#FF0000A0" # 中文状态颜色(红) +color_en = "#0000FFA0" # 英文小写状态颜色(蓝) +color_en_upper = "#008000A0" # 英文大写状态颜色(绿) size = 8 # 提示球大小 offset_x = 2 offset_y = 18 @@ -220,9 +240,11 @@ pub fn get() -> &'static Config { CONFIG.get_or_init(load_config) } pub fn state_poll_interval_ms() -> u64 { get().poll_state_interval_ms } pub fn track_poll_interval_ms() -> u64 { get().poll_track_interval_ms } pub fn tray_enable() -> bool { get().tray_enable } +pub fn debug_console() -> bool { get().debug_console } pub fn caret_enable() -> bool { get().caret_enable } pub fn caret_color_cn() -> u32 { get().caret_color_cn } pub fn caret_color_en() -> u32 { get().caret_color_en } +pub fn caret_color_en_upper() -> u32 { get().caret_color_en_upper } pub fn caret_size() -> i32 { get().caret_size } pub fn caret_offset_x() -> i32 { get().caret_offset_x } pub fn caret_offset_y() -> i32 { get().caret_offset_y } @@ -230,6 +252,7 @@ pub fn caret_show_en() -> bool { get().caret_show_en } pub fn mouse_enable() -> bool { get().mouse_enable } pub fn mouse_color_cn() -> u32 { get().mouse_color_cn } pub fn mouse_color_en() -> u32 { get().mouse_color_en } +pub fn mouse_color_en_upper() -> u32 { get().mouse_color_en_upper } pub fn mouse_size() -> i32 { get().mouse_size } pub fn mouse_offset_x() -> i32 { get().mouse_offset_x } pub fn mouse_offset_y() -> i32 { get().mouse_offset_y } diff --git a/rust_indicator/src/fullscreen_detector.rs b/rust_indicator/src/fullscreen_detector.rs new file mode 100644 index 0000000..6bfd82a --- /dev/null +++ b/rust_indicator/src/fullscreen_detector.rs @@ -0,0 +1,80 @@ +//! 全屏应用检测模块 +//! +//! 用于检测是否有应用程序处于全屏状态(如视频播放、游戏等), +//! 在全屏时自动隐藏 IME 指示器,避免遮挡视频内容。 + +use windows::Win32::Foundation::RECT; +use windows::Win32::UI::WindowsAndMessaging::{ + GetForegroundWindow, GetSystemMetrics, GetWindowRect, SM_CXSCREEN, SM_CYSCREEN, +}; + +/// 检测是否有应用程序处于全屏状态 +/// +/// 判断逻辑: +/// 1. 获取当前活动窗口 +/// 2. 检查窗口是否覆盖整个屏幕 +/// 3. 排除桌面窗口(Progman) +/// +/// # Returns +/// - `true` - 检测到全屏应用 +/// - `false` - 无全屏应用 +pub fn is_fullscreen_app() -> bool { + unsafe { + // 获取当前活动窗口 + let hwnd = GetForegroundWindow(); + + if hwnd.is_invalid() { + return false; + } + + // 获取窗口矩形 + let mut rect = RECT::default(); + if GetWindowRect(hwnd, &mut rect).is_err() { + return false; + } + + // 获取屏幕尺寸 + let screen_width = GetSystemMetrics(SM_CXSCREEN); + let screen_height = GetSystemMetrics(SM_CYSCREEN); + + // 检查窗口是否覆盖整个屏幕 + // 允许小幅偏差(某些播放器可能会有几像素的边框) + let tolerance = 2; + let covers_screen = (rect.left <= tolerance) + && (rect.top <= tolerance) + && (rect.right >= screen_width - tolerance) + && (rect.bottom >= screen_height - tolerance); + + if !covers_screen { + return false; + } + + // 排除桌面窗口 + // 获取窗口类名来判断是否是桌面 + let mut class_name = [0u16; 256]; + let _ = windows::Win32::UI::WindowsAndMessaging::GetClassNameW(hwnd, &mut class_name); + + let class_name_str = String::from_utf16_lossy( + &class_name[..class_name.iter().position(|&c| c == 0).unwrap_or(0)], + ); + + // 排除桌面和某些系统窗口 + if class_name_str.contains("Progman") || class_name_str.contains("WorkerW") { + return false; + } + + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fullscreen_detection() { + // 基本的功能测试 + let result = is_fullscreen_app(); + println!("Fullscreen status: {}", result); + } +} diff --git a/rust_indicator/src/ime_detector.rs b/rust_indicator/src/ime_detector.rs index 3db231e..3cfcab7 100644 --- a/rust_indicator/src/ime_detector.rs +++ b/rust_indicator/src/ime_detector.rs @@ -2,10 +2,13 @@ use windows::Win32::Foundation::HWND; use windows::Win32::UI::Input::Ime::ImmGetDefaultIMEWnd; +use windows::Win32::UI::Input::KeyboardAndMouse::{GetKeyState, GetKeyboardLayout}; use windows::Win32::UI::WindowsAndMessaging::{ GetForegroundWindow, GetGUIThreadInfo, GetWindowThreadProcessId, SendMessageTimeoutW, GUITHREADINFO, SMTO_ABORTIFHUNG, }; +use windows::Win32::System::Console::{AllocConsole, SetConsoleTitleW}; +use windows::core::w; /// IME 控制消息 const WM_IME_CONTROL: u32 = 0x283; @@ -13,6 +16,76 @@ const IMC_GETOPENSTATUS: usize = 0x5; const IMC_GETCONVERSIONMODE: usize = 0x1; const IME_CMODE_NATIVE: u32 = 0x0001; +/// 键盘布局 ID 常量 +const LAYOUT_ZH_CN: u32 = 0x0804; // 中文 (中国) +const LAYOUT_ZH_TW: u32 = 0x0404; // 中文 (台湾) + +/// 调试状态结构体 +#[derive(Clone, PartialEq)] +struct DebugState { + ime_hwnd: isize, + open_status: usize, + conversion_mode: usize, + has_native: bool, + layout_id: u32, + caps_lock: bool, +} + +impl Default for DebugState { + fn default() -> Self { + Self { + ime_hwnd: 0, + open_status: 0, + conversion_mode: 0, + has_native: false, + layout_id: 0, + caps_lock: false, + } + } +} + +/// 全局调试状态 +static mut LAST_STATE: Option = None; +static mut DEBUG_LINE_NUM: u32 = 0; +static mut CONSOLE_INITIALIZED: bool = false; + +/// 初始化调试控制台 +fn init_debug_console() { + unsafe { + if !CONSOLE_INITIALIZED && crate::config::debug_console() { + let _ = AllocConsole(); + let _ = SetConsoleTitleW(w!("IME Indicator Debug Console")); + CONSOLE_INITIALIZED = true; + } + } +} + +/// 打印调试状态 +fn print_debug_state(state: &DebugState, result: bool) { + unsafe { + DEBUG_LINE_NUM += 1; + + let layout_str = match state.layout_id { + 0x0409 => "0x0409(EN)", + 0x0804 => "0x0804(ZH)", + 0x0404 => "0x0404(TW)", + _ => &format!("0x{:04x}(?)", state.layout_id)[..], + }; + + println!( + "[{:03}] hwnd:{:#010x} open:{} conv:{} NATIVE:{} layout:{} caps:{} => {}", + DEBUG_LINE_NUM, + state.ime_hwnd, + state.open_status, + state.conversion_mode, + state.has_native, + layout_str, + state.caps_lock, + if result { "CN" } else { "EN" } + ); + } +} + /// 获取当前焦点窗口 fn get_focused_window() -> HWND { unsafe { @@ -40,6 +113,16 @@ fn get_focused_window() -> HWND { } } +/// 获取键盘布局 ID +fn get_keyboard_layout_id() -> u32 { + unsafe { + let hwnd = get_focused_window(); + let thread_id = GetWindowThreadProcessId(hwnd, None); + let hkl = GetKeyboardLayout(thread_id); + (hkl.0 as usize & 0xFFFF) as u32 + } +} + /// 获取 IME 默认窗口句柄 fn get_ime_window(hwnd: HWND) -> HWND { unsafe { ImmGetDefaultIMEWnd(hwnd) } @@ -68,23 +151,68 @@ fn send_message_timeout(hwnd: HWND, msg: u32, wparam: usize, lparam: isize) -> O /// 检测是否为中文输入模式 pub fn is_chinese_mode() -> bool { + init_debug_console(); + let hwnd = get_focused_window(); let ime_hwnd = get_ime_window(hwnd); - - if ime_hwnd.0.is_null() { - return false; - } - // 获取 IME 开启状态 - let open_status = send_message_timeout(ime_hwnd, WM_IME_CONTROL, IMC_GETOPENSTATUS, 0); - if open_status.unwrap_or(0) == 0 { - return false; + // 收集调试状态 + let caps_lock = unsafe { (GetKeyState(0x14) & 0x0001) != 0 }; + let mut state = DebugState { + ime_hwnd: ime_hwnd.0 as isize, + caps_lock, + ..Default::default() + }; + + let mut has_native = false; + let mut open_status = 0; + + // 检查 IME 状态 + if !ime_hwnd.0.is_null() { + if let Some(conversion_mode) = send_message_timeout(ime_hwnd, WM_IME_CONTROL, IMC_GETCONVERSIONMODE, 0) { + state.conversion_mode = conversion_mode; + has_native = (conversion_mode as u32 & IME_CMODE_NATIVE) != 0; + state.has_native = has_native; + } + + if let Some(open) = send_message_timeout(ime_hwnd, WM_IME_CONTROL, IMC_GETOPENSTATUS, 0) { + open_status = open; + state.open_status = open; + } } - // 获取转换模式并检测是否包含 NATIVE 标志 (中文) - if let Some(conversion_mode) = send_message_timeout(ime_hwnd, WM_IME_CONTROL, IMC_GETCONVERSIONMODE, 0) { - return (conversion_mode as u32 & IME_CMODE_NATIVE) != 0; + let layout_id = get_keyboard_layout_id(); + state.layout_id = layout_id; + + // 检测逻辑 + let result = if has_native { + layout_id == LAYOUT_ZH_CN || layout_id == LAYOUT_ZH_TW + } else if open_status == 1 { + layout_id == LAYOUT_ZH_CN || layout_id == LAYOUT_ZH_TW + } else { + false + }; + + // 调试输出 + unsafe { + let should_print = match &LAST_STATE { + None => true, + Some(last) => state != *last, + }; + + if should_print { + print_debug_state(&state, result); + LAST_STATE = Some(state.clone()); + } } - false + result +} + +/// 检测 Caps Lock 是否开启 +pub fn is_caps_lock_on() -> bool { + unsafe { + let state = GetKeyState(0x14); + (state & 0x0001) != 0 + } } diff --git a/rust_indicator/src/main.rs b/rust_indicator/src/main.rs index 7c7849f..a3c6c21 100644 --- a/rust_indicator/src/main.rs +++ b/rust_indicator/src/main.rs @@ -3,6 +3,7 @@ mod caret_detector; mod config; mod cursor_detector; +mod fullscreen_detector; mod ime_detector; mod overlay; mod tray; @@ -16,7 +17,8 @@ use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, LoadIconW, IDI_APPLI use caret_detector::CaretDetector; use cursor_detector::CursorDetector; -use ime_detector::is_chinese_mode; +use fullscreen_detector::is_fullscreen_app; +use ime_detector::{is_caps_lock_on, is_chinese_mode}; use overlay::IndicatorOverlay; use tray::TrayManager; @@ -104,6 +106,7 @@ fn run_detector_loop(running: Arc) { config::caret_size(), config::caret_color_cn(), config::caret_color_en(), + config::caret_color_en_upper(), config::caret_offset_x(), config::caret_offset_y(), )) @@ -117,6 +120,7 @@ fn run_detector_loop(running: Arc) { config::mouse_size(), config::mouse_color_cn(), config::mouse_color_en(), + config::mouse_color_en_upper(), config::mouse_offset_x(), config::mouse_offset_y(), )) @@ -129,8 +133,10 @@ fn run_detector_loop(running: Arc) { let mut last_state_check_time = Instant::now(); let mut chinese_mode = false; + let mut caps_lock_on = false; let mut caret_active = false; let mut mouse_active = false; + let mut is_fullscreen = false; // 主线程负责消息循环,子线程负责检测 while running.load(Ordering::SeqCst) { @@ -138,21 +144,34 @@ fn run_detector_loop(running: Arc) { // A. 状态检测 (100ms) if now.duration_since(last_state_check_time) >= state_interval { + // 检测全屏状态 + is_fullscreen = is_fullscreen_app(); + chinese_mode = is_chinese_mode(); + caps_lock_on = is_caps_lock_on(); // Caret 状态判断 if config::caret_enable() { if let Some(ref overlay) = caret_overlay { - let caret_pos = caret_detector.get_caret_pos(); - - let should_caret = caret_pos.is_some() && (chinese_mode || config::caret_show_en()); - if should_caret != caret_active { - caret_active = should_caret; + // 全屏时强制隐藏 + if is_fullscreen { if caret_active { - overlay.show(); - } else { + caret_active = false; overlay.hide(); } + } else { + // 非全屏时正常检测 + let caret_pos = caret_detector.get_caret_pos(); + + let should_caret = caret_pos.is_some() && (chinese_mode || config::caret_show_en()); + if should_caret != caret_active { + caret_active = should_caret; + if caret_active { + overlay.show(); + } else { + overlay.hide(); + } + } } } } @@ -160,15 +179,24 @@ fn run_detector_loop(running: Arc) { // Mouse 状态判断 if config::mouse_enable() { if let Some(ref overlay) = mouse_overlay { - let target_cursor = cursor_detector.is_target_cursor(); - let should_mouse = target_cursor && (chinese_mode || config::mouse_show_en()); - if should_mouse != mouse_active { - mouse_active = should_mouse; + // 全屏时强制隐藏 + if is_fullscreen { if mouse_active { - overlay.show(); - } else { + mouse_active = false; overlay.hide(); } + } else { + // 非全屏时正常检测 + let target_cursor = cursor_detector.is_target_cursor(); + let should_mouse = target_cursor && (chinese_mode || config::mouse_show_en()); + if should_mouse != mouse_active { + mouse_active = should_mouse; + if mouse_active { + overlay.show(); + } else { + overlay.hide(); + } + } } } } @@ -177,23 +205,25 @@ fn run_detector_loop(running: Arc) { } // B. 坐标追踪 - - // 1. 追踪文本光标 - if config::caret_enable() && caret_active { - if let Some(ref overlay) = caret_overlay { - if let Some((x, y, h)) = caret_detector.get_caret_pos() { - overlay.update(x, y, chinese_mode, h); + // 全屏时不追踪位置 + if !is_fullscreen { + // 1. 追踪文本光标 + if config::caret_enable() && caret_active { + if let Some(ref overlay) = caret_overlay { + if let Some((x, y, h)) = caret_detector.get_caret_pos() { + overlay.update(x, y, chinese_mode, caps_lock_on, h); + } } } - } - // 2. 追踪鼠标 - if config::mouse_enable() && mouse_active { - if let Some(ref overlay) = mouse_overlay { - let mut pt = POINT::default(); - unsafe { - if GetCursorPos(&mut pt).is_ok() { - overlay.update(pt.x, pt.y, chinese_mode, 0); + // 2. 追踪鼠标 + if config::mouse_enable() && mouse_active { + if let Some(ref overlay) = mouse_overlay { + let mut pt = POINT::default(); + unsafe { + if GetCursorPos(&mut pt).is_ok() { + overlay.update(pt.x, pt.y, chinese_mode, caps_lock_on, 0); + } } } } diff --git a/rust_indicator/src/overlay.rs b/rust_indicator/src/overlay.rs index 49eab74..6d56b26 100644 --- a/rust_indicator/src/overlay.rs +++ b/rust_indicator/src/overlay.rs @@ -39,6 +39,7 @@ pub struct IndicatorOverlay { size: i32, color_cn: u32, color_en: u32, + color_en_upper: u32, offset_x: i32, offset_y: i32, gdi_token: usize, @@ -51,6 +52,7 @@ impl IndicatorOverlay { size: i32, color_cn: u32, color_en: u32, + color_en_upper: u32, offset_x: i32, offset_y: i32, ) -> Self { @@ -62,6 +64,7 @@ impl IndicatorOverlay { size, color_cn, color_en, + color_en_upper, offset_x, offset_y, gdi_token, @@ -140,10 +143,17 @@ impl IndicatorOverlay { } /// 更新渲染内容和屏幕位置 - pub fn update(&self, x: i32, y: i32, is_chinese: bool, caret_h: i32) { - let color = if is_chinese { + /// is_chinese: 是否中文模式 + /// is_upper: 是否大写锁定(Caps Lock 开启) + pub fn update(&self, x: i32, y: i32, is_chinese: bool, is_upper: bool, caret_h: i32) { + let color = if is_upper { + // Caps Lock 开启,不管中英文都显示绿色 + self.color_en_upper + } else if is_chinese { + // Caps Lock 关闭 + 中文模式 → 红色 self.color_cn } else { + // Caps Lock 关闭 + 英文模式 → 蓝色 self.color_en };