vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
namespace KillerPDF.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// What SearchController needs from the window hosting it, beyond the shared shell services.
|
||||
/// Values and intents only, never a control - the highlight painting itself stays in the
|
||||
/// shell (Shell/Search.cs), because it draws onto the page overlay canvases.
|
||||
/// </summary>
|
||||
internal interface ISearchHost : IShellServices
|
||||
{
|
||||
/// <summary>Path of the working (temp) file the search reads. Null when no document.</summary>
|
||||
string? CurrentFile { get; }
|
||||
|
||||
/// <summary>The page currently selected in the sidebar/page list.</summary>
|
||||
int CurrentPageIndex { get; }
|
||||
|
||||
/// <summary>Navigates to the page (the match being stepped to lives there).</summary>
|
||||
void GoToPage(int pageIdx);
|
||||
|
||||
/// <summary>Writes the result counter's text only ("", "No matches", "Search error"),
|
||||
/// leaving the tooltip alone - mirrors what the old code did on those paths.</summary>
|
||||
void SetResultText(string text);
|
||||
|
||||
/// <summary>Writes the "12 / 73" counter and its page-breakdown tooltip.</summary>
|
||||
void SetResultCount(string text, string? tooltip);
|
||||
|
||||
/// <summary>Removes every highlight rectangle from the page overlays.</summary>
|
||||
void ClearHighlights();
|
||||
|
||||
/// <summary>Repaints highlights on every page on screen right now, with the current
|
||||
/// match emphasized.</summary>
|
||||
void RepaintHighlights();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using KillerPDF.Services;
|
||||
|
||||
namespace KillerPDF.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// The document-search state machine: runs SearchService over the working file, keeps the
|
||||
/// per-page rect map and the flat reading-ordered match list, and steps the match cursor.
|
||||
/// Moved out of Search.cs (MainWindow) in the KillerUI refactor.
|
||||
///
|
||||
/// Holds no controls. Talks to the window only through <see cref="ISearchHost"/>; the search
|
||||
/// bar UI, debounce, and the highlight painting stay in the shell half (Shell/Search.cs).
|
||||
/// </summary>
|
||||
internal sealed class SearchController
|
||||
{
|
||||
private readonly ISearchHost _host;
|
||||
private readonly SearchService _searchService = new();
|
||||
|
||||
internal SearchController(ISearchHost host) => _host = host;
|
||||
|
||||
// Whole-document search results (PDF-space rects per page). Settable references on
|
||||
// purpose: Tabs.cs parks these per tab and swaps them back on a tab switch, exactly as
|
||||
// it did when they were MainWindow fields.
|
||||
internal Dictionary<int, List<(double left, double bottom, double right, double top)>> AllSearchRects { get; set; } = [];
|
||||
internal List<int> ResultPages { get; set; } = [];
|
||||
internal int PageCursor { get; set; } = -1;
|
||||
|
||||
// Flat, reading-ordered list of every match (page + rect) so Enter steps word-by-word rather
|
||||
// than page-by-page; _matchCursor indexes it and that match is drawn with extra emphasis.
|
||||
private readonly List<(int page, double left, double bottom, double right, double top)> _matches = [];
|
||||
private int _matchCursor = -1;
|
||||
private int _totalHits;
|
||||
|
||||
/// <summary>True while any page has result rects - the F3 and repaint-on-page-change gate.</summary>
|
||||
internal bool HasResults => AllSearchRects.Count > 0;
|
||||
|
||||
/// <summary>The emphasized match, or null when the cursor is not on one.</summary>
|
||||
internal (int page, double left, double bottom, double right, double top)? CurrentMatch =>
|
||||
_matchCursor >= 0 && _matchCursor < _matches.Count ? _matches[_matchCursor] : null;
|
||||
|
||||
internal bool TryGetPageRects(int page,
|
||||
out List<(double left, double bottom, double right, double top)> rects) =>
|
||||
AllSearchRects.TryGetValue(page, out rects!);
|
||||
|
||||
internal IEnumerable<int> PagesWithResults => AllSearchRects.Keys;
|
||||
|
||||
/// <summary>The query-too-short reset (search box text dropped under 2 chars).</summary>
|
||||
internal void ClearMatches()
|
||||
{
|
||||
AllSearchRects.Clear();
|
||||
ResultPages.Clear();
|
||||
_matches.Clear();
|
||||
_matchCursor = -1;
|
||||
PageCursor = -1;
|
||||
}
|
||||
|
||||
/// <summary>The new/closed-document reset - exactly the three things FileOperations
|
||||
/// cleared when these were fields (the match list is rebuilt by the next Run).</summary>
|
||||
internal void ClearPageResults()
|
||||
{
|
||||
AllSearchRects.Clear();
|
||||
ResultPages.Clear();
|
||||
PageCursor = -1;
|
||||
}
|
||||
|
||||
internal void Run(string query)
|
||||
{
|
||||
_host.ClearHighlights();
|
||||
AllSearchRects.Clear();
|
||||
ResultPages.Clear();
|
||||
_matches.Clear();
|
||||
_matchCursor = -1;
|
||||
PageCursor = -1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query) || _host.CurrentFile is null)
|
||||
{
|
||||
_host.SetResultText("");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sr = _searchService.Search(_host.CurrentFile, query);
|
||||
|
||||
foreach (var kvp in sr.PageRects)
|
||||
AllSearchRects[kvp.Key] = kvp.Value;
|
||||
ResultPages.AddRange(sr.ResultPages);
|
||||
|
||||
// Flatten every match into one reading-ordered list (page asc, then top-to-bottom,
|
||||
// then left-to-right) so navigation steps word-by-word across the whole document.
|
||||
foreach (var page in ResultPages)
|
||||
foreach (var (left, bottom, right, top) in AllSearchRects[page].OrderByDescending(r => r.top).ThenBy(r => r.left))
|
||||
_matches.Add((page, left, bottom, right, top));
|
||||
|
||||
if (_matches.Count == 0)
|
||||
{
|
||||
_host.SetResultText(_host.Loc("Str_Search_NoMatches"));
|
||||
return;
|
||||
}
|
||||
|
||||
_totalHits = sr.TotalHits;
|
||||
|
||||
// Start at the first match on or after the current page.
|
||||
int startPage = _host.CurrentPageIndex;
|
||||
_matchCursor = _matches.FindIndex(m => m.page >= startPage);
|
||||
if (_matchCursor < 0) _matchCursor = 0;
|
||||
|
||||
GoToCurrentMatch();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_host.SetResultText(_host.Loc("Str_Search_Error"));
|
||||
}
|
||||
}
|
||||
|
||||
internal void Next()
|
||||
{
|
||||
if (_matches.Count == 0) return;
|
||||
_matchCursor = (_matchCursor + 1) % _matches.Count;
|
||||
GoToCurrentMatch();
|
||||
}
|
||||
|
||||
internal void Prev()
|
||||
{
|
||||
if (_matches.Count == 0) return;
|
||||
_matchCursor = (_matchCursor - 1 + _matches.Count) % _matches.Count;
|
||||
GoToCurrentMatch();
|
||||
}
|
||||
|
||||
// Navigates to the current match's page (if needed), updates the counter, and repaints
|
||||
// highlights with the current match emphasized. Shared by Run and Next/Prev.
|
||||
private void GoToCurrentMatch()
|
||||
{
|
||||
if (_matchCursor < 0 || _matchCursor >= _matches.Count) return;
|
||||
int targetPage = _matches[_matchCursor].page;
|
||||
PageCursor = ResultPages.IndexOf(targetPage); // keep the persisted page-cursor sane
|
||||
UpdateStatus();
|
||||
if (_host.CurrentPageIndex != targetPage)
|
||||
_host.GoToPage(targetPage);
|
||||
_host.RepaintHighlights();
|
||||
}
|
||||
|
||||
// Compact count ("12 / 73" = current match / total matches); page breakdown in the tooltip.
|
||||
private void UpdateStatus()
|
||||
{
|
||||
if (_matches.Count == 0)
|
||||
{
|
||||
_host.SetResultCount(_host.Loc("Str_Search_NoMatches"), null);
|
||||
return;
|
||||
}
|
||||
int pages = ResultPages.Count;
|
||||
_host.SetResultCount($"{_matchCursor + 1} / {_matches.Count}",
|
||||
string.Format(_host.Loc("Str_Search_Summary"), _matches.Count, pages));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user