Files
AX-Copilot-Codex/src/AxCopilot/Views/ReminderPopupWindow.xaml.cs
lacvet 2ae56b2510
Some checks failed
Release Gate / gate (push) Has been cancelled
격려문구 알림 팝업 자동 종료 안정성 개선
- ReminderPopupWindow의 자동 닫힘 경로를 점검하고 UI 타이머와 실제 종료 타이머를 분리함
- 남은 시간 계산을 절대 시각 기준으로 바꾸고 Task.Delay 기반 종료 fail-safe를 추가해 팝업이 남는 경우를 줄임
- 창 종료 시 CancellationToken과 타이머를 함께 정리해 후속 종료 처리도 안전하게 맞춤
- README와 DEVELOPMENT 문서에 변경 목적과 검증 결과를 로컬 시각 기준으로 기록함
- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0, 오류 0)
2026-04-06 15:41:27 +09:00

173 lines
6.3 KiB
C#

using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using AxCopilot.Services;
namespace AxCopilot.Views;
/// <summary>
/// 잠금 해제 시 화면 모서리에 표시되는 격려 팝업.
/// 테마는 Application.Current.Resources의 DynamicResource로 자동 적용됩니다.
/// 포커스를 빼앗지 않으며(WS_EX_NOACTIVATE), 지정된 초 후 자동으로 닫힙니다.
/// </summary>
public partial class ReminderPopupWindow : Window
{
// ─── 포커스 방지 P/Invoke (64비트 안전) ────────────────────────────────────
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
private static extern IntPtr GetWindowLongPtr(IntPtr hwnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
private static extern IntPtr SetWindowLongPtr(IntPtr hwnd, int nIndex, IntPtr dwNewLong);
private const int GWL_EXSTYLE = -20;
private const int WS_EX_NOACTIVATE = 0x08000000;
private const int WS_EX_TOOLWINDOW = 0x00000080;
// ─── 타이머 ───────────────────────────────────────────────────────────────
private readonly DispatcherTimer _timer;
private readonly EventHandler _tickHandler;
private readonly CancellationTokenSource _autoCloseCts = new();
private readonly DateTime _closeAtUtc;
private readonly int _displaySeconds;
public ReminderPopupWindow(
string quoteText,
string? author,
TimeSpan todayUsage,
SettingsService settings)
{
InitializeComponent();
var cfg = settings.Settings.Reminder;
// ── 문구 / 출처 ──
QuoteText.Text = quoteText;
if (!string.IsNullOrWhiteSpace(author))
{
AuthorText.Text = $"— {author}";
AuthorText.Visibility = Visibility.Visible;
}
// ── 근무 시간 ──
var h = (int)todayUsage.TotalHours;
var m = todayUsage.Minutes;
UsageText.Text = h > 0
? $"오늘 총 {h}시간 {m}분 근무 중"
: m >= 1
? $"오늘 총 {m}분 근무 중"
: "오늘 방금 시작했습니다";
// ── 카운트다운 ──
_displaySeconds = Math.Max(3, cfg.DisplaySeconds);
_closeAtUtc = DateTime.UtcNow.AddSeconds(_displaySeconds);
CountdownBar.Maximum = _displaySeconds;
CountdownBar.Value = _displaySeconds;
// ── 위치: 레이아웃 완료 후 설정 ──
Loaded += (_, _) =>
{
SetNoActivate();
PositionWindow(cfg.Corner);
AnimateIn();
};
// ── 타이머 ──
_tickHandler = (_, _) =>
{
var remainingSeconds = Math.Max(0, (_closeAtUtc - DateTime.UtcNow).TotalSeconds);
CountdownBar.Value = remainingSeconds;
if (remainingSeconds <= 0)
Close();
};
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += _tickHandler;
_timer.Start();
_ = StartAutoCloseAsync(_autoCloseCts.Token);
// ── Esc 키 닫기 ──
KeyDown += (_, e) =>
{
if (e.Key == System.Windows.Input.Key.Escape) Close();
};
}
// ─── 포커스 방지 ─────────────────────────────────────────────────────────
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
SetNoActivate();
}
private void SetNoActivate()
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
var style = (long)GetWindowLongPtr(hwnd, GWL_EXSTYLE);
SetWindowLongPtr(hwnd, GWL_EXSTYLE, (IntPtr)(style | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW));
}
// ─── 위치 계산 ────────────────────────────────────────────────────────────
private void PositionWindow(string corner)
{
var area = SystemParameters.WorkArea;
const double margin = 20;
Left = corner.Contains("left")
? area.Left + margin
: area.Right - ActualWidth - margin;
Top = corner.Contains("top")
? area.Top + margin
: area.Bottom - ActualHeight - margin;
}
// ─── 등장 애니메이션 ──────────────────────────────────────────────────────
private void AnimateIn()
{
Opacity = 0;
var anim = new System.Windows.Media.Animation.DoubleAnimation(0, 1,
TimeSpan.FromMilliseconds(280));
BeginAnimation(OpacityProperty, anim);
}
// ─── 이벤트 ───────────────────────────────────────────────────────────────
private void CloseBtn_Click(object sender, RoutedEventArgs e) => Close();
private async Task StartAutoCloseAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(_displaySeconds), cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
await Dispatcher.InvokeAsync(() =>
{
if (IsVisible)
Close();
}, DispatcherPriority.Background, cancellationToken);
}
catch (TaskCanceledException)
{
}
catch (OperationCanceledException)
{
}
}
protected override void OnClosed(EventArgs e)
{
_autoCloseCts.Cancel();
_timer.Stop();
_timer.Tick -= _tickHandler;
_autoCloseCts.Dispose();
base.OnClosed(e);
}
}