Files
mmd-pdf/Shell/TextSettingsBar.cs
T
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- 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.
2026-08-27 07:37:09 +02:00

773 lines
37 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}
}