vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user