From 3e33f16ae38f5e8fbae563a3932c3d82529072b5 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Fri, 3 Apr 2026 20:04:56 -0500 Subject: [PATCH 1/9] Catch AccessViolationException in UIA text extraction to prevent crash Native UIAutomation (RawTextRange_GetText) can throw a 0xc0000005 AV when a TextPatternRange handle goes stale, which was not caught and crashed the process. This only manifested in self-contained builds where the bundled WPF runtime calls the system UIAutomationCore.dll directly. Co-Authored-By: Claude Sonnet 4.6 --- Text-Grab/Utilities/UIAutomationUtilities.cs | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Text-Grab/Utilities/UIAutomationUtilities.cs b/Text-Grab/Utilities/UIAutomationUtilities.cs index 0987874c..bac31adf 100644 --- a/Text-Grab/Utilities/UIAutomationUtilities.cs +++ b/Text-Grab/Utilities/UIAutomationUtilities.cs @@ -332,6 +332,10 @@ private static string GetTextFromRegion(Rect screenRect, UiAutomationOptions opt return string.Join(Environment.NewLine, extractedText); } + catch (AccessViolationException) + { + return string.Empty; + } catch (ElementNotAvailableException) { return string.Empty; @@ -833,6 +837,10 @@ private static bool TryExtractTextPatternText(AutomationElement element, Rect? f return !string.IsNullOrWhiteSpace(text); } } + catch (AccessViolationException) + { + // Native UIA AV — treat as unavailable + } catch (ElementNotAvailableException) { } @@ -861,12 +869,24 @@ private static bool TryExtractVisibleTextPatternText(TextPattern textPattern, Re if (!RangeIntersectsBounds(range, filterBounds)) continue; - TryAddUniqueText(range.GetText(-1), seenText, extractedText); + try + { + TryAddUniqueText(range.GetText(-1), seenText, extractedText); + } + catch (AccessViolationException) + { + // Native UIAutomation can AV when a range handle becomes stale, + // especially in self-contained builds. Skip the range and continue. + } } text = string.Join(Environment.NewLine, extractedText); return !string.IsNullOrWhiteSpace(text); } + catch (AccessViolationException) + { + // Native UIA AV — treat as unavailable + } catch (ElementNotAvailableException) { } From 190175511cbdf6d1f19d9a197a9a70ce3d0c0b69 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Fri, 3 Apr 2026 20:19:26 -0500 Subject: [PATCH 2/9] remove save file from the default bottom bar buttons --- Text-Grab/Models/ButtonInfo.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Text-Grab/Models/ButtonInfo.cs b/Text-Grab/Models/ButtonInfo.cs index ce3706c8..68bf457f 100644 --- a/Text-Grab/Models/ButtonInfo.cs +++ b/Text-Grab/Models/ButtonInfo.cs @@ -127,13 +127,6 @@ public static List DefaultButtonList SymbolIcon = SymbolRegular.Copy24 }, new() - { - ButtonText = "Save to File...", - SymbolText = "", - ClickEvent = "SaveBTN_Click", - SymbolIcon = SymbolRegular.Save24 - }, - new() { ButtonText = "Make Single Line", SymbolText = "", From c41743b3abd10123335f01d3738c3808fccff43b Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Fri, 3 Apr 2026 20:35:36 -0500 Subject: [PATCH 3/9] Add new buttons and SplitAfterSelectionCmd support Added "Save As...", "Manage Grab Templates...", and "Split Lines After Each Selection" buttons to ButtonInfo. Registered SplitAfterSelectionCmd in EditTextWindow command dictionary for UI integration. --- Text-Grab/Models/ButtonInfo.cs | 24 ++++++++++++++++++++++++ Text-Grab/Views/EditTextWindow.xaml.cs | 1 + 2 files changed, 25 insertions(+) diff --git a/Text-Grab/Models/ButtonInfo.cs b/Text-Grab/Models/ButtonInfo.cs index 68bf457f..442af591 100644 --- a/Text-Grab/Models/ButtonInfo.cs +++ b/Text-Grab/Models/ButtonInfo.cs @@ -208,6 +208,14 @@ public static List AllButtons SymbolIcon = SymbolRegular.DocumentSave24 }, new() + { + OrderNumber = 1.21, + ButtonText = "Save As...", + SymbolText = "", + ClickEvent = "SaveAsBTN_Click", + SymbolIcon = SymbolRegular.DocumentEdit24 + }, + new() { OrderNumber = 1.3, ButtonText = "Make Single Line", @@ -232,6 +240,14 @@ public static List AllButtons SymbolIcon = SymbolRegular.Timer324 }, new() + { + OrderNumber = 1.42, + ButtonText = "Manage Grab Templates...", + SymbolText = "", + ClickEvent = "ManageGrabTemplates_Click", + SymbolIcon = SymbolRegular.GridDots24 + }, + new() { OrderNumber = 1.5, ButtonText = "Open Grab Frame", @@ -408,6 +424,14 @@ public static List AllButtons SymbolIcon = SymbolRegular.TextWrap24 }, new() + { + OrderNumber = 4.51, + ButtonText = "Split Lines After Each Selection", + SymbolText = "", + Command = "SplitAfterSelectionCmd", + SymbolIcon = SymbolRegular.TextWrapOff24 + }, + new() { OrderNumber = 4.6, ButtonText = "Isolate Selection", diff --git a/Text-Grab/Views/EditTextWindow.xaml.cs b/Text-Grab/Views/EditTextWindow.xaml.cs index a5af817a..55afd1a0 100644 --- a/Text-Grab/Views/EditTextWindow.xaml.cs +++ b/Text-Grab/Views/EditTextWindow.xaml.cs @@ -169,6 +169,7 @@ public static Dictionary GetRoutedCommands() {nameof(DeleteAllSelectionCmd), DeleteAllSelectionCmd}, {nameof(DeleteAllSelectionPatternCmd), DeleteAllSelectionPatternCmd}, {nameof(InsertSelectionOnEveryLineCmd), InsertSelectionOnEveryLineCmd}, + {nameof(SplitAfterSelectionCmd), SplitAfterSelectionCmd}, {nameof(OcrPasteCommand), OcrPasteCommand}, {nameof(MakeQrCodeCmd), MakeQrCodeCmd}, {nameof(WebSearchCmd), WebSearchCmd}, From f598f732ec274eeb325910a4b78661377c190c20 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Tue, 7 Apr 2026 22:43:05 -0500 Subject: [PATCH 4/9] Bump version to 4.13.1 Updated application version from 4.13.0 to 4.13.1 in both Package.appxmanifest and Text-Grab.csproj. No other changes were made. --- Text-Grab-Package/Package.appxmanifest | 2 +- Text-Grab/Text-Grab.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Text-Grab-Package/Package.appxmanifest b/Text-Grab-Package/Package.appxmanifest index 2e5db546..93f115da 100644 --- a/Text-Grab-Package/Package.appxmanifest +++ b/Text-Grab-Package/Package.appxmanifest @@ -14,7 +14,7 @@ + Version="4.13.1.0" /> Text Grab diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index b745efce..be238870 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -23,7 +23,7 @@ true win-x86;win-x64;win-arm64 false - 4.13.0 + 4.13.1 From 1a279c55debe67376f34cf67447925be7c80dd77 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Tue, 7 Apr 2026 23:04:24 -0500 Subject: [PATCH 5/9] Expand allowlist for pdm CLI setup and usage Added permissions in settings.local.json to allow the use of the "Skill(pdm)" skill, creation of the bin directory, downloading and configuring the pdm CLI tool, and running various pdm commands. Existing allowlist entries remain unchanged. --- .claude/settings.local.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 448bb646..210267d5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,13 @@ "Bash(dotnet test:*)", "Bash(gh pr list:*)", "Bash(/mnt/c/Program Files/dotnet/dotnet.exe:*)", - "WebFetch(domain:github.com)" + "WebFetch(domain:github.com)", + "Skill(pdm)", + "Bash(mkdir -p bin)", + "Bash(curl -o bin/pdm https://app.produckmap.com/cli/pdm)", + "Bash(chmod +x bin/pdm)", + "Bash(bin/pdm ui-element:*)", + "Bash(bin/pdm *)" ], "deny": [] } From f8f1cc790f3256d395c0f997c9a5dec08bbfc46f Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Tue, 7 Apr 2026 23:04:37 -0500 Subject: [PATCH 6/9] Add "Open Clipboard Content" shortcut and handler Introduced a new shortcut action (Ctrl+Shift+Win+V) to open clipboard content. Updated settings UI and logic to support configuring this shortcut. Enhanced NotifyIconUtilities to handle the new action, opening EditTextWindow for text or GrabFrame for images/files from the clipboard. Added required using directives for clipboard and image processing. --- Text-Grab/Models/ShortcutKeySet.cs | 9 ++++ Text-Grab/Pages/KeysSettings.xaml | 5 ++ Text-Grab/Pages/KeysSettings.xaml.cs | 3 ++ Text-Grab/Utilities/NotifyIconUtilities.cs | 60 ++++++++++++++++++++++ 4 files changed, 77 insertions(+) diff --git a/Text-Grab/Models/ShortcutKeySet.cs b/Text-Grab/Models/ShortcutKeySet.cs index be0544e4..9fbb6a93 100644 --- a/Text-Grab/Models/ShortcutKeySet.cs +++ b/Text-Grab/Models/ShortcutKeySet.cs @@ -175,6 +175,14 @@ public override int GetHashCode() Name = "Edit last Grab Frame", Action = ShortcutKeyActions.PreviousGrabFrame }, + new() + { + Modifiers = {KeyModifiers.Windows, KeyModifiers.Shift, KeyModifiers.Control}, + NonModifierKey = Key.V, + IsEnabled = true, + Name = "Open Clipboard Content", + Action = ShortcutKeyActions.OpenClipboardContent + }, }; } @@ -189,4 +197,5 @@ public enum ShortcutKeyActions PreviousRegionGrab = 6, PreviousEditWindow = 7, PreviousGrabFrame = 8, + OpenClipboardContent = 9, } diff --git a/Text-Grab/Pages/KeysSettings.xaml b/Text-Grab/Pages/KeysSettings.xaml index 405462fc..aba2c285 100644 --- a/Text-Grab/Pages/KeysSettings.xaml +++ b/Text-Grab/Pages/KeysSettings.xaml @@ -79,6 +79,11 @@ KeySetChanged="ShortcutControl_KeySetChanged" RecordingStarted="ShortcutControl_Recording" ShortcutName="Quick Simple Lookup" /> + diff --git a/Text-Grab/Pages/KeysSettings.xaml.cs b/Text-Grab/Pages/KeysSettings.xaml.cs index 4ec8532c..c2a29059 100644 --- a/Text-Grab/Pages/KeysSettings.xaml.cs +++ b/Text-Grab/Pages/KeysSettings.xaml.cs @@ -136,6 +136,9 @@ private void Page_Loaded(object sender, RoutedEventArgs e) case ShortcutKeyActions.PreviousGrabFrame: LgfShortcutControl.KeySet = keySet; break; + case ShortcutKeyActions.OpenClipboardContent: + OccShortcutControl.KeySet = keySet; + break; default: break; } diff --git a/Text-Grab/Utilities/NotifyIconUtilities.cs b/Text-Grab/Utilities/NotifyIconUtilities.cs index 40e96bbb..889841ca 100644 --- a/Text-Grab/Utilities/NotifyIconUtilities.cs +++ b/Text-Grab/Utilities/NotifyIconUtilities.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Windows.Media.Imaging; using Text_Grab.Controls; using Text_Grab.Models; using Text_Grab.Services; @@ -139,6 +142,63 @@ private static void HotKeyManager_HotKeyPressed(object? sender, HotKeyEventArgs Singleton.Instance.GetLastHistoryAsGrabFrame(); })); break; + case ShortcutKeyActions.OpenClipboardContent: + System.Windows.Application.Current.Dispatcher.Invoke(new Action(() => + { + if (System.Windows.Clipboard.ContainsText()) + { + string text = System.Windows.Clipboard.GetText(); + EditTextWindow etw = new(text, false); + etw.Show(); + etw.Activate(); + return; + } + + if (System.Windows.Clipboard.ContainsFileDropList()) + { + StringCollection files = System.Windows.Clipboard.GetFileDropList(); + string? imagePath = files.Cast().FirstOrDefault(f => f is not null && IoUtilities.IsImageFile(f!)); + if (imagePath is not null) + { + GrabFrame gf = new(imagePath); + gf.Show(); + gf.Activate(); + return; + } + } + + (bool success, System.Windows.Media.ImageSource? clipboardImage) = ClipboardUtilities.TryGetImageFromClipboard(); + if (!success || clipboardImage is null) + return; + + BitmapSource? bitmapSource = null; + if (clipboardImage is System.Windows.Interop.InteropBitmap interopBitmap) + { + System.Drawing.Bitmap bmp = ImageMethods.InteropBitmapToBitmap(interopBitmap); + bitmapSource = ImageMethods.BitmapToImageSource(bmp); + bmp.Dispose(); + } + else if (clipboardImage is BitmapSource source) + { + bitmapSource = source; + } + + if (bitmapSource is null) + return; + + string tempPath = Path.Combine(Path.GetTempPath(), $"TextGrab_Clipboard_{Guid.NewGuid()}.png"); + using (FileStream fileStream = new(tempPath, FileMode.Create)) + { + PngBitmapEncoder encoder = new(); + encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); + encoder.Save(fileStream); + } + + GrabFrame grabFrame = new(tempPath); + grabFrame.Show(); + grabFrame.Activate(); + })); + break; default: break; } From e52cd7d8839500b81dcd2a126a03037e47600b8c Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Tue, 7 Apr 2026 23:22:40 -0500 Subject: [PATCH 7/9] Improve settings migration and add file-backed tests Refactored SettingsService constructor to ensure settings migration or upgrade occurs before reading EnableFileBackedManagedSettings, preserving user preferences. Added unit tests to verify file-backed managed settings behavior, upgrade handling, and sidecar file survival. --- Tests/SettingsServiceTests.cs | 71 +++++++++++++++++++++++++++ Text-Grab/Services/SettingsService.cs | 23 +++++++-- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/Tests/SettingsServiceTests.cs b/Tests/SettingsServiceTests.cs index bab8d29e..5ef92f34 100644 --- a/Tests/SettingsServiceTests.cs +++ b/Tests/SettingsServiceTests.cs @@ -133,6 +133,77 @@ public void ClearingManagedSettingClearsLegacyAndSidecar() Assert.Empty(service.LoadStoredRegexes()); } + [Fact] + public void Constructor_FileBackedModeReflectsSettingsValueSetBeforeConstruction() + { + // EnableFileBackedManagedSettings must be read AFTER any migration so the + // persisted user preference is honoured for the current session. + Settings settings = new() + { + FirstRun = false, + EnableFileBackedManagedSettings = true + }; + + SettingsService service = CreateService(settings); + + Assert.True(service.IsFileBackedManagedSettingsEnabled); + } + + [Fact] + public void Constructor_FileBackedModeDefaultsToFalseWhenNotSet() + { + Settings settings = new() + { + FirstRun = false, + EnableFileBackedManagedSettings = false + }; + + SettingsService service = CreateService(settings); + + Assert.False(service.IsFileBackedManagedSettingsEnabled); + } + + [Fact] + public void Constructor_UnpackagedUpgradePathDoesNotThrowWhenNoPreviousVersion() + { + // When saveClassicSettingsChanges is false (test mode) the Upgrade() code path is + // skipped, so this simply verifies that the constructor completes successfully when + // FirstRun is true and localSettings is null. + Settings settings = new() + { + FirstRun = true, + EnableFileBackedManagedSettings = false + }; + + SettingsService service = CreateService(settings); + + // Service initialises without throwing; FileBackedMode reflects the setting. + Assert.False(service.IsFileBackedManagedSettingsEnabled); + } + + [Fact] + public void LoadStoredRegexes_SidecarSurvivesSimulatedPackageUpgrade() + { + // Simulate a package upgrade: sidecar file already exists (from the previous + // version) but ClassicSettings.RegexList is empty (reset by the upgrade). + // The service must load from the sidecar and backfill ClassicSettings. + Settings settings = new() + { + FirstRun = false, + EnableFileBackedManagedSettings = false, + RegexList = string.Empty + }; + string regexFilePath = Path.Combine(_tempFolder, "RegexList.json"); + File.WriteAllText(regexFilePath, SerializeRegexes("survived-upgrade")); + + SettingsService service = CreateService(settings); + + StoredRegex loaded = Assert.Single(service.LoadStoredRegexes()); + Assert.Equal("survived-upgrade", loaded.Id); + // Verify backfill into ClassicSettings so the next migration has something to copy. + Assert.Contains("survived-upgrade", settings.RegexList); + } + private SettingsService CreateService(Settings settings) => new( settings, diff --git a/Text-Grab/Services/SettingsService.cs b/Text-Grab/Services/SettingsService.cs index 04c52f40..fc28f999 100644 --- a/Text-Grab/Services/SettingsService.cs +++ b/Text-Grab/Services/SettingsService.cs @@ -64,10 +64,27 @@ internal SettingsService( _localSettings = localSettings; _managedJsonSettingsFolderPath = managedJsonSettingsFolderPath ?? GetManagedJsonSettingsFolderPath(); _saveClassicSettingsChanges = saveClassicSettingsChanges; - _preferFileBackedManagedSettings = ClassicSettings.EnableFileBackedManagedSettings; - if (ClassicSettings.FirstRun && _localSettings is not null && _localSettings.Values.Count > 0) - MigrateLocalSettingsToClassic(); + if (ClassicSettings.FirstRun) + { + if (_localSettings is not null && _localSettings.Values.Count > 0) + { + // Packaged: ApplicationDataContainer persists across package upgrades, + // so copy saved values back into the freshly-reset classic settings. + MigrateLocalSettingsToClassic(); + if (_saveClassicSettingsChanges) + ClassicSettings.Save(); + } + else if (_localSettings is null && _saveClassicSettingsChanges) + { + // Unpackaged: Properties.Settings stores data in a version-specific path, + // so call Upgrade() to carry forward values from the previous version. + ClassicSettings.Upgrade(); + } + } + + // Must be read after any migration so the user's saved preference is respected. + _preferFileBackedManagedSettings = ClassicSettings.EnableFileBackedManagedSettings; // copy settings from classic to local settings // so that when app updates they can be copied forward From 7398c106517dea69221654386a3d6202df33bd15 Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Tue, 7 Apr 2026 23:22:56 -0500 Subject: [PATCH 8/9] Bump version to 4.13.2 Updated application version from 4.13.1 to 4.13.2 in both Package.appxmanifest and Text-Grab.csproj. No other changes were made. --- Text-Grab-Package/Package.appxmanifest | 2 +- Text-Grab/Text-Grab.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Text-Grab-Package/Package.appxmanifest b/Text-Grab-Package/Package.appxmanifest index 93f115da..bccb2398 100644 --- a/Text-Grab-Package/Package.appxmanifest +++ b/Text-Grab-Package/Package.appxmanifest @@ -14,7 +14,7 @@ + Version="4.13.2.0" /> Text Grab diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index be238870..8be69963 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -23,7 +23,7 @@ true win-x86;win-x64;win-arm64 false - 4.13.1 + 4.13.2 From 4e7013f195ec1498066f75aa4e2a57f5fe4cf10d Mon Sep 17 00:00:00 2001 From: Joe Finney Date: Wed, 8 Apr 2026 20:53:21 -0500 Subject: [PATCH 9/9] Update test SDK and Dapplo.Windows.User32 packages Upgraded Microsoft.NET.Test.Sdk to 18.4.0 in Tests.csproj for improved test infrastructure. Updated Dapplo.Windows.User32 to 2.0.89 in Text-Grab.csproj for latest Windows API interop enhancements. --- Tests/Tests.csproj | 2 +- Text-Grab/Text-Grab.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 69c54639..3a93deb7 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -17,7 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Text-Grab/Text-Grab.csproj b/Text-Grab/Text-Grab.csproj index 8be69963..8ebd0208 100644 --- a/Text-Grab/Text-Grab.csproj +++ b/Text-Grab/Text-Grab.csproj @@ -52,7 +52,7 @@ - +