Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Pointframe.Tests/OverlayWindowInteractionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SelectionSessionMode>(context.Window, "_selectionSessionMode");
Assert.Equal(SelectionSessionMode.WindowClean, sessionMode);
}
finally
{
context.Dispose();
}
});
}

[Fact]
public void InitializeFromSelectionSession_StoresPendingSession()
{
Expand Down
16 changes: 16 additions & 0 deletions Pointframe.Tests/Services/GlobalHotkeyServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions Pointframe.Tests/Services/TrayIconManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ private static TrayIconManager CreateManager(
Mock.Of<ITelemetryService>(),
onNewSnip: static () => { },
onWholeScreenSnip: static () => { },
onCleanWindowSnip: static () => { },
onOpenImage: static () => { },
onTrimRecording: onTrimRecording ?? (static _ => { }),
onShowSettings: static () => { },
Expand Down
16 changes: 16 additions & 0 deletions Pointframe.Tests/SettingsWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
26 changes: 26 additions & 0 deletions Pointframe.Tests/ViewModels/SettingsViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<IUserSettingsService>();
mock.SetupGet(s => s.Current).Returns(new UserSettings());
UserSettings? saved = null;
mock.Setup(s => s.Save(It.IsAny<UserSettings>())).Callback<UserSettings>(s => saved = s);
var vm = new SettingsViewModel(mock.Object, Mock.Of<IThemeService>(), Mock.Of<IDialogService>(), 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()
{
Expand Down
3 changes: 3 additions & 0 deletions Pointframe/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand All @@ -176,6 +178,7 @@ private static void ConfigureServices(IServiceCollection services)
services.AddSingleton<IAppErrorHandler, AppErrorHandler>();
services.AddSingleton<ICaptureLaunchService, CaptureLaunchService>();
services.AddTransient<IScreenCaptureService, ScreenCaptureService>();
services.AddTransient<IWindowCaptureService, WindowCaptureService>();
services.AddTransient<IVideoWriterFactory, VideoWriterFactory>();
services.AddTransient<IScreenRecordingService, ScreenRecordingService>();
services.AddSingleton<IGifExportService, GifExportService>();
Expand Down
3 changes: 2 additions & 1 deletion Pointframe/Models/SelectionSessionMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ internal enum SelectionSessionMode
{
Region,
FullScreen,
OpenedImage
OpenedImage,
WindowClean
}
4 changes: 4 additions & 0 deletions Pointframe/Models/UserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 49 additions & 1 deletion Pointframe/Services/Capture/CaptureLaunchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal sealed class CaptureLaunchService : ICaptureLaunchService
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<CaptureLaunchService> _logger;
private readonly ITelemetryService _telemetry;
private readonly IWindowCaptureService _windowCaptureService;
private readonly BeautifierRenderService _beautifierRenderService;

public CaptureLaunchService(
IServiceProvider services,
Expand All @@ -23,7 +25,9 @@ public CaptureLaunchService(
IFileSystemService fileSystem,
ILoggerFactory loggerFactory,
ILogger<CaptureLaunchService> logger,
ITelemetryService telemetry)
ITelemetryService telemetry,
IWindowCaptureService windowCaptureService,
BeautifierRenderService beautifierRenderService)
{
_services = services;
_userSettings = userSettings;
Expand All @@ -32,6 +36,8 @@ public CaptureLaunchService(
_loggerFactory = loggerFactory;
_logger = logger;
_telemetry = telemetry;
_windowCaptureService = windowCaptureService;
_beautifierRenderService = beautifierRenderService;
}

public void StartRegionSnip(string source = "tray")
Expand All @@ -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<string, string> { ["type"] = "window_clean", ["source"] = source });

var delay = _userSettings.Current.CaptureDelaySeconds;
if (delay > 0)
{
_telemetry.TrackEvent("capture_delay_used", new Dictionary<string, string> { ["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<OverlayWindow>();
overlay.InitializeFromImage(beautified, "window-capture://under-cursor", SelectionSessionMode.WindowClean);
DpiAwarenessScope.RunPerMonitorV2(() => overlay.Show());
}

private void LaunchCapture(bool wholeScreen)
{
var delay = _userSettings.Current.CaptureDelaySeconds;
Expand Down
2 changes: 2 additions & 0 deletions Pointframe/Services/Capture/ICaptureLaunchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ internal interface ICaptureLaunchService

void StartWholeScreenSnip(string source = "tray");

void StartCleanWindowSnip(string source = "tray");

void StartWholeScreenRecord();
}
6 changes: 6 additions & 0 deletions Pointframe/Services/Capture/IWindowCaptureService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Pointframe.Services;

internal interface IWindowCaptureService
{
bool TryCaptureWindowUnderCursor(out BitmapSource? bitmap);
}
Loading
Loading