diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs index 1c54f202..33d02c8c 100644 --- a/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs +++ b/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs @@ -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 @@ private static extern bool SetWindowPos( 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 QuickSendDialog(OpenClawGatewayClient client, string? prefillMessage = nu // 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 QuickSendDialog(OpenClawGatewayClient client, string? prefillMessage = nu 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 QuickSendDialog(OpenClawGatewayClient client, string? prefillMessage = nu 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 @@ private void TryBringToFront() { try { + if (_isClosed) + return; + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); if (hwnd == IntPtr.Zero) return; @@ -191,7 +237,7 @@ private async Task SendMessageAsync() _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 @@ private void ShowErrorDetails(string details) _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 @@ private void ShowDetails(string details) _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 @@ private static void CopyTextToClipboard(string text) 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 + { + var delaysMs = new[] { 60, 160, 320 }; + foreach (var delay in delaysMs) + { + await Task.Delay(delay); + if (_isClosed) + return; + + TryBringToFront(); + QueueFocusMessageInput(); + } + } + finally { - await Task.Delay(delay); - TryBringToFront(); - QueueFocusMessageInput(); + _focusRetryRunning = false; } }