- 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.
773 lines
37 KiB
C#
773 lines
37 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
|
||
{
|
||
// ============================================================
|
||
// Text tool settings bar
|
||
// ============================================================
|
||
|
||
// Underline and/or strikethrough as one decoration collection (either, both, or none).
|
||
private static TextDecorationCollection? BuildDecorations(bool underline, bool strike)
|
||
{
|
||
if (!underline && !strike) return null;
|
||
var d = new TextDecorationCollection();
|
||
if (underline) foreach (var x in TextDecorations.Underline) d.Add(x);
|
||
if (strike) foreach (var x in TextDecorations.Strikethrough) d.Add(x);
|
||
return d;
|
||
}
|
||
|
||
// Apply the current typeface + Bold/Italic/Underline/Strikethrough to an in-canvas edit box, so
|
||
// editing stays WYSIWYG with the text bar. Bad font names fall back to Segoe UI rather than throwing.
|
||
private void StyleEditBox(TextBox tb)
|
||
{
|
||
try { tb.FontFamily = new FontFamily(_textFontName); } catch { tb.FontFamily = UiKit.UiFont; }
|
||
tb.FontWeight = _textBold ? FontWeights.Bold : FontWeights.Normal;
|
||
tb.FontStyle = _textItalic ? FontStyles.Italic : FontStyles.Normal;
|
||
tb.TextDecorations = BuildDecorations(_textUnderline, _textStrike);
|
||
}
|
||
|
||
private void ApplyTextStyleToActiveBox()
|
||
{
|
||
if (_activeTextBox is null) return;
|
||
_activeTextBox.Foreground = new SolidColorBrush(_textColor);
|
||
_activeTextBox.Background = TextEditBackground(); // reflect the chosen fill live
|
||
StyleEditBox(_activeTextBox); // typeface + B/I/S live
|
||
int pg = _activeTextBox.Tag is int tp ? tp : PageList.SelectedIndex;
|
||
double fontCanvas = _textFontSize;
|
||
if (_doc is not null && pg >= 0 && _renderDims.TryGetValue(pg, out var rd) && rd.h > 0)
|
||
{
|
||
double sy = _doc.Pages[pg].Height.Point / rd.h;
|
||
if (sy > 0) fontCanvas = _textFontSize / sy;
|
||
}
|
||
_activeTextBox.FontSize = fontCanvas;
|
||
}
|
||
|
||
// Applies the current text style to whatever is active: the live edit box if one is open,
|
||
// otherwise the selected text box (so its color / fill / size can be changed after placing it).
|
||
// Opens the full RGB color picker seeded with the current color; applies the result on OK.
|
||
private void OpenColorPicker(Color current, Action<Color> apply, Action? refreshBar = null)
|
||
{
|
||
var dlg = new ColorPickerDialog(this, Color.FromRgb(current.R, current.G, current.B));
|
||
// Live-update the annotate bar behind the (modal) dialog whenever the shared palette is edited.
|
||
if (refreshBar is not null) dlg.SwatchesChanged += refreshBar;
|
||
dlg.ShowDialog();
|
||
// dlg.Accepted, NEVER ShowDialog's return: the eyedropper's nested capture modal can
|
||
// corrupt the outer dialog frame so ShowDialog returns false after a real OK, and the
|
||
// pick was silently dropped - shapes then drew whatever color last got through
|
||
// (the "purple/gray rectangles" bug, trace-proven 2026-08-01).
|
||
if (dlg.Accepted) apply(dlg.SelectedColor);
|
||
}
|
||
|
||
// Diagonal rainbow fill for the "more colors" swatches that open the picker.
|
||
private static LinearGradientBrush RainbowBrush() => new()
|
||
{
|
||
StartPoint = new Point(0, 0),
|
||
EndPoint = new Point(1, 1),
|
||
GradientStops =
|
||
{
|
||
new GradientStop(Colors.Red, 0), new GradientStop(Colors.Yellow, 0.25),
|
||
new GradientStop(Colors.Lime, 0.5), new GradientStop(Colors.Cyan, 0.7),
|
||
new GradientStop(Colors.Blue, 1)
|
||
}
|
||
};
|
||
|
||
private void ApplyTextStyleToSelection()
|
||
{
|
||
if (_activeTextBox is not null)
|
||
{
|
||
ApplyTextStyleToActiveBox();
|
||
return;
|
||
}
|
||
// Apply to the primary selection AND every shift-selected text annotation (these can span pages).
|
||
var touched = new HashSet<int>();
|
||
void Apply(TextAnnotation ta)
|
||
{
|
||
ta.SetColor(_textColor);
|
||
ta.SetFill(_textFillColor);
|
||
ta.FontName = _textFontName;
|
||
ta.Bold = _textBold;
|
||
ta.Italic = _textItalic;
|
||
ta.Strike = _textStrike;
|
||
ta.Underline = _textUnderline;
|
||
double sy = 1.0;
|
||
if (_doc is not null && _renderDims.TryGetValue(ta.PageIndex, out var rd) && rd.h > 0)
|
||
sy = _doc.Pages[ta.PageIndex].Height.Point / rd.h;
|
||
if (sy > 0 && _textFontSize > 0) ta.FontSize = _textFontSize / sy;
|
||
touched.Add(ta.PageIndex);
|
||
}
|
||
if (_selectedAnnotation is TextAnnotation primary) Apply(primary);
|
||
foreach (var a in _selectedSet)
|
||
if (a is TextAnnotation ta && !ReferenceEquals(ta, _selectedAnnotation)) Apply(ta);
|
||
if (touched.Count == 0) return;
|
||
|
||
MarkDirty();
|
||
int primPage = (_selectedAnnotation as TextAnnotation)?.PageIndex ?? -1;
|
||
foreach (int p in touched) if (p != primPage) RenderAllAnnotations(p);
|
||
if (primPage >= 0)
|
||
{
|
||
RenderAllAnnotations(primPage); // render primary's page last so its chrome lands on _activeCanvas
|
||
if (_selectionBorder is not null) _activeCanvas.Children.Add(_selectionBorder);
|
||
foreach (var hd in _resizeHandles) _activeCanvas.Children.Add(hd);
|
||
}
|
||
}
|
||
|
||
// Draw-bar counterpart to ApplyTextStyleToSelection: when a highlight / line / ink annotation
|
||
// is selected (not just being freshly drawn), push the bar's current color, opacity and width
|
||
// onto it and repaint - so editing an existing annotation works the same as setting up a new one.
|
||
private void ApplyDrawStyleToSelection()
|
||
{
|
||
// Apply to the primary selection AND every shift-selected highlight / line / ink annotation.
|
||
var touched = new HashSet<int>();
|
||
void Apply(PageAnnotation a)
|
||
{
|
||
if (a is HighlightAnnotation ha)
|
||
{
|
||
ha.SetColor(ha.Style == HighlightStyle.Fill ? _highlightColor : _lineAnnotColor);
|
||
touched.Add(ha.PageIndex);
|
||
}
|
||
else if (a is InkAnnotation ia)
|
||
{
|
||
ia.SetColor(_drawColor);
|
||
ia.StrokeWidth = _drawWidth;
|
||
touched.Add(ia.PageIndex);
|
||
}
|
||
}
|
||
if (_selectedAnnotation is not null) Apply(_selectedAnnotation);
|
||
foreach (var a in _selectedSet)
|
||
if (!ReferenceEquals(a, _selectedAnnotation)) Apply(a);
|
||
if (touched.Count == 0) return;
|
||
|
||
MarkDirty();
|
||
int primPage = _selectedAnnotation?.PageIndex ?? -1;
|
||
foreach (int p in touched) if (p != primPage) RenderAllAnnotations(p);
|
||
if (primPage >= 0)
|
||
{
|
||
RenderAllAnnotations(primPage);
|
||
ReattachSelectionVisuals();
|
||
}
|
||
}
|
||
|
||
private void ShowTextSettings()
|
||
{
|
||
bool appearing = _annotBarTool != EditTool.Text; // real appear/switch vs same-tool refresh
|
||
if (_textSettingsBar is not null)
|
||
{
|
||
if (appearing) FadeOutAndRemoveBar(_textSettingsBar);
|
||
else (PagePreviewPanel.Parent as Grid)?.Children.Remove(_textSettingsBar);
|
||
_textSettingsBar = null;
|
||
}
|
||
|
||
// Six self-contained single-row groups. The finished text bar deliberately pairs them
|
||
// into two aligned rows: font/style over size, text color over fill color, and text
|
||
// opacity over fill opacity. This avoids a lone Fill Opacity group looking like an
|
||
// accidental wrap at otherwise comfortable window widths.
|
||
StackPanel Group()
|
||
{
|
||
return new StackPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
// Right margin = the gap to the next group; top/bottom = the gap between wrapped rows.
|
||
Margin = new Thickness(0, 3, 16, 3)
|
||
};
|
||
}
|
||
TextBlock DimLabel(string text, int top, bool rightAlign = false)
|
||
{
|
||
var t = new TextBlock
|
||
{
|
||
Text = text,
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 11,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
HorizontalAlignment = rightAlign ? HorizontalAlignment.Right : HorizontalAlignment.Left,
|
||
Margin = new Thickness(0, top, 6, 0)
|
||
};
|
||
t.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
||
return t;
|
||
}
|
||
Border ColorSwatch(Color c, bool isActive, MouseButtonEventHandler onClick)
|
||
{
|
||
var sw = new Border
|
||
{
|
||
Width = 18,
|
||
Height = 18,
|
||
Background = new SolidColorBrush(c),
|
||
BorderThickness = new Thickness(isActive ? 2 : 1),
|
||
CornerRadius = new CornerRadius(3),
|
||
Margin = new Thickness(1),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
if (isActive) sw.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush");
|
||
else sw.BorderBrush = _swatchDimBorder;
|
||
sw.MouseLeftButtonDown += onClick;
|
||
return sw;
|
||
}
|
||
// Opens the full RGB picker. When the current color isn't one of the presets it shows that
|
||
// color with an accent ring (and a small rainbow corner), so the bar reflects a custom pick.
|
||
Grid MoreColorsSwatch(Color current, bool customActive, MouseButtonEventHandler onClick)
|
||
{
|
||
var grid = new Grid { Width = 18, Height = 18, Margin = new Thickness(1), Cursor = Cursors.Hand, ToolTip = Loc("Str_Bar_MoreColors") };
|
||
var bg = new Border
|
||
{
|
||
CornerRadius = new CornerRadius(3),
|
||
BorderThickness = new Thickness(customActive ? 2 : 1),
|
||
Background = customActive
|
||
? (Brush)new SolidColorBrush(Color.FromRgb(current.R, current.G, current.B))
|
||
: RainbowBrush()
|
||
};
|
||
if (customActive) bg.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush"); else bg.BorderBrush = _swatchDimBorder;
|
||
grid.Children.Add(bg);
|
||
if (customActive)
|
||
grid.Children.Add(new System.Windows.Shapes.Polygon
|
||
{
|
||
Points = [new Point(18, 7), new Point(18, 18), new Point(7, 18)],
|
||
Fill = RainbowBrush(),
|
||
IsHitTestVisible = false
|
||
});
|
||
grid.MouseLeftButtonDown += onClick;
|
||
return grid;
|
||
}
|
||
|
||
// Drag grip, centered vertically against whatever height the wrapped rows produce.
|
||
var textGrip = MakeBarGrip(4);
|
||
textGrip.VerticalAlignment = VerticalAlignment.Center;
|
||
// (grip is added to the WrapPanel host directly, below.)
|
||
|
||
var swatchRow1 = new StackPanel { Orientation = Orientation.Horizontal };
|
||
foreach (var color in SwatchColors)
|
||
{
|
||
var c = color;
|
||
bool isActive = c.R == _textColor.R && c.G == _textColor.G && c.B == _textColor.B;
|
||
swatchRow1.Children.Add(ColorSwatch(c, isActive, (_, _) =>
|
||
{
|
||
_textColor = Color.FromArgb(_textOpacity, c.R, c.G, c.B);
|
||
ApplyTextStyleToSelection();
|
||
ShowTextSettings();
|
||
}));
|
||
}
|
||
bool textCustom = !SwatchColors.Any(sc => sc.R == _textColor.R && sc.G == _textColor.G && sc.B == _textColor.B);
|
||
swatchRow1.Children.Add(MoreColorsSwatch(_textColor, textCustom, (_, _) => OpenColorPicker(_textColor, c =>
|
||
{
|
||
_textColor = Color.FromArgb(_textOpacity, c.R, c.G, c.B);
|
||
ApplyTextStyleToSelection();
|
||
ShowTextSettings();
|
||
}, () => ShowTextSettings())));
|
||
var grpColor = Group();
|
||
grpColor.Children.Add(DimLabel(Loc("Str_Bar_Color"), 0));
|
||
grpColor.Children.Add(swatchRow1);
|
||
|
||
var swatchRow2 = new StackPanel { Orientation = Orientation.Horizontal };
|
||
bool noneActive = _textFillColor.A == 0;
|
||
var noneGrid = new Grid { Width = 18, Height = 18, Margin = new Thickness(1), Cursor = Cursors.Hand };
|
||
var noneBg = new Border { CornerRadius = new CornerRadius(3), Background = Brushes.White, BorderThickness = new Thickness(noneActive ? 2 : 1) };
|
||
if (noneActive) noneBg.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush"); else noneBg.BorderBrush = _swatchDimBorder;
|
||
noneGrid.Children.Add(noneBg);
|
||
noneGrid.Children.Add(new System.Windows.Shapes.Line { X1 = 3, Y1 = 15, X2 = 15, Y2 = 3, Stroke = Brushes.Red, StrokeThickness = 1.5 });
|
||
noneGrid.MouseLeftButtonDown += (_, _) =>
|
||
{
|
||
_textFillColor = Color.FromArgb(0, _textFillColor.R, _textFillColor.G, _textFillColor.B);
|
||
ApplyTextStyleToSelection();
|
||
ShowTextSettings();
|
||
};
|
||
swatchRow2.Children.Add(noneGrid);
|
||
foreach (var color in SwatchColors)
|
||
{
|
||
var c = color;
|
||
bool isActive = _textFillColor.A > 0 && c.R == _textFillColor.R && c.G == _textFillColor.G && c.B == _textFillColor.B;
|
||
swatchRow2.Children.Add(ColorSwatch(c, isActive, (_, _) =>
|
||
{
|
||
byte a = _textFillColor.A == 0 ? (byte)255 : _textFillColor.A; // enable at full/current opacity
|
||
_textFillColor = Color.FromArgb(a, c.R, c.G, c.B);
|
||
ApplyTextStyleToSelection();
|
||
ShowTextSettings();
|
||
}));
|
||
}
|
||
bool fillCustom = _textFillColor.A > 0 && !SwatchColors.Any(sc => sc.R == _textFillColor.R && sc.G == _textFillColor.G && sc.B == _textFillColor.B);
|
||
swatchRow2.Children.Add(MoreColorsSwatch(_textFillColor.A == 0 ? Colors.White : _textFillColor, fillCustom, (_, _) => OpenColorPicker(_textFillColor.A == 0 ? Colors.White : _textFillColor, c =>
|
||
{
|
||
byte a = _textFillColor.A == 0 ? (byte)255 : _textFillColor.A;
|
||
_textFillColor = Color.FromArgb(a, c.R, c.G, c.B);
|
||
ApplyTextStyleToSelection();
|
||
ShowTextSettings();
|
||
}, () => ShowTextSettings())));
|
||
var grpFill = Group();
|
||
grpFill.Children.Add(DimLabel(Loc("Str_Bar_Fill"), 0));
|
||
grpFill.Children.Add(swatchRow2);
|
||
|
||
// Size group: its own single-row group, packed beside Font whenever the width allows.
|
||
var sizeStack = Group();
|
||
sizeStack.Children.Add(DimLabel(Loc("Str_Bar_Size"), 0));
|
||
var sizeSlider = new Slider
|
||
{
|
||
Minimum = 8,
|
||
Maximum = 72,
|
||
Value = Math.Max(8, Math.Min(72, _textFontSize)),
|
||
Width = 90,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
TickFrequency = 1,
|
||
IsSnapToTickEnabled = true,
|
||
Style = (Style)FindResource("DarkSlider")
|
||
};
|
||
// Editable size box (type an exact value; the slider stays for quick coarse adjustment).
|
||
var sizeBox = new TextBox
|
||
{
|
||
Text = $"{_textFontSize:F0}",
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 11,
|
||
Width = 32,
|
||
MaxLength = 4,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
VerticalContentAlignment = VerticalAlignment.Center,
|
||
TextAlignment = TextAlignment.Center,
|
||
Margin = new Thickness(4, 0, 0, 0),
|
||
BorderThickness = new Thickness(1),
|
||
Template = FlatTextBoxTemplate()
|
||
};
|
||
sizeBox.SetResourceReference(TextBox.BackgroundProperty, "PaneBrush");
|
||
sizeBox.SetResourceReference(TextBox.ForegroundProperty, "TextBrush");
|
||
sizeBox.SetResourceReference(TextBox.BorderBrushProperty, "CardBorderBrush");
|
||
sizeBox.SetResourceReference(TextBox.CaretBrushProperty, "PrimaryBrush");
|
||
sizeBox.SetResourceReference(TextBox.SelectionBrushProperty, "RowSelectedBrush"); // no WPF-default blue
|
||
var ptLabel = new TextBlock
|
||
{
|
||
Text = "pt",
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 11,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(2, 0, 0, 0)
|
||
};
|
||
ptLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
||
// Slider drives the box; the box drives the slider. _suppressSizeSync breaks the feedback loop.
|
||
sizeSlider.ValueChanged += (s, e) =>
|
||
{
|
||
if (_suppressSizeSync) return;
|
||
_textFontSize = e.NewValue;
|
||
sizeBox.Text = $"{e.NewValue:F0}";
|
||
ApplyTextStyleToSelection();
|
||
};
|
||
void CommitSizeBox()
|
||
{
|
||
if (double.TryParse(sizeBox.Text, out double v))
|
||
{
|
||
_textFontSize = Math.Max(1, Math.Min(400, Math.Round(v)));
|
||
_suppressSizeSync = true;
|
||
sizeSlider.Value = Math.Max(8, Math.Min(72, _textFontSize)); // thumb clamps; box keeps exact
|
||
_suppressSizeSync = false;
|
||
ApplyTextStyleToSelection();
|
||
}
|
||
sizeBox.Text = $"{_textFontSize:F0}"; // normalize / revert invalid input
|
||
}
|
||
// Set an exact size and keep the slider + box in step (slider thumb clamps to 8-72).
|
||
void SetSize(double v)
|
||
{
|
||
_textFontSize = Math.Max(1, Math.Min(400, Math.Round(v)));
|
||
_suppressSizeSync = true;
|
||
sizeSlider.Value = Math.Max(8, Math.Min(72, _textFontSize));
|
||
_suppressSizeSync = false;
|
||
sizeBox.Text = $"{_textFontSize:F0}";
|
||
ApplyTextStyleToSelection();
|
||
}
|
||
// Tiny stepper button (− / +) for one-point nudges next to the slider.
|
||
Border StepButton(string glyph, Action onClick)
|
||
{
|
||
var st = new TextBlock
|
||
{
|
||
Text = glyph,
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 13,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
HorizontalAlignment = HorizontalAlignment.Center
|
||
};
|
||
st.SetResourceReference(TextBlock.ForegroundProperty, "TextBrush");
|
||
var sb = new Border
|
||
{
|
||
Width = 18,
|
||
Height = 20,
|
||
CornerRadius = new CornerRadius(3),
|
||
Margin = new Thickness(3, 0, 0, 0),
|
||
Cursor = Cursors.Hand,
|
||
BorderThickness = new Thickness(1),
|
||
Background = Brushes.Transparent,
|
||
Child = st,
|
||
BorderBrush = _swatchDimBorder
|
||
};
|
||
sb.MouseLeftButtonDown += (_, _) => onClick();
|
||
return sb;
|
||
}
|
||
sizeBox.PreviewKeyDown += (s, e) =>
|
||
{
|
||
if (e.Key == Key.Enter) { CommitSizeBox(); e.Handled = true; }
|
||
else if (e.Key == Key.Escape) { sizeBox.Text = $"{_textFontSize:F0}"; e.Handled = true; }
|
||
};
|
||
sizeBox.LostFocus += (s, e) => CommitSizeBox();
|
||
sizeBox.GotFocus += (s, e) => sizeBox.SelectAll();
|
||
sizeStack.Children.Add(sizeSlider);
|
||
sizeStack.Children.Add(StepButton("−", () => SetSize(_textFontSize - 1))); // minus
|
||
sizeStack.Children.Add(StepButton("+", () => SetSize(_textFontSize + 1)));
|
||
sizeStack.Children.Add(sizeBox);
|
||
sizeStack.Children.Add(ptLabel);
|
||
|
||
// Font group: typeface selector + Bold / Italic / Strikethrough / Underline.
|
||
// A small square toggle whose glyph previews its own effect (bold B, italic I, struck-through S).
|
||
Border StyleToggle(string glyph, string tip, bool active, FontWeight fw, FontStyle fs, TextDecorationCollection? deco, Action onClick)
|
||
{
|
||
var gt = new TextBlock
|
||
{
|
||
Text = glyph,
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 12,
|
||
FontWeight = fw,
|
||
FontStyle = fs,
|
||
TextDecorations = deco,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
HorizontalAlignment = HorizontalAlignment.Center
|
||
};
|
||
gt.SetResourceReference(TextBlock.ForegroundProperty, "TextBrush");
|
||
var b = new Border
|
||
{
|
||
Width = 22,
|
||
Height = 20,
|
||
CornerRadius = new CornerRadius(3),
|
||
Margin = new Thickness(2, 0, 0, 0),
|
||
Cursor = Cursors.Hand,
|
||
ToolTip = tip,
|
||
BorderThickness = new Thickness(active ? 2 : 1),
|
||
Background = active ? AccentBrush(40) : Brushes.Transparent,
|
||
Child = gt
|
||
};
|
||
if (active) b.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush"); else b.BorderBrush = _swatchDimBorder;
|
||
b.MouseLeftButtonDown += (_, _) => onClick();
|
||
return b;
|
||
}
|
||
|
||
var fontStack = Group();
|
||
fontStack.Children.Add(DimLabel(Loc("Str_Bar_Font"), 0));
|
||
var fontBox = new ComboBox
|
||
{
|
||
Width = 132,
|
||
Height = 22,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 11,
|
||
MaxDropDownHeight = 320,
|
||
Margin = new Thickness(0, 0, 4, 0)
|
||
};
|
||
if (FindResource("DarkComboBox") is Style cbStyle) fontBox.Style = cbStyle;
|
||
if (FindResource("DarkComboItem") is Style ciStyle) fontBox.ItemContainerStyle = ciStyle;
|
||
foreach (var fn in SystemFontNames) fontBox.Items.Add(fn);
|
||
fontBox.SelectedItem = _textFontName;
|
||
fontBox.SelectionChanged += (s, e) =>
|
||
{
|
||
if (fontBox.SelectedItem is string fn) { _textFontName = fn; ApplyTextStyleToSelection(); }
|
||
};
|
||
fontStack.Children.Add(fontBox);
|
||
fontStack.Children.Add(StyleToggle("B", Loc("Str_Lbl_Bold"), _textBold, FontWeights.Bold, FontStyles.Normal, null,
|
||
() => { _textBold = !_textBold; ApplyTextStyleToSelection(); ShowTextSettings(); }));
|
||
fontStack.Children.Add(StyleToggle("I", Loc("Str_Lbl_Italic"), _textItalic, FontWeights.Normal, FontStyles.Italic, null,
|
||
() => { _textItalic = !_textItalic; ApplyTextStyleToSelection(); ShowTextSettings(); }));
|
||
fontStack.Children.Add(StyleToggle("S", Loc("Str_Lbl_Strike"), _textStrike, FontWeights.Normal, FontStyles.Normal, TextDecorations.Strikethrough,
|
||
() => { _textStrike = !_textStrike; ApplyTextStyleToSelection(); ShowTextSettings(); }));
|
||
fontStack.Children.Add(StyleToggle("U", Loc("Str_Lbl_Underline"), _textUnderline, FontWeights.Normal, FontStyles.Normal, TextDecorations.Underline,
|
||
() => { _textUnderline = !_textUnderline; ApplyTextStyleToSelection(); ShowTextSettings(); }));
|
||
|
||
// Opacity and Fill Opacity: two independent single-row groups.
|
||
var grpOpacity = Group();
|
||
grpOpacity.Children.Add(DimLabel(Loc("Str_Bar_Opacity"), 0));
|
||
var opacitySlider = new Slider
|
||
{
|
||
Minimum = 10,
|
||
Maximum = 255,
|
||
Value = _textOpacity,
|
||
Width = 90,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Style = (Style)FindResource("DarkSlider")
|
||
};
|
||
grpOpacity.Children.Add(opacitySlider);
|
||
var opacityLabel = new TextBlock
|
||
{
|
||
Text = $"{(int)(_textOpacity / 255.0 * 100)}%",
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 11,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(4, 0, 0, 0),
|
||
Width = 40,
|
||
TextAlignment = TextAlignment.Right
|
||
};
|
||
opacityLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
||
grpOpacity.Children.Add(opacityLabel);
|
||
opacitySlider.ValueChanged += (s, e) =>
|
||
{
|
||
byte a = (byte)e.NewValue;
|
||
opacityLabel.Text = $"{(int)(a / 255.0 * 100)}%";
|
||
_textOpacity = a;
|
||
_textColor = Color.FromArgb(a, _textColor.R, _textColor.G, _textColor.B);
|
||
ApplyTextStyleToSelection();
|
||
};
|
||
|
||
var grpFillOp = Group();
|
||
grpFillOp.Children.Add(DimLabel(Loc("Str_Bar_FillOpacity"), 0));
|
||
byte curFillA = _textFillColor.A == 0 ? (byte)255 : _textFillColor.A;
|
||
var fillOpSlider = new Slider
|
||
{
|
||
Minimum = 10,
|
||
Maximum = 255,
|
||
Value = curFillA,
|
||
Width = 90,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Style = (Style)FindResource("DarkSlider")
|
||
};
|
||
grpFillOp.Children.Add(fillOpSlider);
|
||
var fillOpLabel = new TextBlock
|
||
{
|
||
Text = $"{(int)(curFillA / 255.0 * 100)}%",
|
||
FontFamily = UiKit.UiFont,
|
||
FontSize = 11,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
Margin = new Thickness(4, 0, 0, 0),
|
||
Width = 40,
|
||
TextAlignment = TextAlignment.Right
|
||
};
|
||
fillOpLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
||
grpFillOp.Children.Add(fillOpLabel);
|
||
fillOpSlider.ValueChanged += (s, e) =>
|
||
{
|
||
byte a = (byte)e.NewValue;
|
||
fillOpLabel.Text = $"{(int)(a / 255.0 * 100)}%";
|
||
// Dragging opacity turns the fill on (defaults to the current color, white for whiteout).
|
||
_textFillColor = Color.FromArgb(a, _textFillColor.R, _textFillColor.G, _textFillColor.B);
|
||
ApplyTextStyleToSelection();
|
||
};
|
||
|
||
// Intentional two-row interim layout for 1.7.5, assembled as three vertical pairs so
|
||
// related controls share an exact left edge: Font over Size, Text Color over Fill, and
|
||
// Text Opacity over Fill Opacity. The outer WrapPanel moves a whole pair at narrow split-
|
||
// pane widths instead of separating a label from the control directly beneath it.
|
||
var fontPair = new StackPanel();
|
||
fontPair.Children.Add(fontStack);
|
||
fontPair.Children.Add(sizeStack);
|
||
|
||
var colorPair = new StackPanel();
|
||
colorPair.Children.Add(grpColor);
|
||
colorPair.Children.Add(grpFill);
|
||
|
||
var opacityPair = new StackPanel();
|
||
opacityPair.Children.Add(grpOpacity);
|
||
opacityPair.Children.Add(grpFillOp);
|
||
|
||
var pairHost = new WrapPanel
|
||
{
|
||
Orientation = Orientation.Horizontal,
|
||
Margin = new Thickness(8, 2, 8, 2),
|
||
Background = Brushes.Transparent
|
||
};
|
||
pairHost.Children.Add(textGrip);
|
||
pairHost.Children.Add(fontPair);
|
||
pairHost.Children.Add(colorPair);
|
||
pairHost.Children.Add(opacityPair);
|
||
_annotBarDragInners.Clear();
|
||
|
||
_textSettingsBar = new Border
|
||
{
|
||
BorderThickness = new Thickness(1, 0, 1, 1), // no top border - the toolbar above already separates
|
||
HorizontalAlignment = HorizontalAlignment.Right, // right-anchored; slid via the grip
|
||
VerticalAlignment = VerticalAlignment.Top,
|
||
CornerRadius = new CornerRadius(0),
|
||
Padding = new Thickness(4),
|
||
Effect = AnnotBarShadow(),
|
||
Child = BuildBarHost(pairHost),
|
||
Margin = new Thickness(0, 0, 0, 0)
|
||
};
|
||
_textSettingsBar.SetResourceReference(Border.BackgroundProperty, "BgFlyout");
|
||
_textSettingsBar.SetResourceReference(Border.BorderBrushProperty, "PaneBorderBrush");
|
||
_textSettingsBar.SetResourceReference(Border.CornerRadiusProperty, "AnnotationBarCornerRadius");
|
||
|
||
var previewArea = PagePreviewPanel.Parent as Grid;
|
||
if (previewArea is not null)
|
||
{
|
||
Panel.SetZIndex(_textSettingsBar, 100);
|
||
previewArea.Children.Add(_textSettingsBar);
|
||
// Cap the paired layout to the document area. At split-pane widths whole columns wrap
|
||
// together; at normal widths the two rows retain their deliberate vertical alignment.
|
||
pairHost.SetBinding(FrameworkElement.MaxWidthProperty, new System.Windows.Data.Binding("ActualWidth")
|
||
{ Source = previewArea, Converter = _barWidthInset });
|
||
WireBarWrapAdaptation(pairHost, textGrip, fontPair, previewArea);
|
||
PlaceAnnotationBar(_textSettingsBar, textGrip, fadeIn: appearing);
|
||
}
|
||
_annotBarTool = EditTool.Text;
|
||
_annotBarMinimized = false; // a freshly built bar is full-size
|
||
}
|
||
|
||
// (The shared overflow machinery - MakeBarOverflow and WireBarOverflow - lives in
|
||
// AnnotationBars.cs, used by this bar and the draw/highlight/line/shape bar alike.)
|
||
|
||
private void HideTextSettings()
|
||
{
|
||
FadeOutAndRemoveBar(_textSettingsBar);
|
||
_textSettingsBar = null;
|
||
if (_annotBarTool == EditTool.Text) _annotBarTool = null;
|
||
}
|
||
|
||
private void PlaceImageFromDialog(Point pos, int pageIdx)
|
||
{
|
||
var dlg = new Controls.FileDialog(Controls.FileDialogMode.Open)
|
||
{
|
||
Title = Loc("Str_Dlg_InsertImage"),
|
||
Filter = Loc("Str_Filter_Images") + "|*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tiff;*.tif|" + Loc("Str_Filter_AllFiles") + "|*.*",
|
||
ShowImagePreview = true
|
||
};
|
||
if (dlg.ShowDialog(this) != true) return;
|
||
|
||
try
|
||
{
|
||
var imgBytes = File.ReadAllBytes(dlg.FileName);
|
||
var bmp = new BitmapImage();
|
||
bmp.BeginInit();
|
||
bmp.StreamSource = new MemoryStream(imgBytes);
|
||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||
bmp.EndInit();
|
||
|
||
double srcW = bmp.PixelWidth > 0 ? bmp.PixelWidth : 400;
|
||
double srcH = bmp.PixelHeight > 0 ? bmp.PixelHeight : 300;
|
||
|
||
// Default the placed image to ~50% of the page's longest side (in render-dim
|
||
// units) so it is a usable size regardless of page dimensions, never upscaling
|
||
// beyond the source's native resolution.
|
||
double pageMax = _renderDims.TryGetValue(pageIdx, out var rdImg)
|
||
? Math.Max(rdImg.w, rdImg.h) : 2048.0;
|
||
double MaxCanvasDim = pageMax * 0.5;
|
||
double scale = Math.Min(1.0, Math.Min(MaxCanvasDim / srcW, MaxCanvasDim / srcH));
|
||
|
||
var imgAnnot = new ImageAnnotation
|
||
{
|
||
PageIndex = pageIdx,
|
||
Position = pos,
|
||
Scale = scale,
|
||
SourceWidth = srcW,
|
||
SourceHeight = srcH,
|
||
ImageData = Convert.ToBase64String(imgBytes)
|
||
};
|
||
|
||
// Switch to Select FIRST so placement renders last and nothing wipes the image
|
||
// (calling SetTool between render and select was what made the image vanish).
|
||
SetTool(EditTool.Select);
|
||
AddAnnotation(imgAnnot);
|
||
RenderAllAnnotations(pageIdx);
|
||
double w = srcW * scale;
|
||
double h = srcH * scale;
|
||
SelectAnnotation(imgAnnot, new Rect(pos.X, pos.Y, w, h));
|
||
SetStatus(Loc("Str_St_ImagePlaced"));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
KillerDialog.Show(this, Loc("Str_Err_LoadImageFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
// Ctrl+V: drop a clipboard image (as an image annotation) or clipboard text (as a text
|
||
// annotation) onto the current page, centered, then select it. Coordinates are in the page's
|
||
// render-dim space (== _renderDims[page]), matching how clicks place annotations.
|
||
private void PasteFromClipboard()
|
||
{
|
||
if (_doc is null) return;
|
||
int pageIdx = PageList.SelectedIndex;
|
||
if (pageIdx < 0) pageIdx = 0;
|
||
if (pageIdx >= _doc.PageCount) return;
|
||
|
||
double pw = _renderDims.TryGetValue(pageIdx, out var rd) ? rd.w : 2048.0;
|
||
double ph = _renderDims.TryGetValue(pageIdx, out var rd2) ? rd2.h : 2048.0;
|
||
|
||
try
|
||
{
|
||
if (Clipboard.ContainsImage())
|
||
{
|
||
var src = Clipboard.GetImage();
|
||
if (src is null) { SetStatus(Loc("Str_St_ClipImageUnreadable")); return; }
|
||
|
||
// Encode to PNG so ImageAnnotation stores standard bytes (same as file import).
|
||
byte[] imgBytes;
|
||
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(src));
|
||
using (var ms = new MemoryStream()) { encoder.Save(ms); imgBytes = ms.ToArray(); }
|
||
|
||
double srcW = src.PixelWidth > 0 ? src.PixelWidth : 400;
|
||
double srcH = src.PixelHeight > 0 ? src.PixelHeight : 300;
|
||
double pageMax = Math.Max(pw, ph);
|
||
double maxCanvasDim = pageMax * 0.5;
|
||
double scale = Math.Min(1.0, Math.Min(maxCanvasDim / srcW, maxCanvasDim / srcH));
|
||
double w = srcW * scale, h = srcH * scale;
|
||
var pos = new Point((pw - w) / 2, (ph - h) / 2);
|
||
|
||
var imgAnnot = new ImageAnnotation
|
||
{
|
||
PageIndex = pageIdx,
|
||
Position = pos,
|
||
Scale = scale,
|
||
SourceWidth = srcW,
|
||
SourceHeight = srcH,
|
||
ImageData = Convert.ToBase64String(imgBytes)
|
||
};
|
||
SetTool(EditTool.Select);
|
||
AddAnnotation(imgAnnot);
|
||
RenderAllAnnotations(pageIdx);
|
||
SelectAnnotation(imgAnnot, new Rect(pos.X, pos.Y, w, h));
|
||
SetStatus(Loc("Str_St_PastedImage"));
|
||
}
|
||
else if (Clipboard.ContainsText())
|
||
{
|
||
string content = Clipboard.GetText().Trim();
|
||
if (string.IsNullOrEmpty(content)) { SetStatus(Loc("Str_St_ClipNoText")); return; }
|
||
|
||
// Convert the point size to the page's canvas units (see PlaceTextBox).
|
||
double fontCanvas = _textFontSize;
|
||
double sy = _doc.Pages[pageIdx].Height.Point / Math.Max(1.0, ph);
|
||
if (sy > 0) fontCanvas = _textFontSize / sy;
|
||
|
||
var ta = new TextAnnotation
|
||
{
|
||
PageIndex = pageIdx,
|
||
Position = new Point(pw * 0.25, ph * 0.45),
|
||
Content = content,
|
||
FontSize = fontCanvas
|
||
};
|
||
ta.SetColor(_textColor);
|
||
SetTool(EditTool.Select);
|
||
AddAnnotation(ta);
|
||
RenderAllAnnotations(pageIdx);
|
||
SelectAnnotation(ta, AnnotBounds(ta));
|
||
SetStatus(Loc("Str_St_PastedText"));
|
||
}
|
||
else
|
||
{
|
||
SetStatus(Loc("Str_St_ClipEmpty"));
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
KillerDialog.Show(this, Loc("Str_Err_PasteFailed") + "\n" + ex.Message, "MmdPdf", MessageBoxButton.OK, MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|