Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6f93d78
Add PDF OCR support and context menu integration
TheJoeFin Apr 25, 2026
be3b874
Add PDF text selection and navigation to GrabFrame
TheJoeFin Apr 25, 2026
3d49a18
Add PDF support to GrabFrame and update file checks
TheJoeFin Apr 25, 2026
02fec33
Add tests for file type classification and PDF rendering
TheJoeFin Apr 25, 2026
4a0b99a
Add "Open File..." option to NotifyIcon context menu
TheJoeFin Apr 27, 2026
a81f2dd
Refactor file filters and improve spacebar pan in GrabFrame
TheJoeFin Apr 27, 2026
3ec02aa
Refactor ZoomBorder panning and event handling
TheJoeFin Apr 27, 2026
c2407f7
Enhance file open and drag-and-drop support
TheJoeFin Apr 27, 2026
b2a2efa
Add unit tests for file dialog filters and drag-drop
TheJoeFin Apr 27, 2026
06b256e
Improve pan/zoom UX and focus handling in GrabFrame
TheJoeFin Apr 28, 2026
2766e95
Add clipboard tests
TheJoeFin Apr 30, 2026
da3ee2d
improve the way the clipboard can handle html table data and more
TheJoeFin Apr 30, 2026
a8b61c9
send table data to etw properly
TheJoeFin Apr 30, 2026
5721793
improve perf and options around markdown
TheJoeFin Apr 30, 2026
9e8aeb1
enhance the find and replace to be compatible with etw spreadsheet mode
TheJoeFin Apr 30, 2026
493b304
update github actions
TheJoeFin May 2, 2026
2091e93
remove unused file
TheJoeFin May 2, 2026
38b679f
Simplify Markdig pipeline with UseAdvancedExtensions
TheJoeFin May 2, 2026
703f09f
Fix review comments: disposal, race conditions, cache eviction, find/…
Copilot May 2, 2026
5f4772a
Optimize: materialize native rects once before OCR predicate
Copilot May 2, 2026
d1cabc5
Support PDFs in folder OCR, improve menu usability
TheJoeFin May 2, 2026
fceb621
Add Cut/Copy/Paste support to spreadsheet editor
TheJoeFin May 5, 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
20 changes: 0 additions & 20 deletions .codetesting/AnalysisReport_20260125_220624_678.md

This file was deleted.

10 changes: 5 additions & 5 deletions .github/workflows/Release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
Expand Down Expand Up @@ -231,25 +231,25 @@ jobs:
}

- name: Upload build artifact (x64 framework-dependent)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Text-Grab-win-x64-framework-dependent
path: ${{ env.BUILD_X64 }}

- name: Upload build artifact (x64 self-contained)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Text-Grab-win-x64-self-contained
path: ${{ steps.compute.outputs.archive_x64_sc }}

- name: Upload build artifact (ARM64 framework-dependent)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Text-Grab-win-arm64-framework-dependent
path: ${{ env.BUILD_ARM64 }}

- name: Upload build artifact (ARM64 self-contained)
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Text-Grab-win-arm64-self-contained
path: ${{ steps.compute.outputs.archive_arm64_sc }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/buildDev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
Expand All @@ -33,7 +33,7 @@ jobs:
run: dotnet publish ${{ env.PROJECT_PATH }} -c Release --self-contained -r win-x64 -p:PublishSingleFile=true -p:EnableMsixTooling=true -o publish

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Text-Grab
path: .\publish
137 changes: 137 additions & 0 deletions Tests/ClipboardUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Text_Grab.Utilities;

namespace Tests;

public class ClipboardUtilitiesTests
{
private const string SampleCfHtml = """
Version:1.0
StartHTML:00000097
EndHTML:00002353
StartFragment:00000153
EndFragment:00002320
<!DOCTYPE><HTML><HEAD></HEAD><BODY><!--StartFragment --><html>
<body>
<table>
<tr>
<td>Month</td>
<td>Int</td>
<td>Season</td>
</tr>
<tr>
<td>January</td>
<td>1</td>
<td>Winter</td>
</tr>
<tr>
<td>February</td>
<td>2</td>
<td>Winter</td>
</tr>
</table>
</body>
</html><!--EndFragment --></BODY></HTML>
""";

[Fact]
public void ConvertHtmlToTabSeparated_ParsesBasicTable()
{
string result = ClipboardUtilities.ConvertHtmlToTabSeparated(SampleCfHtml);

string[] lines = result.Split('\n');
Assert.Equal(3, lines.Length);
Assert.Equal("Month\tInt\tSeason", lines[0]);
Assert.Equal("January\t1\tWinter", lines[1]);
Assert.Equal("February\t2\tWinter", lines[2]);
}

[Fact]
public void ConvertHtmlToTabSeparated_HandlesBrTag()
{
string html = """
<!--StartFragment--><table>
<tr><td>4<br/>A</td><td>Spring</td></tr>
</table><!--EndFragment-->
""";

string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html);

Assert.Equal("4 A\tSpring", result);
}

[Fact]
public void ConvertHtmlToTabSeparated_ReturnsEmptyWhenNoTable()
{
string html = "<!--StartFragment--><p>No table here</p><!--EndFragment-->";
string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html);
Assert.Empty(result);
}

[Fact]
public void ConvertHtmlToTabSeparated_DecodesHtmlEntities()
{
string html = """
<!--StartFragment--><table>
<tr><td>A &amp; B</td><td>&lt;tag&gt;</td></tr>
</table><!--EndFragment-->
""";

string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html);

Assert.Equal("A & B\t<tag>", result);
}

[Fact]
public void ConvertHtmlToTabSeparated_HandlesThElements()
{
string html = """
<!--StartFragment--><table>
<tr><th>Name</th><th>Value</th></tr>
<tr><td>Foo</td><td>42</td></tr>
</table><!--EndFragment-->
""";

string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html);

string[] lines = result.Split('\n');
Assert.Equal(2, lines.Length);
Assert.Equal("Name\tValue", lines[0]);
Assert.Equal("Foo\t42", lines[1]);
}

[Fact]
public void ConvertHtmlToTabSeparated_HandlesColspan()
{
string html = """
<!--StartFragment--><table>
<tr><td colspan="2">Merged</td><td>Right</td></tr>
<tr><td>A</td><td>B</td><td>C</td></tr>
</table><!--EndFragment-->
""";

string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html);

string[] lines = result.Split('\n');
Assert.Equal(2, lines.Length);
Assert.Equal("Merged\tMerged\tRight", lines[0]);
Assert.Equal("A\tB\tC", lines[1]);
}

[Fact]
public void ConvertHtmlToTabSeparated_HandlesRowspan()
{
string html = """
<!--StartFragment--><table>
<tr><td rowspan="2">Tall</td><td>Top</td></tr>
<tr><td>Bottom</td></tr>
</table><!--EndFragment-->
""";

string result = ClipboardUtilities.ConvertHtmlToTabSeparated(html);

string[] lines = result.Split('\n');
Assert.Equal(2, lines.Length);
Assert.Equal("Tall\tTop", lines[0]);
Assert.Equal("Tall\tBottom", lines[1]);
}
}
59 changes: 59 additions & 0 deletions Tests/EditTextWindowSpreadsheetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,65 @@ public void ClearSpreadsheetCellValues_ClearsOnlyRequestedCells()
Assert.Equal(string.Empty, dataTable.Rows[1][2]);
}

[Fact]
public void TryCutSpreadsheetCellValues_CopiesThenClearsRequestedCells()
{
DataTable dataTable = new();
dataTable.Columns.Add("A", typeof(string));
dataTable.Columns.Add("B", typeof(string));
dataTable.Columns.Add("C", typeof(string));
dataTable.Rows.Add("a1", "b1", "c1");
dataTable.Rows.Add("a2", "b2", "c2");

string clipboardText = string.Empty;

bool didCut = EditTextWindow.TryCutSpreadsheetCellValues(
dataTable,
[
(1, 2),
(0, 1),
(1, 0),
(0, 1),
(-1, 0),
(5, 5)
],
text =>
{
clipboardText = text;
return true;
});

Assert.True(didCut);
Assert.Equal("b1" + Environment.NewLine + "a2\tc2", clipboardText);
Assert.Equal("a1", dataTable.Rows[0][0]);
Assert.Equal(string.Empty, dataTable.Rows[0][1]);
Assert.Equal("c1", dataTable.Rows[0][2]);
Assert.Equal(string.Empty, dataTable.Rows[1][0]);
Assert.Equal("b2", dataTable.Rows[1][1]);
Assert.Equal(string.Empty, dataTable.Rows[1][2]);
}

[Fact]
public void TryCutSpreadsheetCellValues_DoesNotClearWhenClipboardCopyFails()
{
DataTable dataTable = new();
dataTable.Columns.Add("A", typeof(string));
dataTable.Columns.Add("B", typeof(string));
dataTable.Rows.Add("a1", "b1");

bool didCut = EditTextWindow.TryCutSpreadsheetCellValues(
dataTable,
[
(0, 0),
(0, 1)
],
_ => false);

Assert.False(didCut);
Assert.Equal("a1", dataTable.Rows[0][0]);
Assert.Equal("b1", dataTable.Rows[0][1]);
}

[Fact]
public void BuildSpreadsheetSelectionText_IncludesOnlySelectedCells()
{
Expand Down
91 changes: 90 additions & 1 deletion Tests/FilesIoTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Drawing;
using System.Drawing;
using System.IO;
using System.Windows;
using Text_Grab;
using Text_Grab.Models;
using Text_Grab.Utilities;
Expand Down Expand Up @@ -107,4 +109,91 @@ public void GetEditorModeForPath_UsesFileExtension(string path, EtwEditorMode ex
{
Assert.Equal(expectedMode, IoUtilities.GetEditorModeForPath(path));
}

[Theory]
[InlineData(@"C:\Temp\scan.png", OpenContentKind.Image)]
[InlineData(@"C:\Temp\scan.PDF", OpenContentKind.PdfDocument)]
[InlineData(@"C:\Temp\notes.txt", OpenContentKind.TextFile)]
public void GetOpenContentKindForPath_ClassifiesVisualDocumentsAndText(string path, OpenContentKind expectedKind)
{
Assert.Equal(expectedKind, IoUtilities.GetOpenContentKindForPath(path));
}

[Theory]
[InlineData(".png", true)]
[InlineData(".PDF", true)]
[InlineData(".txt", false)]
[InlineData("", false)]
public void IsVisualDocumentFileExtension_RecognizesImagesAndPdf(string extension, bool expected)
{
Assert.Equal(expected, IoUtilities.IsVisualDocumentFileExtension(extension));
}

[Fact]
public void GetVisualDocumentFilter_IncludesPdfSupport()
{
string filter = FileUtilities.GetVisualDocumentFilter();

Assert.Contains("Image and PDF files|", filter);
Assert.Contains("PDF files|*.pdf", filter);
Assert.Contains("Image files|", filter);
}

[Fact]
public void GetOpenDocumentFilter_IncludesVisualAndTextOptions()
{
string filter = FileUtilities.GetOpenDocumentFilter();

Assert.Contains("Supported documents|", filter);
Assert.Contains("Image and PDF files|", filter);
Assert.Contains("Spreadsheet documents|*.csv;*.tsv;*.tab", filter);
Assert.Contains("Markdown documents|*.md;*.markdown", filter);
Assert.Contains("Text documents (*.txt)|*.txt", filter);
Assert.Contains("All files (*.*)|*.*", filter);
}

[WpfFact]
public void GetDroppedFilePaths_ReturnsExistingFilesOnly()
{
string firstPath = Path.GetTempFileName();
string secondPath = Path.GetTempFileName();
string missingPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.txt");
DataObject dataObject = new(DataFormats.FileDrop, new[] { firstPath, missingPath, secondPath });

try
{
IReadOnlyList<string> paths = App.GetDroppedFilePaths(dataObject);

Assert.Equal([firstPath, secondPath], paths);
}
finally
{
File.Delete(firstPath);
File.Delete(secondPath);
}
}

[WpfFact]
public void GetDroppedFileEffect_ReturnsCopyWhenExistingFilesAreDropped()
{
string path = Path.GetTempFileName();
DataObject dataObject = new(DataFormats.FileDrop, new[] { path });

try
{
Assert.Equal(DragDropEffects.Copy, App.GetDroppedFileEffect(dataObject));
}
finally
{
File.Delete(path);
}
}

[WpfFact]
public void GetDroppedFileEffect_ReturnsNoneWhenNoFilesCanBeOpened()
{
DataObject dataObject = new(DataFormats.Text, "hello");

Assert.Equal(DragDropEffects.None, App.GetDroppedFileEffect(dataObject));
}
}
Loading
Loading