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
8 changes: 7 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
Expand Down
71 changes: 71 additions & 0 deletions Tests/SettingsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NCalcAsync" Version="5.12.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
Expand Down
2 changes: 1 addition & 1 deletion Text-Grab-Package/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<Identity
Name="40087JoeFinApps.TextGrab"
Publisher="CN=153F3B0F-BA3D-4964-8098-71AC78A1DF6A"
Version="4.13.0.0" />
Version="4.13.2.0" />

<Properties>
<DisplayName>Text Grab</DisplayName>
Expand Down
31 changes: 24 additions & 7 deletions Text-Grab/Models/ButtonInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,6 @@ public static List<ButtonInfo> DefaultButtonList
SymbolIcon = SymbolRegular.Copy24
},
new()
{
ButtonText = "Save to File...",
SymbolText = "",
ClickEvent = "SaveBTN_Click",
SymbolIcon = SymbolRegular.Save24
},
new()
{
ButtonText = "Make Single Line",
SymbolText = "",
Expand Down Expand Up @@ -215,6 +208,14 @@ public static List<ButtonInfo> 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",
Expand All @@ -239,6 +240,14 @@ public static List<ButtonInfo> 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",
Expand Down Expand Up @@ -415,6 +424,14 @@ public static List<ButtonInfo> 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",
Expand Down
9 changes: 9 additions & 0 deletions Text-Grab/Models/ShortcutKeySet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
};
}

Expand All @@ -189,4 +197,5 @@ public enum ShortcutKeyActions
PreviousRegionGrab = 6,
PreviousEditWindow = 7,
PreviousGrabFrame = 8,
OpenClipboardContent = 9,
}
5 changes: 5 additions & 0 deletions Text-Grab/Pages/KeysSettings.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
KeySetChanged="ShortcutControl_KeySetChanged"
RecordingStarted="ShortcutControl_Recording"
ShortcutName="Quick Simple Lookup" />
<controls:ShortcutControl
x:Name="OccShortcutControl"
KeySetChanged="ShortcutControl_KeySetChanged"
RecordingStarted="ShortcutControl_Recording"
ShortcutName="Open Clipboard Content" />
</StackPanel>
</StackPanel>
</Page>
3 changes: 3 additions & 0 deletions Text-Grab/Pages/KeysSettings.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
23 changes: 20 additions & 3 deletions Text-Grab/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Text-Grab/Text-Grab.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<EnableMsixTooling>false</EnableMsixTooling>
<Version>4.13.0</Version>
<Version>4.13.2</Version>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -52,7 +52,7 @@

<ItemGroup>
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="Dapplo.Windows.User32" Version="2.0.85" />
<PackageReference Include="Dapplo.Windows.User32" Version="2.0.89" />
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<PackageReference Include="Magick.NET.SystemDrawing" Version="8.0.19" />
Expand Down
60 changes: 60 additions & 0 deletions Text-Grab/Utilities/NotifyIconUtilities.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -139,6 +142,63 @@ private static void HotKeyManager_HotKeyPressed(object? sender, HotKeyEventArgs
Singleton<HistoryService>.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<string?>().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;
}
Expand Down
22 changes: 21 additions & 1 deletion Text-Grab/Utilities/UIAutomationUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
}
Expand Down Expand Up @@ -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)
{
}
Expand Down
1 change: 1 addition & 0 deletions Text-Grab/Views/EditTextWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
{nameof(DeleteAllSelectionCmd), DeleteAllSelectionCmd},
{nameof(DeleteAllSelectionPatternCmd), DeleteAllSelectionPatternCmd},
{nameof(InsertSelectionOnEveryLineCmd), InsertSelectionOnEveryLineCmd},
{nameof(SplitAfterSelectionCmd), SplitAfterSelectionCmd},
{nameof(OcrPasteCommand), OcrPasteCommand},
{nameof(MakeQrCodeCmd), MakeQrCodeCmd},
{nameof(WebSearchCmd), WebSearchCmd},
Expand Down Expand Up @@ -1676,7 +1677,7 @@
_calculationService.ClearParameters();
UpdateAggregateStatusDisplay();
// Keep scrolls aligned even when clearing
Dispatcher.BeginInvoke(SyncCalcScrollToMain, DispatcherPriority.Render);

Check warning on line 1680 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1680 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1680 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1680 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1680 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1680 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
return;
}

Expand All @@ -1694,7 +1695,7 @@
UpdateAggregateStatusDisplay();

// After updating calc text, its ScrollViewer resets; resync to main scroll
Dispatcher.BeginInvoke(SyncCalcScrollToMain, DispatcherPriority.Render);

Check warning on line 1698 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1698 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1698 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1698 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1698 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Check warning on line 1698 in Text-Grab/Views/EditTextWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

// Optional status (kept commented)
// if (result.ErrorCount == 0) { } else { }
Expand Down
Loading