Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
da044ec
Add template-based capture system with region support
TheJoeFin Mar 1, 2026
2dc268c
Add file-based GrabTemplate storage and executor
TheJoeFin Mar 1, 2026
ae3aa3d
Add Grab Template support as post-grab actions
TheJoeFin Mar 1, 2026
376b0fa
Add new user settings for templates and context menu options
TheJoeFin Mar 1, 2026
6fb2a4b
Handle ApplicationDataContainer size limit exception
TheJoeFin Mar 1, 2026
58b7ba3
Add Grab Templates management to Post-Grab Actions editor
TheJoeFin Mar 1, 2026
61af2a0
Add Grab Template creation and overlay support
TheJoeFin Mar 1, 2026
66b7856
Enhance Grab Template editing and region visualization
TheJoeFin Mar 1, 2026
092bf5d
Add inline chip picker control for RichTextBox
TheJoeFin Mar 2, 2026
0a4df29
Add chip picker for template output editing in GrabFrame
TheJoeFin Mar 2, 2026
64875df
Remove unused OutputTemplateBox; enhance TemplateOutputBox
TheJoeFin Mar 2, 2026
dc563f3
Update packages
TheJoeFin Mar 2, 2026
d4b1101
Code style
TheJoeFin Mar 2, 2026
4411659
Add Microsoft.WindowsAppSDK.WinUI package reference
TheJoeFin Mar 2, 2026
7c8c891
Improve GrabFrame content area calculation and DPI accuracy
TheJoeFin Mar 2, 2026
3cd3001
Improve GrabFrame overlay scaling and image cropping accuracy
TheJoeFin Mar 3, 2026
bfe084b
Highlight unused template regions in GrabFrame/preview
TheJoeFin Mar 4, 2026
cdca429
Add Grab Template support to lookup and grab workflows
TheJoeFin Mar 4, 2026
e411b46
Fix ListView scrollbar, improve Post-Grab Actions UI
TheJoeFin Mar 4, 2026
aacf37e
Add regex pattern matching support to Grab Templates
TheJoeFin Mar 4, 2026
20bc475
Fix NullReferenceException in PatternMatchModeDialog
TheJoeFin Mar 4, 2026
6802c6a
Fix ArgumentNullException in CommitSelection when dialog steals focus
TheJoeFin Mar 4, 2026
209ca5f
Skip region overlays in FSG for pattern-only templates
TheJoeFin Mar 4, 2026
5205db2
Refactor GrabTemplate, improve regex usage, cleanup code
TheJoeFin Mar 5, 2026
78c3762
Sync overlay canvas to DIP; use [GeneratedRegex] patterns
TheJoeFin Mar 5, 2026
87e0c38
Initial plan
Copilot Mar 5, 2026
90a8704
fix: address review feedback on docs, error messages, and unused vari…
Copilot Mar 5, 2026
d62d7eb
Merge pull request #627 from TheJoeFin/copilot/sub-pr-620
TheJoeFin Mar 5, 2026
8bf9482
Refactor context menu for post-grab actions and templates
TheJoeFin Mar 5, 2026
d606fec
Synchronize post-grab actions across FullscreenGrab windows
TheJoeFin Mar 6, 2026
2b7bfce
Add fullscreen grab selection styles
TheJoeFin Mar 6, 2026
9c50c38
Harden barcode bitmap handling
TheJoeFin Mar 6, 2026
2df0c51
Improve selection cropping for zoom/pan and add tests
TheJoeFin Mar 6, 2026
d28f1b4
Initial plan
Copilot Mar 7, 2026
28a021d
Fix 5 review comments: shortcut cap, ItemsDictionary clear, null Item…
Copilot Mar 7, 2026
27df35c
Merge pull request #629 from TheJoeFin/copilot/sub-pr-620
TheJoeFin Mar 7, 2026
6952739
Switch pattern match mode selection to RadioButtons
TheJoeFin Mar 7, 2026
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
44 changes: 44 additions & 0 deletions Tests/BarcodeUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using Text_Grab;
using Text_Grab.Models;
using Text_Grab.Utilities;
using Windows.Storage.Streams;

namespace Tests;

public class BarcodeUtilitiesTests
{
[Fact]
public void TryToReadBarcodes_WithDisposedBitmap_ReturnsEmptyBarcodeOutput()
{
Bitmap disposedBitmap = new(8, 8);
disposedBitmap.Dispose();

OcrOutput result = BarcodeUtilities.TryToReadBarcodes(disposedBitmap);

Assert.Equal(OcrOutputKind.Barcode, result.Kind);
Assert.Equal(string.Empty, result.RawOutput);
}

[Fact]
public async Task GetBitmapFromIRandomAccessStream_ReturnsBitmapIndependentOfSourceStream()
{
using Bitmap sourceBitmap = new(8, 8);
sourceBitmap.SetPixel(0, 0, Color.Red);

using MemoryStream memoryStream = new();
sourceBitmap.Save(memoryStream, ImageFormat.Png);

using InMemoryRandomAccessStream randomAccessStream = new();
_ = await randomAccessStream.WriteAsync(memoryStream.ToArray().AsBuffer());

Bitmap clonedBitmap = ImageMethods.GetBitmapFromIRandomAccessStream(randomAccessStream);

Assert.Equal(8, clonedBitmap.Width);
Assert.Equal(8, clonedBitmap.Height);
Assert.Equal(Color.Red.ToArgb(), clonedBitmap.GetPixel(0, 0).ToArgb());
}
}
63 changes: 63 additions & 0 deletions Tests/FreeformCaptureUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Drawing;
using System.Windows;
using System.Windows.Media;
using Text_Grab.Utilities;
using Point = System.Windows.Point;

namespace Tests;

public class FreeformCaptureUtilitiesTests
{
[WpfFact]
public void GetBounds_RoundsOutwardToIncludeAllPoints()
{
List<Point> points =
[
new(1.2, 2.8),
new(10.1, 4.2),
new(4.6, 9.9)
];

Rect bounds = FreeformCaptureUtilities.GetBounds(points);

Assert.Equal(new Rect(new Point(1, 2), new Point(11, 10)), bounds);
}

[WpfFact]
public void BuildGeometry_CreatesClosedFigure()
{
List<Point> points =
[
new(0, 0),
new(4, 0),
new(4, 4)
];

PathGeometry geometry = FreeformCaptureUtilities.BuildGeometry(points);

Assert.Single(geometry.Figures);
Assert.Equal(points[0], geometry.Figures[0].StartPoint);
Assert.True(geometry.Figures[0].IsClosed);
Assert.Equal(2, geometry.Figures[0].Segments.Count);
}

[WpfFact]
public void CreateMaskedBitmap_WhitensPixelsOutsideThePolygon()
{
using Bitmap sourceBitmap = new(10, 10);
using Graphics graphics = Graphics.FromImage(sourceBitmap);
graphics.Clear(System.Drawing.Color.Black);

using Bitmap maskedBitmap = FreeformCaptureUtilities.CreateMaskedBitmap(
sourceBitmap,
[
new Point(2, 2),
new Point(7, 2),
new Point(7, 7),
new Point(2, 7)
]);

Assert.Equal(System.Drawing.Color.Gray.ToArgb(), maskedBitmap.GetPixel(0, 0).ToArgb());
Assert.Equal(System.Drawing.Color.Black.ToArgb(), maskedBitmap.GetPixel(4, 4).ToArgb());
}
}
32 changes: 32 additions & 0 deletions Tests/FullscreenCaptureResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Windows;
using Text_Grab;
using Text_Grab.Models;

namespace Tests;

public class FullscreenCaptureResultTests
{
[Theory]
[InlineData(FsgSelectionStyle.Region, true)]
[InlineData(FsgSelectionStyle.Window, true)]
[InlineData(FsgSelectionStyle.Freeform, false)]
[InlineData(FsgSelectionStyle.AdjustAfter, true)]
public void SupportsTemplateActions_MatchesSelectionStyle(FsgSelectionStyle selectionStyle, bool expected)
{
FullscreenCaptureResult result = new(selectionStyle, Rect.Empty);

Assert.Equal(expected, result.SupportsTemplateActions);
}

[Theory]
[InlineData(FsgSelectionStyle.Region, true)]
[InlineData(FsgSelectionStyle.Window, false)]
[InlineData(FsgSelectionStyle.Freeform, false)]
[InlineData(FsgSelectionStyle.AdjustAfter, true)]
public void SupportsPreviousRegionReplay_MatchesSelectionStyle(FsgSelectionStyle selectionStyle, bool expected)
{
FullscreenCaptureResult result = new(selectionStyle, Rect.Empty);

Assert.Equal(expected, result.SupportsPreviousRegionReplay);
}
}
127 changes: 127 additions & 0 deletions Tests/FullscreenGrabPostGrabActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Windows.Controls;
using Text_Grab.Models;
using Text_Grab.Views;
using Wpf.Ui.Controls;
using MenuItem = System.Windows.Controls.MenuItem;

namespace Tests;

public class FullscreenGrabPostGrabActionTests
{
[Fact]
public void GetPostGrabActionKey_UsesTemplateIdForTemplateActions()
{
ButtonInfo action = new("Template Action", "ApplyTemplate_Click", SymbolRegular.Apps24, DefaultCheckState.Off)
{
TemplateId = "template-123"
};

string key = FullscreenGrab.GetPostGrabActionKey(action);

Assert.Equal("template:template-123", key);
}

[Fact]
public void GetPostGrabActionKey_FallsBackToButtonTextWhenClickEventMissing()
{
ButtonInfo action = new()
{
ButtonText = "Custom action"
};

string key = FullscreenGrab.GetPostGrabActionKey(action);

Assert.Equal("text:Custom action", key);
}

[WpfFact]
public void GetActionablePostGrabMenuItems_ExcludesUtilityEntriesAndPreservesOrder()
{
ContextMenu contextMenu = new();
MenuItem firstAction = new()
{
Header = "First action",
Tag = new ButtonInfo("First action", "First_Click", SymbolRegular.Apps24, DefaultCheckState.Off)
};
MenuItem utilityItem = new()
{
Header = "Customize",
Tag = "EditPostGrabActions"
};
MenuItem secondAction = new()
{
Header = "Second action",
Tag = new ButtonInfo("Second action", "Second_Click", SymbolRegular.Apps24, DefaultCheckState.Off)
};

contextMenu.Items.Add(firstAction);
contextMenu.Items.Add(new Separator());
contextMenu.Items.Add(utilityItem);
contextMenu.Items.Add(secondAction);
contextMenu.Items.Add(new MenuItem
{
Header = "Close this menu",
Tag = "ClosePostGrabMenu"
});

List<MenuItem> actionableItems = FullscreenGrab.GetActionablePostGrabMenuItems(contextMenu);

Assert.Collection(actionableItems,
item => Assert.Same(firstAction, item),
item => Assert.Same(secondAction, item));
}

[WpfFact]
public void BuildPostGrabActionSnapshot_KeepsChangedTemplateCheckedAndUnchecksOthers()
{
ButtonInfo regularAction = new("Trim each line", "TrimEachLine_Click", SymbolRegular.Apps24, DefaultCheckState.Off);
ButtonInfo firstTemplate = new("Template A", "ApplyTemplate_Click", SymbolRegular.Apps24, DefaultCheckState.Off)
{
TemplateId = "template-a"
};
ButtonInfo secondTemplate = new("Template B", "ApplyTemplate_Click", SymbolRegular.Apps24, DefaultCheckState.Off)
{
TemplateId = "template-b"
};

Dictionary<string, bool> snapshot = FullscreenGrab.BuildPostGrabActionSnapshot(
[
new MenuItem { Tag = regularAction, IsCheckable = true, IsChecked = true },
new MenuItem { Tag = firstTemplate, IsCheckable = true, IsChecked = true },
new MenuItem { Tag = secondTemplate, IsCheckable = true, IsChecked = false }
],
FullscreenGrab.GetPostGrabActionKey(secondTemplate),
true);

Assert.True(snapshot[FullscreenGrab.GetPostGrabActionKey(regularAction)]);
Assert.False(snapshot[FullscreenGrab.GetPostGrabActionKey(firstTemplate)]);
Assert.True(snapshot[FullscreenGrab.GetPostGrabActionKey(secondTemplate)]);
}

[Fact]
public void ShouldPersistLastUsedState_ForForcedSourceAction_ReturnsTrue()
{
ButtonInfo lastUsedAction = new("Remove duplicate lines", "RemoveDuplicateLines_Click", SymbolRegular.Apps24, DefaultCheckState.LastUsed);

bool shouldPersist = FullscreenGrab.ShouldPersistLastUsedState(
lastUsedAction,
previousChecked: true,
isChecked: true,
forcePersistActionKey: FullscreenGrab.GetPostGrabActionKey(lastUsedAction));

Assert.True(shouldPersist);
}

[Fact]
public void ShouldPersistLastUsedState_DoesNotPersistUnchangedNonSourceAction()
{
ButtonInfo lastUsedAction = new("Remove duplicate lines", "RemoveDuplicateLines_Click", SymbolRegular.Apps24, DefaultCheckState.LastUsed);

bool shouldPersist = FullscreenGrab.ShouldPersistLastUsedState(
lastUsedAction,
previousChecked: true,
isChecked: true);

Assert.False(shouldPersist);
}
}
74 changes: 74 additions & 0 deletions Tests/FullscreenGrabSelectionStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Windows;
using Text_Grab;
using Text_Grab.Models;
using Text_Grab.Views;

namespace Tests;

public class FullscreenGrabSelectionStyleTests
{
[Theory]
[InlineData(FsgSelectionStyle.Window, false, true)]
[InlineData(FsgSelectionStyle.Window, true, true)]
[InlineData(FsgSelectionStyle.Region, true, true)]
[InlineData(FsgSelectionStyle.Region, false, false)]
[InlineData(FsgSelectionStyle.Freeform, false, false)]
[InlineData(FsgSelectionStyle.AdjustAfter, false, false)]
public void ShouldKeepTopToolbarVisible_MatchesSelectionState(
FsgSelectionStyle selectionStyle,
bool isAwaitingAdjustAfterCommit,
bool expected)
{
bool shouldKeepVisible = FullscreenGrab.ShouldKeepTopToolbarVisible(
selectionStyle,
isAwaitingAdjustAfterCommit);

Assert.Equal(expected, shouldKeepVisible);
}

[Theory]
[InlineData(FsgSelectionStyle.Region, true)]
[InlineData(FsgSelectionStyle.Window, false)]
[InlineData(FsgSelectionStyle.Freeform, false)]
[InlineData(FsgSelectionStyle.AdjustAfter, true)]
public void ShouldUseOverlayCutout_MatchesSelectionStyle(FsgSelectionStyle selectionStyle, bool expected)
{
bool shouldUseCutout = FullscreenGrab.ShouldUseOverlayCutout(selectionStyle);

Assert.Equal(expected, shouldUseCutout);
}

[Theory]
[InlineData(FsgSelectionStyle.Region, true)]
[InlineData(FsgSelectionStyle.Window, false)]
[InlineData(FsgSelectionStyle.Freeform, false)]
[InlineData(FsgSelectionStyle.AdjustAfter, true)]
public void ShouldDrawSelectionOutline_MatchesSelectionStyle(FsgSelectionStyle selectionStyle, bool expected)
{
bool shouldDrawOutline = FullscreenGrab.ShouldDrawSelectionOutline(selectionStyle);

Assert.Equal(expected, shouldDrawOutline);
}

[Fact]
public void ShouldCommitWindowSelection_RequiresSameWindowHandleOnMouseUp()
{
WindowSelectionCandidate pressedCandidate = new((nint)1, new Rect(0, 0, 40, 40), "Target", 100);
WindowSelectionCandidate releasedSameCandidate = new((nint)1, new Rect(0, 0, 40, 40), "Target", 100);
WindowSelectionCandidate releasedDifferentCandidate = new((nint)2, new Rect(0, 0, 40, 40), "Other", 200);

Assert.True(FullscreenGrab.ShouldCommitWindowSelection(pressedCandidate, releasedSameCandidate));
Assert.False(FullscreenGrab.ShouldCommitWindowSelection(pressedCandidate, releasedDifferentCandidate));
Assert.False(FullscreenGrab.ShouldCommitWindowSelection(pressedCandidate, null));
Assert.False(FullscreenGrab.ShouldCommitWindowSelection(null, releasedSameCandidate));
}

[Fact]
public void WindowSelectionCandidate_DisplayText_UsesFallbacksWhenMetadataMissing()
{
WindowSelectionCandidate candidate = new((nint)1, new Rect(0, 0, 40, 40), string.Empty, 100);

Assert.Equal("Application", candidate.DisplayAppName);
Assert.Equal("Untitled window", candidate.DisplayTitle);
}
}
Loading
Loading