- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc. Fixes edited values staying recoverable by text extraction. - Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode. - Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon. - Remove the upstream author credit and the in-app install button.
271 lines
12 KiB
C#
271 lines
12 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Shapes;
|
|
|
|
namespace MmdPdf
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
// ============================================================
|
|
// Shapes tool (#127 Phase 3)
|
|
// ============================================================
|
|
// Three sub-modes (ShapeKind): Rectangle drags out the translucent filled box the old
|
|
// rectangle-highlight gesture used to make (a HighlightAnnotation, so move/resize/erase/
|
|
// flatten/undo all behave exactly as before); Ellipse drags out an outline ellipse; and
|
|
// Polygon places vertices click by click, closing on the first vertex (lit snap target),
|
|
// on double-click, or via Enter - Esc cancels, Backspace removes the last vertex.
|
|
// Ellipse and Polygon commit as CLOSED InkAnnotations (the last point repeats the first),
|
|
// so they ride the existing ink pipeline: render, hit-test, drag, resize, flatten, export.
|
|
// All three draw with the shared draw bar's color/size/opacity (_drawColor/_drawWidth).
|
|
|
|
private ShapeKind _shapeKind = ShapeKind.Rectangle;
|
|
private bool _shapeFill = true; // fill the inside; toggled in the shape bar
|
|
|
|
// In-progress polygon state. _shapePolyPoints non-empty = a polygon is being placed.
|
|
private List<Point> _shapePolyPoints => ActiveViewer.ShapePolyPointsRef;
|
|
private Polyline? _shapePolyPreview; // committed vertices
|
|
private Polyline? _shapePolyRubber; // last vertex -> cursor, dashed closing preview
|
|
private Ellipse? _shapePolySnapDot; // lit ring over the first vertex when close enough to close
|
|
private int _shapePolyPage = -1;
|
|
private Canvas? _shapePolyCanvas;
|
|
|
|
private const double ShapeSnapPx = 8; // close the polygon when a click lands this near the start
|
|
|
|
/// <summary>Interior color for filled ellipse/polygon shapes: the stroke color at half its
|
|
/// alpha, so the edge still reads against the fill (the filled Box keeps the legacy
|
|
/// full-alpha HighlightAnnotation look instead).</summary>
|
|
private Color ShapeFillColor()
|
|
=> Color.FromArgb((byte)Math.Max(20, _drawColor.A / 2), _drawColor.R, _drawColor.G, _drawColor.B);
|
|
|
|
/// <summary>Mouse-down entry for the Shapes tool (called from Canvas_MouseLeftButtonDown).</summary>
|
|
private void ShapeToolMouseDown(int pageIdx, Point pos, MouseButtonEventArgs e)
|
|
{
|
|
if (_shapeKind == ShapeKind.Polygon)
|
|
{
|
|
// Double-click closes an in-progress polygon (the first click of the pair already
|
|
// added a vertex at this spot; the commit dedupes trailing repeats).
|
|
if (e.ClickCount == 2 && _shapePolyPoints.Count >= 3) CommitShapePolygon();
|
|
else ShapePolyClick(pageIdx, pos);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// Rectangle / Ellipse: drag out a preview, commit on release (CommitShapeDrag).
|
|
ClearSelection();
|
|
_isDrawing = true;
|
|
_drawStart = pos;
|
|
Shape preview;
|
|
if (_shapeKind == ShapeKind.Rectangle && _shapeFill)
|
|
{
|
|
// The old rectangle-highlight look: translucent fill, no stroke.
|
|
preview = new Rectangle { Fill = new SolidColorBrush(_drawColor) };
|
|
}
|
|
else
|
|
{
|
|
preview = _shapeKind == ShapeKind.Rectangle ? new Rectangle() : new Ellipse();
|
|
preview.Stroke = new SolidColorBrush(_drawColor);
|
|
preview.StrokeThickness = _drawWidth;
|
|
preview.Fill = _shapeKind == ShapeKind.Ellipse && _shapeFill
|
|
? new SolidColorBrush(ShapeFillColor())
|
|
: Brushes.Transparent;
|
|
}
|
|
preview.Width = 0;
|
|
preview.Height = 0;
|
|
Canvas.SetLeft(preview, pos.X);
|
|
Canvas.SetTop(preview, pos.Y);
|
|
_activeCanvas.Children.Add(preview);
|
|
_activePreview = preview;
|
|
_activeCanvas.CaptureMouse();
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>Mouse-up commit for the Rectangle / Ellipse drag (called from
|
|
/// Canvas_MouseLeftButtonUp). The preview element carries the final geometry.</summary>
|
|
private void CommitShapeDrag(int pageIdx)
|
|
{
|
|
if (_activePreview is not Shape shp) return;
|
|
double x = Canvas.GetLeft(shp), y = Canvas.GetTop(shp);
|
|
double w = shp.Width, h = shp.Height;
|
|
_activeCanvas?.Children.Remove(shp);
|
|
if (w <= 3 || h <= 3) return; // click or a sliver - nothing to keep
|
|
|
|
if (shp is Rectangle && _shapeFill)
|
|
{
|
|
// Same annotation the old rectangle-highlight gesture produced.
|
|
var ha = new HighlightAnnotation
|
|
{
|
|
PageIndex = pageIdx,
|
|
Bounds = new Rect(x, y, w, h),
|
|
Style = HighlightStyle.Fill
|
|
};
|
|
ha.SetColor(_drawColor);
|
|
AddAnnotation(ha);
|
|
}
|
|
else if (shp is Rectangle)
|
|
{
|
|
// Outline box: a closed 4-corner ink stroke.
|
|
var ink = new InkAnnotation { PageIndex = pageIdx, StrokeWidth = _drawWidth };
|
|
ink.SetColor(_drawColor);
|
|
ink.Points.Add(new Point(x, y));
|
|
ink.Points.Add(new Point(x + w, y));
|
|
ink.Points.Add(new Point(x + w, y + h));
|
|
ink.Points.Add(new Point(x, y + h));
|
|
ink.Points.Add(new Point(x, y));
|
|
AddAnnotation(ink);
|
|
}
|
|
else
|
|
{
|
|
// Ellipse: a closed 64-segment ink stroke centered in the drag rect.
|
|
var ink = new InkAnnotation { PageIndex = pageIdx, StrokeWidth = _drawWidth };
|
|
ink.SetColor(_drawColor);
|
|
if (_shapeFill) ink.SetFillColor(ShapeFillColor());
|
|
double cx = x + w / 2, cy = y + h / 2, rx = w / 2, ry = h / 2;
|
|
for (int k = 0; k <= 64; k++)
|
|
{
|
|
double a = k * 2 * Math.PI / 64;
|
|
ink.Points.Add(new Point(cx + rx * Math.Cos(a), cy + ry * Math.Sin(a)));
|
|
}
|
|
AddAnnotation(ink);
|
|
}
|
|
RenderAllAnnotations(pageIdx);
|
|
}
|
|
|
|
/// <summary>One polygon click: start the shape, add a vertex, or close when the click lands
|
|
/// on the start vertex's snap target.</summary>
|
|
private void ShapePolyClick(int pageIdx, Point pos)
|
|
{
|
|
if (_activeCanvas is null) return;
|
|
|
|
if (_shapePolyPoints.Count == 0)
|
|
{
|
|
ClearSelection();
|
|
_shapePolyPage = pageIdx;
|
|
_shapePolyCanvas = _activeCanvas;
|
|
|
|
_shapePolyPreview = new Polyline
|
|
{
|
|
Stroke = new SolidColorBrush(_drawColor),
|
|
StrokeThickness = _drawWidth,
|
|
StrokeLineJoin = PenLineJoin.Round,
|
|
IsHitTestVisible = false
|
|
};
|
|
_shapePolyPreview.Points.Add(pos);
|
|
|
|
_shapePolyRubber = new Polyline
|
|
{
|
|
Stroke = new SolidColorBrush(Color.FromArgb((byte)Math.Max(60, _drawColor.A / 2),
|
|
_drawColor.R, _drawColor.G, _drawColor.B)),
|
|
StrokeThickness = Math.Max(1, _drawWidth / 2),
|
|
StrokeDashArray = [4, 3],
|
|
IsHitTestVisible = false
|
|
};
|
|
_shapePolyRubber.Points.Add(pos);
|
|
_shapePolyRubber.Points.Add(pos);
|
|
|
|
// Snap ring over the start vertex - hidden until the polygon can actually close.
|
|
_shapePolySnapDot = new Ellipse
|
|
{
|
|
Width = 14,
|
|
Height = 14,
|
|
StrokeThickness = 2,
|
|
Fill = Brushes.Transparent,
|
|
IsHitTestVisible = false,
|
|
Visibility = Visibility.Collapsed
|
|
};
|
|
_shapePolySnapDot.SetResourceReference(Shape.StrokeProperty, "SelectionAccent");
|
|
Canvas.SetLeft(_shapePolySnapDot, pos.X - 7);
|
|
Canvas.SetTop(_shapePolySnapDot, pos.Y - 7);
|
|
|
|
_shapePolyCanvas.Children.Add(_shapePolyPreview);
|
|
_shapePolyCanvas.Children.Add(_shapePolyRubber);
|
|
_shapePolyCanvas.Children.Add(_shapePolySnapDot);
|
|
_shapePolyPoints.Add(pos);
|
|
SetStatus(Loc("Str_St_ShapeHint"));
|
|
return;
|
|
}
|
|
|
|
if (pageIdx != _shapePolyPage) return; // the polygon stays on the page it started on
|
|
|
|
if (_shapePolyPoints.Count >= 3 && (pos - _shapePolyPoints[0]).Length <= ShapeSnapPx)
|
|
{
|
|
CommitShapePolygon();
|
|
return;
|
|
}
|
|
|
|
_shapePolyPoints.Add(pos);
|
|
_shapePolyPreview!.Points.Add(pos);
|
|
_shapePolyRubber!.Points[0] = pos;
|
|
}
|
|
|
|
/// <summary>Mouse-move while a polygon is being placed: rubber-band from the last vertex,
|
|
/// and light the snap ring when the cursor is close enough to the start to close.</summary>
|
|
private void UpdateShapePolyRubber(MouseEventArgs e)
|
|
{
|
|
if (_shapePolyCanvas is null || _shapePolyRubber is null) return;
|
|
var p = e.GetPosition(_shapePolyCanvas);
|
|
_shapePolyRubber.Points[_shapePolyRubber.Points.Count - 1] = p;
|
|
if (_shapePolySnapDot is not null)
|
|
_shapePolySnapDot.Visibility =
|
|
_shapePolyPoints.Count >= 3 && (p - _shapePolyPoints[0]).Length <= ShapeSnapPx
|
|
? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
/// <summary>Commit the in-progress polygon as a closed ink stroke.</summary>
|
|
private void CommitShapePolygon()
|
|
{
|
|
int page = _shapePolyPage;
|
|
var pts = new List<Point>(_shapePolyPoints);
|
|
ResetShapePolyState();
|
|
|
|
// Drop trailing points that repeat the last committed vertex (a double-click close adds
|
|
// one at the same spot as the click before it).
|
|
while (pts.Count >= 2 && (pts[pts.Count - 1] - pts[pts.Count - 2]).Length < 2) pts.RemoveAt(pts.Count - 1);
|
|
if (pts.Count < 3 || page < 0) return;
|
|
|
|
var ink = new InkAnnotation { PageIndex = page, StrokeWidth = _drawWidth };
|
|
ink.SetColor(_drawColor);
|
|
if (_shapeFill) ink.SetFillColor(ShapeFillColor());
|
|
foreach (var p in pts) ink.Points.Add(p);
|
|
ink.Points.Add(pts[0]); // close
|
|
AddAnnotation(ink);
|
|
RenderAllAnnotations(page);
|
|
}
|
|
|
|
/// <summary>Esc: abandon the in-progress polygon. Safe no-op when none is active.</summary>
|
|
private void CancelShapePolygon()
|
|
{
|
|
if (_shapePolyPoints.Count == 0) return;
|
|
ResetShapePolyState();
|
|
SetStatus(Loc("Str_St_ShapeCanceled"));
|
|
}
|
|
|
|
/// <summary>Backspace: remove the last placed vertex; removing the only one cancels.</summary>
|
|
private void ShapePolyBackspace()
|
|
{
|
|
if (_shapePolyPoints.Count == 0) return;
|
|
if (_shapePolyPoints.Count == 1) { CancelShapePolygon(); return; }
|
|
_shapePolyPoints.RemoveAt(_shapePolyPoints.Count - 1);
|
|
_shapePolyPreview!.Points.RemoveAt(_shapePolyPreview.Points.Count - 1);
|
|
_shapePolyRubber!.Points[0] = _shapePolyPoints[_shapePolyPoints.Count - 1];
|
|
}
|
|
|
|
private void ResetShapePolyState()
|
|
{
|
|
if (_shapePolyCanvas is not null)
|
|
{
|
|
if (_shapePolyPreview is not null) _shapePolyCanvas.Children.Remove(_shapePolyPreview);
|
|
if (_shapePolyRubber is not null) _shapePolyCanvas.Children.Remove(_shapePolyRubber);
|
|
if (_shapePolySnapDot is not null) _shapePolyCanvas.Children.Remove(_shapePolySnapDot);
|
|
}
|
|
_shapePolyPoints.Clear();
|
|
_shapePolyPreview = null;
|
|
_shapePolyRubber = null;
|
|
_shapePolySnapDot = null;
|
|
_shapePolyPage = -1;
|
|
_shapePolyCanvas = null;
|
|
}
|
|
}
|
|
}
|