11//! 倒计时窗口模块
22
3+ use std:: ptr;
34use std:: time:: { Duration , Instant } ;
45
5- use objc2:: { MainThreadOnly , sel} ;
6+ use block2:: global_block;
7+ use objc2:: { MainThreadOnly , define_class, msg_send, sel} ;
68use objc2_app_kit:: {
7- NSBackingStoreType , NSColor , NSFont , NSScreen , NSStatusWindowLevel , NSTextField , NSWindow ,
8- NSWindowCollectionBehavior , NSWindowStyleMask ,
9+ NSEvent , NSEventMask , NSApplication , NSBackingStoreType , NSColor , NSFont , NSScreen ,
10+ NSStatusWindowLevel , NSTextField , NSWindow , NSWindowCollectionBehavior , NSWindowStyleMask ,
911} ;
1012use objc2_core_foundation:: CGFloat ;
11- use objc2_foundation:: { NSPoint , NSRect , NSSize , NSString , NSTimer } ;
13+ use objc2_foundation:: { NSPoint , NSRect , NSSize , NSString , NSTimer , NSObjectProtocol } ;
1214
1315use super :: super :: constants:: APP_NAME_ZH ;
1416use super :: super :: delegate:: RestGapDelegate ;
1517use super :: super :: state:: with_state;
1618use super :: super :: utils:: { format_countdown, play_sound} ;
1719use super :: status_bar:: target_anyobject;
1820
21+ const SKIP_PHRASE_SMART : & str = "i don’t care about my health." ;
22+ const SKIP_PHRASE_ASCII : & str = "i don't care about my health." ;
23+
24+ define_class ! (
25+ #[ unsafe ( super ( NSWindow ) ) ]
26+ #[ thread_kind = MainThreadOnly ]
27+ pub struct CountdownWindow ;
28+
29+ unsafe impl NSObjectProtocol for CountdownWindow { }
30+
31+ impl CountdownWindow {
32+ #[ unsafe ( method( canBecomeKeyWindow) ) ]
33+ fn can_become_key_window( & self ) -> bool {
34+ true
35+ }
36+
37+ #[ unsafe ( method( canBecomeMainWindow) ) ]
38+ fn can_become_main_window( & self ) -> bool {
39+ true
40+ }
41+ }
42+ ) ;
43+
44+ fn advance_phrase_idx ( idx : & mut usize , phrase : & str , ch : char ) -> bool {
45+ let mut buf = [ 0u8 ; 4 ] ;
46+ let ch_bytes = ch. encode_utf8 ( & mut buf) . as_bytes ( ) ;
47+ let phrase_bytes = phrase. as_bytes ( ) ;
48+
49+ if * idx + ch_bytes. len ( ) <= phrase_bytes. len ( )
50+ && & phrase_bytes[ * idx..( * idx + ch_bytes. len ( ) ) ] == ch_bytes
51+ {
52+ * idx += ch_bytes. len ( ) ;
53+ return * idx == phrase_bytes. len ( ) ;
54+ }
55+
56+ // 朴素回退:短语以 'I' 开头且几乎不重叠,足够可靠。
57+ * idx = 0 ;
58+ if ch_bytes. len ( ) <= phrase_bytes. len ( ) && & phrase_bytes[ ..ch_bytes. len ( ) ] == ch_bytes {
59+ * idx = ch_bytes. len ( ) ;
60+ return * idx == phrase_bytes. len ( ) ;
61+ }
62+ false
63+ }
64+
65+ fn on_countdown_keydown ( event : & NSEvent ) {
66+ let Some ( text) = event
67+ . charactersIgnoringModifiers ( )
68+ . map ( |s| s. to_string ( ) )
69+ else {
70+ return ;
71+ } ;
72+
73+ with_state ( |state| {
74+ if state. countdown_window . is_none ( ) {
75+ return ;
76+ }
77+ if state. countdown_skip_requested {
78+ return ;
79+ }
80+
81+ for ch in text. chars ( ) {
82+ let ch = if ch. is_ascii ( ) {
83+ ch. to_ascii_lowercase ( )
84+ } else {
85+ ch
86+ } ;
87+ let smart_done =
88+ advance_phrase_idx ( & mut state. countdown_skip_smart_idx , SKIP_PHRASE_SMART , ch) ;
89+ let ascii_done =
90+ advance_phrase_idx ( & mut state. countdown_skip_ascii_idx , SKIP_PHRASE_ASCII , ch) ;
91+ if smart_done || ascii_done {
92+ state. countdown_skip_requested = true ;
93+ state. countdown_skip_smart_idx = 0 ;
94+ state. countdown_skip_ascii_idx = 0 ;
95+ break ;
96+ }
97+ }
98+ } ) ;
99+ }
100+
101+ global_block ! {
102+ static COUNTDOWN_KEY_MONITOR = |event: core:: ptr:: NonNull <NSEvent >| -> * mut NSEvent {
103+ let event_ref: & NSEvent = unsafe { event. as_ref( ) } ;
104+ on_countdown_keydown( event_ref) ;
105+ ptr:: null_mut( )
106+ } ;
107+ }
108+
19109/// 显示倒计时窗口
20110#[ allow( clippy:: too_many_lines) ]
21111pub fn show_countdown_window ( delegate : & RestGapDelegate , seconds : u64 , play_start_sound : bool ) {
@@ -42,15 +132,10 @@ pub fn show_countdown_window(delegate: &RestGapDelegate, seconds: u64, play_star
42132
43133 // 创建窗口 - 使用 Borderless 样式隐藏标题栏
44134 let style = NSWindowStyleMask :: Borderless ;
45- let window = unsafe {
46- NSWindow :: initWithContentRect_styleMask_backing_defer (
47- NSWindow :: alloc ( mtm) ,
48- frame,
49- style,
50- NSBackingStoreType :: Buffered ,
51- false ,
52- )
135+ let window: objc2:: rc:: Retained < CountdownWindow > = unsafe {
136+ msg_send ! [ CountdownWindow :: alloc( mtm) , initWithContentRect: frame styleMask: style backing: NSBackingStoreType :: Buffered defer: false ]
53137 } ;
138+ let window: objc2:: rc:: Retained < NSWindow > = window. into_super ( ) ;
54139
55140 // 设置窗口属性
56141 window. setLevel ( NSStatusWindowLevel ) ; // 使用状态窗口级别,确保在最前
@@ -134,9 +219,28 @@ pub fn show_countdown_window(delegate: &RestGapDelegate, seconds: u64, play_star
134219 content_view. addSubview ( & hint_label) ;
135220 }
136221
222+ // 让倒计时窗口能获取键盘事件(Borderless 默认不可成为 key window)
223+ NSApplication :: sharedApplication ( mtm) . activateIgnoringOtherApps ( true ) ;
224+
225+ // 先标记窗口存在,避免用户刚弹窗就开始输入时被忽略
226+ with_state ( |state| {
227+ state. countdown_window = Some ( window. clone ( ) ) ;
228+ state. countdown_skip_smart_idx = 0 ;
229+ state. countdown_skip_ascii_idx = 0 ;
230+ state. countdown_skip_requested = false ;
231+ } ) ;
232+
137233 // 显示窗口
138234 window. makeKeyAndOrderFront ( None ) ;
139235
236+ // 仅在倒计时窗口存在时安装本地键盘监听器,避免非休息时任何额外开销。
237+ let key_monitor = unsafe {
238+ NSEvent :: addLocalMonitorForEventsMatchingMask_handler (
239+ NSEventMask :: KeyDown ,
240+ & COUNTDOWN_KEY_MONITOR ,
241+ )
242+ } ;
243+
140244 // 设置结束时间
141245 let end_time = Instant :: now ( ) + Duration :: from_secs ( seconds) ;
142246
@@ -154,10 +258,10 @@ pub fn show_countdown_window(delegate: &RestGapDelegate, seconds: u64, play_star
154258
155259 // 保存状态
156260 with_state ( |state| {
157- state. countdown_window = Some ( window) ;
158261 state. countdown_label = Some ( countdown_label) ;
159262 state. countdown_timer = Some ( timer) ;
160263 state. countdown_end_time = Some ( end_time) ;
264+ state. countdown_key_monitor = key_monitor;
161265 } ) ;
162266}
163267
@@ -188,6 +292,11 @@ pub fn update_countdown() -> bool {
188292/// 关闭倒计时窗口
189293pub fn close_countdown_window ( ) {
190294 with_state ( |state| {
295+ if let Some ( monitor) = state. countdown_key_monitor . take ( ) {
296+ // Safety: The monitor handle is created by NSEvent::addLocalMonitor...
297+ unsafe { NSEvent :: removeMonitor ( & monitor) } ;
298+ }
299+
191300 // 先使定时器无效,防止后续触发
192301 if let Some ( timer) = state. countdown_timer . take ( ) {
193302 timer. invalidate ( ) ;
@@ -198,6 +307,9 @@ pub fn close_countdown_window() {
198307 }
199308 state. countdown_label = None ;
200309 state. countdown_end_time = None ;
310+ state. countdown_skip_smart_idx = 0 ;
311+ state. countdown_skip_ascii_idx = 0 ;
312+ state. countdown_skip_requested = false ;
201313 } ) ;
202314}
203315
0 commit comments