Skip to content

Commit 59eab55

Browse files
committed
fix: click-outside-to-close via WM_NCACTIVATE hook; update icon transparency
1 parent 06aebf3 commit 59eab55

2 files changed

Lines changed: 58 additions & 30 deletions

File tree

assets/icon/ClipHive.ico

-10.9 KB
Binary file not shown.

src/ClipHive/Views/SidebarWindow.xaml.cs

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System.Runtime.InteropServices;
12
using System.Windows;
23
using System.Windows.Controls;
34
using System.Windows.Input;
5+
using System.Windows.Interop;
46
using ClipHive.ViewModels;
57
using 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>
1416
public 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

Comments
 (0)