fix(quicksend): preserve focus workaround with custom titlebar (#285)

Keeps the Quick Send custom titlebar styling while preserving the Windows hotkey foreground/topmost retry path and avoiding close-on-deactivation data loss.\n\nValidation: local ARM64 build passed; Shared tests 1319 passed / 20 skipped; Tray tests 466 passed; remote CI green.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot 2026-05-07 19:04:39 -04:00 committed by GitHub
parent 56d956d723
commit bcd1e633e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -24,7 +24,11 @@ public sealed class QuickSendDialog : WindowEx
private readonly TextBox _errorDetailsTextBox;
private readonly Button _sendButton;
private bool _isSending;
private bool _isClosed;
private bool _focusRetryRunning;
private const string TitleIcon = "🦞";
private const double WindowControlsReservedWidth = 140;
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
@ -42,6 +46,7 @@ public sealed class QuickSendDialog : WindowEx
uint uFlags);
private static readonly IntPtr HWND_TOPMOST = new(-1);
private const int TitleBarHeight = 48;
private const int SW_SHOWNORMAL = 1;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOSIZE = 0x0001;
@ -53,7 +58,8 @@ public sealed class QuickSendDialog : WindowEx
// Window setup
Title = LocalizationHelper.GetString("WindowTitle_QuickSend");
this.SetWindowSize(420, 260);
ExtendsContentIntoTitleBar = true;
this.SetWindowSize(420, 260 + TitleBarHeight);
this.CenterOnScreen();
this.SetIcon(IconHelper.GetStatusIconPath(ConnectionStatus.Connected));
@ -62,9 +68,9 @@ public sealed class QuickSendDialog : WindowEx
BackdropHelper.TrySetAcrylicBackdrop((Microsoft.UI.Xaml.Window)this);
// Hotkey-launched windows can fail to foreground on Windows 10 due to
// foreground activation restrictions. Ensure the window is topmost.
// foreground activation restrictions. Keep the existing topmost promotion.
this.IsAlwaysOnTop = true;
// Build UI programmatically (simple dialog)
var root = new Grid
{
@ -130,20 +136,57 @@ public sealed class QuickSendDialog : WindowEx
Grid.SetRow(buttonPanel, 3);
root.Children.Add(buttonPanel);
Content = new Border
var body = new Border
{
Padding = new Thickness(24),
Child = root
};
// Focus the text box when shown
var outerGrid = new Grid();
outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(TitleBarHeight) });
outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
var titleBar = new Grid { Padding = new Thickness(16, 0, WindowControlsReservedWidth, 0) };
var titleStack = new StackPanel { Orientation = Orientation.Horizontal };
titleStack.Children.Add(new TextBlock
{
Text = TitleIcon,
FontSize = 20,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 10, 0)
});
titleStack.Children.Add(new TextBlock
{
Text = LocalizationHelper.GetString("WindowTitle_QuickSend"),
FontSize = 13,
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"],
VerticalAlignment = VerticalAlignment.Center
});
titleBar.Children.Add(titleStack);
Grid.SetRow(titleBar, 0);
outerGrid.Children.Add(titleBar);
Grid.SetRow(body, 1);
outerGrid.Children.Add(body);
Content = outerGrid;
SetTitleBar(titleBar);
// Focus the text box when shown without closing on transient deactivation.
Activated += (s, e) =>
{
TryBringToFront();
RequestInputFocus();
if (e.WindowActivationState != WindowActivationState.Deactivated)
{
TryBringToFront();
RequestInputFocus();
}
};
Closed += (s, e) => Logger.Info("[QuickSend] Dialog closed");
Closed += (s, e) =>
{
_isClosed = true;
Logger.Info("[QuickSend] Dialog closed");
};
Logger.Info($"[QuickSend] Dialog opened (prefill={!string.IsNullOrEmpty(prefillMessage)})");
}
@ -152,6 +195,9 @@ public sealed class QuickSendDialog : WindowEx
{
try
{
if (_isClosed)
return;
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
if (hwnd == IntPtr.Zero) return;
@ -191,7 +237,7 @@ public sealed class QuickSendDialog : WindowEx
_errorDetailsTextBox.Visibility = Visibility.Collapsed;
_errorDetailsTextBox.Text = string.Empty;
this.SetWindowSize(420, 260);
this.SetWindowSize(420, 260 + TitleBarHeight);
_isSending = true;
_sendButton.IsEnabled = false;
@ -257,7 +303,7 @@ public sealed class QuickSendDialog : WindowEx
_errorDetailsTextBox.MinHeight = 140;
_errorDetailsTextBox.Text = details;
_errorDetailsTextBox.Visibility = Visibility.Visible;
this.SetWindowSize(520, 400);
this.SetWindowSize(520, 400 + TitleBarHeight);
// Move focus to the details box so users can immediately select/copy text.
_errorDetailsTextBox.Focus(FocusState.Programmatic);
@ -269,7 +315,7 @@ public sealed class QuickSendDialog : WindowEx
_errorDetailsTextBox.MinHeight = 80;
_errorDetailsTextBox.Text = details;
_errorDetailsTextBox.Visibility = Visibility.Visible;
this.SetWindowSize(500, 320);
this.SetWindowSize(500, 320 + TitleBarHeight);
}
private static bool TryExtractMissingScope(string? message, out string scope)
@ -311,23 +357,40 @@ public sealed class QuickSendDialog : WindowEx
private void QueueFocusMessageInput()
{
if (_isClosed)
return;
DispatcherQueue?.TryEnqueue(FocusMessageInput);
}
private void RequestInputFocus()
{
QueueFocusMessageInput();
_ = RetryFocusMessageInputAsync();
if (!_focusRetryRunning)
{
_focusRetryRunning = true;
_ = RetryFocusMessageInputAsync();
}
}
private async Task RetryFocusMessageInputAsync()
{
var delaysMs = new[] { 60, 160, 320 };
foreach (var delay in delaysMs)
try
{
await Task.Delay(delay);
TryBringToFront();
QueueFocusMessageInput();
var delaysMs = new[] { 60, 160, 320 };
foreach (var delay in delaysMs)
{
await Task.Delay(delay);
if (_isClosed)
return;
TryBringToFront();
QueueFocusMessageInput();
}
}
finally
{
_focusRetryRunning = false;
}
}