942 lines
52 KiB
C#
942 lines
52 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.Controls.Primitives;
|
|
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 KillerPDF.Services;
|
|
using PdfPigDoc = UglyToad.PdfPig.PdfDocument;
|
|
|
|
namespace KillerPDF
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
// ============================================================
|
|
// Draw/Highlight settings bar
|
|
// ============================================================
|
|
|
|
// The quick-colors shown in the annotate bars. User-configurable via the color picker's swatch
|
|
// row (shared "UserSwatches" setting); seeded with these 8 defaults, restorable via the picker's
|
|
// Reset. SwatchColors reads the live set each time a bar is built, so edits show up immediately.
|
|
private static readonly Color[] DefaultSwatchColors = UiKit.DefaultSwatches;
|
|
private static Color[] SwatchColors => LoadUserSwatches();
|
|
private static Color[] LoadUserSwatches()
|
|
{
|
|
var raw = App.GetSetting("UserSwatches");
|
|
if (string.IsNullOrWhiteSpace(raw)) return [.. DefaultSwatchColors];
|
|
List<Color> list = [];
|
|
foreach (var part in raw!.Split(','))
|
|
{
|
|
var t = part.Trim().TrimStart('#');
|
|
if (t.Length == 6 && int.TryParse(t, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out int v))
|
|
list.Add(Color.FromRgb((byte)((v >> 16) & 0xFF), (byte)((v >> 8) & 0xFF), (byte)(v & 0xFF)));
|
|
}
|
|
return list.Count > 0 ? [.. list] : [.. DefaultSwatchColors];
|
|
}
|
|
|
|
// Frozen cached brushes for hot-path UI construction
|
|
private static readonly SolidColorBrush _swatchDimBorder = Freeze(new SolidColorBrush(Color.FromRgb(0x44, 0x44, 0x44)));
|
|
private static readonly SolidColorBrush _drawBarBackground = Freeze(new SolidColorBrush(Color.FromRgb(0x1a, 0x1a, 0x1a)));
|
|
private static readonly SolidColorBrush _thumbBorderBrush = Freeze(new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)));
|
|
|
|
private static T Freeze<T>(T freezable) where T : System.Windows.Freezable
|
|
{
|
|
freezable.Freeze();
|
|
return freezable;
|
|
}
|
|
|
|
// Wraps a floating annotation bar's content with the app's film-grain layer so these bars
|
|
// carry the same texture as the Settings / signature / dialog surfaces. The grain extends
|
|
// under the host border's 4px padding (negative margin) and matches its bottom corners.
|
|
private Grid GrainWrap(UIElement content)
|
|
{
|
|
var g = new Grid();
|
|
// SetResourceReference, not a FindResource snapshot: these hosts are built once and
|
|
// outlive theme switches, so a snapshot kept the grain painting on 98SE (whose
|
|
// GrainOpacity is 0) and its -4 margin overhang made the bevel read as misaligned.
|
|
var grain = new Border { Margin = new Thickness(-4), IsHitTestVisible = false };
|
|
grain.SetResourceReference(Border.CornerRadiusProperty, "AnnotationBarCornerRadius");
|
|
grain.SetResourceReference(UIElement.OpacityProperty, "GrainOpacity");
|
|
grain.SetResourceReference(Border.BackgroundProperty, "GrainBrushShared");
|
|
g.Children.Add(grain);
|
|
g.Children.Add(content);
|
|
return g;
|
|
}
|
|
|
|
// A grab handle (vertical dots) placed at the left of an annotation bar so it can be slid
|
|
// left/right along the top of the document. Returns the handle for EnableBarSlide.
|
|
private Border MakeBarGrip(int dotCount = 3)
|
|
{
|
|
// Real ellipse dots (not a braille glyph, which didn't render on some fonts/themes - the
|
|
// grip looked empty on the Light bars). Matches the sidebar splitter / minimized-bar dots.
|
|
// dotCount scales with bar height: 3 for single-row bars, 4 for the double-height text bar.
|
|
var dots = new StackPanel { Orientation = Orientation.Vertical, VerticalAlignment = VerticalAlignment.Center };
|
|
var fill = (Brush)FindResource("MutedTextBrush");
|
|
for (int i = 0; i < dotCount; i++)
|
|
dots.Children.Add(new System.Windows.Shapes.Ellipse
|
|
{ Width = 3, Height = 3, Margin = new Thickness(0, 1.5, 0, 1.5), Fill = fill });
|
|
return new Border
|
|
{
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
Padding = new Thickness(1, 0, 10, 0), // hard to the left edge, more gap before the labels
|
|
VerticalAlignment = VerticalAlignment.Stretch,
|
|
Child = dots
|
|
};
|
|
}
|
|
|
|
// Wraps an annotate bar's content with a film-grain layer (always visible, even minimized) and a
|
|
// hidden grip-dots strip (the same dots the sidebar splitter uses) revealed when minimized.
|
|
private FrameworkElement BuildBarHost(FrameworkElement content)
|
|
{
|
|
var host = new Grid();
|
|
|
|
// KillerNotes ribbon contract: 98SE adds the dark right edge to the host's light
|
|
// top/left edge. Other themes define this as transparent/zero, so their floating
|
|
// annotation card remains unchanged.
|
|
var darkEdge = new Border { IsHitTestVisible = false };
|
|
darkEdge.SetResourceReference(Border.BorderBrushProperty, "BarEdgeDarkBrush");
|
|
darkEdge.SetResourceReference(Border.BorderThicknessProperty, "BarEdgeDarkThickness");
|
|
Panel.SetZIndex(darkEdge, 20);
|
|
host.Children.Add(darkEdge);
|
|
|
|
// Grain stays put when the controls collapse, so the minimized strip keeps the texture.
|
|
// SetResourceReference so a theme switch retargets it - 98SE's GrainOpacity of 0 must
|
|
// actually clear the texture on an already-built bar (a FindResource snapshot did not).
|
|
var hostGrain = new Border { Margin = new Thickness(-4), IsHitTestVisible = false };
|
|
hostGrain.SetResourceReference(Border.CornerRadiusProperty, "AnnotationBarCornerRadius");
|
|
hostGrain.SetResourceReference(UIElement.OpacityProperty, "GrainOpacity");
|
|
hostGrain.SetResourceReference(Border.BackgroundProperty, "GrainBrushShared");
|
|
host.Children.Add(hostGrain);
|
|
|
|
host.Children.Add(content); // the collapsible controls
|
|
|
|
var dots = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
IsHitTestVisible = false
|
|
};
|
|
var fill = TryFindResource("MutedTextBrush") as Brush ?? Brushes.Gray; // match the sidebar handle dots
|
|
for (int i = 0; i < 6; i++)
|
|
dots.Children.Add(new System.Windows.Shapes.Ellipse
|
|
{ Width = 3, Height = 3, Margin = new Thickness(2, 0, 2, 0), Fill = fill });
|
|
// The dots live in a transparent, hit-testable strip that fills the bar. While the bar is
|
|
// minimized this strip is shown, so the whole peek strip can be dragged left/right (the slide
|
|
// is wired to it in PlaceAnnotationBar) - same as dragging the grip on the expanded bar.
|
|
var dotsStrip = new Border
|
|
{
|
|
Background = Brushes.Transparent,
|
|
Cursor = Cursors.Hand,
|
|
Visibility = Visibility.Collapsed,
|
|
Child = dots
|
|
};
|
|
host.Children.Add(dotsStrip);
|
|
|
|
_annotBarContent = content;
|
|
_annotBarDots = dotsStrip;
|
|
return host;
|
|
}
|
|
|
|
// Drop shadow for the annotate bars: offset straight down with depth >= blur, so it falls on the
|
|
// sides and bottom but never above the bar (no halo between it and the toolbar). Removed entirely
|
|
// while minimized.
|
|
private static System.Windows.Media.Effects.DropShadowEffect AnnotBarShadow()
|
|
=> new() { Color = Colors.Black, BlurRadius = 6, ShadowDepth = 3, Direction = 270,
|
|
Opacity = Application.Current.TryFindResource("BarShadowOpacity") is double opacity ? opacity : 0.38 };
|
|
|
|
// Lets the annotation bars slide horizontally along the top via their grip, clamped inside
|
|
// the document area, with the X position remembered (shared across the draw/text bars).
|
|
private void EnableBarSlide(FrameworkElement grip, Border bar, FrameworkElement bounds, bool backgroundOnly = false)
|
|
{
|
|
grip.MouseLeftButtonDown += (s, e) =>
|
|
{
|
|
// When wired on a content panel, only act on clicks that hit the panel's OWN background
|
|
// (empty gaps). A click on a child control (slider, swatch, combo) reports that child as
|
|
// the source, so we bail and let the control handle it - even controls that don't mark the
|
|
// event handled never start a drag. The grip / peek strip pass backgroundOnly=false.
|
|
if (backgroundOnly && !ReferenceEquals(e.OriginalSource, grip)) return;
|
|
// Double-click any draggable surface (grip, peek strip, or an empty area of the bar)
|
|
// toggles minimize - same gesture everywhere.
|
|
if (e.ClickCount == 2) { e.Handled = true; ToggleAnnotBarMinimized(); return; }
|
|
double w = bar.ActualWidth;
|
|
// Drag uniformly in left-edge coordinates whatever the current anchor; the edge it
|
|
// anchors to is decided on release from where it ends up.
|
|
double curLeft = bar.HorizontalAlignment == HorizontalAlignment.Right
|
|
? bounds.ActualWidth - bar.Margin.Right - w
|
|
: bar.Margin.Left;
|
|
bar.HorizontalAlignment = HorizontalAlignment.Left;
|
|
bar.Margin = new Thickness(curLeft, bar.Margin.Top, 0, 0);
|
|
bar.Tag = (e.GetPosition(bounds).X, curLeft); // (startX, origLeft)
|
|
grip.CaptureMouse();
|
|
e.Handled = true;
|
|
};
|
|
grip.MouseMove += (s, e) =>
|
|
{
|
|
if (bar.Tag is not (double startX, double origLeft) || !grip.IsMouseCaptured) return;
|
|
double w = bar.ActualWidth;
|
|
// Stop the drag at the scrollbar's left edge (not the pane edge) so the bar never
|
|
// overshoots the scrollbar and then snaps back on release - the "bounce" the user saw.
|
|
double sb = VerticalScrollBarInset();
|
|
double maxLeft = Math.Max(0, bounds.ActualWidth - w - sb);
|
|
double nl = Math.Max(0, Math.Min(maxLeft, origLeft + (e.GetPosition(bounds).X - startX)));
|
|
bar.Margin = new Thickness(nl, bar.Margin.Top, 0, 0);
|
|
// Merge the docked-side border with the pane border live while dragging (no footprint
|
|
// change), so it doesn't pop in on release. The right side only docks flush when no
|
|
// scrollbar sits between the bar and the pane edge.
|
|
SetBarDockedBorder(bar, dockedLeft: nl <= 0.5, dockedRight: sb <= 0 && nl >= maxLeft - 0.5);
|
|
};
|
|
grip.MouseLeftButtonUp += (s, e) =>
|
|
{
|
|
if (!grip.IsMouseCaptured) return;
|
|
grip.ReleaseMouseCapture();
|
|
double w = bar.ActualWidth;
|
|
double left = bar.Margin.Left;
|
|
// Measure the right gap from the scrollbar's left edge (the usable content edge), so a
|
|
// bar parked against the scrollbar records gap ~0 and PositionAnnotationBar re-adds the
|
|
// scrollbar width once - no double inset, no jump.
|
|
double sb = VerticalScrollBarInset();
|
|
double rightGap = Math.Max(0, (bounds.ActualWidth - sb) - (left + w));
|
|
const double snap = 24; // within this many px of an edge, cling to that edge exactly
|
|
if (left <= snap)
|
|
{
|
|
_annotBarCenterFrac = null; _annotBarAnchorRight = false; _annotBarGap = Math.Max(0, left);
|
|
}
|
|
else if (rightGap <= snap)
|
|
{
|
|
_annotBarCenterFrac = null; _annotBarAnchorRight = true; _annotBarGap = rightGap;
|
|
}
|
|
else
|
|
{
|
|
// Away from both edges: remember it as a fraction of the width so resizing scales it
|
|
// smoothly rather than snapping it to an edge.
|
|
_annotBarCenterFrac = (left + w / 2) / bounds.ActualWidth;
|
|
}
|
|
App.SetSetting("AnnotBarFrac",
|
|
_annotBarCenterFrac?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "");
|
|
App.SetSetting("AnnotBarGap", ((int)(_annotBarGap ?? 8)).ToString());
|
|
App.SetSetting("AnnotBarRightSide", _annotBarAnchorRight ? "1" : "0");
|
|
if (PagePreviewPanel?.Parent is Grid area) PositionAnnotationBar(bar, area);
|
|
e.Handled = true;
|
|
};
|
|
}
|
|
|
|
// Saved horizontal placement for the floating draw/text settings bars (shared by both). The bar
|
|
// anchors to whichever edge it sits nearer and remembers its gap from that edge, so it clings to
|
|
// that edge on resize. RepositionAnnotationBars re-applies it - clamped fully inside the document
|
|
// area - from the same window events that keep the Settings panel in-window, so it can never end
|
|
// up off-screen regardless of which edge it was parked against.
|
|
private double? _annotBarGap;
|
|
private bool _annotBarAnchorRight = true;
|
|
private double? _annotBarCenterFrac; // set when parked away from both edges: hold this fraction of the width
|
|
private bool _vScrollVisible; // last-known document vertical scrollbar state, to reposition bars on change
|
|
private EditTool? _annotBarTool { get => ActiveViewer.AnnotBarToolRef; set => ActiveViewer.AnnotBarToolRef = value; }
|
|
private bool _annotBarMinimized { get => ActiveViewer.AnnotBarMinimizedRef; set => ActiveViewer.AnnotBarMinimizedRef = value; }
|
|
private double _annotBarFullHeight; // remembered full height to expand back to
|
|
private FrameworkElement? _annotBarContent; // the bar's normal content (hidden while minimized)
|
|
private FrameworkElement? _annotBarDots; // grip-dots strip shown while minimized
|
|
private List<FrameworkElement> _annotBarDragInners => ActiveViewer.AnnotBarDragInnersRef;
|
|
|
|
// Positions an annotation bar and wires up sliding. If we already know the X (this session or
|
|
// saved), set it synchronously so the bar appears in place; only the very first time do we
|
|
// defer to compute the default top-right from the laid-out width.
|
|
private void PlaceAnnotationBar(Border bar, Border grip, bool fadeIn = false)
|
|
{
|
|
if (PagePreviewPanel.Parent is not Grid area) return;
|
|
|
|
if (_annotBarGap is null && _annotBarCenterFrac is null)
|
|
{
|
|
if (double.TryParse(App.GetSetting("AnnotBarFrac"), System.Globalization.NumberStyles.Float,
|
|
System.Globalization.CultureInfo.InvariantCulture, out double f))
|
|
{
|
|
_annotBarCenterFrac = f; // parked away from both edges last time
|
|
}
|
|
else
|
|
{
|
|
_annotBarGap = int.TryParse(App.GetSetting("AnnotBarGap"), out int sg) ? sg : 8;
|
|
_annotBarAnchorRight = App.GetSetting("AnnotBarRightSide") != "0"; // default: right edge
|
|
}
|
|
}
|
|
// Track the bar's own size (first measure, wrap to a second row) so the 98SE scroller
|
|
// inset and the floating themes' edge clamp stay in step with the real height/width.
|
|
bar.SizeChanged -= AnnotBarSizeChanged;
|
|
bar.SizeChanged += AnnotBarSizeChanged;
|
|
EnableBarSlide(grip, bar, area);
|
|
// The minimized peek strip drags the bar too, so a collapsed bar can be repositioned.
|
|
if (_annotBarDots is not null) EnableBarSlide(_annotBarDots, bar, area);
|
|
// Empty areas of the bar content drag it too (and double-click them to minimize). The content
|
|
// panel(s) have a Transparent background, so only the gaps between controls trigger this -
|
|
// clicks that land on a slider/swatch/combo still go to that control.
|
|
if (_annotBarContent is not null) EnableBarSlide(_annotBarContent, bar, area, backgroundOnly: true);
|
|
foreach (var inner in _annotBarDragInners)
|
|
if (inner is not null) EnableBarSlide(inner, bar, area, backgroundOnly: true);
|
|
// A freshly built bar has no measured width yet, so PositionAnnotationBar can't place it until
|
|
// layout runs. Hide it for that one frame (Opacity 0 still lays out, so width measures), then
|
|
// anchor and reveal it - otherwise it renders at its default right edge first and visibly
|
|
// jumps to its saved spot on every tool switch.
|
|
bar.Opacity = 0;
|
|
PositionAnnotationBar(bar, area); // sync position (edge-anchored modes are correct without a width)
|
|
if (fadeIn)
|
|
{
|
|
// Start the fade-in immediately so it overlaps the outgoing bar's fade-out (a true
|
|
// crossfade). Waiting for the deferred layout pass left a frame where neither bar was
|
|
// visible, which read as a blink. Final clamp/center still happens once laid out.
|
|
bar.BeginAnimation(UIElement.OpacityProperty,
|
|
new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(110)))
|
|
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } });
|
|
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded,
|
|
(Action)(() => PositionAnnotationBar(bar, area)));
|
|
}
|
|
else if (_annotBarCenterFrac is not null)
|
|
{
|
|
// Center-parked needs a measured width to place, so stay hidden one layout frame so it
|
|
// can't render at the default edge first and then jump.
|
|
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded,
|
|
(Action)(() => { PositionAnnotationBar(bar, area); bar.Opacity = 1; }));
|
|
}
|
|
else
|
|
{
|
|
// Edge-anchored: already placed correctly above (no width needed), so reveal it right away
|
|
// instead of hiding for a frame - that one hidden frame was the "blink" on a same-tool
|
|
// refresh / same-family tool switch.
|
|
bar.Opacity = 1;
|
|
}
|
|
}
|
|
|
|
// Subtracts the annotate-bar chrome (padding + border + a gap from the scrollbar) from the document
|
|
// area's width, giving the wrap panel the width it's actually allowed to occupy before wrapping.
|
|
private static readonly InsetWidthConverter _barWidthInset = new();
|
|
private sealed class InsetWidthConverter : System.Windows.Data.IValueConverter
|
|
{
|
|
public object Convert(object value, Type t, object p, System.Globalization.CultureInfo c)
|
|
=> value is double d ? Math.Max(0.0, d - 28.0) : value;
|
|
public object ConvertBack(object value, Type t, object p, System.Globalization.CultureInfo c) => value;
|
|
}
|
|
|
|
// Adapts a wrapping annotate bar to very narrow widths: once its content blocks can no longer sit on
|
|
// a single row, the drag grip is hidden (non-essential at that size) and the blocks gain a little
|
|
// vertical breathing room as they stack; the inline separators are hidden so they don't float as
|
|
// stray ticks between stacked rows. The single-row fit test deliberately uses only fixed quantities
|
|
// (block widths + a constant gap allowance, never the grip or separator visibility we toggle), so a
|
|
// toggle can never change the test result and oscillate.
|
|
private void WireBarWrapAdaptation(WrapPanel host, FrameworkElement grip, FrameworkElement primary,
|
|
FrameworkElement sizeSource)
|
|
{
|
|
// The grip only shrinks to a thin draggable nub once the grip + the first block can no longer
|
|
// share a row (the window's skinniest) - not merely when the bar wraps. The threshold is the
|
|
// grip + first-block widths, measured ONCE at full size and frozen, so shrinking the grip can
|
|
// never feed back into the decision and oscillate. Spacing between groups is handled by the
|
|
// groups' own margins (not separators), so it survives wrapping with no stray ticks.
|
|
double gripThreshold = 0;
|
|
Thickness gripFull = (grip as Border)?.Padding ?? new Thickness();
|
|
bool? lastGripMin = null;
|
|
void SetGripMinimized(bool min)
|
|
{
|
|
if (grip is not Border gb) return;
|
|
if (gb.Child is UIElement dots) dots.Visibility = min ? Visibility.Collapsed : Visibility.Visible;
|
|
gb.Padding = min ? new Thickness(1, 0, 2, 0) : gripFull; // keep a few draggable px
|
|
}
|
|
void Apply()
|
|
{
|
|
double avail = host.MaxWidth;
|
|
if (double.IsNaN(avail) || double.IsInfinity(avail) || avail <= 0) return;
|
|
if (gripThreshold <= 0)
|
|
{
|
|
double g = grip.DesiredSize.Width, p = primary.DesiredSize.Width;
|
|
if (g <= 0 || p <= 0) return; // not measured yet; a later pass will retry
|
|
gripThreshold = g + p + 8.0;
|
|
}
|
|
bool gripMin = avail + 0.5 < gripThreshold;
|
|
if (lastGripMin == gripMin) return;
|
|
lastGripMin = gripMin;
|
|
SetGripMinimized(gripMin);
|
|
}
|
|
// Drive off the document area's width (the monotonic signal) rather than the panel's own size:
|
|
// once the grip shrinks and the bar fits a row again, the panel stops resizing, so only the
|
|
// source's width change tells us there is room to restore the grip. Unhook on teardown - the bar
|
|
// is rebuilt often (every swatch click), so a lingering handler would leak.
|
|
void onSource(object? sender, SizeChangedEventArgs e) => Apply();
|
|
sizeSource.SizeChanged += onSource;
|
|
host.Unloaded += (_, _) => sizeSource.SizeChanged -= onSource;
|
|
host.SizeChanged += (_, _) => Apply(); // catches the first valid measurement
|
|
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)Apply);
|
|
}
|
|
|
|
// Shared overflow chevron + popup for the annotate bars: an E712 "More" button whose popup
|
|
// (anchored to the button - flyouts anchor to their own button, always) stacks whatever
|
|
// groups WireBarOverflow sheds.
|
|
private Border MakeBarOverflow(out Popup popup, out StackPanel stack)
|
|
{
|
|
var s = new StackPanel { Margin = new Thickness(10, 6, 10, 6) };
|
|
// Family flyout rule: the film grain is the LAST child, OVER the items, non-hit-testable.
|
|
var inner = new Grid();
|
|
inner.Children.Add(s);
|
|
// SetResourceReference so a theme switch retargets it (98SE clears grain via opacity 0).
|
|
var popGrain = new Border { CornerRadius = new CornerRadius(4), IsHitTestVisible = false };
|
|
popGrain.SetResourceReference(UIElement.OpacityProperty, "GrainOpacity");
|
|
popGrain.SetResourceReference(Border.BackgroundProperty, "GrainBrushShared");
|
|
inner.Children.Add(popGrain);
|
|
var border = new Border
|
|
{
|
|
BorderThickness = new Thickness(1),
|
|
CornerRadius = new CornerRadius(4),
|
|
Child = inner
|
|
};
|
|
border.SetResourceReference(Border.BackgroundProperty, "MenuBackgroundBrush");
|
|
border.SetResourceReference(Border.BorderBrushProperty, "MenuBorderBrush");
|
|
var glyph = new TextBlock
|
|
{
|
|
Text = ((char)0xE712).ToString(), // MDL2 More - the same chevron as the toolbar overflow
|
|
FontFamily = UiKit.IconFont,
|
|
FontSize = 13,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
};
|
|
glyph.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
var btn = new Border
|
|
{
|
|
Width = 22,
|
|
Height = 20,
|
|
CornerRadius = new CornerRadius(3),
|
|
Margin = new Thickness(0, 3, 0, 3),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Cursor = Cursors.Hand,
|
|
Background = Brushes.Transparent,
|
|
ToolTip = Loc("Str_Bar_More"),
|
|
Child = glyph,
|
|
Visibility = Visibility.Collapsed
|
|
};
|
|
var p = new Popup
|
|
{
|
|
PlacementTarget = btn,
|
|
Placement = PlacementMode.Bottom,
|
|
VerticalOffset = 4,
|
|
StaysOpen = false,
|
|
AllowsTransparency = true,
|
|
Child = border
|
|
};
|
|
// Open on mouse UP, not down: a StaysOpen=false popup opened mid-mouse-down inherits
|
|
// the press's capture and closes again the moment the button is released, so it only
|
|
// stayed open while the click was held. Down is still handled so the press can't start
|
|
// a bar drag. The 200ms guard covers the toggle-close case (the closing click's up
|
|
// would otherwise instantly reopen it).
|
|
var closedAt = DateTime.MinValue;
|
|
p.Closed += (_, _) => closedAt = DateTime.UtcNow;
|
|
btn.MouseLeftButtonDown += (_, e) => e.Handled = true;
|
|
btn.MouseLeftButtonUp += (_, e) =>
|
|
{
|
|
e.Handled = true;
|
|
if ((DateTime.UtcNow - closedAt).TotalMilliseconds < 200) return;
|
|
p.IsOpen = true;
|
|
};
|
|
popup = p;
|
|
stack = s;
|
|
return btn;
|
|
}
|
|
|
|
// Caps an annotate bar at two wrapped rows. Group widths are measured ONCE and frozen (the
|
|
// WireBarWrapAdaptation anti-oscillation rule): the fit test simulates the WrapPanel's own
|
|
// first-fit line breaking against the available width and, while the visible groups would
|
|
// need a third row, sheds the next group in shedOrder into the overflow popup - restoring
|
|
// them in reverse when there is room again. Groups not named in shedOrder never collapse.
|
|
private void WireBarOverflow(WrapPanel host, Border overflowBtn, Popup popup,
|
|
StackPanel popupStack, StackPanel[] groups, int[] shedOrder,
|
|
FrameworkElement sizeSource)
|
|
{
|
|
double[] widths = new double[groups.Length];
|
|
double btnWidth = 26; // chevron + margin; always reserved in the test (constant input)
|
|
int shedCount = 0;
|
|
|
|
int LinesNeeded(int shed)
|
|
{
|
|
var inPopup = new bool[groups.Length];
|
|
for (int i = 0; i < shed; i++) inPopup[shedOrder[i]] = true;
|
|
double avail = host.MaxWidth;
|
|
double line = btnWidth; int lines = 1;
|
|
for (int i = 0; i < groups.Length; i++)
|
|
{
|
|
if (inPopup[i]) continue;
|
|
double w = widths[i];
|
|
if (line + w > avail && line > btnWidth) { lines++; line = btnWidth; }
|
|
line += w;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
void MoveToPopup(int gi)
|
|
{
|
|
host.Children.Remove(groups[gi]);
|
|
int at = 0; // keep display order inside the popup too
|
|
foreach (UIElement child in popupStack.Children)
|
|
if (child is StackPanel sp && Array.IndexOf(groups, sp) is int ci && ci >= 0 && ci < gi) at++;
|
|
popupStack.Children.Insert(at, groups[gi]);
|
|
}
|
|
void MoveToBar(int gi)
|
|
{
|
|
popupStack.Children.Remove(groups[gi]);
|
|
int at = 1; // index 0 is the grip
|
|
for (int i = 0; i < gi; i++)
|
|
if (host.Children.Contains(groups[i])) at++;
|
|
host.Children.Insert(at, groups[gi]);
|
|
}
|
|
|
|
void Apply()
|
|
{
|
|
double avail = host.MaxWidth;
|
|
if (double.IsNaN(avail) || double.IsInfinity(avail) || avail <= 0) return;
|
|
if (widths[0] <= 0)
|
|
{
|
|
for (int i = 0; i < groups.Length; i++)
|
|
{
|
|
double w = groups[i].DesiredSize.Width;
|
|
if (w <= 0) return; // not measured yet; a later pass retries
|
|
widths[i] = w;
|
|
}
|
|
}
|
|
int want = shedCount;
|
|
while (want < shedOrder.Length && LinesNeeded(want) > 2) want++;
|
|
while (want > 0 && LinesNeeded(want - 1) <= 2) want--;
|
|
if (want == shedCount) return;
|
|
if (want > shedCount)
|
|
for (int i = shedCount; i < want; i++) MoveToPopup(shedOrder[i]);
|
|
else
|
|
for (int i = shedCount - 1; i >= want; i--) MoveToBar(shedOrder[i]);
|
|
shedCount = want;
|
|
overflowBtn.Visibility = shedCount > 0 ? Visibility.Visible : Visibility.Collapsed;
|
|
if (shedCount == 0 && popup.IsOpen) popup.IsOpen = false;
|
|
}
|
|
|
|
void onSource(object? sender, SizeChangedEventArgs e) => Apply();
|
|
sizeSource.SizeChanged += onSource;
|
|
host.Unloaded += (_, _) => sizeSource.SizeChanged -= onSource;
|
|
host.SizeChanged += (_, _) => Apply();
|
|
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)Apply);
|
|
}
|
|
|
|
private void ShowDrawSettings(EditTool tool)
|
|
{
|
|
// Fade the bar in only when it's genuinely appearing (no bar yet, or coming from the text
|
|
// bar). Switching between tools that share this same draw bar (Highlight / Underline /
|
|
// Strikethrough / Draw) swaps it in place instantly - otherwise the two near-identical bars
|
|
// crossfade through ~50% opacity and read as a blink even though nothing visually changed.
|
|
bool prevWasDrawBar = _annotBarTool is EditTool.Draw or EditTool.Highlight
|
|
or EditTool.Underline or EditTool.Strikethrough or EditTool.Line
|
|
or EditTool.Shape;
|
|
bool appearing = _annotBarTool != tool && !prevWasDrawBar;
|
|
if (_drawSettingsBar is not null)
|
|
{
|
|
// On a switch, fade the old bar out (crossfades with the new one); on a refresh, swap it
|
|
// out instantly so clicking a swatch doesn't flicker the whole bar.
|
|
if (appearing) FadeOutAndRemoveBar(_drawSettingsBar);
|
|
else (PagePreviewPanel.Parent as Grid)?.Children.Remove(_drawSettingsBar);
|
|
_drawSettingsBar = null;
|
|
}
|
|
|
|
// WrapPanel so that on a too-narrow window the control groups drop to a second/third row
|
|
// instead of overflowing. Its MaxWidth is pinned to the document area below so it knows when
|
|
// to wrap; controls are added in self-contained groups so a wrap never splits a label from
|
|
// its slider.
|
|
var panel = new WrapPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(8, 2, 8, 2), Background = Brushes.Transparent };
|
|
|
|
// Drag grip so the bar can be slid left/right along the top.
|
|
var drawGrip = MakeBarGrip();
|
|
panel.Children.Add(drawGrip);
|
|
|
|
// A small checkbox + label for the bar (Level on the Line tool, Eraser on Highlight / Draw).
|
|
// Toggling rebuilds the bar to reflect the new state.
|
|
StackPanel BarCheck(string label, bool active, string tip, Action onClick)
|
|
{
|
|
var p = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(2, 0, 18, 0),
|
|
Cursor = Cursors.Hand,
|
|
ToolTip = tip
|
|
};
|
|
var box = new Border
|
|
{
|
|
Width = 15,
|
|
Height = 15,
|
|
CornerRadius = new CornerRadius(3),
|
|
BorderThickness = new Thickness(active ? 0 : 1),
|
|
Background = Brushes.Transparent,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 5, 0)
|
|
};
|
|
if (active)
|
|
{
|
|
// Live theme reference (not a snapshot) so the checked fill tracks the current theme's
|
|
// accent - the old AccentBrush() snapshot kept whatever accent was active when the bar
|
|
// was first built, so it showed the wrong (often green) color after a theme switch.
|
|
box.SetResourceReference(Border.BackgroundProperty, "SelectionAccent");
|
|
box.Child = new TextBlock
|
|
{
|
|
Text = "✓",
|
|
Foreground = Brushes.White,
|
|
FontSize = 10,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
};
|
|
}
|
|
else box.BorderBrush = _swatchDimBorder;
|
|
var lbl = new TextBlock
|
|
{
|
|
Text = label,
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
VerticalAlignment = VerticalAlignment.Center
|
|
};
|
|
lbl.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
p.Children.Add(box);
|
|
p.Children.Add(lbl);
|
|
p.MouseLeftButtonDown += (_, _) => onClick();
|
|
return p;
|
|
}
|
|
// Built here, added at the END of the bar (after Opacity) where there's more room than
|
|
// squeezed in before the color swatches.
|
|
StackPanel? barCheck = tool switch
|
|
{
|
|
EditTool.Line => BarCheck(Loc("Str_Bar_Level"), _lineLevel,
|
|
Loc("Str_Bar_TT_Level"),
|
|
() => { _lineLevel = !_lineLevel; ShowDrawSettings(tool); }),
|
|
EditTool.Highlight => BarCheck(Loc("Str_Bar_Eraser"), _highlightErase,
|
|
Loc("Str_Bar_TT_EraserBox"),
|
|
() => { _highlightErase = !_highlightErase; ShowDrawSettings(tool); }),
|
|
EditTool.Draw => BarCheck(Loc("Str_Bar_Eraser"), _drawErase,
|
|
Loc("Str_Bar_TT_EraserBrush"),
|
|
() => { _drawErase = !_drawErase; ShowDrawSettings(tool); }),
|
|
EditTool.Shape => ShapeKindPicker(),
|
|
_ => null
|
|
};
|
|
|
|
// Shapes tool (#127 Phase 3): radio-style buttons whose glyphs are literal mini shapes
|
|
// (drawn WPF elements, not font glyphs, so they render on every locale/font setup),
|
|
// plus the Fill toggle. Clicking a shape switches the sub-mode and rebuilds the bar.
|
|
StackPanel ShapeKindPicker()
|
|
{
|
|
Border KindBtn(ShapeKind kind, Shape glyph, string tip)
|
|
{
|
|
bool active = _shapeKind == kind;
|
|
glyph.StrokeThickness = 1.5;
|
|
glyph.Fill = Brushes.Transparent;
|
|
glyph.HorizontalAlignment = HorizontalAlignment.Center;
|
|
glyph.VerticalAlignment = VerticalAlignment.Center;
|
|
glyph.SetResourceReference(Shape.StrokeProperty, active ? "SelectionAccent" : "MutedTextBrush");
|
|
var b = new Border
|
|
{
|
|
Width = 26,
|
|
Height = 20,
|
|
CornerRadius = new CornerRadius(3),
|
|
BorderThickness = new Thickness(active ? 1.5 : 1),
|
|
Background = Brushes.Transparent,
|
|
Margin = new Thickness(1, 0, 1, 0),
|
|
Cursor = Cursors.Hand,
|
|
ToolTip = tip,
|
|
Child = glyph
|
|
};
|
|
if (active) b.SetResourceReference(Border.BorderBrushProperty, "SelectionAccent");
|
|
else b.BorderBrush = _swatchDimBorder;
|
|
b.MouseLeftButtonDown += (_, _) =>
|
|
{
|
|
if (_shapeKind == kind) return;
|
|
_shapeKind = kind;
|
|
if (kind != ShapeKind.Polygon) CancelShapePolygon();
|
|
ShowDrawSettings(tool);
|
|
};
|
|
return b;
|
|
}
|
|
|
|
var row = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(2, 0, 0, 0) };
|
|
row.Children.Add(KindBtn(ShapeKind.Rectangle, new Rectangle { Width = 13, Height = 9 },
|
|
Loc("Str_Bar_TT_ShapeBox")));
|
|
row.Children.Add(KindBtn(ShapeKind.Ellipse, new Ellipse { Width = 13, Height = 9 },
|
|
Loc("Str_Bar_TT_ShapeEllipse")));
|
|
var pent = new Polygon
|
|
{
|
|
Width = 13,
|
|
Height = 11,
|
|
Points = [new Point(6.5, 0), new Point(13, 4.5), new Point(10.5, 11), new Point(2.5, 11), new Point(0, 4.5)]
|
|
};
|
|
row.Children.Add(KindBtn(ShapeKind.Polygon, pent,
|
|
Loc("Str_Bar_TT_ShapeFreeform")));
|
|
|
|
var fillCheck = BarCheck(Loc("Str_Bar_ShapeFill"), _shapeFill,
|
|
Loc("Str_Bar_TT_ShapeFill"),
|
|
() => { _shapeFill = !_shapeFill; ShowDrawSettings(tool); });
|
|
fillCheck.Margin = new Thickness(10, 0, 18, 0);
|
|
row.Children.Add(fillCheck);
|
|
return row;
|
|
}
|
|
|
|
// Color group (label + swatches + more) - one wrap unit.
|
|
var colorGroup = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 2, 16, 2) };
|
|
var colorLbl = new TextBlock
|
|
{
|
|
Text = Loc("Str_Bar_Color"),
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0)
|
|
};
|
|
colorLbl.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
colorGroup.Children.Add(colorLbl);
|
|
|
|
// Color swatches
|
|
bool isLineTool = tool == EditTool.Strikethrough || tool == EditTool.Underline;
|
|
var activeColor = (tool is EditTool.Draw or EditTool.Line or EditTool.Shape) ? _drawColor
|
|
: isLineTool ? Color.FromRgb(_lineAnnotColor.R, _lineAnnotColor.G, _lineAnnotColor.B)
|
|
: Color.FromRgb(_highlightColor.R, _highlightColor.G, _highlightColor.B);
|
|
foreach (var color in SwatchColors)
|
|
{
|
|
bool isActive = color == activeColor;
|
|
var swatch = new Border
|
|
{
|
|
Width = 18,
|
|
Height = 18,
|
|
Background = Freeze(new SolidColorBrush(color)),
|
|
BorderThickness = new Thickness(isActive ? 2 : 1),
|
|
CornerRadius = new CornerRadius(3),
|
|
Margin = new Thickness(1),
|
|
Cursor = Cursors.Hand,
|
|
Tag = color
|
|
};
|
|
if (isActive)
|
|
swatch.SetResourceReference(Border.BorderBrushProperty, "PrimaryBrush");
|
|
else
|
|
swatch.BorderBrush = _swatchDimBorder;
|
|
swatch.MouseLeftButtonDown += (s, e) =>
|
|
{
|
|
var c = (Color)((Border)s!).Tag;
|
|
if ((tool is EditTool.Draw or EditTool.Line or EditTool.Shape))
|
|
_drawColor = Color.FromArgb(_drawOpacity, c.R, c.G, c.B);
|
|
else if (isLineTool)
|
|
_lineAnnotColor = Color.FromArgb(_lineAnnotColor.A, c.R, c.G, c.B);
|
|
else
|
|
_highlightColor = Color.FromArgb(_highlightColor.A, c.R, c.G, c.B);
|
|
ApplyDrawStyleToSelection(); // edit the selected annotation, if any
|
|
ShowDrawSettings(tool); // refresh selection
|
|
};
|
|
colorGroup.Children.Add(swatch);
|
|
}
|
|
|
|
// "More colors..." -> full RGB picker, applied to whichever draw color this bar drives.
|
|
var moreDraw = new Border
|
|
{
|
|
Width = 18,
|
|
Height = 18,
|
|
CornerRadius = new CornerRadius(3),
|
|
Margin = new Thickness(1),
|
|
Cursor = Cursors.Hand,
|
|
BorderThickness = new Thickness(1),
|
|
ToolTip = Loc("Str_Bar_MoreColors"),
|
|
BorderBrush = _swatchDimBorder,
|
|
Background = new LinearGradientBrush
|
|
{
|
|
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)
|
|
}
|
|
}
|
|
};
|
|
moreDraw.MouseLeftButtonDown += (_, _) => OpenColorPicker(activeColor, c =>
|
|
{
|
|
if ((tool is EditTool.Draw or EditTool.Line or EditTool.Shape)) _drawColor = Color.FromArgb(_drawOpacity, c.R, c.G, c.B);
|
|
else if (isLineTool) _lineAnnotColor = Color.FromArgb(_lineAnnotColor.A, c.R, c.G, c.B);
|
|
else _highlightColor = Color.FromArgb(_highlightColor.A, c.R, c.G, c.B);
|
|
ApplyDrawStyleToSelection();
|
|
ShowDrawSettings(tool);
|
|
}, () => ShowDrawSettings(tool));
|
|
colorGroup.Children.Add(moreDraw);
|
|
panel.Children.Add(colorGroup);
|
|
|
|
// Single-row groups (Color, Size, Opacity+toggle) packed directly by the outer WrapPanel;
|
|
// Opacity and the Level/Eraser toggle share one unit so the toggle never lands on a row by
|
|
// itself. When even one group per row would need a third row, the least important groups
|
|
// collapse into the overflow chevron - see WireBarOverflow below.
|
|
var dOpacityUnit = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
|
StackPanel? sizeGroupRef = null;
|
|
_annotBarDragInners.Clear();
|
|
|
|
// Size slider (draw only)
|
|
if ((tool is EditTool.Draw or EditTool.Line or EditTool.Shape))
|
|
{
|
|
var sizeGroup = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 2, 16, 2) };
|
|
var sizeLbl = new TextBlock
|
|
{
|
|
Text = Loc("Str_Bar_Size"),
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0)
|
|
};
|
|
sizeLbl.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
sizeGroup.Children.Add(sizeLbl);
|
|
|
|
var sizeSlider = new Slider
|
|
{
|
|
Minimum = 1,
|
|
Maximum = 60,
|
|
Value = _drawWidth,
|
|
Width = 90,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
TickFrequency = 1,
|
|
IsSnapToTickEnabled = true,
|
|
Style = (Style)FindResource("DarkSlider")
|
|
};
|
|
sizeSlider.ValueChanged += (s, e) => { _drawWidth = e.NewValue; ApplyDrawStyleToSelection(); };
|
|
sizeGroup.Children.Add(sizeSlider);
|
|
|
|
var sizeLabel = new TextBlock
|
|
{
|
|
Text = $"{_drawWidth:F0}px",
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(4, 0, 0, 0),
|
|
Width = 34,
|
|
TextAlignment = TextAlignment.Right
|
|
};
|
|
sizeLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
sizeSlider.ValueChanged += (s, e) => sizeLabel.Text = $"{e.NewValue:F0}px";
|
|
sizeGroup.Children.Add(sizeLabel);
|
|
panel.Children.Add(sizeGroup);
|
|
sizeGroupRef = sizeGroup;
|
|
}
|
|
|
|
// Opacity group (label + slider + value) - one wrap unit.
|
|
var opacityGroup = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 2, 16, 2) };
|
|
var opacityLbl = new TextBlock
|
|
{
|
|
Text = Loc("Str_Bar_Opacity"),
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(0, 0, 6, 0)
|
|
};
|
|
opacityLbl.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
opacityGroup.Children.Add(opacityLbl);
|
|
|
|
byte currentOpacity = (tool is EditTool.Draw or EditTool.Line or EditTool.Shape) ? _drawOpacity : isLineTool ? _lineAnnotColor.A : _highlightColor.A;
|
|
var opacitySlider = new Slider
|
|
{
|
|
Minimum = 10,
|
|
Maximum = 255,
|
|
Value = currentOpacity,
|
|
Width = 90,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Style = (Style)FindResource("DarkSlider")
|
|
};
|
|
var opacityLabel = new TextBlock
|
|
{
|
|
Text = $"{(int)(currentOpacity / 255.0 * 100)}%",
|
|
FontFamily = UiKit.UiFont,
|
|
FontSize = 11,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Margin = new Thickness(4, 0, 6, 0),
|
|
Width = 40,
|
|
TextAlignment = TextAlignment.Right
|
|
};
|
|
opacityLabel.SetResourceReference(TextBlock.ForegroundProperty, "MutedTextBrush");
|
|
opacitySlider.ValueChanged += (s, e) =>
|
|
{
|
|
byte a = (byte)e.NewValue;
|
|
opacityLabel.Text = $"{(int)(a / 255.0 * 100)}%";
|
|
if ((tool is EditTool.Draw or EditTool.Line or EditTool.Shape))
|
|
{
|
|
_drawOpacity = a;
|
|
_drawColor = Color.FromArgb(a, _drawColor.R, _drawColor.G, _drawColor.B);
|
|
}
|
|
else if (isLineTool)
|
|
{
|
|
_lineAnnotColor = Color.FromArgb(a, _lineAnnotColor.R, _lineAnnotColor.G, _lineAnnotColor.B);
|
|
}
|
|
else
|
|
{
|
|
_highlightColor = Color.FromArgb(a, _highlightColor.R, _highlightColor.G, _highlightColor.B);
|
|
}
|
|
ApplyDrawStyleToSelection(); // edit the selected annotation, if any
|
|
};
|
|
opacityGroup.Children.Add(opacitySlider);
|
|
opacityGroup.Children.Add(opacityLabel);
|
|
dOpacityUnit.Children.Add(opacityGroup);
|
|
|
|
// Level (Line) / Eraser (Highlight, Draw) toggle rides at the end of the Opacity unit, so it
|
|
// stays beside Opacity as the bar collapses instead of dropping onto a row by itself.
|
|
if (barCheck is not null)
|
|
{
|
|
barCheck.Margin = new Thickness(0, 2, 2, 2);
|
|
dOpacityUnit.Children.Add(barCheck);
|
|
}
|
|
panel.Children.Add(dOpacityUnit);
|
|
var drawOverflowBtn = MakeBarOverflow(out var drawOverflowPopup, out var drawOverflowStack);
|
|
panel.Children.Add(drawOverflowBtn);
|
|
panel.Children.Add(drawOverflowPopup); // renders nothing; keeps the popup in the tree for DynamicResource
|
|
|
|
_drawSettingsBar = new Border
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Right, // right-anchored; slid via the grip
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
CornerRadius = new CornerRadius(0),
|
|
Effect = AnnotBarShadow(),
|
|
Child = BuildBarHost(panel),
|
|
Margin = new Thickness(0, 0, 0, 0)
|
|
};
|
|
_drawSettingsBar.SetResourceReference(Border.BackgroundProperty, "BgFlyout");
|
|
_drawSettingsBar.SetResourceReference(Border.CornerRadiusProperty, "AnnotationBarCornerRadius");
|
|
// Match the Text bar's pane-integrated edge. BarEdgeBrush is white in 98SE and made
|
|
// this one bar look like a separate raised slab even though both use the same host.
|
|
_drawSettingsBar.SetResourceReference(Border.BorderBrushProperty, "PaneBorderBrush");
|
|
_drawSettingsBar.SetResourceReference(Border.BorderThicknessProperty, "BarEdgeThickness");
|
|
_drawSettingsBar.SetResourceReference(Border.PaddingProperty, "BarPadding");
|
|
|
|
var previewArea = PagePreviewPanel.Parent as Grid;
|
|
if (previewArea is not null)
|
|
{
|
|
Panel.SetZIndex(_drawSettingsBar, 100);
|
|
previewArea.Children.Add(_drawSettingsBar);
|
|
// Cap the wrap panel to the document area's width (less the bar chrome) so it wraps the
|
|
// control groups to new rows once the window is too narrow to hold them on one line.
|
|
panel.SetBinding(FrameworkElement.MaxWidthProperty, new System.Windows.Data.Binding("ActualWidth")
|
|
{ Source = previewArea, Converter = _barWidthInset });
|
|
WireBarWrapAdaptation(panel, drawGrip, colorGroup, previewArea);
|
|
var drawGroups = sizeGroupRef is null
|
|
? new StackPanel[] { colorGroup, dOpacityUnit }
|
|
: new StackPanel[] { colorGroup, sizeGroupRef, dOpacityUnit };
|
|
int[] drawShed = sizeGroupRef is null ? [1] : [2, 1]; // opacity unit first, then size; color anchored
|
|
WireBarOverflow(panel, drawOverflowBtn, drawOverflowPopup, drawOverflowStack, drawGroups, drawShed, previewArea);
|
|
PlaceAnnotationBar(_drawSettingsBar, drawGrip, fadeIn: appearing);
|
|
}
|
|
_annotBarTool = tool;
|
|
_annotBarMinimized = false; // a freshly built bar is full-size
|
|
}
|
|
|
|
private void HideDrawSettings()
|
|
{
|
|
FadeOutAndRemoveBar(_drawSettingsBar);
|
|
_drawSettingsBar = null;
|
|
if (_annotBarTool is EditTool.Draw or EditTool.Highlight
|
|
or EditTool.Strikethrough or EditTool.Underline or EditTool.Shape)
|
|
_annotBarTool = null;
|
|
}
|
|
}
|
|
}
|