Files
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation
  from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc.
  Fixes edited values staying recoverable by text extraction.
- Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode.
- Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon.
- Remove the upstream author credit and the in-app install button.
2026-08-27 07:37:09 +02:00

173 lines
9.6 KiB
C#

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using MmdPdf.Features;
namespace MmdPdf.Controls
{
/// <summary>
/// One document view: its tab strip, its card and everything inside. Two instances make the
/// split.
///
/// The handlers here are one-line forwards to the host, following KillerShell's FilePane idiom -
/// chrome that belongs to the window stays on the window.
/// </summary>
public partial class PdfViewer : UserControl
{
/// <summary>The explicit shell boundary used by the viewer.</summary>
internal IViewerHost? Host { get; private set; }
/// <summary>Document dark-mode invert, PER PANE (was a global that flipped both panes of
/// a split at once). Display only; every render path in this pane reads this flag. The
/// moon toggles the focused pane and its lit state follows pane focus.</summary>
internal bool DocInvert;
internal void AttachHost(IViewerHost host) => Host = host;
/// <summary>This viewer's per-view state - page maps, view mode, zoom, render cancellation,
/// continuous bookkeeping. MainWindow's `_view` reads it back from here, so a second pane
/// gets its own simply by existing.</summary>
internal ViewerState State { get; } = new();
public PdfViewer() => InitializeComponent();
/// <summary>
/// Build this pane's tile tree. Every pane must do this for itself: routed through
/// ActiveViewer it would run twice on pane A and leave pane B's State.AnnotationCanvas
/// null, which AllPageCanvases() yields first and the first text-selection repaint then
/// dereferences. The panel references must be assigned before BuildPrimaryTile, which
/// inserts into PageContentPanel.
/// </summary>
internal void InitTiles()
{
State.PageContentGrid = PageContentGrid;
State.PageContentPanel = PageContentPanel;
State.ContinuousPanel = ContinuousPanel;
BuildPrimaryTile();
State.ActiveCanvas = State.AnnotationCanvas;
}
/// <summary>Accent ring marking this pane as the focused one in a split.
///
/// SetResourceReference, not a brush snapshot, on both states: an assigned brush would not
/// follow a live theme switch.
///
/// "SelectionAccent", not "AccentBrush". MmdPdf uses the older family resource set
/// (BgCanvas / AccentLogo / TextPrimary) and has no AccentBrush key in any theme.
/// SetResourceReference to a missing key does not throw, it silently leaves the property
/// unset, which blanks the border instead of accenting it.
///
/// Both borders, matching KillerShell's UpdatePaneFocusRing: TabBarRing is the card's top
/// border drawn again inside the tab band, so lighting only the card leaves the ring open
/// along its whole top edge.
///
/// The brush moves, never the thickness - a thickness change would reflow the pane on every
/// click between panes.
///
/// PaneHasFocus below records the same state for the tab halo, which continues the ring up
/// around the active tab in the focused pane only.</summary>
internal bool PaneHasFocus { get; private set; }
internal void SetFocusHalo(bool focused)
{
PaneHasFocus = focused;
bool retro = Services.ThemeManager.Current == Services.Theme.SE98;
string key = focused && !retro ? "SelectionAccent" : "PaneBorderBrush";
PaneBorder.SetResourceReference(Border.BorderBrushProperty, key);
// The 98SE band is the raised client's white top ledge. Replacing it with the gray
// outer-frame brush on focus made the tab/pane join visibly change after a click.
TabBarRing.SetResourceReference(Border.BorderBrushProperty,
retro ? "BevelLightBrush" : key);
// The ring runs on around the active tab, so it moves with the pane border. The tab's own
// share of that is a template trigger on PaneFocused / PaneDimmed, which this sets, plus
// the band-drawn outer verticals.
UpdatePaneFocusRing();
}
// ---- Element access for the window --------------------------------------------------
// A UserControl is its OWN NAMESCOPE, so the window's FindName cannot reach any of these -
// and it fails SILENTLY, returning null rather than throwing. The window ctor assigns
// _view.X from these instead, which works because those fields are forwarding properties
// onto ViewerState.
internal Border PaneShadowBorder => PaneShadow;
internal Border PaneCardBorder => PaneBorder;
internal Grid ContentHost => DocPaneContent;
internal StackPanel ContinuousHost => ContinuousPanel;
internal WrapPanel PageHost => PageContentPanel;
internal Grid PageGrid => PageContentGrid;
internal ScrollViewer PreviewScroller => PagePreviewPanel;
internal Border DropSurface => DropZone;
internal Border RecentBox => RecentFilesBox;
internal ItemsControl RecentList => RecentFilesList;
internal Canvas Marquee => MarqueeLayer;
internal Border SurfacePad => DocSurfacePad;
internal System.Windows.Media.ImageBrush Grain => GrainBrush;
// ---- Forwards to the owning window ----------------------------------------------------
// MainWindow's copies were private; they are internal now purely so these can reach them.
private void DocPane_SizeChanged(object s, SizeChangedEventArgs e)
{
Host?.ViewerSizeChanged(this, s, e);
// Window resizing changes a pane's usable width without going through the split-pane
// callbacks. Keep the empty-state recents panel on the same width gate in that path too.
// SyncRecentBoxWidth only writes materially changed values and guards re-entry, so the
// follow-up layout pass caused by crossing the threshold settles immediately.
if (e.WidthChanged) SyncRecentBoxWidth();
}
/// <summary>Size the start screen's Recent panel to this pane, and drop it entirely once the
/// pane is too narrow to carry both it and the drop target. At its old fixed 340 it took
/// most of a half-width pane and left the "Drop PDF here" zone as a sliver. Owns the
/// panel's visibility outright, so PopulateRecentFilesList defers to it rather than the two
/// of them setting it from different rules.</summary>
private bool _syncingRecentBox;
internal void SyncRecentBoxWidth()
{
if (RecentFilesBox is null || RecentFilesList is null || _syncingRecentBox) return;
_syncingRecentBox = true;
try
{
double w = ActualWidth;
double want = Math.Min(340, Math.Max(220, w * 0.4));
var vis = RecentFilesList.Items.Count > 0 && w >= 560
? Visibility.Visible : Visibility.Collapsed;
// Only write when the value actually changes. Both of these re-trigger layout, and
// this runs FROM a size handler - an unconditional assignment gives the layout pass
// something new to react to every time round and it never settles.
if (Math.Abs(RecentFilesBox.Width - want) > 0.5) RecentFilesBox.Width = want;
if (RecentFilesBox.Visibility != vis) RecentFilesBox.Visibility = vis;
}
finally { _syncingRecentBox = false; }
}
// Focus THIS pane before forwarding: the open path routes through ActiveViewer, and a
// drag-drop raises no PreviewMouseDown (the focus trigger), so a drop on the unfocused
// pane opened the file in the OTHER pane. FocusPane is cheap and idempotent.
private void DropZone_Drop(object s, DragEventArgs e) => Host?.ViewerDrop(this, s, e);
private void DropZone_DragOver(object s, DragEventArgs e) => Host?.ViewerDragOver(s, e);
private void DropZone_Click(object s, MouseButtonEventArgs e) => Host?.ViewerDropZoneClick(s, e);
private void RecentClearAll_Click(object s, MouseButtonEventArgs e) => Host?.ClearRecentFiles(s, e);
// The five preview/scroll handlers do NOT forward from here: their bodies are in this class
// (PdfViewer.Zoom.cs, PdfViewer.Viewport.cs), so a forward would call straight back into
// itself. The XAML binds them directly.
private void DocPaneBackground_RightClick(object s, MouseButtonEventArgs e) => Host?.ViewerBackgroundRightClick(s, e);
/// <summary>Empty space on this pane's tab strip drags the window, the way a strip in the
/// title-bar row would. Named apart from MainWindow's TitleBar_MouseLeftButtonDown, which
/// keeps the body - it is window chrome, not pane behavior.</summary>
private void TabScroll_MouseLeftButtonDown(object s, MouseButtonEventArgs e) => Host?.ViewerTabStripMouseDown(s, e);
/// <summary>This pane's active document, for the window's chrome. The session list lives
/// here, so the window asks the focused pane rather than owning one itself.</summary>
internal DocumentSession? ActiveSessionRef => _active;
/// <summary>Every open document in THIS pane. The quit prompt has to union both panes to
/// decide whether anything is unsaved, and the settings writer needs each pane's list.</summary>
internal System.Collections.ObjectModel.ObservableCollection<DocumentSession> SessionsRef => _sessions;
}
}