1+ using System . Runtime . InteropServices ;
12using System . Windows ;
23using System . Windows . Controls ;
34using System . Windows . Input ;
5+ using System . Windows . Interop ;
46using ClipHive . ViewModels ;
57using KeyEventArgs = System . Windows . Input . KeyEventArgs ;
68
@@ -9,13 +11,21 @@ namespace ClipHive.Views;
911/// <summary>
1012/// Code-behind for the dark overlay sidebar window.
1113/// All business logic lives in <see cref="SidebarViewModel"/>;
12- /// this file handles only keyboard navigation and window lifecycle.
14+ /// this file handles keyboard navigation, window lifecycle, and click-outside dismissal .
1315/// </summary>
1416public partial class SidebarWindow : Window
1517{
1618 private SidebarViewModel ? _viewModel ;
1719 private bool _closing ;
1820
21+ // Guard: don't close on deactivation until the window is fully shown.
22+ // Without this, the hotkey keypress itself can deactivate the window immediately.
23+ private bool _isReady ;
24+
25+ // Win32 message constants
26+ private const int WM_ACTIVATEAPP = 0x001C ;
27+ private const int WM_NCACTIVATE = 0x0086 ;
28+
1929 public SidebarWindow ( )
2030 {
2131 InitializeComponent ( ) ;
@@ -35,30 +45,60 @@ private void OnDataContextChanged(object sender, DependencyPropertyChangedEventA
3545 _viewModel . CloseRequested += OnCloseRequested ;
3646 }
3747
38- private void OnCloseRequested ( object ? sender , EventArgs e )
48+ private void OnCloseRequested ( object ? sender , EventArgs e ) => DismissWindow ( ) ;
49+
50+ // ── Window Lifecycle ──────────────────────────────────────────────────────
51+
52+ protected override void OnSourceInitialized ( EventArgs e )
3953 {
40- _closing = true ;
41- Close ( ) ;
54+ base . OnSourceInitialized ( e ) ;
55+
56+ // Hook Win32 messages on the window's HWND.
57+ // WPF's Deactivated event is unreliable for WindowStyle=None windows when
58+ // clicking the desktop, taskbar, or other apps — WM_NCACTIVATE is authoritative.
59+ var src = HwndSource . FromHwnd ( new WindowInteropHelper ( this ) . Handle ) ;
60+ src ? . AddHook ( WndProc ) ;
4261 }
4362
44- // ── Window Lifecycle ──────────────────────────────────────────────────────
63+ private IntPtr WndProc ( IntPtr hwnd , int msg , IntPtr wParam ,
64+ IntPtr lParam , ref bool handled )
65+ {
66+ if ( ! _isReady ) return IntPtr . Zero ;
67+
68+ switch ( msg )
69+ {
70+ // WM_NCACTIVATE wParam=0 → this window is being deactivated
71+ case WM_NCACTIVATE when wParam == IntPtr . Zero :
72+ // WM_ACTIVATEAPP wParam=0 → another app is taking focus
73+ case WM_ACTIVATEAPP when wParam == IntPtr . Zero :
74+ DismissWindow ( ) ;
75+ break ;
76+ }
77+
78+ return IntPtr . Zero ;
79+ }
4580
4681 private void Window_Loaded ( object sender , RoutedEventArgs e )
4782 {
48- // Auto-focus the search box so the user can start typing immediately.
4983 SearchBox . Focus ( ) ;
5084 Keyboard . Focus ( SearchBox ) ;
85+
86+ // Enable deactivation-close only after the dispatcher has fully rendered
87+ // the window. This prevents the hotkey's own keypress from immediately
88+ // deactivating the window before the user sees it.
89+ Dispatcher . BeginInvoke (
90+ System . Windows . Threading . DispatcherPriority . Input ,
91+ new Action ( ( ) => _isReady = true ) ) ;
5192 }
5293
53- private void Window_Deactivated ( object sender , EventArgs e )
94+ // Keep Deactivated as a secondary safety net for edge cases the WndProc misses.
95+ private void Window_Deactivated ( object sender , EventArgs e ) => DismissWindow ( ) ;
96+
97+ private void DismissWindow ( )
5498 {
55- // Close the window whenever it loses focus (click elsewhere, Alt+Tab, etc.).
56- // Guard against re-entrant Close() — Deactivated fires again while closing.
57- if ( ! _closing )
58- {
59- _closing = true ;
60- Close ( ) ;
61- }
99+ if ( _closing ) return ;
100+ _closing = true ;
101+ Close ( ) ;
62102 }
63103
64104 // ── Keyboard Navigation ───────────────────────────────────────────────────
@@ -68,7 +108,7 @@ private void Window_KeyDown(object sender, KeyEventArgs e)
68108 switch ( e . Key )
69109 {
70110 case Key . Escape :
71- Close ( ) ;
111+ DismissWindow ( ) ;
72112 e . Handled = true ;
73113 break ;
74114
@@ -108,33 +148,21 @@ private void MoveSelection(int delta)
108148 if ( items . Count == 0 ) return ;
109149
110150 var current = _viewModel . SelectedItem ;
111- int index ;
112- if ( current is null )
151+ int index = - 1 ;
152+ for ( int i = 0 ; i < items . Count ; i ++ )
113153 {
114- index = - 1 ;
115- }
116- else
117- {
118- index = - 1 ;
119- for ( int i = 0 ; i < items . Count ; i ++ )
120- {
121- if ( ReferenceEquals ( items [ i ] , current ) ) { index = i ; break ; }
122- }
154+ if ( ReferenceEquals ( items [ i ] , current ) ) { index = i ; break ; }
123155 }
124156
125- // Clamp to valid range.
126157 var newIndex = Math . Clamp ( index + delta , 0 , items . Count - 1 ) ;
127158 _viewModel . SelectedItem = items [ newIndex ] ;
128-
129- // Scroll the ListBox to keep the selected item visible.
130159 ItemsList . ScrollIntoView ( _viewModel . SelectedItem ) ;
131160 }
132161
133162 // ── Mouse interaction ─────────────────────────────────────────────────────
134163
135164 private void ItemsList_MouseLeftButtonUp ( object sender , MouseButtonEventArgs e )
136165 {
137- // Only fire when clicking on an actual clipboard item (not the scrollbar)
138166 if ( e . OriginalSource is FrameworkElement { DataContext : ClipboardItemViewModel item } )
139167 {
140168 _viewModel ? . SelectItemCommand . Execute ( item ) ;
0 commit comments