- 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.
881 lines
41 KiB
C#
881 lines
41 KiB
C#
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 MmdPdf.Services;
|
|
using PdfPigDoc = UglyToad.PdfPig.PdfDocument;
|
|
|
|
namespace MmdPdf
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
private void LoadSignatures() => _signatureStore.Load();
|
|
|
|
private void PersistSignatures() => _signatureStore.Persist();
|
|
|
|
// Rebuild the signature popup (if open) so its Loc()-built labels - section headers, pen sizes -
|
|
// switch immediately on a language change rather than only on the next open.
|
|
private void RefreshSignaturePopupLanguage()
|
|
{
|
|
if (_signaturePopup is not null) ShowSignaturePopup();
|
|
}
|
|
|
|
private void PlaceSignature(Point pos, int pageIdx)
|
|
{
|
|
if (_pendingSignature is null) return;
|
|
|
|
var sig = _pendingSignature;
|
|
double scale = sig.Kind == SignatureKind.Initials ? 0.3 : 0.5;
|
|
|
|
var annot = new SignatureAnnotation
|
|
{
|
|
PageIndex = pageIdx,
|
|
Position = pos,
|
|
Scale = scale,
|
|
StrokeWidth = sig.StrokeWidth,
|
|
SourceWidth = sig.CanvasWidth,
|
|
SourceHeight = sig.CanvasHeight,
|
|
ImageData = sig.ImageData
|
|
};
|
|
|
|
// Drawn signature - convert serializable points to WPF points
|
|
if (sig.ImageData is null)
|
|
{
|
|
foreach (var stroke in sig.Strokes)
|
|
annot.Strokes.Add([..stroke.Select(p => new Point(p.X, p.Y))]);
|
|
}
|
|
|
|
AddAnnotation(annot);
|
|
RenderAllAnnotations(pageIdx);
|
|
|
|
// Auto-switch to Select and select the placed signature so the user
|
|
// can immediately reposition or resize without an extra click.
|
|
SetTool(EditTool.Select);
|
|
double sigW = sig.CanvasWidth * scale;
|
|
double sigH = sig.CanvasHeight * scale;
|
|
SelectAnnotation(annot, new Rect(pos.X, pos.Y, sigW, sigH));
|
|
SetStatus(Loc("Str_St_SignaturePlaced"));
|
|
}
|
|
|
|
// Already signed -> change/remove menu. Otherwise drop the reusable choice, or open the
|
|
// picker the first time and route the pick back to this field.
|
|
private void FillSignField(bool initials, int objNum, int pageIndex, double x, double y, double w, double h)
|
|
{
|
|
if (_signedFields.ContainsKey(objNum))
|
|
{
|
|
ShowSignedFieldMenu(initials, objNum, pageIndex, x, y, w, h);
|
|
return;
|
|
}
|
|
var choice = initials ? _activeInitialsChoice : _activeSignatureChoice;
|
|
if (choice is null)
|
|
{
|
|
_pendingSignField = (initials, objNum, pageIndex, x, y, w, h);
|
|
ShowSignaturePopup();
|
|
SetStatus(initials
|
|
? "Choose initials - they will be reused for every initials field"
|
|
: "Choose a signature - it will be reused for every signature field");
|
|
return;
|
|
}
|
|
DropSignatureInField(objNum, choice, pageIndex, x, y, w, h);
|
|
}
|
|
|
|
// Re-clicking a signed field: change (re-pick) or remove it.
|
|
private void ShowSignedFieldMenu(bool initials, int objNum, int pageIndex, double x, double y, double w, double h)
|
|
{
|
|
string what = initials ? "initials" : "signature";
|
|
var menu = MakeThemedMenu();
|
|
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
|
|
menu.Items.Add(MakeMenuItem("Change " + what, (_, _) =>
|
|
{
|
|
RemoveSignedField(objNum, pageIndex);
|
|
_pendingSignField = (initials, objNum, pageIndex, x, y, w, h);
|
|
ShowSignaturePopup();
|
|
}, glyph: ""));
|
|
menu.Items.Add(MakeMenuItem("Remove " + what, (_, _) => RemoveSignedField(objNum, pageIndex), glyph: ""));
|
|
menu.IsOpen = true;
|
|
}
|
|
|
|
// Deletes the signature placed in a field and clears its signed state.
|
|
private void RemoveSignedField(int objNum, int pageIndex)
|
|
{
|
|
if (!_signedFields.TryGetValue(objNum, out var annot)) return;
|
|
if (_annotations.TryGetValue(pageIndex, out var list)) list.Remove(annot);
|
|
_signedFields.Remove(objNum);
|
|
RenderAllAnnotations(pageIndex);
|
|
MarkDirty(true);
|
|
SetStatus(Loc("Str_St_FieldCleared"));
|
|
}
|
|
|
|
// Places a SignatureAnnotation centered in and scaled to fit the field rectangle.
|
|
private void DropSignatureInField(int objNum, SavedSignature sig, int pageIndex, double x, double y, double w, double h)
|
|
{
|
|
const double pad = 2;
|
|
double sw = sig.CanvasWidth, sh = sig.CanvasHeight;
|
|
|
|
// Older or damaged signature-store entries can carry zero or non-finite canvas
|
|
// dimensions. Dividing the field size by those values produces an infinite scale,
|
|
// which later crashes WPF when the annotation is assigned an infinite Height (#181).
|
|
if (double.IsNaN(sw) || double.IsInfinity(sw) || sw <= 0) sw = 400;
|
|
if (double.IsNaN(sh) || double.IsInfinity(sh) || sh <= 0) sh = 150;
|
|
double scale = Math.Min((w - 2 * pad) / sw, (h - 2 * pad) / sh);
|
|
if (scale <= 0) scale = Math.Min(w / sw, h / sh);
|
|
if (double.IsNaN(scale) || double.IsInfinity(scale) || scale <= 0) scale = 0.5;
|
|
double drawW = sw * scale, drawH = sh * scale;
|
|
double px = x + (w - drawW) / 2;
|
|
double py = y + (h - drawH) / 2;
|
|
|
|
var annot = new SignatureAnnotation
|
|
{
|
|
PageIndex = pageIndex,
|
|
Position = new Point(px, py),
|
|
Scale = scale,
|
|
StrokeWidth = sig.StrokeWidth,
|
|
SourceWidth = sw,
|
|
SourceHeight = sh,
|
|
ImageData = sig.ImageData,
|
|
};
|
|
if (sig.ImageData is null)
|
|
foreach (var stroke in sig.Strokes)
|
|
annot.Strokes.Add([.. stroke.Select(pt => new Point(pt.X, pt.Y))]);
|
|
|
|
_signedFields[objNum] = annot;
|
|
AddAnnotation(annot);
|
|
RenderAllAnnotations(pageIndex);
|
|
MarkDirty(true);
|
|
SetStatus(Loc("Str_St_FieldSigned"));
|
|
}
|
|
|
|
private void HideSignaturePopup()
|
|
{
|
|
if (_signaturePopup is not null)
|
|
{
|
|
var previewGrid = PagePreviewPanel.Parent as Grid;
|
|
var popup = _signaturePopup;
|
|
_signaturePopup = null; // detach now so a re-open builds a fresh popup
|
|
// Fade out (it's a Border, not a Window, so WindowFx doesn't apply) then remove.
|
|
var fade = new DoubleAnimation(popup.Opacity, 0,
|
|
new Duration(TimeSpan.FromMilliseconds(WindowFx.FadeMs)))
|
|
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } };
|
|
fade.Completed += (_, _) => previewGrid?.Children.Remove(popup);
|
|
popup.BeginAnimation(UIElement.OpacityProperty, fade);
|
|
}
|
|
}
|
|
|
|
// ---- Draggable floating panels (signature popup, settings panel) -------------------------
|
|
|
|
/// <summary>
|
|
/// Clamps a Left/Top so a panel stays fully inside its container's bounds.
|
|
/// </summary>
|
|
private static void ClampPanelToBounds(FrameworkElement panel, FrameworkElement bounds,
|
|
ref double left, ref double top)
|
|
{
|
|
double w = panel.ActualWidth > 0 ? panel.ActualWidth : panel.Width;
|
|
double h = panel.ActualHeight > 0 ? panel.ActualHeight : panel.Height;
|
|
if (double.IsNaN(w)) w = 0;
|
|
if (double.IsNaN(h)) h = 0;
|
|
double maxLeft = Math.Max(0, bounds.ActualWidth - w);
|
|
double maxTop = Math.Max(0, bounds.ActualHeight - h);
|
|
left = Math.Max(0, Math.Min(maxLeft, left));
|
|
top = Math.Max(0, Math.Min(maxTop, top));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Positions a Left/Top-aligned floating panel from its saved position, falling back to a
|
|
/// top-right inset when nothing is stored. Always clamped inside <paramref name="bounds"/>.
|
|
/// Must run after layout so the panel's ActualWidth/Height are known.
|
|
/// </summary>
|
|
private void ApplySavedPanelPosition(FrameworkElement panel, FrameworkElement bounds, string keyPrefix,
|
|
double fallbackRightInset, double fallbackTop)
|
|
{
|
|
double w = panel.ActualWidth > 0 ? panel.ActualWidth : (double.IsNaN(panel.Width) ? 0 : panel.Width);
|
|
double left, top;
|
|
if (int.TryParse(App.GetSetting(keyPrefix + "Left"), out int sl) &&
|
|
int.TryParse(App.GetSetting(keyPrefix + "Top"), out int st))
|
|
{
|
|
left = sl; top = st;
|
|
}
|
|
else
|
|
{
|
|
left = bounds.ActualWidth - w - fallbackRightInset;
|
|
top = fallbackTop;
|
|
}
|
|
ClampPanelToBounds(panel, bounds, ref left, ref top);
|
|
panel.Margin = new Thickness(left, top, 0, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes <paramref name="handle"/> drag <paramref name="panel"/> within <paramref name="bounds"/>,
|
|
/// clamped to stay inside, and persists the resulting position under <paramref name="keyPrefix"/>.
|
|
/// </summary>
|
|
private void EnablePanelDrag(FrameworkElement handle, FrameworkElement panel, FrameworkElement bounds,
|
|
string keyPrefix)
|
|
{
|
|
handle.Cursor = Cursors.SizeAll;
|
|
Point start = default;
|
|
Thickness orig = default;
|
|
bool dragging = false;
|
|
|
|
handle.MouseLeftButtonDown += (s, e) =>
|
|
{
|
|
dragging = true;
|
|
start = e.GetPosition(bounds);
|
|
orig = panel.Margin;
|
|
handle.CaptureMouse();
|
|
e.Handled = true;
|
|
};
|
|
handle.MouseMove += (s, e) =>
|
|
{
|
|
if (!dragging) return;
|
|
var p = e.GetPosition(bounds);
|
|
double nl = orig.Left + (p.X - start.X);
|
|
double nt = orig.Top + (p.Y - start.Y);
|
|
ClampPanelToBounds(panel, bounds, ref nl, ref nt);
|
|
panel.Margin = new Thickness(nl, nt, 0, 0);
|
|
};
|
|
handle.MouseLeftButtonUp += (s, e) =>
|
|
{
|
|
if (!dragging) return;
|
|
dragging = false;
|
|
handle.ReleaseMouseCapture();
|
|
App.SetSetting(keyPrefix + "Left", ((int)panel.Margin.Left).ToString());
|
|
App.SetSetting(keyPrefix + "Top", ((int)panel.Margin.Top).ToString());
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
|
|
private void RenderSignaturePreview(Canvas canvas, SavedSignature sig, double targetW, double targetH)
|
|
{
|
|
double scaleX = targetW / sig.CanvasWidth;
|
|
double scaleY = targetH / sig.CanvasHeight;
|
|
double scale = Math.Min(scaleX, scaleY) * 0.9;
|
|
|
|
double offsetX = (targetW - sig.CanvasWidth * scale) / 2;
|
|
double offsetY = (targetH - sig.CanvasHeight * scale) / 2;
|
|
|
|
foreach (var stroke in sig.Strokes)
|
|
{
|
|
if (stroke.Count < 2) continue;
|
|
var poly = new Polyline
|
|
{
|
|
Stroke = Brushes.Black,
|
|
StrokeThickness = Math.Max(0.8, sig.StrokeWidth * scale),
|
|
StrokeLineJoin = PenLineJoin.Round,
|
|
StrokeStartLineCap = PenLineCap.Round,
|
|
StrokeEndLineCap = PenLineCap.Round
|
|
};
|
|
foreach (var pt in stroke)
|
|
poly.Points.Add(new Point(pt.X * scale + offsetX, pt.Y * scale + offsetY));
|
|
canvas.Children.Add(poly);
|
|
}
|
|
}
|
|
|
|
private void ShowSignaturePopup()
|
|
{
|
|
// NOTE: this popup is rebuilt on every open. All event handlers here are lambdas
|
|
// on the popup's own child elements - no external source subscriptions, so no leak.
|
|
// If SignatureStore.Signatures ever becomes ObservableCollection and this popup
|
|
// subscribes to CollectionChanged, use CollectionChangedEventManager instead of +=.
|
|
HideSignaturePopup();
|
|
|
|
bool classicCaption = FindResource("UseDialogCaption") is true;
|
|
var stack = new StackPanel { Margin = classicCaption ? new Thickness(0) : new Thickness(4) };
|
|
|
|
// Title doubles as a drag handle so the user can move the popup anywhere inside the
|
|
// document area (position is remembered). Wrapped in a transparent Border so the whole
|
|
// title strip is grabbable, not just the text glyphs.
|
|
var sigHeaderGrid = new Grid();
|
|
sigHeaderGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
sigHeaderGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
|
var sigTitleText = new TextBlock
|
|
{
|
|
Text = Loc("Str_Sig_Title"),
|
|
Foreground = (Brush)FindResource(classicCaption ? "ChromeTextBrush" : "PrimaryBrush"),
|
|
FontFamily = classicCaption
|
|
? (FontFamily)FindResource("ChromeFontFamily")
|
|
: UiKit.MonoFont,
|
|
FontWeight = FontWeights.Bold,
|
|
FontSize = classicCaption ? 11 : 14,
|
|
Margin = classicCaption ? new Thickness(0) : new Thickness(4, 2, 4, 2),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Effect = classicCaption ? null : new System.Windows.Media.Effects.DropShadowEffect { Color = Colors.Black, BlurRadius = 2, ShadowDepth = 1, Direction = 270, Opacity = 0.7 }
|
|
};
|
|
Grid.SetColumn(sigTitleText, 0);
|
|
void ClosePicker()
|
|
{
|
|
HideSignaturePopup();
|
|
if (_currentTool == EditTool.Signature && _pendingSignature is null)
|
|
SetTool(EditTool.Select);
|
|
}
|
|
|
|
FrameworkElement sigCloseControl;
|
|
if (classicCaption)
|
|
{
|
|
var closeButton = new Button
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
Background = Brushes.Transparent,
|
|
BorderThickness = new Thickness(0),
|
|
Cursor = Cursors.Hand,
|
|
FocusVisualStyle = null,
|
|
Style = (Style)FindResource("ChromeCloseButton")
|
|
};
|
|
closeButton.SetResourceReference(FrameworkElement.WidthProperty, "DialogCloseWidth");
|
|
closeButton.SetResourceReference(FrameworkElement.HeightProperty, "DialogCloseHeight");
|
|
closeButton.SetResourceReference(FrameworkElement.MarginProperty, "DialogCaptionButtonsMargin");
|
|
closeButton.PreviewMouseLeftButtonDown += (_, e) => { e.Handled = true; ClosePicker(); };
|
|
sigCloseControl = closeButton;
|
|
}
|
|
else
|
|
{
|
|
var closeText = new TextBlock
|
|
{
|
|
Text = "",
|
|
FontFamily = UiKit.IconFont,
|
|
FontSize = 11,
|
|
Foreground = (SolidColorBrush)FindResource("MutedTextBrush"),
|
|
Cursor = Cursors.Hand,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0),
|
|
Padding = new Thickness(4)
|
|
};
|
|
closeText.MouseEnter += (_, _) => { closeText.Foreground = (SolidColorBrush)FindResource("DangerRed"); closeText.Effect = new System.Windows.Media.Effects.DropShadowEffect { Color = Colors.Black, BlurRadius = 4, ShadowDepth = 1, Direction = 270, Opacity = 0.5 }; };
|
|
closeText.MouseLeave += (_, _) => { closeText.Foreground = (SolidColorBrush)FindResource("MutedTextBrush"); closeText.Effect = null; };
|
|
closeText.MouseLeftButtonDown += (_, e) => { e.Handled = true; ClosePicker(); };
|
|
sigCloseControl = closeText;
|
|
}
|
|
Grid.SetColumn(sigCloseControl, 1);
|
|
sigHeaderGrid.Children.Add(sigTitleText);
|
|
sigHeaderGrid.Children.Add(sigCloseControl);
|
|
var sigHeader = new Border
|
|
{
|
|
Background = classicCaption ? (Brush)FindResource("DialogTitleBarBrush") : Brushes.Transparent,
|
|
Height = classicCaption && FindResource("DialogTitleBarHeight") is double captionHeight
|
|
? captionHeight : double.NaN,
|
|
Margin = classicCaption ? new Thickness(0) : new Thickness(0, 0, 0, 4),
|
|
Child = sigHeaderGrid
|
|
};
|
|
if (classicCaption)
|
|
sigHeaderGrid.SetResourceReference(FrameworkElement.MarginProperty, "TitleBarPadding");
|
|
stack.Children.Add(sigHeader);
|
|
|
|
// Saved signatures and initials, shown as two labeled sections so the HR-style
|
|
// "initial here, sign there" flow can pick each independently. One tile builder is shared.
|
|
UIElement MakeSigItem(SavedSignature sigCopy)
|
|
{
|
|
var item = new Border
|
|
{
|
|
Background = Brushes.White,
|
|
BorderBrush = _swatchDimBorder,
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(0),
|
|
Margin = new Thickness(4, 2, 4, 2),
|
|
Padding = new Thickness(4),
|
|
Cursor = Cursors.Hand,
|
|
Height = 60,
|
|
HorizontalAlignment = HorizontalAlignment.Stretch
|
|
};
|
|
item.SetResourceReference(Border.CornerRadiusProperty, "ControlCornerRadius");
|
|
|
|
if (sigCopy.ImageData is not null)
|
|
{
|
|
try
|
|
{
|
|
var imgBytes = Convert.FromBase64String(sigCopy.ImageData);
|
|
var bmpImg = new System.Windows.Media.Imaging.BitmapImage();
|
|
bmpImg.BeginInit();
|
|
bmpImg.StreamSource = new System.IO.MemoryStream(imgBytes);
|
|
bmpImg.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
|
|
bmpImg.EndInit();
|
|
item.Child = new System.Windows.Controls.Image
|
|
{
|
|
Source = bmpImg,
|
|
Height = 50,
|
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
Stretch = System.Windows.Media.Stretch.Uniform,
|
|
IsHitTestVisible = false
|
|
};
|
|
}
|
|
catch { item.Child = new TextBlock { Text = "(image)", IsHitTestVisible = false }; }
|
|
}
|
|
else
|
|
{
|
|
var canvas = new Canvas
|
|
{
|
|
Width = 288, Height = 50,
|
|
Background = Brushes.Transparent,
|
|
IsHitTestVisible = false
|
|
};
|
|
RenderSignaturePreview(canvas, sigCopy, 288, 50);
|
|
item.Child = canvas;
|
|
}
|
|
|
|
item.MouseLeftButtonDown += (s, e) =>
|
|
{
|
|
HideSignaturePopup();
|
|
// If a signature field is waiting, fill it and remember the choice (pick once, reuse).
|
|
if (_pendingSignField is { } tgt)
|
|
{
|
|
if (tgt.Initials) _activeInitialsChoice = sigCopy; else _activeSignatureChoice = sigCopy;
|
|
var t = tgt; _pendingSignField = null;
|
|
DropSignatureInField(t.ObjNum, sigCopy, t.Page, t.X, t.Y, t.W, t.H);
|
|
return;
|
|
}
|
|
_pendingSignature = sigCopy;
|
|
_annotationCanvas.Cursor = Cursors.Cross;
|
|
SetStatus(sigCopy.Kind == SignatureKind.Initials
|
|
? "Click on the page to place your initials"
|
|
: "Click on the page to place your signature");
|
|
};
|
|
item.MouseEnter += (s, e) =>
|
|
((Border)s!).BorderBrush = (SolidColorBrush)FindResource("PrimaryBrush");
|
|
item.MouseLeave += (s, e) =>
|
|
((Border)s!).BorderBrush = _swatchDimBorder;
|
|
|
|
var itemGrid = new Grid();
|
|
itemGrid.Children.Add(item);
|
|
|
|
var delBtn = new Button
|
|
{
|
|
Content = "",
|
|
FontSize = 10,
|
|
Width = 18, Height = 18,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
Margin = new Thickness(0, 0, 2, 0),
|
|
Background = new SolidColorBrush(Color.FromArgb(200, 30, 30, 30)),
|
|
Foreground = (SolidColorBrush)FindResource("DangerRed"),
|
|
BorderThickness = new Thickness(0),
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(0),
|
|
Style = (Style)FindResource("ToolbarButton")
|
|
};
|
|
delBtn.Click += (s, e) =>
|
|
{
|
|
_signatureStore.Remove(sigCopy);
|
|
PersistSignatures();
|
|
ShowSignaturePopup(); // refresh
|
|
};
|
|
itemGrid.Children.Add(delBtn);
|
|
return itemGrid;
|
|
}
|
|
|
|
// Section = header + saved tiles (or a "none yet" hint) + Create/Import for that Kind.
|
|
void AddSigSection(string sectionTitle, SignatureKind kind)
|
|
{
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = sectionTitle,
|
|
Foreground = (SolidColorBrush)FindResource("MutedTextBrush"),
|
|
FontFamily = UiKit.UiFont,
|
|
FontWeight = FontWeights.SemiBold,
|
|
FontSize = 11,
|
|
Margin = new Thickness(4, 6, 4, 2)
|
|
});
|
|
|
|
var items = _signatureStore.Signatures.Where(x => x.Kind == kind).ToList();
|
|
if (items.Count > 0)
|
|
{
|
|
var scroll = new ScrollViewer
|
|
{
|
|
MaxHeight = 170,
|
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
|
|
};
|
|
var listPanel = new StackPanel();
|
|
foreach (var sig in items)
|
|
listPanel.Children.Add(MakeSigItem(sig));
|
|
scroll.Content = listPanel;
|
|
stack.Children.Add(scroll);
|
|
}
|
|
else
|
|
{
|
|
stack.Children.Add(new TextBlock
|
|
{
|
|
Text = Loc("Str_Sig_None"),
|
|
Foreground = (SolidColorBrush)FindResource("MutedTextBrush"),
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
FontStyle = FontStyles.Italic,
|
|
Margin = new Thickness(4, 2, 4, 6),
|
|
HorizontalAlignment = HorizontalAlignment.Center
|
|
});
|
|
}
|
|
|
|
var rowBtns = new Grid { Margin = new Thickness(4, 8, 4, 2) };
|
|
rowBtns.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
rowBtns.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
|
var createBtn = UiKit.Make(Loc("Str_Sig_Create"), accent: true);
|
|
createBtn.HorizontalAlignment = HorizontalAlignment.Stretch;
|
|
createBtn.Margin = new Thickness(0, 0, 3, 0);
|
|
createBtn.Click += (s, e) => { HideSignaturePopup(); OpenSignatureCreator(kind); ShowSignaturePopup(); };
|
|
var importBtn = UiKit.Make(Loc("Str_Sig_Import"), accent: false);
|
|
importBtn.HorizontalAlignment = HorizontalAlignment.Stretch;
|
|
importBtn.Margin = new Thickness(3, 0, 0, 0);
|
|
importBtn.Click += (s, e) => { HideSignaturePopup(); ImportImageSignature(kind); ShowSignaturePopup(); };
|
|
Grid.SetColumn(createBtn, 0);
|
|
Grid.SetColumn(importBtn, 1);
|
|
rowBtns.Children.Add(createBtn);
|
|
rowBtns.Children.Add(importBtn);
|
|
stack.Children.Add(rowBtns);
|
|
}
|
|
|
|
AddSigSection(Loc("Str_Sig_Signatures"), SignatureKind.Signature);
|
|
|
|
stack.Children.Add(new Rectangle
|
|
{
|
|
Height = 1,
|
|
Fill = (SolidColorBrush)FindResource("CardBorderBrush"),
|
|
Margin = new Thickness(4, 6, 4, 2)
|
|
});
|
|
|
|
AddSigSection(Loc("Str_Sig_Initials"), SignatureKind.Initials);
|
|
|
|
// Match the Settings/menu popups: themed modal surface + accent border + film grain.
|
|
var sigContent = new Grid();
|
|
var sigGrain = new Border
|
|
{
|
|
CornerRadius = new CornerRadius(0),
|
|
IsHitTestVisible = false,
|
|
Opacity = (double)FindResource("GrainOpacity"),
|
|
Background = (System.Windows.Media.Brush)FindResource("GrainBrushShared")
|
|
};
|
|
sigGrain.SetResourceReference(Border.CornerRadiusProperty, "FlyoutCornerRadius");
|
|
sigContent.Children.Add(sigGrain);
|
|
sigContent.Children.Add(stack);
|
|
|
|
var sigBody = new Border
|
|
{
|
|
Background = (Brush)FindResource("MenuBackgroundBrush"),
|
|
Padding = classicCaption ? new Thickness(0) : new Thickness(4),
|
|
Child = sigContent
|
|
};
|
|
sigBody.SetResourceReference(Border.MarginProperty, "DialogWindowFramePadding");
|
|
sigBody.SetResourceReference(Border.CornerRadiusProperty, "FlyoutCornerRadius");
|
|
|
|
Border SignatureFrameRing(string brushKey, string thicknessKey, string? marginKey = null)
|
|
{
|
|
var ring = new Border { IsHitTestVisible = false };
|
|
ring.SetResourceReference(Border.BorderBrushProperty, brushKey);
|
|
ring.SetResourceReference(Border.BorderThicknessProperty, thicknessKey);
|
|
if (marginKey is not null)
|
|
ring.SetResourceReference(Border.MarginProperty, marginKey);
|
|
return ring;
|
|
}
|
|
|
|
var sigFrame = new Grid();
|
|
sigFrame.Children.Add(sigBody);
|
|
sigFrame.Children.Add(SignatureFrameRing("WindowFrameBrush", "DialogWindowFrameThickness"));
|
|
sigFrame.Children.Add(SignatureFrameRing("FrameInnerLightBrush", "FrameInnerLightThickness", "FrameInnerMargin"));
|
|
sigFrame.Children.Add(SignatureFrameRing("FrameInnerDarkBrush", "FrameInnerDarkThickness", "FrameInnerMargin"));
|
|
sigFrame.Children.Add(SignatureFrameRing("FrameOuterLightBrush", "FrameOuterLightThickness"));
|
|
sigFrame.Children.Add(SignatureFrameRing("FrameOuterDarkBrush", "FrameOuterDarkThickness"));
|
|
|
|
_signaturePopup = new Border
|
|
{
|
|
Background = (SolidColorBrush)FindResource("MenuBackgroundBrush"),
|
|
BorderBrush = (SolidColorBrush)FindResource("MenuBorderBrush"),
|
|
BorderThickness = classicCaption ? new Thickness(0) : new Thickness(1),
|
|
CornerRadius = new CornerRadius(0),
|
|
Padding = new Thickness(0),
|
|
Child = sigFrame,
|
|
// Free-positioned (Left/Top) inside the document grid so it can be dragged; the
|
|
// exact spot is set after layout from the saved position (or a default top-right).
|
|
Width = 320,
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
Margin = new Thickness(0, 4, 0, 0),
|
|
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
|
{
|
|
Color = Colors.Black, BlurRadius = 16,
|
|
Opacity = FindResource("FlyoutShadowOpacity") is double shadowOpacity ? shadowOpacity : 0.55,
|
|
ShadowDepth = 3
|
|
}
|
|
};
|
|
_signaturePopup.SetResourceReference(Border.CornerRadiusProperty, "FlyoutCornerRadius");
|
|
|
|
var previewGrid = PagePreviewPanel.Parent as Grid;
|
|
if (previewGrid is not null)
|
|
{
|
|
Panel.SetZIndex(_signaturePopup, 200);
|
|
previewGrid.Children.Add(_signaturePopup);
|
|
// Freshly-inserted element: defer the fade until it's laid out, otherwise the
|
|
// animation is missed (unlike the always-present Settings/About overlays).
|
|
_signaturePopup.Opacity = 0;
|
|
var popup = _signaturePopup;
|
|
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)(() =>
|
|
{
|
|
if (popup is null) return;
|
|
// Place it (saved position, or default to the old top-right spot) and wire up
|
|
// dragging now that ActualWidth/Height are known.
|
|
ApplySavedPanelPosition(popup, previewGrid, "SigPopup", fallbackRightInset: 80, fallbackTop: 4);
|
|
EnablePanelDrag(sigHeader, popup, previewGrid, "SigPopup");
|
|
popup.BeginAnimation(UIElement.OpacityProperty,
|
|
new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(110)))
|
|
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
|
}));
|
|
}
|
|
}
|
|
|
|
private void OpenSignatureCreator(SignatureKind kind = SignatureKind.Signature)
|
|
{
|
|
var win = new Window
|
|
{
|
|
Title = Loc("Str_Sig_Create"),
|
|
Width = 460,
|
|
SizeToContent = SizeToContent.Height // size to content so there's no empty padding below
|
|
};
|
|
DialogChrome.Configure(win, this);
|
|
// This separate window can't see MainWindow's ChromeCloseCorner, so the close button's
|
|
// {DynamicResource ChromeCloseCorner} fell back to 0 (square hover). Provide it here so the
|
|
// hover rounds the top-right corner to match the window.
|
|
win.Resources["ChromeCloseCorner"] = new CornerRadius(0, UiKit.RadWindow.TopRight, 0, 0);
|
|
|
|
var contentArea = new StackPanel();
|
|
|
|
// Drawing canvas
|
|
var canvasBorder = new Border
|
|
{
|
|
Background = Brushes.White,
|
|
// Faint outline so the white drawing pane reads as a distinct field on the modal.
|
|
BorderBrush = new SolidColorBrush(Color.FromRgb(0xcc, 0xcc, 0xcc)),
|
|
BorderThickness = new Thickness(1),
|
|
Margin = new Thickness(12, 12, 12, 4),
|
|
CornerRadius = UiKit.RadControl,
|
|
Height = 170
|
|
};
|
|
var drawCanvas = new Canvas
|
|
{
|
|
Background = Brushes.White,
|
|
ClipToBounds = true,
|
|
Cursor = Cursors.Pen
|
|
};
|
|
canvasBorder.Child = drawCanvas;
|
|
|
|
// Placeholder text
|
|
// In a Canvas, alignment is ignored, so position the hint with a little padding rather
|
|
// than leaving it jammed in the top-left corner. A script face suits a signature prompt.
|
|
var placeholder = new TextBlock
|
|
{
|
|
Text = Loc("Str_Sig_DrawHere"),
|
|
Foreground = new SolidColorBrush(Color.FromRgb(0xb0, 0xb0, 0xb0)),
|
|
FontFamily = new FontFamily("Segoe Script, Segoe UI"),
|
|
FontSize = 18,
|
|
IsHitTestVisible = false
|
|
};
|
|
Canvas.SetLeft(placeholder, 18);
|
|
Canvas.SetTop(placeholder, 14);
|
|
drawCanvas.Children.Add(placeholder);
|
|
|
|
// Drawing state
|
|
var strokes = new List<List<Point>>();
|
|
List<Point>? currentStroke = null;
|
|
Polyline? currentPoly = null;
|
|
double penWidth = 2.5; // medium; set by the pen-width selector below
|
|
|
|
drawCanvas.MouseLeftButtonDown += (s, e) =>
|
|
{
|
|
if (placeholder.Visibility == Visibility.Visible)
|
|
placeholder.Visibility = Visibility.Collapsed;
|
|
currentStroke = [];
|
|
var pos = e.GetPosition(drawCanvas);
|
|
currentStroke.Add(pos);
|
|
currentPoly = new Polyline
|
|
{
|
|
Stroke = Brushes.Black,
|
|
StrokeThickness = penWidth,
|
|
StrokeLineJoin = PenLineJoin.Round,
|
|
StrokeStartLineCap = PenLineCap.Round,
|
|
StrokeEndLineCap = PenLineCap.Round
|
|
};
|
|
currentPoly.Points.Add(pos);
|
|
drawCanvas.Children.Add(currentPoly);
|
|
drawCanvas.CaptureMouse();
|
|
};
|
|
|
|
drawCanvas.MouseMove += (s, e) =>
|
|
{
|
|
if (currentStroke is null || currentPoly is null) return;
|
|
var pos = e.GetPosition(drawCanvas);
|
|
pos.X = Math.Max(0, Math.Min(drawCanvas.ActualWidth, pos.X));
|
|
pos.Y = Math.Max(0, Math.Min(drawCanvas.ActualHeight, pos.Y));
|
|
currentStroke.Add(pos);
|
|
currentPoly.Points.Add(pos);
|
|
};
|
|
|
|
drawCanvas.MouseLeftButtonUp += (s, e) =>
|
|
{
|
|
if (currentStroke is not null && currentStroke.Count > 1)
|
|
strokes.Add(currentStroke);
|
|
else if (currentPoly is not null)
|
|
drawCanvas.Children.Remove(currentPoly);
|
|
currentStroke = null;
|
|
currentPoly = null;
|
|
drawCanvas.ReleaseMouseCapture();
|
|
};
|
|
|
|
contentArea.Children.Add(canvasBorder);
|
|
|
|
// Pen-width selector: three preset thicknesses, active one highlighted. On the left so the
|
|
// modal does not read bottom-right-heavy.
|
|
var penRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(14, 6, 12, 0), VerticalAlignment = VerticalAlignment.Center };
|
|
penRow.Children.Add(new TextBlock { Text = Loc("Str_Sig_Pen"), Foreground = (SolidColorBrush)FindResource("MutedTextBrush"), FontFamily = UiKit.UiFont, FontSize = 11, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0) });
|
|
var penOptions = new (string Label, double W)[] { (Loc("Str_Sig_Thin"), 2.0), (Loc("Str_Sig_Medium"), 4.5), (Loc("Str_Sig_Thick"), 9.0) };
|
|
var penBtns = new List<Button>();
|
|
void RefreshPen()
|
|
{
|
|
for (int bi = 0; bi < penBtns.Count; bi++)
|
|
{
|
|
bool active = Math.Abs(penOptions[bi].W - penWidth) < 0.01;
|
|
penBtns[bi].Background = active ? (SolidColorBrush)FindResource("SelectionBg") : (SolidColorBrush)FindResource("PaneBrush");
|
|
penBtns[bi].Foreground = active ? (SolidColorBrush)FindResource("SelectionFg") : (SolidColorBrush)FindResource("TextBrush");
|
|
penBtns[bi].BorderBrush = active ? (SolidColorBrush)FindResource("PrimaryBrush") : (SolidColorBrush)FindResource("CardBorderBrush");
|
|
}
|
|
}
|
|
foreach (var (lbl, w) in penOptions)
|
|
{
|
|
double ww = w;
|
|
var pb = new Button
|
|
{
|
|
Content = lbl,
|
|
Style = (Style)FindResource("DarkButton"),
|
|
Padding = new Thickness(12, 3, 12, 3),
|
|
Margin = new Thickness(0, 0, 6, 0),
|
|
BorderThickness = new Thickness(1),
|
|
Cursor = Cursors.Hand,
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11
|
|
};
|
|
pb.Click += (s2, e2) => { penWidth = ww; RefreshPen(); };
|
|
penBtns.Add(pb);
|
|
penRow.Children.Add(pb);
|
|
}
|
|
RefreshPen();
|
|
|
|
// Buttons
|
|
var btnPanel = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right
|
|
};
|
|
|
|
var clearBtn = UiKit.Make(Loc("Str_Sig_Clear"), accent: false);
|
|
clearBtn.Margin = new Thickness(0, 0, 8, 0);
|
|
clearBtn.Click += (s, e) =>
|
|
{
|
|
strokes.Clear();
|
|
drawCanvas.Children.Clear();
|
|
placeholder.Visibility = Visibility.Visible;
|
|
drawCanvas.Children.Add(placeholder);
|
|
};
|
|
|
|
var saveBtn = UiKit.Make(Loc("Str_Sig_SaveSig"), accent: true);
|
|
saveBtn.Click += (s, e) =>
|
|
{
|
|
if (strokes.Count == 0)
|
|
{
|
|
KillerDialog.Show(this, Loc("Str_Dlg_DrawSignatureFirst"), "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
double cw = drawCanvas.ActualWidth > 0 ? drawCanvas.ActualWidth : 400;
|
|
double ch = drawCanvas.ActualHeight > 0 ? drawCanvas.ActualHeight : 150;
|
|
|
|
var saved = new SavedSignature
|
|
{
|
|
Kind = kind,
|
|
StrokeWidth = penWidth,
|
|
CanvasWidth = cw,
|
|
CanvasHeight = ch,
|
|
Name = $"{(kind == SignatureKind.Initials ? "Initials" : "Signature")} {_signatureStore.Signatures.Count(x => x.Kind == kind) + 1}"
|
|
};
|
|
foreach (var stroke in strokes)
|
|
{
|
|
var sPts = stroke.Select(p => new SerializablePoint { X = p.X, Y = p.Y }).ToList();
|
|
saved.Strokes.Add(sPts);
|
|
}
|
|
_signatureStore.Add(saved);
|
|
PersistSignatures();
|
|
|
|
// Auto-select the new signature for placement
|
|
_pendingSignature = saved;
|
|
_annotationCanvas.Cursor = Cursors.Cross;
|
|
SetStatus(Loc("Str_St_SignatureSaved"));
|
|
|
|
win.Close();
|
|
};
|
|
|
|
btnPanel.Children.Add(clearBtn);
|
|
btnPanel.Children.Add(saveBtn);
|
|
|
|
// Pen-size selector on its own row above the buttons. A single shared row doesn't survive
|
|
// longer translated labels (e.g. Bengali Clear/Save) - the last pen option ("Thick") clipped.
|
|
contentArea.Children.Add(penRow);
|
|
btnPanel.Margin = new Thickness(12, 4, 12, 12);
|
|
contentArea.Children.Add(btnPanel);
|
|
|
|
win.Content = DialogChrome.Frame(win, this, "MmdPdf - " + Loc("Str_Sig_Create"), () => win.Close(), contentArea);
|
|
win.ShowDialog();
|
|
}
|
|
|
|
private void ImportImageSignature(SignatureKind kind = SignatureKind.Signature)
|
|
{
|
|
var dlg = new Controls.FileDialog(Controls.FileDialogMode.Open)
|
|
{
|
|
Filter = Loc("Str_Filter_Images") + "|*.png;*.jpg;*.jpeg;*.bmp;*.gif|" + Loc("Str_Filter_AllFiles") + "|*.*",
|
|
Title = Loc("Str_Sign_ImportImage"),
|
|
ShowImagePreview = true
|
|
};
|
|
if (dlg.ShowDialog(this) != true) return;
|
|
|
|
try
|
|
{
|
|
var bmp = new System.Windows.Media.Imaging.BitmapImage(new Uri(dlg.FileName));
|
|
byte[] pngBytes;
|
|
using (var ms = new System.IO.MemoryStream())
|
|
{
|
|
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
|
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(bmp));
|
|
encoder.Save(ms);
|
|
pngBytes = ms.ToArray();
|
|
}
|
|
|
|
var saved = new SavedSignature
|
|
{
|
|
Kind = kind,
|
|
Name = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName),
|
|
CanvasWidth = bmp.PixelWidth,
|
|
CanvasHeight = bmp.PixelHeight,
|
|
ImageData = Convert.ToBase64String(pngBytes)
|
|
};
|
|
_signatureStore.Add(saved);
|
|
PersistSignatures();
|
|
|
|
_pendingSignature = saved;
|
|
_annotationCanvas.Cursor = Cursors.Cross;
|
|
SetStatus(Loc("Str_St_ImageLoaded"));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
KillerDialog.Show(this, Loc("Str_Err_ImportImageFailed") + "\n" + ex.Message, "MmdPdf",
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|