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 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(); 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(); 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); } } } }