173 lines
9.6 KiB
C#
173 lines
9.6 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using KillerPDF.Features;
|
|
|
|
namespace KillerPDF.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". KillerPDF 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;
|
|
}
|
|
}
|