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 { // ============================================================ // Picker state sync + appearance handlers. (The Settings panel itself retired 2026-07-31: // every section moved to where the thing it configures lives - theme/language/view onto // rail flyouts, toolbar onto the bar's right-click menu, sidebar side onto the sidebar's.) // ============================================================ // Syncs every picker's radios and accent dots to live state before a flyout shows - // exactly ONE sync implementation, shared by all the rail flyouts (Shell/RailFlyouts.cs). // ── Quick fade in/out for the full-window overlay panels (Shortcuts/About) ── private static void FadeOverlayIn(UIElement el) { el.BeginAnimation(UIElement.OpacityProperty, null); el.Opacity = 0; el.Visibility = Visibility.Visible; el.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(110))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } }); } private static void FadeOverlayOut(UIElement el) { if (el.Visibility != Visibility.Visible) return; var anim = new DoubleAnimation(1, 0, new Duration(TimeSpan.FromMilliseconds(90))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } }; anim.Completed += (_, _) => { el.Visibility = Visibility.Collapsed; el.BeginAnimation(UIElement.OpacityProperty, null); el.Opacity = 1; }; el.BeginAnimation(UIElement.OpacityProperty, anim); } // Fades an annotate (draw/text) settings bar out over ~90ms, then removes it from its parent - // so the bar dissolves when its tool is deselected and crossfades when switching tools, matching // the About/Settings overlays. private static void FadeOutAndRemoveBar(Border? bar) { if (bar is null) return; var anim = new DoubleAnimation(bar.Opacity, 0, new Duration(TimeSpan.FromMilliseconds(90))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } }; anim.Completed += (_, _) => { bar.BeginAnimation(UIElement.OpacityProperty, null); // Release the 98SE scroller inset before the bar leaves its area, so the document // slides back up to the pane top when the band disappears. if (bar.Parent is Grid area) SyncSe98BarInset(bar, area, removing: true); (bar.Parent as Panel)?.Children.Remove(bar); }; bar.BeginAnimation(UIElement.OpacityProperty, anim); } // Collapses the visible annotate bar to a thin peek strip, or expands it back. Triggered by // re-clicking the already-active tool, so a second click tucks the bar away instead of the // old behavior of rebuilding it (which flickered). private void ToggleAnnotBarMinimized() { var bar = _textSettingsBar ?? _drawSettingsBar ?? _cropConfirmBar; if (bar is null) return; _annotBarMinimized = !_annotBarMinimized; bar.ClipToBounds = true; const double peek = 13; // thin strip, just enough for the grip dots if (_annotBarMinimized) { // Freeze the current width so collapsing the content can't shrink the bar to the dots and // slide it to the corner - it stays a same-width strip in place. bar.Width = bar.ActualWidth; bar.Effect = null; // minimized strips never carry a drop shadow _annotBarFullHeight = bar.ActualHeight > 0 ? bar.ActualHeight : bar.DesiredSize.Height; var anim = new DoubleAnimation(_annotBarFullHeight, peek, new Duration(TimeSpan.FromMilliseconds(120))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } }; anim.Completed += (_, _) => { if (_annotBarContent is not null) _annotBarContent.Visibility = Visibility.Collapsed; if (_annotBarDots is not null) _annotBarDots.Visibility = Visibility.Visible; bar.ClipToBounds = false; // content is hidden now, nothing to clip }; bar.BeginAnimation(FrameworkElement.HeightProperty, anim); } else { // Show the full content again before growing back, and let the width track content again. bar.Width = double.NaN; bar.Effect = AnnotBarShadow(); // restore the drop shadow on the expanded bar if (_annotBarContent is not null) _annotBarContent.Visibility = Visibility.Visible; if (_annotBarDots is not null) _annotBarDots.Visibility = Visibility.Collapsed; double full = _annotBarFullHeight > 0 ? _annotBarFullHeight : bar.ActualHeight; var anim = new DoubleAnimation(peek, full, new Duration(TimeSpan.FromMilliseconds(120))) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } }; anim.Completed += (_, _) => { bar.BeginAnimation(FrameworkElement.HeightProperty, null); bar.Height = double.NaN; // back to auto so it tracks its content again bar.ClipToBounds = false; }; bar.BeginAnimation(FrameworkElement.HeightProperty, anim); } } // Confirm-before-opening-links (About card footer). One key, positive sense - see the // ConfirmLinksSetting comment in Links.cs. Writing the same value back on the init sync is // a harmless no-op, so this needs no change guard. private void LinkConfirmCheck_Toggled(object sender, RoutedEventArgs e) => App.SetSetting(ConfirmLinksSetting, LinkConfirmCheck.IsChecked == true ? "1" : "0"); // Privacy section (#146): don't remember recently opened files. Turning it ON also clears // the existing list (matching the user's privacy expectation on a shared machine); the // guard makes the settings-open sync a no-op so opening the panel never wipes anything. private void NoRecentCheck_Toggled(object sender, RoutedEventArgs e) { bool off = NoRecentCheck.IsChecked == true; if ((App.GetSetting(App.NoRecentFilesSetting) == "1") == off) return; // sync, not a user change if (off) { App.SetSetting(App.NoRecentFilesSetting, "1"); App.ClearRecentFiles(); PopulateRecentFilesList(); // start screen hides its Recent box immediately } else { App.RemoveSetting(App.NoRecentFilesSetting); } } // Invert document colors (#135): flips the display-only dark mode. Shared by the rail's // moon toggle and Ctrl+I. The state is baked into rendered pixels, so flush the render // caches and repaint IN PLACE - never through ApplyViewMode. The mode-switch rebuild // re-laid-out the whole view and restored scroll approximately, so pages visibly // shuffled before the new colors arrived. Invert changes pixels, never geometry: // layout and scroll stay exactly where they are and only the bitmaps re-render. private void ToggleDocInvert(bool on) { // Per pane: only the FOCUSED pane flips, so a split can read one document inverted // beside a normal one. The moon lights for the focused pane; FocusPane re-syncs it. if (ActiveViewer.DocInvert == on) return; ActiveViewer.DocInvert = on; App.SetSetting("DocInvert", on ? "1" : "0"); DocInvertBtn.Tag = on ? "on" : null; // lights the rail icon in the accent while active RepaintForInvertChange(bothPanes: false); // per-pane toggle: the other pane is untouched } // Right-click on the moon: night-mode options. One checkable item - "Invert images too" // (default off since the #135 carve-out; some scanned documents ARE one full-page image, // where the carve-out makes night mode a no-op, so the old full inversion stays reachable). // Built fresh on each open so the caption follows the active language. private void DocInvertBtn_RightClick(object sender, MouseButtonEventArgs e) { var menu = MakeThemedMenu(); var mi = new System.Windows.Controls.MenuItem { Header = Loc("Str_InvertImagesToo"), IsCheckable = true, IsChecked = BitmapHelpers.DocInvertImages, InputGestureText = "Shift+N", // right-aligned in the family MenuItem template }; mi.Click += (_, _2) => ToggleInvertImages(!BitmapHelpers.DocInvertImages); menu.Items.Add(mi); menu.PlacementTarget = (UIElement)sender; menu.IsOpen = true; e.Handled = true; } private void ToggleInvertImages(bool on) { if (BitmapHelpers.DocInvertImages == on) return; BitmapHelpers.DocInvertImages = on; App.SetSetting("DocInvertImages", on ? "1" : "0"); // Only repaint when night mode is actually showing in either pane; otherwise it just // takes effect the next time a moon is toggled on. Both panes: this option changes // how EVERY inverted pane renders. if (Viewer.DocInvert || ViewerB.DocInvert) RepaintForInvertChange(bothPanes: true); } // Shared by the moon toggle and its right-click option: the invert state is baked into // rendered pixels, so flush the render caches and repaint IN PLACE - never through // ApplyViewMode (see the comment above ToggleDocInvert). private void RepaintForInvertChange(bool bothPanes) { // Per-pane toggle flushes and repaints ONLY the focused pane; the other pane's // pixels are correct and repainting them read as a spurious refresh (2026-08-15). // The images-too option still touches every inverted pane. if (bothPanes) FlushAllRenderCaches(); else ActiveViewer.FlushOwnRenderCaches(); // The invert flag is GLOBAL, but everything below this block runs through the shared // fields and so repaints only the FOCUSED pane - the other pane's already-painted // tiles kept their old colors until any scroll or focus change forced a re-render, // which read as one pane being inverted and scrolling the other pane inverting it // (2026-08-01). Re-render it with its own session swapped in - the same // WithOwnSession idiom the cross-pane tab drag uses. BEFORE the _doc guard: the // focused pane being empty must not strand the other pane's stale pixels. if (bothPanes && IsSplit) { var other = ReferenceEquals(ActiveViewer, Viewer) ? ViewerB : Viewer; // PIXELS ONLY (RepaintPixelsExt, 2026-08-15): the full RenderActiveSessionExt ran // Bootstrap/ShowEmptyState under the swapped session, and their Host chrome // mutations kept wrecking the focused pane's sidebar - replaced thumbnails when // the other pane held a doc, cleared and disabled the sidebar when it was empty. other.WithOwnSession(other.RepaintPixelsExt); } if (_doc is null) return; if (_viewMode == ViewMode.Continuous) { // Null every slot's bitmap so the render pass (which skips filled slots) // repaints the window around the viewport; far slots stay empty scaffolds // until virtualization brings them back, exactly as after a long scroll. // Slot sizes are untouched, so scroll geometry cannot move. _continuousSharpenCts?.Cancel(); _continuousSharpPages.Clear(); foreach (var child in _continuousPanel.Children) if (child is Border b && b.Child is Grid g && g.Children.Count > 0 && g.Children[0] is System.Windows.Controls.Image img) img.Source = null; _ = RenderContinuousPages(Math.Max(0, PageList.SelectedIndex)); StartRerenderTimer(); // then re-sharpen the visible pages at the current zoom } else { // Single/Two-Page/Grid: RenderPage repaints the primary and streams the // secondary tiles back (grid anchors at page 0, like ApplyViewMode). // keepTiles: the tile set is unchanged, so existing tiles stay put and get // their bitmaps swapped in place - no clear-and-refill jitter in grid. ActiveViewer.RenderPage(_viewMode == ViewMode.Grid ? 0 : Math.Max(0, PageList.SelectedIndex), keepTiles: true); } } private void DocInvertBtn_Click(object sender, RoutedEventArgs e) => ToggleDocInvert(!ActiveViewer.DocInvert); private void OnThemeChanged() { _appliedSquared = null; UpdateWindowChrome(); // The 98SE pane reaches the frame while rounded themes retain an 8px outer gutter. // Reapply the saved sidebar side so switching away from 98SE restores that right edge. ApplySidebarSide(); // Refresh snapshot FindResource calls that were set as local values. // SetResourceReference bindings update automatically; sidebar tabs and // active tool button background still need an explicit refresh. SetTool(_currentTool); if (_sidebarShowingOutlines) SwitchSidebarToOutlinesTab(); else SwitchSidebarToPagesTab(); // Re-evaluate the page-list edge overlays immediately. 98SE sets the theme // multiplier to zero; modern themes leave it at the default full strength. SyncPageListEdgeFades(); RefreshSelectionAccent(); // Both panes carry local border-thickness state. Rebuild both so an inactive pane cannot // retain the previous theme's right-edge geometry until it happens to receive focus. Viewer.RebuildTabStripExt(); ViewerB.RebuildTabStripExt(); // The signature popup is built from snapshot (FindResource) colors, so rebuild it in place // if it's open so it picks up the new theme without the user having to close and reopen it. if (_signaturePopup is not null) ShowSignaturePopup(); // The keyboard's layer selectors use the current theme's actual button template // (98SE raised bevel versus the modern card button), so rebuild the lazy board when // that template family changes. Key labels and mappings remain table-driven. if (_kbBuilt) { var layer = _kbLayer; _kbBuilt = false; BuildKeyboardView(); SetKbLayer(layer); } // Code-built annotate bars can be open in either pane. Refresh both panes so a bar // cannot retain the accent or control template from the theme that created it. RefreshOpenAnnotationBars(); } // #199 (Ryokoxx's design): ONE vertical strip of six swatches beside the theme list, // repainted per family, instead of a row under each accented theme. The list height is // identical for every theme, so the bottom-pinned card never moves; only the strip's // width animates, growing rightward from the pinned left edge. Per-family accent memory // in ThemeManager is untouched. // Per-family swatch colors, in each family's display order (moved here from the four // retired XAML accent rows; the strip repaints these onto its six shared dots). private static readonly (DarkAccent Accent, string Hex)[] DarkStripColors = []; private static readonly (DarkAccent Accent, string Hex)[] LightStripColors = []; private static readonly (DarkAccent Accent, string Hex)[] BlackStripColors = []; private static readonly (DarkAccent Accent, string Hex)[] SE98StripColors = []; private static (DarkAccent Accent, string Hex)[] StripColorsFor(Theme family) => family switch { _ => DarkStripColors, }; private bool _stripOpen; private const double AccentStripWidth = 39; // 1px rule + 10 gap + 26 swatch + 2 air private Border[] StripDots => []; // Ring the strip's selected swatch for the family it is showing. // Width slide, eased - one element animates now, so the old linear lockstep constraint // that kept two rows summing to a constant height no longer applies. // Localized display name for each theme, shown on the picker row. private string ThemeDisplayName(Theme t) => t switch { _ => Loc("Str_Theme_Dark"), }; // #211: translator test mode - when the external translation file live-reloads, re-run the // same rebuilds a language switch does, so code-built captions update alongside the // DynamicResource strings. Called once from the window ctor; the event only ever fires // when the app was started with --lang-file. internal void HookExternalLangReload() => MmdPdf.Services.LocaleManager.ExternalReloaded += () => { ApplyToolNumberTooltips(); BuildToolbarMenu(); BuildContextMenu(); ApplyToolbarAppearance(); RefreshOpenAnnotationBars(); }; private void LangEnRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.EnUS); private void LangCsRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.CsCZ); private void LangEsRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.Es); private void LangFrRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.Fr); private void LangZhTWRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.ZhTW); private void LangZhCNRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.ZhCN); private void LangBnRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.Bn); private void LangTrRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.TrTR); private void LangDeRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.De); private void LangJaRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.JaJP); private void LangPlRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.PlPL); private void LangHuRadio_Checked(object sender, RoutedEventArgs e) => SelectLocale(MmdPdf.Services.Locale.HuHU); private void SelectLocale(MmdPdf.Services.Locale loc) { MmdPdf.Services.LocaleManager.Apply(loc); ApplyToolNumberTooltips(); // re-append the numbers to the now-localized tool tooltips BuildToolbarMenu(); // the toolbar right-click picker's items carry Loc() captions LangFlyout.IsOpen = false; // a pick closes the rail flyout, like the accordion used to collapse // The status bar text is a formatted string (not a DynamicResource), so it keeps the // language it was last set in. Re-set it in the new locale instead of leaving it stale. if (_doc is not null && PageList.SelectedIndex >= 0) SetStatus(string.Format(Loc("Str_PageOf"), PageList.SelectedIndex + 1, _doc.PageCount)); else SetStatus(Loc("Str_Ready")); // The canvas right-click menu is built once with Loc() values captured at build time, // so rebuild it in the new language. (The sidebar menu is rebuilt on each open.) BuildContextMenu(); // Toolbar captions are built with Loc() at apply time (they don't auto-update like a // DynamicResource), so rebuild the toolbar on every language change. Harmless for the // icon-only modes; refreshes the captions for Text-beside / Text-under / Text-only. ApplyToolbarAppearance(); // Refresh code-built annotate bars in both panes. A language change comes from a // window flyout, so the pane containing the visible bar is not guaranteed to be the // current ActiveViewer by the time these controls are rebuilt. RefreshOpenAnnotationBars(); // Page thumbnails and outline tooltips snapshot Loc() strings when built; rebuild both // lists so their "Page N" labels switch to the new language immediately. RefreshPageList(); RefreshOutlines(); // A visible signature popup is built with Loc() too; rebuild it so its section headers and // pen labels switch immediately. RefreshSignaturePopupLanguage(); // The fit-mode terms differ in length per language; resize the zoom box so the longest never clips. AdjustZoomBoxWidth(); } private void RefreshOpenAnnotationBars() { var active = ActiveViewer; var other = ReferenceEquals(active, Viewer) ? ViewerB : Viewer; // Refresh the inactive pane first. BuildBarHost keeps a few drag/minimize references // at window scope, so finishing on the active pane leaves those references pointed at // the controls the shared toolbar is currently operating. if (!ReferenceEquals(other, active)) RefreshOpenAnnotationBars(other); RefreshOpenAnnotationBars(active); } private void RefreshOpenAnnotationBars(MmdPdf.Controls.PdfViewer viewer) { ((MmdPdf.Features.IViewerHost)this).RunWithViewerContext(viewer, () => { var tool = _annotBarTool; if (tool == EditTool.Text) ShowTextSettings(); else if (tool is EditTool bt && bt is EditTool.Draw or EditTool.Highlight or EditTool.Line or EditTool.Strikethrough or EditTool.Underline or EditTool.Shape) ShowDrawSettings(bt); RebuildCropBarForLocale(); }); } // Size the editable zoom ComboBox to its widest item in the CURRENT language, so localized fit-mode // terms (e.g. French "Ajuster a la largeur") are never clipped. Re-run on locale change and at load. private void AdjustZoomBoxWidth() { if (ZoomBox is null) return; try { double pixelsPerDip = VisualTreeHelper.GetDpi(ZoomBox).PixelsPerDip; var typeface = new System.Windows.Media.Typeface( ZoomBox.FontFamily, ZoomBox.FontStyle, ZoomBox.FontWeight, ZoomBox.FontStretch); double emSize = ZoomBox.FontSize > 0 ? ZoomBox.FontSize : 12; double max = 0; foreach (var item in ZoomBox.Items) { string text = item is System.Windows.Controls.ComboBoxItem ci ? ci.Content?.ToString() ?? "" : item?.ToString() ?? ""; var ft = new System.Windows.Media.FormattedText( text, System.Globalization.CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, emSize, System.Windows.Media.Brushes.Black, pixelsPerDip); if (ft.WidthIncludingTrailingWhitespace > max) max = ft.WidthIncludingTrailingWhitespace; } // Measured text + text insets (12) + chevron column (22) + borders (2), with // breathing room between the value and arrow. Keep the compact toolbar control // usable even when the current locale happens to have very short labels. ZoomBox.Width = System.Math.Max(88, System.Math.Ceiling(max) + 40); } catch { /* best-effort; leave the XAML default width */ } } // Native name (autonym) for each language, shown in the picker regardless of UI locale. private static string LangDisplayName(MmdPdf.Services.Locale loc) => loc switch { MmdPdf.Services.Locale.CsCZ => "Čeština", MmdPdf.Services.Locale.Es => "Español", MmdPdf.Services.Locale.Fr => "Français", MmdPdf.Services.Locale.ZhTW => "中文 (繁體)", MmdPdf.Services.Locale.ZhCN => "中文 (简体)", MmdPdf.Services.Locale.Bn => "বাংলা", MmdPdf.Services.Locale.TrTR => "Türkçe", MmdPdf.Services.Locale.De => "Deutsch", MmdPdf.Services.Locale.JaJP => "日本語", _ => "English", }; private void ViewContinuousRadio_Checked(object sender, RoutedEventArgs e) => SelectViewMode(ViewMode.Continuous); private void ViewSingleRadio_Checked(object sender, RoutedEventArgs e) => SelectViewMode(ViewMode.Single); private void ViewTwoPageRadio_Checked(object sender, RoutedEventArgs e) => SelectViewMode(ViewMode.TwoPage); private void ViewGridRadio_Checked(object sender, RoutedEventArgs e) => SelectViewMode(ViewMode.Grid); // #193: book layout toggle (cover page alone in Two-Page). Global, persisted; both panes // re-run their current layout so an active Two-Page view re-pairs in place. Reached from // the page context menu while in Two-Page, and the bare B key. internal void ToggleBookMode() { App.SetSetting("TwoPageBook", Controls.PdfViewer.BookMode ? "0" : "1"); Viewer.ReapplyViewMode(); ViewerB.ReapplyViewMode(); } // ── Toolbar appearance (right-click picker on the bar) ──────────── // Hover tooltips stay on in every mode, so the text modes are about preference, not // discoverability. // TWO AXES, NOT ONE (family standard, 2026-07-30; MmdPdfUI/Shell/ToolbarStyle.cs is // the reference). The old five-way ToolbarStyle could not express "large icons WITH text" - // icon size and text placement were never one axis, they only looked like one. The old // enum survives solely so an existing install's saved setting migrates (InitToolbarStyle). private enum ToolbarStyle { SmallIcons, LargeIcons, TextBeside, TextUnder, TextOnly } private enum ToolbarIconSize { Small, Large } private enum ToolbarLabelMode { None, Beside, Under, Only } // Large icons with the text underneath is the family default for new installs; migration // keeps whatever an existing install was on. private ToolbarIconSize _toolbarIconSize = ToolbarIconSize.Large; private ToolbarLabelMode _toolbarLabelMode = ToolbarLabelMode.Under; // Each toolbar icon button paired with its glyph and label-resource key, built once so the // appearance can be rebuilt without re-walking the tree. private readonly List<(Button btn, string glyph, string labelKey)> _toolbarButtons = []; // Maps each toolbar glyph (Segoe MDL2 Assets code point) to its caption string key. Buttons // whose glyph isn't listed keep their icon with no caption. private static readonly Dictionary _toolbarLabelKeys = new() { [""] = "Str_Lbl_New", [""] = "Str_Lbl_Open", [""] = "Str_Lbl_Close", [""] = "Str_Lbl_Save", [""] = "Str_Lbl_Flatten", [""] = "Str_Lbl_Ocr", [""] = "Str_Lbl_Print", ["\ue8fe"] = "Str_Lbl_Scan", [""] = "Str_Lbl_Merge", [""] = "Str_Lbl_Extract", [""] = "Str_Lbl_Delete", [""] = "Str_Lbl_MoveUp", [""] = "Str_Lbl_MoveDown", [""] = "Str_Lbl_Select", [""] = "Str_Lbl_Text", [""] = "Str_Lbl_Highlight", [""] = "Str_Lbl_Strike", [""] = "Str_Lbl_Underline", [""] = "Str_Lbl_Draw", [""] = "Str_Lbl_Crop", [""] = "Str_Lbl_Rotate", [""] = "Str_Lbl_Image", [""] = "Str_Lbl_Signature", [""] = "Str_Lbl_Undo", [""] = "Str_Lbl_Clear", [""] = "Str_Lbl_ZoomOut", [""] = "Str_Lbl_ZoomIn", [""] = "Str_Lbl_Highlight", // current highlighter glyph (see ToolHighlightBtn) [""] = "Str_Lbl_Line", // repurposed ToolUnderlineBtn glyph = the Line tool [""] = "Str_Lbl_ZoomOut", // boxed minus (RemoveFrom) - new zoom-out glyph [""] = "Str_Lbl_ZoomIn", // boxed plus (AddTo) - new zoom-in glyph [""] = "Str_Lbl_Search", // magnifier - toolbar search button [""] = "Str_Lbl_Stamp", // page-number / watermark stamp tool [""] = "Str_Lbl_Shape", // Shapes tool (rect / ellipse / polygon) }; // Walks LeftBar + RightBar once and records each icon button with its glyph + label key. private void IndexToolbarButtons() { _toolbarButtons.Clear(); foreach (Panel? bar in new Panel?[] { LeftBar, RightBar }) { if (bar is null) continue; foreach (var btn in DescendantButtons(bar)) if (btn.Content is string g && g.Length > 0 && _toolbarLabelKeys.TryGetValue(g, out var key)) _toolbarButtons.Add((btn, g, key)); } } private static IEnumerable