vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF

This commit is contained in:
2026-08-27 06:58:22 +02:00
commit 532485a830
577 changed files with 149058 additions and 0 deletions
+382
View File
@@ -0,0 +1,382 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Docnet.Core;
using Docnet.Core.Models;
using Microsoft.Win32;
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.IO;
using KillerPDF.Services;
using PdfPigDoc = UglyToad.PdfPig.PdfDocument;
namespace KillerPDF
{
public partial class MainWindow
{
// ============================================================
// Tool selection
// ============================================================
// Maps an editing tool to its mouse cursor. Shared by SetTool and by the
// per-page overlay creation so freshly rendered tiles get the right cursor.
internal static Cursor CursorForTool(EditTool tool) => tool switch
{
EditTool.Text => Cursors.IBeam,
EditTool.Highlight => Cursors.Cross,
EditTool.Strikethrough => Cursors.Cross,
EditTool.Underline => Cursors.Cross,
EditTool.Draw => Cursors.Pen,
EditTool.Line => Cursors.Cross,
EditTool.Shape => Cursors.Cross,
EditTool.Signature => Cursors.Pen,
EditTool.Image => Cursors.Hand,
EditTool.Crop => Cursors.Cross,
EditTool.Rotate => Cursors.Cross,
_ => Cursors.Arrow
};
private void SetTool(EditTool tool, bool restoringPane = false)
{
// Re-clicking the tool that owns the visible annotate bar tucks the bar away (or brings it
// back) instead of rebuilding it - no flicker, and a quick way to get it out of the way.
bool reclickedAnnotTool = !restoringPane && tool == _currentTool && tool == _annotBarTool
&& (_textSettingsBar is not null || _drawSettingsBar is not null || _cropConfirmBar is not null);
if (reclickedAnnotTool)
{
ToggleAnnotBarMinimized();
return;
}
// Continuous view now supports annotation tools inline via per-page overlays.
CommitActiveTextBox();
ClearTextSelection();
CancelShapePolygon(); // abandon an in-progress polygon when switching tools
if (tool != EditTool.Draw) HideBrushPreview(); // drop the brush cursor when leaving Draw
_currentTool = tool;
var map = new (Button btn, EditTool t)[]
{
(_toolSelectBtn, EditTool.Select),
(_toolTextBtn, EditTool.Text),
(_toolHighlightBtn, EditTool.Highlight),
(_toolUnderlineBtn, EditTool.Line), // the old Underline button is now the Line tool
(_toolDrawBtn, EditTool.Draw),
(_toolShapeBtn, EditTool.Shape),
(_toolSignatureBtn, EditTool.Signature),
(_toolImageBtn, EditTool.Image),
(_toolCropBtn, EditTool.Crop),
(_toolRotateBtn, EditTool.Rotate)
};
foreach (var (btn, t) in map)
{
if (t == tool)
{
btn.SetResourceReference(Control.BackgroundProperty, "SelectionBg");
btn.SetResourceReference(Control.ForegroundProperty, "SelectionFg");
}
else
{
// Clear local values so the ToolbarButton style (incl. its hover trigger) applies.
// Setting Background locally here would override the style and kill the hover.
btn.ClearValue(Control.BackgroundProperty);
btn.ClearValue(Control.ForegroundProperty);
}
}
// Apply the tool cursor to every page surface, not just the primary page.
// In Grid / Two-Page / Continuous modes the secondary tiles are separate
// overlay canvases tracked in _continuousCanvases; without this they keep
// the default arrow cursor while only page 1 (_annotationCanvas) updates.
var toolCursor = CursorForTool(tool);
_annotationCanvas.Cursor = toolCursor;
foreach (var overlay in _continuousCanvases.Values)
overlay.Cursor = toolCursor;
// Show/hide draw settings bar
if ((tool is EditTool.Draw or EditTool.Line or EditTool.Shape) || tool == EditTool.Highlight
|| tool == EditTool.Strikethrough || tool == EditTool.Underline)
ShowDrawSettings(tool);
else
HideDrawSettings();
// Show/hide text tool settings bar
if (tool == EditTool.Text)
ShowTextSettings();
else
HideTextSettings();
// Hide signature popup when switching away
if (tool != EditTool.Signature)
{
HideSignaturePopup();
_pendingSignature = null;
}
// Dismiss crop confirm bar when switching away from Crop; entering Crop drops a default box + bar.
if (tool != EditTool.Crop)
HideCropConfirmBar();
else if (_cropConfirmBar is null)
ShowDefaultCropBox();
// NOTE: deliberately NOT reflowing the toolbar here. Reflowing on every tool switch at a narrow
// width visibly thrashes the whole bar. The active-tool protection still runs on resize (the
// next time the window changes width the active tool is pulled back onto the bar), which the
// user preferred over the jank.
UpdateOverflowActiveHighlight(); // mark the active tool in the overflow menu (cheap, no reflow)
}
// Tints the active tool's row in the overflow menu with the selection colors, so when a tool lives
// in the chevron (collapsed off the bar) you can still see which one is active - same cue as the
// highlighted icon on the bar. No-op for the rows that aren't tools.
private void UpdateOverflowActiveHighlight()
{
var map = new (Button mi, EditTool t)[]
{
(MiText, EditTool.Text), (MiUnderline, EditTool.Line), (MiHighlight, EditTool.Highlight),
(MiDraw, EditTool.Draw), (MiImage, EditTool.Image), (MiCrop, EditTool.Crop),
(MiSignature, EditTool.Signature),
};
foreach (var (mi, t) in map)
{
if (mi is null) continue;
bool active = t == _currentTool;
if (active) mi.SetResourceReference(Control.BackgroundProperty, "SelectionBg");
else mi.ClearValue(Control.BackgroundProperty);
if (mi.Content is Panel sp)
foreach (var ch in sp.Children)
if (ch is TextBlock tb)
tb.SetResourceReference(TextBlock.ForegroundProperty, active ? "SelectionFg" : "TextBrush");
}
}
private void SidebarToggle_Click(object sender, RoutedEventArgs e)
{
// A toggle from the USER makes the state deliberate: a closed sidebar then stays
// closed until the user opens it - SyncSidebarToDocState may only undo its own
// automatic collapse (see _sidebarAutoCollapsed).
if (!_sidebarAutoToggling) _sidebarAutoCollapsed = false;
_sidebarCollapsed = !_sidebarCollapsed;
if (_sidebarCollapsed)
{
// Save current width before collapsing so expand restores it.
if (_sidebarCol.ActualWidth > 24)
{
if (_sidebarShowingOutlines)
_savedOutlinesWidth = Math.Min(_sidebarCol.ActualWidth, SbPx(SidebarMaxOutlines));
else
_savedPagesWidth = Math.Min(_sidebarCol.ActualWidth, SbPx(SidebarMaxPages));
}
_sidebarToggleBtn.ToolTip = Loc("Str_TT_ExpandSidebar");
// Glide shut sliding, not squishing: freeze the content at its open width so
// the shrinking border CLIPS it (thumbnails hold their size), then hide the
// border once the strip width is reached. MinWidth drops first so the
// animation isn't clamped at the readable floor.
BeginSidebarSlide(SidebarContentPanel.ActualWidth);
_sidebarCol.MinWidth = SbPx(24);
AnimateSidebarWidth(SbPx(24), () =>
{
_sidebarBorder.Visibility = Visibility.Collapsed;
EndSidebarSlide();
});
// Splitter stays enabled so the user can grab it and drag the sidebar back open.
}
else
{
_sidebarBorder.Visibility = Visibility.Visible;
double restore = _sidebarShowingOutlines ? _savedOutlinesWidth : _savedPagesWidth;
// Slide in at full size: content fixed at the target width from the first
// frame, revealed by the growing border instead of reflowing up to size.
BeginSidebarSlide(_sbSlideContentW > 0 ? _sbSlideContentW
: Math.Max(0, restore / Math.Max(0.01, _appScale) - 24));
AnimateSidebarWidth(restore, () =>
{
_sidebarCol.MinWidth = SbPx(SidebarMinOpen); // open: clamp so the list can't be dragged below readable
EndSidebarSlide();
});
_sidebarToggleBtn.ToolTip = Loc("Str_TT_CollapseSidebar");
SidebarSplitter.IsEnabled = true;
}
UpdateSidebarToggleGlyph();
}
// Sidebar slide freeze: while the column animates, the content keeps a FIXED width
// (anchored at the outer edge) and the border clips it - so thumbnails and labels
// hold their size and slide out of view instead of reflowing every frame. Restored
// to normal stretch layout when the glide lands.
private double _sbSlideContentW;
private void BeginSidebarSlide(double contentW)
{
if (contentW <= 0) return;
_sbSlideContentW = contentW;
SidebarContentPanel.Width = contentW;
SidebarContentPanel.HorizontalAlignment =
_sidebarRight ? HorizontalAlignment.Right : HorizontalAlignment.Left;
_sidebarBorder.ClipToBounds = true;
}
private void EndSidebarSlide()
{
SidebarContentPanel.Width = double.NaN;
SidebarContentPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
_sidebarBorder.ClipToBounds = false;
}
// Animates the sidebar column between widths (toggle collapse / expand). No explicit
// re-render here: every frame fires PagePreviewPanel_SizeChanged, whose lite fit keeps
// the page tracking the pane smoothly and whose settle timer runs ONE crisp pass a beat
// after the last frame - exactly the splitter-drag pipeline. An extra RefreshPageView at
// Completed doubled up with that settle pass and read as a two-step stutter. Completion
// clears the animation and writes the target as a plain local value so the splitter,
// side flip, and full screen can keep setting Width directly.
private void AnimateSidebarWidth(double target, Action? onDone = null)
{
var anim = new GridLengthAnimation
{
From = new GridLength(Math.Max(0, _sidebarCol.ActualWidth)),
To = new GridLength(target),
Duration = TimeSpan.FromMilliseconds(280),
Easing = new System.Windows.Media.Animation.CubicEase
{ EasingMode = System.Windows.Media.Animation.EasingMode.EaseInOut },
};
anim.Completed += (_, _) =>
{
_sidebarCol.BeginAnimation(ColumnDefinition.WidthProperty, null);
_sidebarCol.Width = new GridLength(target);
onDone?.Invoke();
};
_sidebarCol.BeginAnimation(ColumnDefinition.WidthProperty, anim);
}
// Pressing the splitter while the sidebar is collapsed begins pulling it open: reveal the page
// list (still 0-width against the 24px strip, so there's no flash) so the drag grows it live. If
// the user doesn't drag past the threshold, OnSidebarResized snaps it shut again on release.
private bool _sidebarDragOpening; // true while dragging the splitter open from the collapsed strip
private bool _sidebarWantClose; // set during an open-resize drag when pulled past the close edge
private void OnSidebarSplitterPress()
{
if (!_sidebarCollapsed) return;
// Begin pulling open from the strip. Show the sidebar background + grain right away so the
// growing strip matches the sidebar, but keep the content (header + list) hidden until it's
// pulled to the readable minimum (OnSidebarSplitterMove) - so nothing shows clipped, and the
// column grows from the 24px edge tracking the mouse with no dead zone.
_sidebarCollapsed = false;
_sidebarDragOpening = true;
_sidebarBorder.Visibility = Visibility.Visible;
SidebarContentPanel.Visibility = Visibility.Collapsed;
_sidebarToggleBtn.ToolTip = Loc("Str_TT_CollapseSidebar");
UpdateSidebarToggleGlyph();
}
// Drives the splitter drag. While opening from the strip, reveal the list only once it's past the
// readable minimum. Once open, pulling the mouse well past the minimum closes the sidebar (the
// MinWidth clamp already stops the column from ever shrinking below the readable floor).
private void OnSidebarSplitterMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (_sidebarCollapsed || e.LeftButton != System.Windows.Input.MouseButtonState.Pressed) return;
if (_sidebarDragOpening)
{
SidebarContentPanel.Visibility = _sidebarCol.ActualWidth >= SbPx(SidebarMinOpen)
? Visibility.Visible : Visibility.Collapsed;
return;
}
// The column is clamped at the readable minimum (MinWidth), so it can't clip and the document
// isn't resized as you pull. Just record intent; the actual close (and its single re-render)
// happens on release in OnSidebarResized - so dragging the splitter never re-renders the page.
double mx = e.GetPosition(MainContentGrid).X;
const double closeBuffer = 36; // pull this far past the minimum edge to close on release
_sidebarWantClose = _sidebarRight
? mx > MainContentGrid.ActualWidth - SbPx(SidebarMinOpen) + closeBuffer
: mx < SbPx(SidebarMinOpen) - closeBuffer;
}
// Called when the user finishes dragging the sidebar splitter. If they dragged it narrower than
// the close threshold, snap it fully closed (to the toggle strip) instead of leaving an unusable
// sliver; otherwise remember the new width for the current mode.
private void OnSidebarResized()
{
_sidebarDragOpening = false;
if (_sidebarCollapsed) { _sidebarWantClose = false; return; }
if (_sidebarWantClose) { _sidebarWantClose = false; CollapseSidebarToStrip(); return; }
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)(() =>
{
if (_sidebarCollapsed) return;
double w = _sidebarCol.ActualWidth;
if (w < SbPx(SidebarMinOpen))
{
// Released below the readable minimum: snap to whichever end the user let go nearer
// to - fully closed, or the minimum open width.
double mid = SbPx((24 + SidebarMinOpen) / 2.0);
if (w < mid) { CollapseSidebarToStrip(); return; }
_sidebarCol.Width = new GridLength(SbPx(SidebarMinOpen));
w = SbPx(SidebarMinOpen);
}
_sidebarBorder.Visibility = Visibility.Visible; // ensure the list shows when settled open
SidebarContentPanel.Visibility = Visibility.Visible;
_sidebarCol.MinWidth = SbPx(SidebarMinOpen); // clamp future resizes so they can't clip
if (_sidebarShowingOutlines) _savedOutlinesWidth = Math.Min(w, SbPx(SidebarMaxOutlines));
else _savedPagesWidth = Math.Min(w, SbPx(SidebarMaxPages));
}));
}
// Collapse to the 24px toggle strip without overwriting the saved width, so re-expand restores the
// last good size rather than the thin dragged one. Mirrors SidebarToggle_Click's collapse branch.
private void CollapseSidebarToStrip()
{
_sidebarCollapsed = true;
_sidebarToggleBtn.ToolTip = Loc("Str_TT_ExpandSidebar");
_sidebarBorder.Visibility = Visibility.Collapsed;
SidebarContentPanel.Visibility = Visibility.Visible; // reset so the next border-show has content
_sidebarCol.Width = new GridLength(SbPx(24));
_sidebarCol.MinWidth = SbPx(24);
UpdateSidebarToggleGlyph(); // splitter stays enabled so it can be dragged back open
}
// The rail auto-collapses when no PDF is open and re-opens when one is (2026-07-23): with
// nothing loaded there are no thumbnails to show, so the strip gets out of the way. The empty
// page-jump box + "/ -" hide alongside. Only fires on the open/close transition (FinishOpenFile /
// ShowEmptyState) and at startup - NOT on tab switches - so a manual toggle mid-session sticks.
// startup=true collapses instantly (no glide before the first paint); runtime transitions animate.
//
// A sidebar the USER closed stays closed: the auto-open may only undo this method's OWN
// collapse. Materializing a lazy tab runs the document-open transition too, so without the
// distinction a tab click was popping a deliberately closed sidebar back open.
private bool _sidebarAutoCollapsed; // the current collapse was made HERE, not by the user
private bool _sidebarAutoToggling; // this method is driving SidebarToggle_Click
private void SyncSidebarToDocState(bool hasDoc, bool startup)
{
if (PageControlsRow != null)
PageControlsRow.Visibility = (hasDoc && !_sidebarShowingOutlines)
? Visibility.Visible : Visibility.Collapsed;
_sidebarAutoToggling = true;
try
{
if (hasDoc && _sidebarCollapsed && _sidebarAutoCollapsed)
{
_sidebarAutoCollapsed = false;
SidebarToggle_Click(this, new RoutedEventArgs()); // open the rail for the document
}
else if (!hasDoc && !_sidebarCollapsed)
{
_sidebarAutoCollapsed = true;
if (startup) CollapseSidebarToStrip(); // instant at launch
else SidebarToggle_Click(this, new RoutedEventArgs()); // animated on last-tab close
}
}
finally { _sidebarAutoToggling = false; }
}
}
}