diff --git a/Pointframe.AutomationTests/Smoke/LaunchModeSmokeTests.cs b/Pointframe.AutomationTests/Smoke/LaunchModeSmokeTests.cs index 37bbc6e..8b01f44 100644 --- a/Pointframe.AutomationTests/Smoke/LaunchModeSmokeTests.cs +++ b/Pointframe.AutomationTests/Smoke/LaunchModeSmokeTests.cs @@ -26,6 +26,20 @@ public void AboutLaunchMode_OpensAboutWindowAndCloses() app.WaitForExit(); } + [Fact] + [Trait("Category", "DesktopAutomation")] + public void LibraryLaunchMode_OpensLibraryWindowAndCloses() + { + _fixture.SeedSettings(autoSaveScreenshots: false); + + using var app = AutomationApp.Launch("--automation-open-library", _fixture.CreateEnvironmentVariables()); + Assert.Equal(AutomationIds.LibraryWindowRoot, app.MainWindowAutomationId); + Assert.NotNull(app.FindRequiredElement(AutomationIds.LibraryWindowSearch)); + + app.CloseMainWindow(); + app.WaitForExit(); + } + [Fact] [Trait("Category", "DesktopAutomation")] public void SampleOverlayLaunchMode_OpensOpenedImageOverlayAndCloses() diff --git a/Pointframe.AutomationTests/Support/AutomationIds.cs b/Pointframe.AutomationTests/Support/AutomationIds.cs index e716a90..97bd296 100644 --- a/Pointframe.AutomationTests/Support/AutomationIds.cs +++ b/Pointframe.AutomationTests/Support/AutomationIds.cs @@ -4,6 +4,9 @@ internal static class AutomationIds { public const string AboutWindowRoot = "AboutWindow.Root"; public const string AboutWindowClose = "AboutWindow.Close"; + public const string LibraryWindowRoot = "LibraryWindow.Root"; + public const string LibraryWindowSearch = "LibraryWindow.Search"; + public const string LibraryWindowClose = "LibraryWindow.Close"; public const string SettingsWindowRoot = "SettingsWindow.Root"; public const string SettingsWindowAutoSaveScreenshots = "SettingsWindow.AutoSaveScreenshots"; public const string SettingsWindowCancel = "SettingsWindow.Cancel"; diff --git a/Pointframe.Tests/Automation/AutomationLaunchOptionsTests.cs b/Pointframe.Tests/Automation/AutomationLaunchOptionsTests.cs index 07bbf0c..28d0d15 100644 --- a/Pointframe.Tests/Automation/AutomationLaunchOptionsTests.cs +++ b/Pointframe.Tests/Automation/AutomationLaunchOptionsTests.cs @@ -6,15 +6,17 @@ namespace Pointframe.Tests.Automation; public sealed class AutomationLaunchOptionsTests { [Theory] - [InlineData("--automation-open-settings", true, false, false, false, false)] - [InlineData("--automation-open-about", false, true, false, false, false)] - [InlineData("--automation-open-sample-overlay", false, false, true, false, false)] - [InlineData("--automation-open-sample-recording-overlay", false, false, false, true, false)] - [InlineData("--automation-open-tray-sample-overlay", false, false, false, false, true)] + [InlineData("--automation-open-settings", true, false, false, false, false, false)] + [InlineData("--automation-open-about", false, true, false, false, false, false)] + [InlineData("--automation-open-library", false, false, true, false, false, false)] + [InlineData("--automation-open-sample-overlay", false, false, false, true, false, false)] + [InlineData("--automation-open-sample-recording-overlay", false, false, false, false, true, false)] + [InlineData("--automation-open-tray-sample-overlay", false, false, false, false, false, true)] public void Parse_WhenSingleAutomationArgumentIsPresent_SetsMatchingFlag( string argument, bool openSettingsWindow, bool openAboutWindow, + bool openLibraryWindow, bool openSampleOverlayWindow, bool openSampleRecordingOverlayWindow, bool openTraySampleOverlayWindow) @@ -23,6 +25,7 @@ public void Parse_WhenSingleAutomationArgumentIsPresent_SetsMatchingFlag( Assert.Equal(openSettingsWindow, options.OpenSettingsWindow); Assert.Equal(openAboutWindow, options.OpenAboutWindow); + Assert.Equal(openLibraryWindow, options.OpenLibraryWindow); Assert.Equal(openSampleOverlayWindow, options.OpenSampleOverlayWindow); Assert.Equal(openSampleRecordingOverlayWindow, options.OpenSampleRecordingOverlayWindow); Assert.Equal(openTraySampleOverlayWindow, options.OpenTraySampleOverlayWindow); @@ -35,11 +38,13 @@ public void Parse_WhenMultipleAutomationArgumentsArePresent_SetsAllMatchingFlags var options = AutomationLaunchOptions.Parse( [ "--automation-open-settings", + "--automation-open-library", "--automation-open-sample-recording-overlay", "--automation-open-tray-sample-overlay", ]); Assert.True(options.OpenSettingsWindow); + Assert.True(options.OpenLibraryWindow); Assert.True(options.OpenSampleRecordingOverlayWindow); Assert.True(options.OpenTraySampleOverlayWindow); Assert.True(options.IsAutomationMode); @@ -52,6 +57,7 @@ public void Parse_WhenNoAutomationArgumentsArePresent_DisablesAutomationMode() Assert.False(options.OpenSettingsWindow); Assert.False(options.OpenAboutWindow); + Assert.False(options.OpenLibraryWindow); Assert.False(options.OpenSampleOverlayWindow); Assert.False(options.OpenSampleRecordingOverlayWindow); Assert.False(options.OpenTraySampleOverlayWindow); diff --git a/Pointframe.Tests/Services/TrayIconManagerTests.cs b/Pointframe.Tests/Services/TrayIconManagerTests.cs index 7284a3c..24a71d3 100644 --- a/Pointframe.Tests/Services/TrayIconManagerTests.cs +++ b/Pointframe.Tests/Services/TrayIconManagerTests.cs @@ -158,6 +158,106 @@ public void CreateTrayMenuItem_AssignsHeaderAndClickHandler() }); } + [Fact] + public void CreateTrayContextMenu_UsesExpectedSectionOrderAndLabels() + { + StaTestHelper.Run(() => + { + var manager = CreateManager(); + + var contextMenu = (System.Windows.Controls.ContextMenu)InvokePrivateResult(manager, "CreateTrayContextMenu"); + var headers = GetTopLevelHeaders(contextMenu); + + Assert.Equal( + [ + "New Snip", + "Whole Screen Snip", + "Clean Window Snip", + "Open Image...", + "", + "Recent Captures", + "Recent Recordings", + "", + "Library", + "Open Folders", + "", + "Settings", + "Check for Updates", + "About", + "", + "Quit Pointframe", + ], + headers); + }); + } + + [Fact] + public void CreateTrayContextMenu_AssignsIconsToTopLevelCommands() + { + StaTestHelper.Run(() => + { + var manager = CreateManager(); + + var contextMenu = (System.Windows.Controls.ContextMenu)InvokePrivateResult(manager, "CreateTrayContextMenu"); + + AssertMenuItemHasIcon(contextMenu, "New Snip"); + AssertMenuItemHasIcon(contextMenu, "Whole Screen Snip"); + AssertMenuItemHasIcon(contextMenu, "Clean Window Snip"); + AssertMenuItemHasIcon(contextMenu, "Open Image..."); + AssertMenuItemHasIcon(contextMenu, "Recent Captures"); + AssertMenuItemHasIcon(contextMenu, "Recent Recordings"); + AssertMenuItemHasIcon(contextMenu, "Library"); + AssertMenuItemHasIcon(contextMenu, "Open Folders"); + AssertMenuItemHasIcon(contextMenu, "Settings"); + AssertMenuItemHasIcon(contextMenu, "Check for Updates"); + AssertMenuItemHasIcon(contextMenu, "About"); + AssertMenuItemHasIcon(contextMenu, "Quit Pointframe"); + }); + } + + [Fact] + public void HandleUpdateAvailable_UpdatesTrayMenuHeaderToInstall() + { + StaTestHelper.Run(() => + { + var manager = CreateManager(); + var contextMenu = (System.Windows.Controls.ContextMenu)InvokePrivateResult(manager, "CreateTrayContextMenu"); + var checkForUpdatesItem = FindTopLevelMenuItem(contextMenu, "Check for Updates"); + Assert.NotNull(checkForUpdatesItem); + + manager.HandleUpdateAvailable(new UpdateCheckResult(true, new Version(2, 4, 1), "https://example.com/download")); + + Assert.Equal("Install Update (2.4.1)", checkForUpdatesItem!.Header?.ToString()); + }); + } + + [Fact] + public void CheckForUpdates_Click_WhenPendingUpdate_InstallsPendingAndResetsHeader() + { + StaTestHelper.RunAsync(async () => + { + var autoUpdateMock = new Mock(); + autoUpdateMock + .Setup(service => service.ConfirmAndInstall(It.IsAny())) + .Returns(Task.CompletedTask); + var manager = CreateManager(autoUpdate: autoUpdateMock.Object); + + var contextMenu = (System.Windows.Controls.ContextMenu)InvokePrivateResult(manager, "CreateTrayContextMenu"); + var checkForUpdatesItem = FindTopLevelMenuItem(contextMenu, "Check for Updates"); + Assert.NotNull(checkForUpdatesItem); + + var pendingUpdate = new UpdateCheckResult(true, new Version(3, 1, 0), "https://example.com/download"); + manager.HandleUpdateAvailable(pendingUpdate); + + InvokePrivate(manager, "CheckForUpdates_Click", checkForUpdatesItem!, new RoutedEventArgs()); + await WaitForCondition(() => checkForUpdatesItem!.IsEnabled, TimeSpan.FromSeconds(3)); + + autoUpdateMock.Verify(service => service.ConfirmAndInstall(pendingUpdate), Times.Once); + Assert.Equal("Check for Updates", checkForUpdatesItem.Header?.ToString()); + Assert.Null(GetField(manager, "_pendingUpdate")); + }); + } + [Fact] public void OpenRecentRecordingFolder_Click_WithValidTag_OpensFolder() { @@ -471,8 +571,8 @@ public void RebuildRecentCapturesMenu_WhenEmpty_ShowsOpenSnipsFolderShortcut() InvokePrivate(manager, "RebuildRecentCapturesMenu"); - Assert.True(HasMenuItemHeader(menuItem, "No recent captures")); - Assert.True(HasMenuItemHeader(menuItem, "Open Snips folder")); + Assert.True(HasMenuItemHeader(menuItem, "No Recent Captures")); + Assert.True(HasMenuItemHeader(menuItem, "Open Snips Folder")); }); } @@ -490,8 +590,8 @@ public void RebuildRecentCapturesMenu_WhenNotEmpty_HidesOpenSnipsFolderShortcut( InvokePrivate(manager, "RebuildRecentCapturesMenu"); - Assert.False(HasMenuItemHeader(menuItem, "Open Snips folder")); - Assert.True(HasMenuItemHeader(menuItem, "Clear recent captures")); + Assert.False(HasMenuItemHeader(menuItem, "Open Snips Folder")); + Assert.True(HasMenuItemHeader(menuItem, "Clear Recent Captures")); }); } @@ -506,8 +606,8 @@ public void RebuildRecentRecordingsMenu_WhenEmpty_ShowsOpenVideosFolderShortcut( InvokePrivate(manager, "RebuildRecentRecordingsMenu"); - Assert.True(HasMenuItemHeader(menuItem, "No recent recordings")); - Assert.True(HasMenuItemHeader(menuItem, "Open Videos folder")); + Assert.True(HasMenuItemHeader(menuItem, "No Recent Recordings")); + Assert.True(HasMenuItemHeader(menuItem, "Open Videos Folder")); }); } @@ -527,8 +627,8 @@ public void RebuildRecentRecordingsMenu_WhenNotEmpty_HidesOpenVideosFolderShortc InvokePrivate(manager, "RebuildRecentRecordingsMenu"); - Assert.False(HasMenuItemHeader(menuItem, "Open Videos folder")); - Assert.True(HasMenuItemHeader(menuItem, "Clear recent recordings")); + Assert.False(HasMenuItemHeader(menuItem, "Open Videos Folder")); + Assert.True(HasMenuItemHeader(menuItem, "Clear Recent Recordings")); }); } @@ -585,6 +685,13 @@ private static void InvokePrivate(object target, string methodName, params objec method.Invoke(target, args); } + private static object InvokePrivateResult(object target, string methodName, params object[] args) + { + var method = target.GetType().GetMethod(methodName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + Assert.NotNull(method); + return method.Invoke(target, args)!; + } + private static object? GetField(object target, string fieldName) { var field = target.GetType().GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); @@ -633,4 +740,45 @@ private static bool HasMenuItemHeader(System.Windows.Controls.MenuItem menuItem, return false; } + + private static List GetTopLevelHeaders(System.Windows.Controls.ContextMenu contextMenu) + { + var headers = new List(); + foreach (var item in contextMenu.Items) + { + if (item is System.Windows.Controls.Separator) + { + headers.Add(""); + continue; + } + + if (item is System.Windows.Controls.MenuItem menuItem) + { + headers.Add(menuItem.Header?.ToString() ?? string.Empty); + } + } + + return headers; + } + + private static System.Windows.Controls.MenuItem? FindTopLevelMenuItem(System.Windows.Controls.ContextMenu contextMenu, string header) + { + foreach (var item in contextMenu.Items) + { + if (item is System.Windows.Controls.MenuItem menuItem && + string.Equals(menuItem.Header?.ToString(), header, StringComparison.Ordinal)) + { + return menuItem; + } + } + + return null; + } + + private static void AssertMenuItemHasIcon(System.Windows.Controls.ContextMenu contextMenu, string header) + { + var item = FindTopLevelMenuItem(contextMenu, header); + Assert.NotNull(item); + Assert.NotNull(item!.Icon); + } } diff --git a/Pointframe/Services/Infrastructure/TrayIconManager.cs b/Pointframe/Services/Infrastructure/TrayIconManager.cs index 807e0a8..5ef96ec 100644 --- a/Pointframe/Services/Infrastructure/TrayIconManager.cs +++ b/Pointframe/Services/Infrastructure/TrayIconManager.cs @@ -33,6 +33,7 @@ internal sealed class TrayIconManager : ITrayIconManager private TaskbarIcon? _trayIcon; private WpfMenuItem? _recentRecordingsMenuItem; private WpfMenuItem? _recentCapturesMenuItem; + private WpfMenuItem? _checkForUpdatesMenuItem; private UpdateCheckResult? _pendingUpdate; private string? _pendingRecordingBalloonPath; private readonly List _recentRecordings = []; @@ -87,8 +88,8 @@ public void Initialize() _trayIcon.TrayLeftMouseUp += TrayIcon_LeftClick; _trayIcon.TrayBalloonTipClicked += OnTrayBalloonClicked; - InitializeRecentCapturesMenu(); - InitializeRecentRecordingsMenu(); + RebuildRecentCapturesMenu(); + RebuildRecentRecordingsMenu(); } public void HandleUpdateAvailable(UpdateCheckResult result) @@ -101,6 +102,7 @@ public void HandleUpdateAvailable(UpdateCheckResult result) "Update Available", $"Version {v.Major}.{v.Minor}.{v.Build} is ready to download.", BalloonIcon.Info); + UpdateCheckForUpdatesMenuItemHeader(); } public void HandleRecordingCompleted(string outputPath, string elapsedText) @@ -170,18 +172,39 @@ public void Dispose() 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(CreateTrayMenuItem("Library", Library_Click)); + contextMenu.Items.Add(CreateTrayMenuItem("New Snip", NewSnip_Click, "\uE722")); + contextMenu.Items.Add(CreateTrayMenuItem("Whole Screen Snip", WholeScreenSnip_Click, "\uE7F4")); + contextMenu.Items.Add(CreateTrayMenuItem("Clean Window Snip", CleanWindowSnip_Click, "\uE8A7")); + contextMenu.Items.Add(CreateTrayMenuItem("Open Image...", OpenImage_Click, "\uE91B")); + contextMenu.Items.Add(new WpfSeparator()); + + _recentCapturesMenuItem = new WpfMenuItem + { + Header = "Recent Captures", + Icon = CreateMenuGlyphIcon("\uE823"), + }; + contextMenu.Items.Add(_recentCapturesMenuItem); + + _recentRecordingsMenuItem = new WpfMenuItem + { + Header = "Recent Recordings", + Icon = CreateMenuGlyphIcon("\uE919"), + }; + contextMenu.Items.Add(_recentRecordingsMenuItem); + + contextMenu.Items.Add(new WpfSeparator()); + contextMenu.Items.Add(CreateTrayMenuItem("Library", Library_Click, "\uE8F1")); contextMenu.Items.Add(CreateOpenFoldersMenuItem()); contextMenu.Items.Add(new WpfSeparator()); - contextMenu.Items.Add(CreateTrayMenuItem("Settings", Settings_Click)); - contextMenu.Items.Add(CreateTrayMenuItem("Check for Updates", CheckForUpdates_Click)); - contextMenu.Items.Add(CreateTrayMenuItem("About", About_Click)); + contextMenu.Items.Add(CreateTrayMenuItem("Settings", Settings_Click, "\uE713")); + + _checkForUpdatesMenuItem = CreateTrayMenuItem("Check for Updates", CheckForUpdates_Click, "\uE895"); + contextMenu.Items.Add(_checkForUpdatesMenuItem); + contextMenu.Items.Add(CreateTrayMenuItem("About", About_Click, "\uE946")); contextMenu.Items.Add(new WpfSeparator()); - contextMenu.Items.Add(CreateTrayMenuItem("Exit", Exit_Click)); + contextMenu.Items.Add(CreateTrayMenuItem("Quit Pointframe", Exit_Click, "\uE8BB")); + + UpdateCheckForUpdatesMenuItemHeader(); return contextMenu; } @@ -189,20 +212,22 @@ private WpfMenuItem CreateOpenFoldersMenuItem() { var openFoldersMenuItem = new WpfMenuItem { - Header = "Open folders", + Header = "Open Folders", + Icon = CreateMenuGlyphIcon("\uE838"), }; - openFoldersMenuItem.Items.Add(CreateTrayMenuItem("Snips folder", OpenSnipsFolder_Click)); - openFoldersMenuItem.Items.Add(CreateTrayMenuItem("Videos folder", OpenVideosFolder_Click)); - openFoldersMenuItem.Items.Add(CreateTrayMenuItem("Logs folder", OpenLogsFolder_Click)); + openFoldersMenuItem.Items.Add(CreateTrayMenuItem("Snips Folder", OpenSnipsFolder_Click, "\uE8A5")); + openFoldersMenuItem.Items.Add(CreateTrayMenuItem("Videos Folder", OpenVideosFolder_Click, "\uE714")); + openFoldersMenuItem.Items.Add(CreateTrayMenuItem("Logs Folder", OpenLogsFolder_Click, "\uE9D9")); return openFoldersMenuItem; } - internal static WpfMenuItem CreateTrayMenuItem(string header, RoutedEventHandler clickHandler) + internal static WpfMenuItem CreateTrayMenuItem(string header, RoutedEventHandler clickHandler, string? glyph = null) { var menuItem = new WpfMenuItem { Header = header, + Icon = string.IsNullOrWhiteSpace(glyph) ? null : CreateMenuGlyphIcon(glyph), }; menuItem.Click += clickHandler; return menuItem; @@ -246,22 +271,6 @@ private void ClearRecentRecordings_Click(object sender, RoutedEventArgs e) RebuildRecentRecordingsMenu(); } - private void InitializeRecentCapturesMenu() - { - if (_trayIcon?.ContextMenu is not { } contextMenu) - { - return; - } - - _recentCapturesMenuItem = new WpfMenuItem - { - Header = "Recent captures", - }; - - contextMenu.Items.Insert(3, _recentCapturesMenuItem); - RebuildRecentCapturesMenu(); - } - private void RebuildRecentCapturesMenu() { if (_recentCapturesMenuItem is null) @@ -275,11 +284,11 @@ private void RebuildRecentCapturesMenu() { _recentCapturesMenuItem.Items.Add(new WpfMenuItem { - Header = "No recent captures", + Header = "No Recent Captures", IsEnabled = false, }); _recentCapturesMenuItem.Items.Add(new WpfSeparator()); - var openFolder = CreateTrayMenuItem("Open Snips folder", OpenSnipsFolder_Click); + var openFolder = CreateTrayMenuItem("Open Snips Folder", OpenSnipsFolder_Click); _recentCapturesMenuItem.Items.Add(openFolder); return; } @@ -327,26 +336,10 @@ private void RebuildRecentCapturesMenu() } _recentCapturesMenuItem.Items.Add(new WpfSeparator()); - var clearRecent = CreateTrayMenuItem("Clear recent captures", ClearRecentCaptures_Click); + var clearRecent = CreateTrayMenuItem("Clear Recent Captures", ClearRecentCaptures_Click); _recentCapturesMenuItem.Items.Add(clearRecent); } - private void InitializeRecentRecordingsMenu() - { - if (_trayIcon?.ContextMenu is not { } contextMenu) - { - return; - } - - _recentRecordingsMenuItem = new WpfMenuItem - { - Header = "Recent recordings", - }; - - contextMenu.Items.Insert(4, _recentRecordingsMenuItem); - RebuildRecentRecordingsMenu(); - } - private void RebuildRecentRecordingsMenu() { if (_recentRecordingsMenuItem is null) @@ -360,11 +353,11 @@ private void RebuildRecentRecordingsMenu() { _recentRecordingsMenuItem.Items.Add(new WpfMenuItem { - Header = "No recent recordings", + Header = "No Recent Recordings", IsEnabled = false, }); _recentRecordingsMenuItem.Items.Add(new WpfSeparator()); - var openFolder = CreateTrayMenuItem("Open Videos folder", OpenVideosFolder_Click); + var openFolder = CreateTrayMenuItem("Open Videos Folder", OpenVideosFolder_Click); _recentRecordingsMenuItem.Items.Add(openFolder); return; } @@ -437,7 +430,7 @@ private void RebuildRecentRecordingsMenu() } _recentRecordingsMenuItem.Items.Add(new WpfSeparator()); - var clearRecent = CreateTrayMenuItem("Clear recent recordings", ClearRecentRecordings_Click); + var clearRecent = CreateTrayMenuItem("Clear Recent Recordings", ClearRecentRecordings_Click); _recentRecordingsMenuItem.Items.Add(clearRecent); } @@ -469,10 +462,19 @@ private async void CheckForUpdates_Click(object sender, RoutedEventArgs e) { var menuItem = (WpfMenuItem)sender; menuItem.IsEnabled = false; - _telemetry.TrackEvent("update_check_manual"); try { + if (_pendingUpdate is not null) + { + var pendingUpdate = _pendingUpdate; + _pendingUpdate = null; + UpdateCheckForUpdatesMenuItemHeader(); + await _autoUpdate.ConfirmAndInstall(pendingUpdate); + return; + } + + _telemetry.TrackEvent("update_check_manual"); var result = await _updateService.CheckForUpdates(); if (!result.IsUpdateAvailable) @@ -484,7 +486,11 @@ private async void CheckForUpdates_Click(object sender, RoutedEventArgs e) return; } + _pendingUpdate = result; + UpdateCheckForUpdatesMenuItemHeader(); await _autoUpdate.ConfirmAndInstall(result); + _pendingUpdate = null; + UpdateCheckForUpdatesMenuItemHeader(); } catch (Exception ex) { @@ -594,6 +600,7 @@ private async void OnTrayBalloonClicked(object sender, RoutedEventArgs e) { var update = _pendingUpdate; _pendingUpdate = null; + UpdateCheckForUpdatesMenuItemHeader(); await _autoUpdate.ConfirmAndInstall(update); return; } @@ -699,6 +706,34 @@ private static System.Windows.Controls.Button CreateRecentActionButton( return button; } + private static UIElement CreateMenuGlyphIcon(string glyph) + { + return new System.Windows.Controls.TextBlock + { + Text = glyph, + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 13, + VerticalAlignment = VerticalAlignment.Center, + }; + } + + private void UpdateCheckForUpdatesMenuItemHeader() + { + if (_checkForUpdatesMenuItem is null) + { + return; + } + + if (_pendingUpdate is null) + { + _checkForUpdatesMenuItem.Header = "Check for Updates"; + return; + } + + var version = _pendingUpdate.LatestVersion; + _checkForUpdatesMenuItem.Header = $"Install Update ({version.Major}.{version.Minor}.{version.Build})"; + } + private void SimulateUiError_Click(object sender, RoutedEventArgs e) { _logger.LogDebug("Simulating UI recovery smoke test from tray menu"); diff --git a/README.md b/README.md index 3b1d0a6..7d0ab74 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,37 @@ If you find Pointframe useful, a ⭐ on GitHub helps others discover it — than - **Built-in OCR:** Lasso any text on your screen (even in images or videos) to instantly copy it to your clipboard. - **Pin to Screen:** Pin captured screenshots as floating, always-on-top windows for quick reference while coding or writing. -## Latest features +## 🆕 What shipped in recent releases -- **Show your point while recording** — Draw on the captured region as you record, then switch between interactive and drawing modes from the HUD. -- **Stay in control without breaking flow** — Pause, resume, stop, switch tools, clear annotations, and open the output folder from one floating HUD. -- **Redact sensitive content live** — Blur annotations now sample from the live recording region, so private details stay hidden in the final video. +### Jul 2026 + +- **Tray menu UX refresh** — Improved command grouping, clearer labels, and iconized top-level actions. +- **Capture Library OCR hardening** — Better reliability and scale for OCR-backed library search. +- **Capture + recording hot-path optimizations** — Smoother performance in frequent capture/recording flows. + +### Jun 2026 + +- **Clean Window Snip** — Capture cleaner active-window results via tray action and hotkey. +- **Video watermark support** — Configurable watermark overlays for recorded MP4 output. +- **Video trim workflow** — Trim recordings directly in-app from recent recordings actions. +- **Auto-update tray notification improvements** — Better update signaling and install flow behavior from tray. + +### May 2026 + +- **Capture delay customization** — Adjustable delay presets to capture menus and transient UI states. +- **Screenshot watermark support** — Add configurable watermarking for screenshots. +- **Library and tray workflow upgrades** — Open folders submenu, richer recents actions, and improved tray ergonomics. +- **Expanded auto-update intervals** — Additional cadence options including short intervals and disable mode. + +### Apr-Mar 2026 (foundation releases) + +- **Whole-screen snip mode** and **whole-screen record hotkey**. +- **Recording HUD improvements** including compact mode and better in-recording controls. +- **GIF export**, **cursor highlight**, and **click ripple** for clearer instructional recordings. +- **Open existing image**, **callout tool**, **color picker**, **pixel ruler**, and **style presets**. +- **Telemetry and usage reporting foundation** for anonymous feature adoption metrics. + +For full detail by version, see the [Releases](https://github.com/dimitar-radenkov/Pointframe/releases) page. ## Why people use it @@ -72,6 +98,7 @@ If you find Pointframe useful, a ⭐ on GitHub helps others discover it — than - **Region capture** — Press the configured hotkey (default: `Print Screen`) to draw a selection on screen - **Whole-screen snip** — Instantly capture the entire screen from the tray icon or a dedicated hotkey +- **Clean window snip** — Capture a cleaner active-window result directly from tray and dedicated hotkey - **Frozen screen snapshot** — The screen is captured instantly when the hotkey is pressed, freezing menus, tooltips, and popups exactly as they appear - **Selection magnifier** — A zoomed loupe follows your cursor while drawing the capture region for pixel-accurate selection - **Configurable capture hotkeys** — Change the region-capture hotkey and the whole-screen record hotkey independently from Settings @@ -81,17 +108,20 @@ If you find Pointframe useful, a ⭐ on GitHub helps others discover it — than - **Pixel ruler tool** — Draw a ruler across the screenshot to measure distances in pixels - **Blur tool** — Drag over sensitive content (faces, emails, passwords) to apply a Gaussian blur before sharing - **OCR — Copy Text** — Draw a lasso around text in the screenshot to extract it via OCR and copy to clipboard (uses Windows.Media.Ocr, no external dependencies) +- **Capture Library** — Browse your saved captures, filter by date range, and search by filename or OCR text from the tray Library entry - **Open existing image** — Load a PNG, JPG/JPEG, or BMP from the tray menu and annotate it without taking a new screenshot - **Pin screenshot** — Pin the captured screenshot as a floating, always-on-top, resizable window for quick reference while you work - **Undo / redo** — Full undo/redo stack during annotation - **Copy & auto-save** — Copy to clipboard; optional auto-save to a configurable folder - **Screen recording** — Record a selected region to MP4 (H.264 via ffmpeg) or start a whole-screen recording instantly with `Ctrl+Shift+R` (default); optional microphone audio from a selected Windows input device - **Recording-time annotations** — Add shapes and text directly on top of a recording while it is in progress; switch between draw mode and interact mode from the floating HUD +- **Video watermark** — Optionally burn a configurable watermark into MP4 recordings +- **Tray menu icons** — Core tray actions now include consistent glyph icons for faster scanning - **Cursor highlight** — Configurable glowing ring around the cursor during recording so viewers never lose track of your pointer - **Click ripple** — Visual ripple effect on mouse clicks during recording to make interactions obvious - **GIF export** — Export any recent recording to GIF directly from the tray's Recent recordings menu (requires ffmpeg) - **Capture delay** — Configurable countdown (0 / 3 / 5 / 10 s) before the selection overlay appears, useful for capturing menus and hover states -- **Auto-updates** — A background service checks GitHub Releases on every launch and on a configurable schedule (every day / 2 days / 3 days). When a new version is found a tray balloon appears; click it to confirm, watch the progress bar, and the installer runs automatically — no browser, no manual downloads +- **Auto-updates** — A background service checks GitHub Releases on launch and on a configurable schedule (every 2 hours / 6 hours / 12 hours / day / 2 days / 3 days / never). When a new version is found a tray balloon appears; click it to confirm and install without opening the browser - **System tray** — Runs silently in the background; all actions accessible from the tray icon - **Theme support** — Choose Light, Dark, or follow the system theme from Settings @@ -110,14 +140,17 @@ Right-click the tray icon to access all actions: | Item | Description | |---|---| | New Snip | Open the region-capture overlay (same as the capture hotkey) | -| Whole screen snip | Instantly capture the entire screen | -| Open image… | Load a PNG / JPG / BMP file and open it in the annotation overlay | -| Recent captures | Submenu listing the last 5 saved screenshots; each has **Open** and **Open folder** actions | -| Recent recordings | Submenu listing the last 5 recordings; each has **Open**, **Open folder**, and **Export to GIF** actions | +| Whole Screen Snip | Instantly capture the entire screen | +| Clean Window Snip | Capture the active window with a cleaner result | +| Open Image... | Load a PNG / JPG / BMP file and open it in the annotation overlay | +| Recent Captures | Submenu listing the last 5 saved screenshots; each has **Open** and **Open folder** actions | +| Recent Recordings | Submenu listing the last 5 recordings; each has **Open**, **Trim**, **Export to GIF**, and **Open folder** actions | +| Library | Open the capture library window | +| Open Folders | Quick access to Snips Folder, Videos Folder, and Logs Folder | | Settings | Open the Settings window | -| Check for Updates | Manually trigger an update check against GitHub Releases | +| Check for Updates / Install Update | Manually check for updates or install a pending update directly from tray | | About | Show version information | -| Exit | Quit the application | +| Quit Pointframe | Quit the application | Left-clicking the tray icon triggers **New Snip** directly. @@ -140,6 +173,7 @@ Open **Settings** from the tray icon to configure: |---|---| | Recording output folder | Where recorded MP4 files are saved | | Record hotkey | The key combination that starts a whole-screen recording (default: `Ctrl+Shift+R`) | +| Video watermark | Optional watermark overlay in MP4 recordings | | Cursor highlight | Show a glowing ring around the cursor during recording; configurable size | | Click ripple | Show a ripple effect on mouse clicks during recording | | Microphone *(advanced)* | Include microphone audio when recording starts | @@ -154,6 +188,15 @@ Open **Settings** from the tray icon to configure: | Stroke thickness | Default pen/shape width | | Style presets | Up to 5 named color-and-thickness shortcuts shown in the annotation toolbar | +### Shortcuts + +| Setting | Description | +|---|---| +| Region capture hotkey | Opens the region capture overlay (default: `Print Screen`) | +| Whole-screen record hotkey | Starts whole-screen recording (default: `Ctrl+Shift+R`) | +| Clean window snip hotkey | Starts clean-window capture (default: `Ctrl+Shift+W`) | +| Overlay shortcuts | Configure copy, save-as, undo, redo, show-shortcuts, and close keys for the overlay | + ### App | Setting | Description | @@ -167,6 +210,7 @@ Open **Settings** from the tray icon to configure: |---|---| | `Print Screen` (default, configurable) | Open region-capture overlay | | `Ctrl+Shift+R` (default, configurable) | Start whole-screen recording | +| `Ctrl+Shift+W` (default, configurable) | Start clean-window snip | | `Ctrl+Z` | Undo last annotation | | `Ctrl+Y` | Redo annotation | | `Ctrl+C` | Copy screenshot to clipboard |