using System.Collections.Generic; namespace MmdPdf.Controls { /// /// This pane's tab surface, exposed to the window. Wrappers inside the same partial class, so /// they can reach members PdfViewer.Tabs.cs keeps private; the Ext suffix exists only because a /// wrapper cannot share a name with what it wraps. /// /// The window calls these against ActiveViewer, so a shortcut or toolbar button acts on the /// focused pane. /// public partial class PdfViewer { // ── Opening and closing ────────────────────────────────────────────────────────────── internal void OpenInNewTabExt(string path) => OpenInNewTab(path); internal void CloseTabExt(DocumentSession? s) => CloseTab(s); internal void CloseAllTabsExt() => CloseAllTabs(); internal void CloseOtherTabsExt(DocumentSession? keep = null) { var target = keep ?? _active; if (target != null) CloseOtherTabs(target); } internal void CycleTabExt(int dir) => CycleTab(dir); internal void EnsureInitialSessionExt() => EnsureInitialSession(); internal void MaterializeDeferredExt(DocumentSession target) => MaterializeDeferred(target); internal void SwitchToTabExt(DocumentSession target) => SwitchToTab(target); // ── The load handshake (FileOperations / ImportAndZip drive this) ──────────────────── internal DocumentSession BeginTabLoadExt(out DocumentSession? prev, out bool createdNew) => BeginTabLoad(out prev, out createdNew); internal void AbortTabLoadExt(DocumentSession target, DocumentSession? prev, bool createdNew) => AbortTabLoad(target, prev, createdNew); // ── Session state ──────────────────────────────────────────────────────────────────── internal void CaptureSessionStateExt(DocumentSession s) => CaptureSessionState(s); /// Fold this pane's live fields back into its own active session, if it has one. /// The close path has to do this for BOTH panes before asking about unsaved work. internal void CaptureActiveIfAny() { if (_active != null) CaptureSessionState(_active); } internal void ApplySessionStateExt(DocumentSession s) => ApplySessionState(s); /// Swap this pane's active session into the window's shared document fields. The /// counterpart to CaptureActiveIfAny; FocusPane runs both across a pane switch. /// /// The empty-pane branch is not an optimization, it prevents cross-pane corruption. The /// shared fields still describe the pane we just left, and EnsureInitialSession ends with /// CaptureSessionState, which copies _doc, _annotations, _undoStack and the rest BY /// REFERENCE. So the first session an empty pane created would alias the other pane's live /// document: opening a file in one pane replaced the other's, and switching tabs in one /// moved the other. Blanking the shared fields here means there is nothing to alias. internal void ApplyActiveSessionIfAny() { if (_active != null) { ApplySessionState(_active); return; } var blank = new DocumentSession(); // every collection field has its own initializer _sessions.Add(blank); SetActiveSession(blank); ApplySessionState(blank); ShowEmptyState(); } /// Put this pane's active session back into the shared fields and nothing else - /// pure assignment, no UI, and no session created if there is none. Used by WithOwnSession, /// which runs from layout events and must not cause any further layout. internal void RestoreActiveFieldsOnly() { if (_active != null) ApplySessionState(_active); } /// Run view math with THIS pane's document in the window's shared fields. /// /// _doc, _viewMode, _fitMode, _zoomLevel and _gridColumns are WINDOW fields, but the /// handlers that do view math - the viewport's SizeChanged, the resize-settle timer, /// ReapplyGridOrFit - are per-pane: each pane's own ScrollViewer raises them. So an /// unfocused pane whose viewport ticked was refitting itself against the FOCUSED pane's /// document and fit mode, and writing that pane's zoom back out. That is why switching /// tabs in one pane kept changing the other pane's view. /// /// Swap this pane's session in, run, fold the view values back, then restore the focused /// pane. Only the view fields are folded back, not a full CaptureSessionState: that also /// writes DocStates to the registry, which a resize would do dozens of times a second. private bool _inOwnSessionScope; internal void WithOwnSession(System.Action work) { if (Host == null || _inOwnSessionScope || Host.IsViewerFocused(this)) { work(); return; } _inOwnSessionScope = true; // The element accessors have to follow the fields. Swapping only the document state // left the render path resolving PageHost / PreviewScroller through ActiveViewer, so // this pane's fit measured the OTHER pane's viewport and painted into its tiles. try { Host.RunWithViewerContext(this, () => { if (_active != null) { ApplySessionState(_active); // ApplySessionState deliberately leaves PageIndex to RenderActiveSession, // which never runs on this path. Seed it from this pane's session. State.CurrentPage = _active.PageIndex; } work(); if (_active != null) { _active.ZoomLevel = _zoomLevel; _active.LastRenderZoom = _lastRenderZoom; _active.Fit = _fitMode; _active.View = _viewMode; _active.GridColumns = _gridColumns; _active.ScrollH = PagePreviewPanel?.HorizontalOffset ?? _active.ScrollH; _active.ScrollV = PagePreviewPanel?.VerticalOffset ?? _active.ScrollV; } }); } finally { _inOwnSessionScope = false; } } /// This pane's sidebar thumbnails, kept so focusing it again does not re-decode the /// document. Read by RestorePageListForActivePane (PageOperations.cs). internal PageThumbnailVm[]? ThumbCache { get; set; } internal string? ThumbCacheFile { get; set; } /// This pane's thumbnail loader cancellation. PER PANE, not per window: one shared /// token meant focusing either pane canceled whatever the other was still decoding, and /// since the half-filled cache still matched the page count it counted as usable - so the /// list re-seated with the labels and no pictures, permanently. internal System.Threading.CancellationTokenSource? ThumbCts { get; set; } /// Highlight this pane's current page after the list is re-seated: assigning /// ItemsSource clears the selection. internal int CurrentPageIndex => State.CurrentPage; internal void SyncPageListSelection(int? preservedPage = null) { if (preservedPage.HasValue) State.CurrentPage = preservedPage.Value; if (State.CurrentPage < 0) return; Host?.ViewerPageChanged(this, State.CurrentPage); if (Host != null) Host.PageJumpText = (State.CurrentPage + 1).ToString(); Host?.EnsureSidebarPageVisible(this, State.CurrentPage); } internal void SaveDocStateExt(string? path, FitMode fit, double zoom, ViewMode view, int page) => SaveDocState(path, fit, zoom, view, page); internal bool TryGetDocStateExt(string? path, out FitMode fit, out double zoom, out ViewMode view, out int page) => TryGetDocState(path, out fit, out zoom, out view, out page); // ── Strip and render ───────────────────────────────────────────────────────────────── internal void InitTabStripExt() => InitTabStrip(); internal void RebuildTabStripExt() => RebuildTabStrip(); /// The band changed width. Kept under the old name because the window still wires /// the focused pane's SizeChanged to it; each pane also raises its own now, and the call is /// guarded and idempotent, so the two agreeing costs nothing. internal void ScheduleTabReflowExt() => TabBarResized(); internal void RenderActiveSessionExt() => RenderActiveSession(); internal void ShowEmptyStateExt() => ShowEmptyState(); internal void FlushAllRenderCachesExt() => FlushAllRenderCaches(); internal void InvalidateRenderCacheExt(DocumentSession? s) => InvalidateRenderCache(s); /// Make a brand-new empty session the active one. The startup restore builds the /// session list itself, so it needs to place the result rather than go through /// EnsureInitialSession. internal void SetSessionsExt(IEnumerable sessions, DocumentSession? active) { _sessions.Clear(); foreach (var s in sessions) _sessions.Add(s); // ObservableCollection has no AddRange SetActiveSession(active); } /// Build a deferred (lazy) session for the restore path - a tab that shows its /// title but does not load its document until it is first switched to. internal static DocumentSession MakeDeferredSession(string path) => new() { OriginalFile = path, CurrentFile = path, DeferredPath = path }; // ── This pane's strip elements, for the window chrome that still positions them ────── // AppScale scales them, FullScreen hides them, SidebarLayout flips their margins - all of // which now have to act on BOTH panes rather than one window-level band. internal System.Windows.Controls.Border TabStripBorderCtl => TabStripBorder; internal System.Windows.Controls.Border TabStripFadeCtl => TabStripFade; internal System.Windows.Controls.Border TabBarRingCtl => TabBarRing; } }