From 1ca45129f45bfa64b6199b0689d4f729e972868c Mon Sep 17 00:00:00 2001 From: Dimitar Radenkov Date: Sat, 27 Jun 2026 18:47:16 +0300 Subject: [PATCH] Add clean window snip feature with hotkey and UI support --- .../OverlayWindowInteractionTests.cs | 22 +++ .../Services/GlobalHotkeyServiceTests.cs | 16 ++ .../Services/TrayIconManagerTests.cs | 1 + Pointframe.Tests/SettingsWindowTests.cs | 16 ++ .../ViewModels/SettingsViewModelTests.cs | 26 +++ Pointframe/App.xaml.cs | 3 + Pointframe/Models/SelectionSessionMode.cs | 3 +- Pointframe/Models/UserSettings.cs | 4 + .../Services/Capture/CaptureLaunchService.cs | 50 ++++- .../Services/Capture/ICaptureLaunchService.cs | 2 + .../Services/Capture/IWindowCaptureService.cs | 6 + .../Services/Capture/WindowCaptureService.cs | 158 +++++++++++++++ .../Infrastructure/GlobalHotkeyService.cs | 9 + .../Infrastructure/IGlobalHotkeyService.cs | 2 + .../Infrastructure/TrayIconManager.cs | 5 + .../Infrastructure/UserSettingsService.cs | 2 + Pointframe/ViewModels/SettingsViewModel.cs | 45 +++++ Pointframe/Views/OverlayWindow.Layout.cs | 7 +- Pointframe/Views/OverlayWindow.xaml.cs | 3 +- Pointframe/Views/SettingsWindow.xaml | 181 +++++++++++++++--- Pointframe/Views/SettingsWindow.xaml.cs | 111 +++++++++++ 21 files changed, 644 insertions(+), 28 deletions(-) create mode 100644 Pointframe/Services/Capture/IWindowCaptureService.cs create mode 100644 Pointframe/Services/Capture/WindowCaptureService.cs diff --git a/Pointframe.Tests/OverlayWindowInteractionTests.cs b/Pointframe.Tests/OverlayWindowInteractionTests.cs index 98e5fe99..0b8bb179 100644 --- a/Pointframe.Tests/OverlayWindowInteractionTests.cs +++ b/Pointframe.Tests/OverlayWindowInteractionTests.cs @@ -62,6 +62,28 @@ public void InitializeFromImage_StoresOpenedImageAndPath() }); } + [Fact] + public void InitializeFromImage_WithWindowCleanMode_PreservesSessionMode() + { + StaTestHelper.Run(() => + { + var context = CreateContext(); + try + { + var image = CreateBitmap(32, 24); + + context.Window.InitializeFromImage(image, @"C:\\images\\window-clean.png", SelectionSessionMode.WindowClean); + + var sessionMode = GetPrivateField(context.Window, "_selectionSessionMode"); + Assert.Equal(SelectionSessionMode.WindowClean, sessionMode); + } + finally + { + context.Dispose(); + } + }); + } + [Fact] public void InitializeFromSelectionSession_StoresPendingSession() { diff --git a/Pointframe.Tests/Services/GlobalHotkeyServiceTests.cs b/Pointframe.Tests/Services/GlobalHotkeyServiceTests.cs index ec864cc0..74486b58 100644 --- a/Pointframe.Tests/Services/GlobalHotkeyServiceTests.cs +++ b/Pointframe.Tests/Services/GlobalHotkeyServiceTests.cs @@ -94,4 +94,20 @@ public void IsModifierVk_ReturnsExpected(uint vk, bool expected) Assert.Equal(expected, result); } + + [Fact] + public void CleanWindowSnipRequested_Event_CanBeSubscribedAndInvoked() + { + var svc = CreateService(); + var raised = false; + svc.CleanWindowSnipRequested += () => raised = true; + + var eventField = typeof(GlobalHotkeyService).GetField("CleanWindowSnipRequested", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(eventField); + + var handler = (Action?)eventField!.GetValue(svc); + handler?.Invoke(); + + Assert.True(raised); + } } diff --git a/Pointframe.Tests/Services/TrayIconManagerTests.cs b/Pointframe.Tests/Services/TrayIconManagerTests.cs index 197c5290..6839247d 100644 --- a/Pointframe.Tests/Services/TrayIconManagerTests.cs +++ b/Pointframe.Tests/Services/TrayIconManagerTests.cs @@ -570,6 +570,7 @@ private static TrayIconManager CreateManager( Mock.Of(), onNewSnip: static () => { }, onWholeScreenSnip: static () => { }, + onCleanWindowSnip: static () => { }, onOpenImage: static () => { }, onTrimRecording: onTrimRecording ?? (static _ => { }), onShowSettings: static () => { }, diff --git a/Pointframe.Tests/SettingsWindowTests.cs b/Pointframe.Tests/SettingsWindowTests.cs index 31c600dc..42fa34fb 100644 --- a/Pointframe.Tests/SettingsWindowTests.cs +++ b/Pointframe.Tests/SettingsWindowTests.cs @@ -162,6 +162,22 @@ public void OnRecordHotkeyKeyPressed_Escape_CancelsCapture() }); } + [Fact] + public void OnCleanWindowHotkeyKeyPressed_StoresNewHotkey() + { + StaTestHelper.Run(() => + { + var window = CreateWindow(out var viewModel); + viewModel.IsCapturingCleanWindowCaptureHotkey = true; + + InvokeCallback(window, "OnCleanWindowHotkeyKeyPressed", (uint)KeyInterop.VirtualKeyFromKey(Key.W), HotkeyModifiers.Ctrl | HotkeyModifiers.Shift); + + Assert.Equal((uint)KeyInterop.VirtualKeyFromKey(Key.W), viewModel.CleanWindowCaptureHotkey); + Assert.Equal(HotkeyModifiers.Ctrl | HotkeyModifiers.Shift, viewModel.CleanWindowCaptureHotkeyModifiers); + Assert.False(viewModel.IsCapturingCleanWindowCaptureHotkey); + }); + } + [Fact] public void WholeScreenRecordHotkeyRecordingPanel_IsVisibleChanged_WhenVisible_FocusesPanel() { diff --git a/Pointframe.Tests/ViewModels/SettingsViewModelTests.cs b/Pointframe.Tests/ViewModels/SettingsViewModelTests.cs index 9aa0988c..375db013 100644 --- a/Pointframe.Tests/ViewModels/SettingsViewModelTests.cs +++ b/Pointframe.Tests/ViewModels/SettingsViewModelTests.cs @@ -631,6 +631,15 @@ public void UserSettings_Default_WholeScreenRecordHotkey_IsCtrlShiftR() Assert.Equal(HotkeyModifiers.Ctrl | HotkeyModifiers.Shift, settings.WholeScreenRecordHotkeyModifiers); } + [Fact] + public void UserSettings_Default_CleanWindowCaptureHotkey_IsCtrlShiftW() + { + var settings = new UserSettings(); + + Assert.Equal(0x57u, settings.CleanWindowCaptureHotkey); + Assert.Equal(HotkeyModifiers.Ctrl | HotkeyModifiers.Shift, settings.CleanWindowCaptureHotkeyModifiers); + } + [Fact] public void LoadsFromSettings_WholeScreenRecordHotkey() { @@ -661,6 +670,23 @@ public void Save_PersistsWholeScreenRecordHotkey() Assert.Equal(HotkeyModifiers.Ctrl | HotkeyModifiers.Shift, saved?.WholeScreenRecordHotkeyModifiers); } + [Fact] + public void Save_PersistsCleanWindowCaptureHotkey() + { + var mock = new Mock(); + mock.SetupGet(s => s.Current).Returns(new UserSettings()); + UserSettings? saved = null; + mock.Setup(s => s.Save(It.IsAny())).Callback(s => saved = s); + var vm = new SettingsViewModel(mock.Object, Mock.Of(), Mock.Of(), CreateMicrophoneDeviceService()); + vm.CleanWindowCaptureHotkey = 0x4Eu; // 'N' + vm.CleanWindowCaptureHotkeyModifiers = HotkeyModifiers.Alt; + + vm.SaveCommand.Execute(null); + + Assert.Equal(0x4Eu, saved?.CleanWindowCaptureHotkey); + Assert.Equal(HotkeyModifiers.Alt, saved?.CleanWindowCaptureHotkeyModifiers); + } + [Fact] public void SetWholeScreenRecordHotkey_UpdatesDisplayName() { diff --git a/Pointframe/App.xaml.cs b/Pointframe/App.xaml.cs index 3bb96152..d8794356 100644 --- a/Pointframe/App.xaml.cs +++ b/Pointframe/App.xaml.cs @@ -135,6 +135,7 @@ protected override void OnStartup(StartupEventArgs e) _telemetry, onNewSnip: () => _captureLaunch.StartRegionSnip("tray"), onWholeScreenSnip: () => _captureLaunch.StartWholeScreenSnip("tray"), + onCleanWindowSnip: () => _captureLaunch.StartCleanWindowSnip("tray"), onOpenImage: () => Dispatcher.InvokeAsync(OpenImage, System.Windows.Threading.DispatcherPriority.ApplicationIdle), onTrimRecording: ShowTrimWindow, onShowSettings: ShowSettingsWindow, @@ -151,6 +152,7 @@ protected override void OnStartup(StartupEventArgs e) _globalHotkey.RegionSnipRequested += () => _captureLaunch.StartRegionSnip("hotkey"); _globalHotkey.WholeScreenSnipRequested += () => _captureLaunch.StartWholeScreenSnip("hotkey"); _globalHotkey.WholeScreenRecordRequested += _captureLaunch.StartWholeScreenRecord; + _globalHotkey.CleanWindowSnipRequested += () => _captureLaunch.StartCleanWindowSnip("hotkey"); _globalHotkey.Register(); _logger.LogInformation("Global hotkey registered"); _host.StartAsync().GetAwaiter().GetResult(); @@ -176,6 +178,7 @@ private static void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddSingleton(); diff --git a/Pointframe/Models/SelectionSessionMode.cs b/Pointframe/Models/SelectionSessionMode.cs index 7f665e0c..2892cac6 100644 --- a/Pointframe/Models/SelectionSessionMode.cs +++ b/Pointframe/Models/SelectionSessionMode.cs @@ -4,5 +4,6 @@ internal enum SelectionSessionMode { Region, FullScreen, - OpenedImage + OpenedImage, + WindowClean } diff --git a/Pointframe/Models/UserSettings.cs b/Pointframe/Models/UserSettings.cs index be2be9e3..7ab3ec70 100644 --- a/Pointframe/Models/UserSettings.cs +++ b/Pointframe/Models/UserSettings.cs @@ -35,6 +35,10 @@ public sealed class UserSettings public HotkeyModifiers WholeScreenRecordHotkeyModifiers { get; set; } = HotkeyModifiers.Ctrl | HotkeyModifiers.Shift; // Ctrl+Shift+R + public uint CleanWindowCaptureHotkey { get; set; } = 0x57; // VK_W + + public HotkeyModifiers CleanWindowCaptureHotkeyModifiers { get; set; } = HotkeyModifiers.Ctrl | HotkeyModifiers.Shift; // Ctrl+Shift+W + public uint OverlayCopyHotkey { get; set; } = 0x43; // VK_C public HotkeyModifiers OverlayCopyHotkeyModifiers { get; set; } = HotkeyModifiers.Ctrl; diff --git a/Pointframe/Services/Capture/CaptureLaunchService.cs b/Pointframe/Services/Capture/CaptureLaunchService.cs index 312aba94..aba2aa6e 100644 --- a/Pointframe/Services/Capture/CaptureLaunchService.cs +++ b/Pointframe/Services/Capture/CaptureLaunchService.cs @@ -15,6 +15,8 @@ internal sealed class CaptureLaunchService : ICaptureLaunchService private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly ITelemetryService _telemetry; + private readonly IWindowCaptureService _windowCaptureService; + private readonly BeautifierRenderService _beautifierRenderService; public CaptureLaunchService( IServiceProvider services, @@ -23,7 +25,9 @@ public CaptureLaunchService( IFileSystemService fileSystem, ILoggerFactory loggerFactory, ILogger logger, - ITelemetryService telemetry) + ITelemetryService telemetry, + IWindowCaptureService windowCaptureService, + BeautifierRenderService beautifierRenderService) { _services = services; _userSettings = userSettings; @@ -32,6 +36,8 @@ public CaptureLaunchService( _loggerFactory = loggerFactory; _logger = logger; _telemetry = telemetry; + _windowCaptureService = windowCaptureService; + _beautifierRenderService = beautifierRenderService; } public void StartRegionSnip(string source = "tray") @@ -48,6 +54,48 @@ public void StartWholeScreenSnip(string source = "tray") LaunchCapture(wholeScreen: true); } + public void StartCleanWindowSnip(string source = "tray") + { + _logger.LogDebug("Clean window snip started"); + _telemetry.TrackEvent("snip_started", new Dictionary { ["type"] = "window_clean", ["source"] = source }); + + var delay = _userSettings.Current.CaptureDelaySeconds; + if (delay > 0) + { + _telemetry.TrackEvent("capture_delay_used", new Dictionary { ["delay_seconds"] = delay.ToString() }); + new CountdownWindow(delay, () => ExecuteCleanWindowSnip()).Show(); + return; + } + + ExecuteCleanWindowSnip(); + } + + private void ExecuteCleanWindowSnip() + { + + if (!_windowCaptureService.TryCaptureWindowUnderCursor(out var rawWindowBitmap) || rawWindowBitmap is null) + { + _messageBox.ShowWarning( + "Could not capture the window under the cursor. Move the cursor over a visible app window and try again.", + "Clean window snip"); + return; + } + + var beautified = _beautifierRenderService.Render( + rawWindowBitmap, + BeautifyBackground.White, + padding: 36, + cornerRadius: 12, + shadowEnabled: true, + shadowBlur: 28, + shadowOffsetY: 16, + shadowOpacity: 0.5); + + var overlay = _services.GetRequiredService(); + overlay.InitializeFromImage(beautified, "window-capture://under-cursor", SelectionSessionMode.WindowClean); + DpiAwarenessScope.RunPerMonitorV2(() => overlay.Show()); + } + private void LaunchCapture(bool wholeScreen) { var delay = _userSettings.Current.CaptureDelaySeconds; diff --git a/Pointframe/Services/Capture/ICaptureLaunchService.cs b/Pointframe/Services/Capture/ICaptureLaunchService.cs index 7ddbd347..9964919d 100644 --- a/Pointframe/Services/Capture/ICaptureLaunchService.cs +++ b/Pointframe/Services/Capture/ICaptureLaunchService.cs @@ -6,5 +6,7 @@ internal interface ICaptureLaunchService void StartWholeScreenSnip(string source = "tray"); + void StartCleanWindowSnip(string source = "tray"); + void StartWholeScreenRecord(); } diff --git a/Pointframe/Services/Capture/IWindowCaptureService.cs b/Pointframe/Services/Capture/IWindowCaptureService.cs new file mode 100644 index 00000000..d4047784 --- /dev/null +++ b/Pointframe/Services/Capture/IWindowCaptureService.cs @@ -0,0 +1,6 @@ +namespace Pointframe.Services; + +internal interface IWindowCaptureService +{ + bool TryCaptureWindowUnderCursor(out BitmapSource? bitmap); +} diff --git a/Pointframe/Services/Capture/WindowCaptureService.cs b/Pointframe/Services/Capture/WindowCaptureService.cs new file mode 100644 index 00000000..1079ff89 --- /dev/null +++ b/Pointframe/Services/Capture/WindowCaptureService.cs @@ -0,0 +1,158 @@ +using System.Windows; +using System.Windows.Interop; + +namespace Pointframe.Services; + +internal sealed class WindowCaptureService : IWindowCaptureService +{ + private const uint PwRenderFullContent = 0x00000002; + private readonly IScreenCaptureService _screenCaptureService; + private readonly ILogger _logger; + + public WindowCaptureService( + IScreenCaptureService screenCaptureService, + ILogger logger) + { + _screenCaptureService = screenCaptureService; + _logger = logger; + } + + public bool TryCaptureWindowUnderCursor(out BitmapSource? bitmap) + { + bitmap = null; + + if (!GetCursorPos(out var cursor)) + { + _logger.LogWarning("GetCursorPos failed while trying to capture window under cursor."); + return false; + } + + var hitWindow = WindowFromPoint(cursor); + if (hitWindow == IntPtr.Zero) + { + _logger.LogWarning("WindowFromPoint returned no window at {X},{Y}.", cursor.X, cursor.Y); + return false; + } + + var rootWindow = GetAncestor(hitWindow, GetRootAncestorFlag); + if (rootWindow == IntPtr.Zero) + { + rootWindow = hitWindow; + } + + if (!IsWindowVisible(rootWindow)) + { + _logger.LogInformation("Window under cursor is not visible."); + return false; + } + + if (!GetWindowRect(rootWindow, out var rect)) + { + _logger.LogWarning("GetWindowRect failed for window under cursor."); + return false; + } + + var width = rect.Right - rect.Left; + var height = rect.Bottom - rect.Top; + if (width <= 1 || height <= 1) + { + _logger.LogInformation("Window under cursor has invalid bounds {Width}x{Height}.", width, height); + return false; + } + + using var windowBitmap = new System.Drawing.Bitmap( + width, + height, + System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + using var graphics = System.Drawing.Graphics.FromImage(windowBitmap); + var hdc = graphics.GetHdc(); + var printed = false; + try + { + printed = PrintWindow(rootWindow, hdc, PwRenderFullContent); + } + finally + { + graphics.ReleaseHdc(hdc); + } + + if (printed) + { + var hBitmap = windowBitmap.GetHbitmap(); + try + { + bitmap = Imaging.CreateBitmapSourceFromHBitmap( + hBitmap, + IntPtr.Zero, + Int32Rect.Empty, + BitmapSizeOptions.FromEmptyOptions()); + bitmap.Freeze(); + return true; + } + finally + { + DeleteObject(hBitmap); + } + } + + _logger.LogDebug( + "PrintWindow failed for hwnd {Handle}. Falling back to screen capture at {X},{Y} {W}x{H}.", + rootWindow, + rect.Left, + rect.Top, + width, + height); + + try + { + bitmap = _screenCaptureService.Capture(rect.Left, rect.Top, width, height); + bitmap.Freeze(); + return true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Fallback screen capture failed for window under cursor."); + return false; + } + } + + private const uint GetRootAncestorFlag = 2; + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int X; + public int Y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll")] + private static extern bool GetCursorPos(out POINT lpPoint); + + [DllImport("user32.dll")] + private static extern IntPtr WindowFromPoint(POINT point); + + [DllImport("user32.dll")] + private static extern IntPtr GetAncestor(IntPtr hwnd, uint gaFlags); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags); + + [DllImport("gdi32.dll")] + private static extern bool DeleteObject(IntPtr hObject); +} diff --git a/Pointframe/Services/Infrastructure/GlobalHotkeyService.cs b/Pointframe/Services/Infrastructure/GlobalHotkeyService.cs index 9d1aab32..3bb387e3 100644 --- a/Pointframe/Services/Infrastructure/GlobalHotkeyService.cs +++ b/Pointframe/Services/Infrastructure/GlobalHotkeyService.cs @@ -17,6 +17,7 @@ internal sealed class GlobalHotkeyService : IGlobalHotkeyService public event Action? RegionSnipRequested; public event Action? WholeScreenSnipRequested; public event Action? WholeScreenRecordRequested; + public event Action? CleanWindowSnipRequested; public GlobalHotkeyService(IUserSettingsService userSettings, ILogger logger) { @@ -95,6 +96,14 @@ private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) return (IntPtr)1; } + var cleanWindowHotkey = _userSettings.Current.CleanWindowCaptureHotkey; + var cleanWindowModifiers = _userSettings.Current.CleanWindowCaptureHotkeyModifiers; + if (cleanWindowHotkey != 0 && kb.vkCode == cleanWindowHotkey && ModifiersMatch(cleanWindowModifiers, ctrlHeld, shiftHeld, altHeld)) + { + WpfApplication.Current.Dispatcher.InvokeAsync(() => CleanWindowSnipRequested?.Invoke()); + return (IntPtr)1; + } + var regionHotkey = _userSettings.Current.RegionCaptureHotkey; var regionModifiers = _userSettings.Current.RegionCaptureHotkeyModifiers; if (regionHotkey != 0 && kb.vkCode == regionHotkey) diff --git a/Pointframe/Services/Infrastructure/IGlobalHotkeyService.cs b/Pointframe/Services/Infrastructure/IGlobalHotkeyService.cs index c9a95126..09f3c588 100644 --- a/Pointframe/Services/Infrastructure/IGlobalHotkeyService.cs +++ b/Pointframe/Services/Infrastructure/IGlobalHotkeyService.cs @@ -8,6 +8,8 @@ public interface IGlobalHotkeyService : IDisposable event Action WholeScreenRecordRequested; + event Action CleanWindowSnipRequested; + void Register(); /// diff --git a/Pointframe/Services/Infrastructure/TrayIconManager.cs b/Pointframe/Services/Infrastructure/TrayIconManager.cs index 4bd648ad..0d679a93 100644 --- a/Pointframe/Services/Infrastructure/TrayIconManager.cs +++ b/Pointframe/Services/Infrastructure/TrayIconManager.cs @@ -21,6 +21,7 @@ internal sealed class TrayIconManager : ITrayIconManager private readonly ITelemetryService _telemetry; private readonly Action _onNewSnip; private readonly Action _onWholeScreenSnip; + private readonly Action _onCleanWindowSnip; private readonly Action _onOpenImage; private readonly Action _onTrimRecording; private readonly Action _onShowSettings; @@ -48,6 +49,7 @@ public TrayIconManager( ITelemetryService telemetry, Action onNewSnip, Action onWholeScreenSnip, + Action onCleanWindowSnip, Action onOpenImage, Action onTrimRecording, Action onShowSettings, @@ -64,6 +66,7 @@ public TrayIconManager( _telemetry = telemetry; _onNewSnip = onNewSnip; _onWholeScreenSnip = onWholeScreenSnip; + _onCleanWindowSnip = onCleanWindowSnip; _onOpenImage = onOpenImage; _onTrimRecording = onTrimRecording; _onShowSettings = onShowSettings; @@ -166,6 +169,7 @@ private WpfContextMenu CreateTrayContextMenu() var contextMenu = new WpfContextMenu(); contextMenu.Items.Add(CreateTrayMenuItem("New Snip", NewSnip_Click)); contextMenu.Items.Add(CreateTrayMenuItem("Whole screen snip", WholeScreenSnip_Click)); + contextMenu.Items.Add(CreateTrayMenuItem("Clean window snip", CleanWindowSnip_Click)); contextMenu.Items.Add(CreateTrayMenuItem("Open image...", OpenImage_Click)); contextMenu.Items.Add(CreateOpenFoldersMenuItem()); contextMenu.Items.Add(new WpfSeparator()); @@ -203,6 +207,7 @@ internal static WpfMenuItem CreateTrayMenuItem(string header, RoutedEventHandler private void TrayIcon_LeftClick(object sender, RoutedEventArgs e) => _onNewSnip(); private void NewSnip_Click(object sender, RoutedEventArgs e) => _onNewSnip(); private void WholeScreenSnip_Click(object sender, RoutedEventArgs e) => _onWholeScreenSnip(); + private void CleanWindowSnip_Click(object sender, RoutedEventArgs e) => _onCleanWindowSnip(); private void Settings_Click(object sender, RoutedEventArgs e) => _onShowSettings(); private void About_Click(object sender, RoutedEventArgs e) => _onShowAbout(); private void OpenImage_Click(object sender, RoutedEventArgs e) => _onOpenImage(); diff --git a/Pointframe/Services/Infrastructure/UserSettingsService.cs b/Pointframe/Services/Infrastructure/UserSettingsService.cs index 1574498c..081032cb 100644 --- a/Pointframe/Services/Infrastructure/UserSettingsService.cs +++ b/Pointframe/Services/Infrastructure/UserSettingsService.cs @@ -133,6 +133,8 @@ private static UserSettings Clone(UserSettings settings) => RegionCaptureHotkeyModifiers = settings.RegionCaptureHotkeyModifiers, WholeScreenRecordHotkey = settings.WholeScreenRecordHotkey, WholeScreenRecordHotkeyModifiers = settings.WholeScreenRecordHotkeyModifiers, + CleanWindowCaptureHotkey = settings.CleanWindowCaptureHotkey, + CleanWindowCaptureHotkeyModifiers = settings.CleanWindowCaptureHotkeyModifiers, OverlayCopyHotkey = settings.OverlayCopyHotkey, OverlayCopyHotkeyModifiers = settings.OverlayCopyHotkeyModifiers, OverlaySaveAsHotkey = settings.OverlaySaveAsHotkey, diff --git a/Pointframe/ViewModels/SettingsViewModel.cs b/Pointframe/ViewModels/SettingsViewModel.cs index e7f5c396..c8ee3386 100644 --- a/Pointframe/ViewModels/SettingsViewModel.cs +++ b/Pointframe/ViewModels/SettingsViewModel.cs @@ -53,6 +53,8 @@ public SettingsViewModel(IUserSettingsService settingsService, IThemeService the _regionCaptureHotkeyModifiers = s.RegionCaptureHotkeyModifiers; _wholeScreenRecordHotkey = s.WholeScreenRecordHotkey; _wholeScreenRecordHotkeyModifiers = s.WholeScreenRecordHotkeyModifiers; + _cleanWindowCaptureHotkey = s.CleanWindowCaptureHotkey; + _cleanWindowCaptureHotkeyModifiers = s.CleanWindowCaptureHotkeyModifiers; _overlayCopyHotkey = s.OverlayCopyHotkey; _overlayCopyHotkeyModifiers = s.OverlayCopyHotkeyModifiers; _overlaySaveAsHotkey = s.OverlaySaveAsHotkey; @@ -171,6 +173,14 @@ public SettingsViewModel(IUserSettingsService settingsService, IThemeService the [NotifyPropertyChangedFor(nameof(WholeScreenRecordHotkeyDisplayName))] private HotkeyModifiers _wholeScreenRecordHotkeyModifiers; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CleanWindowCaptureHotkeyDisplayName))] + private uint _cleanWindowCaptureHotkey; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CleanWindowCaptureHotkeyDisplayName))] + private HotkeyModifiers _cleanWindowCaptureHotkeyModifiers; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(OverlayCopyHotkeyDisplayName))] private uint _overlayCopyHotkey; @@ -235,6 +245,9 @@ public SettingsViewModel(IUserSettingsService settingsService, IThemeService the [ObservableProperty] private bool _isCapturingWholeScreenRecordHotkey; + [ObservableProperty] + private bool _isCapturingCleanWindowCaptureHotkey; + [ObservableProperty] private UpdateCheckInterval _autoUpdateCheckInterval; @@ -257,6 +270,7 @@ public SettingsViewModel(IUserSettingsService settingsService, IThemeService the public string RegionCaptureHotkeyDisplayName => BuildHotkeyDisplayName(RegionCaptureHotkey, RegionCaptureHotkeyModifiers); public string WholeScreenRecordHotkeyDisplayName => BuildHotkeyDisplayName(WholeScreenRecordHotkey, WholeScreenRecordHotkeyModifiers); + public string CleanWindowCaptureHotkeyDisplayName => BuildHotkeyDisplayName(CleanWindowCaptureHotkey, CleanWindowCaptureHotkeyModifiers); public string OverlayCopyHotkeyDisplayName => BuildHotkeyDisplayName(OverlayCopyHotkey, OverlayCopyHotkeyModifiers); public string OverlaySaveAsHotkeyDisplayName => BuildHotkeyDisplayName(OverlaySaveAsHotkey, OverlaySaveAsHotkeyModifiers); public string OverlayUndoHotkeyDisplayName => BuildHotkeyDisplayName(OverlayUndoHotkey, OverlayUndoHotkeyModifiers); @@ -400,6 +414,8 @@ private void Save() RegionCaptureHotkeyModifiers = RegionCaptureHotkeyModifiers, WholeScreenRecordHotkey = WholeScreenRecordHotkey, WholeScreenRecordHotkeyModifiers = WholeScreenRecordHotkeyModifiers, + CleanWindowCaptureHotkey = CleanWindowCaptureHotkey, + CleanWindowCaptureHotkeyModifiers = CleanWindowCaptureHotkeyModifiers, OverlayCopyHotkey = OverlayCopyHotkey, OverlayCopyHotkeyModifiers = OverlayCopyHotkeyModifiers, OverlaySaveAsHotkey = OverlaySaveAsHotkey, @@ -427,6 +443,7 @@ private void Save() private void StartRecordingHotkey() { IsCapturingWholeScreenRecordHotkey = false; + IsCapturingCleanWindowCaptureHotkey = false; IsCapturingOverlayShortcut = false; OverlayShortcutCaptureTarget = string.Empty; OverlayShortcutCaptureDisplayName = string.Empty; @@ -446,6 +463,7 @@ private void ResetHotkey() private void StartCapturingWholeScreenRecordHotkey() { IsRecordingHotkey = false; + IsCapturingCleanWindowCaptureHotkey = false; IsCapturingOverlayShortcut = false; OverlayShortcutCaptureTarget = string.Empty; OverlayShortcutCaptureDisplayName = string.Empty; @@ -453,6 +471,18 @@ private void StartCapturingWholeScreenRecordHotkey() IsCapturingWholeScreenRecordHotkey = true; } + [RelayCommand] + private void StartCapturingCleanWindowCaptureHotkey() + { + IsRecordingHotkey = false; + IsCapturingOverlayShortcut = false; + OverlayShortcutCaptureTarget = string.Empty; + OverlayShortcutCaptureDisplayName = string.Empty; + OverlayShortcutConflictMessage = string.Empty; + IsCapturingWholeScreenRecordHotkey = false; + IsCapturingCleanWindowCaptureHotkey = true; + } + [RelayCommand] private void ResetRecordHotkey() { @@ -461,6 +491,14 @@ private void ResetRecordHotkey() IsCapturingWholeScreenRecordHotkey = false; } + [RelayCommand] + private void ResetCleanWindowCaptureHotkey() + { + CleanWindowCaptureHotkey = 0x57; // VK_W + CleanWindowCaptureHotkeyModifiers = HotkeyModifiers.Ctrl | HotkeyModifiers.Shift; + IsCapturingCleanWindowCaptureHotkey = false; + } + [RelayCommand] private void StartCapturingOverlayShortcut(string shortcutKey) { @@ -471,6 +509,7 @@ private void StartCapturingOverlayShortcut(string shortcutKey) IsRecordingHotkey = false; IsCapturingWholeScreenRecordHotkey = false; + IsCapturingCleanWindowCaptureHotkey = false; OverlayShortcutConflictMessage = string.Empty; OverlayShortcutCaptureTarget = shortcutKey; OverlayShortcutCaptureDisplayName = OverlayShortcutLabel(shortcutKey); @@ -635,6 +674,9 @@ private void ResetCurrentSection() WholeScreenRecordHotkey = defaults.WholeScreenRecordHotkey; WholeScreenRecordHotkeyModifiers = defaults.WholeScreenRecordHotkeyModifiers; IsCapturingWholeScreenRecordHotkey = false; + CleanWindowCaptureHotkey = defaults.CleanWindowCaptureHotkey; + CleanWindowCaptureHotkeyModifiers = defaults.CleanWindowCaptureHotkeyModifiers; + IsCapturingCleanWindowCaptureHotkey = false; break; case SettingsSection.Annotation: DefaultAnnotationColor = ParseAnnotationColorOrFallback(defaults.DefaultAnnotationColor); @@ -698,6 +740,9 @@ private void RestoreDefaults() WholeScreenRecordHotkey = defaults.WholeScreenRecordHotkey; WholeScreenRecordHotkeyModifiers = defaults.WholeScreenRecordHotkeyModifiers; IsCapturingWholeScreenRecordHotkey = false; + CleanWindowCaptureHotkey = defaults.CleanWindowCaptureHotkey; + CleanWindowCaptureHotkeyModifiers = defaults.CleanWindowCaptureHotkeyModifiers; + IsCapturingCleanWindowCaptureHotkey = false; OverlayCopyHotkey = defaults.OverlayCopyHotkey; OverlayCopyHotkeyModifiers = defaults.OverlayCopyHotkeyModifiers; OverlaySaveAsHotkey = defaults.OverlaySaveAsHotkey; diff --git a/Pointframe/Views/OverlayWindow.Layout.cs b/Pointframe/Views/OverlayWindow.Layout.cs index 340110ba..5dadf371 100644 --- a/Pointframe/Views/OverlayWindow.Layout.cs +++ b/Pointframe/Views/OverlayWindow.Layout.cs @@ -11,7 +11,10 @@ public partial class OverlayWindow { private void InitializeFromOpenedImage(BitmapSource openedImage) { - _selectionSessionMode = SelectionSessionMode.OpenedImage; + if (_selectionSessionMode != SelectionSessionMode.WindowClean) + { + _selectionSessionMode = SelectionSessionMode.OpenedImage; + } _openedImageDisplayRect = CalculateOpenedImageDisplayRect(openedImage); _openedImageScaleX = openedImage.PixelWidth / _openedImageDisplayRect.Width; _openedImageScaleY = openedImage.PixelHeight / _openedImageDisplayRect.Height; @@ -30,7 +33,7 @@ private void InitializeFromOpenedImage(BitmapSource openedImage) _openedImageScaleX, _openedImageScaleY, allowRecording: false, - SelectionSessionMode.OpenedImage); + _selectionSessionMode); } private Rect CalculateOpenedImageDisplayRect(BitmapSource openedImage) diff --git a/Pointframe/Views/OverlayWindow.xaml.cs b/Pointframe/Views/OverlayWindow.xaml.cs index 47c583c9..08a48a01 100644 --- a/Pointframe/Views/OverlayWindow.xaml.cs +++ b/Pointframe/Views/OverlayWindow.xaml.cs @@ -123,13 +123,14 @@ internal OverlayWindow( }; } - public void InitializeFromImage(BitmapSource bitmap, string sourcePath) + internal void InitializeFromImage(BitmapSource bitmap, string sourcePath, SelectionSessionMode sessionMode = SelectionSessionMode.OpenedImage) { ArgumentNullException.ThrowIfNull(bitmap); ArgumentException.ThrowIfNullOrWhiteSpace(sourcePath); _openedImage = bitmap; _openedImagePath = sourcePath; + _selectionSessionMode = sessionMode; } internal void InitializeFromSelectionSession(SelectionSessionResult selectionSession) diff --git a/Pointframe/Views/SettingsWindow.xaml b/Pointframe/Views/SettingsWindow.xaml index 8c611196..60274846 100644 --- a/Pointframe/Views/SettingsWindow.xaml +++ b/Pointframe/Views/SettingsWindow.xaml @@ -257,6 +257,8 @@ + + @@ -413,6 +415,7 @@ + @@ -436,6 +439,8 @@ + + @@ -543,6 +548,93 @@ + + + + + + + + + + + + + + + +