Files
mmd-pdf/Controls/Viewer/PdfViewer.PageSelection.cs
T
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

185 lines
8.9 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.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.Controls
{
// Moved from Shell/PageSelection.cs; the namespace and class line are the only changes. Window
// members spelled bare here resolve through PdfViewer.Bridge.cs.
public partial class PdfViewer
{
// ============================================================
// Page selection handler
// ============================================================
private void PageList_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// Same speed knob as the document viewport (WheelScrollFactor in Zoom.cs).
Host?.ScrollSidebar(this, -e.Delta * (48.0 / 120.0) * Controls.PdfViewer.WheelScrollFactor);
e.Handled = true;
}
private void PageJumpBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || _doc is null) return;
e.Handled = true;
if (int.TryParse(Host?.PageJumpText, out int pg))
{
int idx = Math.Max(0, Math.Min(_doc.PageCount - 1, pg - 1));
RecordNavJump(); // Alt+Left retraces the typed jump
_currentPage = idx;
}
else
{
// Restore current page number if input was invalid
if (Host != null) Host.PageJumpText = (_currentPage + 1).ToString();
}
Keyboard.ClearFocus();
}
private void PageJumpBox_GotFocus(object sender, RoutedEventArgs e)
{
Host?.SelectAllPageJumpText();
}
/// <summary>Set by SyncCurrentPageTo (and only it) around its programmatic SelectedIndex
/// write, so the scroll-driven sync does not re-enter the render path through the window's
/// XAML-bound handler. Replaces the old detach/attach of a cached delegate, which never
/// worked: the list's real subscription is the WINDOW stub, so the -= removed nothing and
/// the += accumulated direct subscriptions (2026-08-01).</summary>
private bool _syncingPageList;
private void PageList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_syncingPageList) return; // programmatic sync, not a user selection
// The sidebar page list is WINDOW chrome - there is one of it, and BOTH panes attach
// their own handler to it. It describes the focused pane only, so the other pane has
// to sit this out. Scrolling a pane calls SyncCurrentPageTo, which detaches its OWN
// handler before setting SelectedIndex to avoid re-entering the render path; the other
// pane's handler stayed attached and navigated that pane to the page you had just
// scrolled to in this one, which is why scrolling pane A scrolled pane B.
if (Host != null && !Host.IsViewerFocused(this)) return;
// Mirror the sidebar into the view's own current page. Set
// BEFORE the >= 0 guard on purpose - clearing the list (tab close, document close)
// drops SelectedIndex to -1 and that has to be mirrored too, or a closed document
// leaves a stale page number behind. Assigns _view.CurrentPage directly rather than
// going through _currentPage, whose setter would write back into PageList and re-enter
// this handler.
State.CurrentPage = (sender as ListBox)?.SelectedIndex ?? -1;
if (_currentPage >= 0)
{
CommitActiveTextBox();
ClearSelection();
ClearTextSelection();
Host?.EnsureSidebarPageVisible(this, _currentPage);
if (_viewMode == ViewMode.Continuous)
{
if (Host != null) Host.PageJumpText = (_currentPage + 1).ToString();
ScrollContinuousToPage(_currentPage);
return;
}
if (_viewMode == ViewMode.Grid)
{
// Grid is a stable overview: selecting a page highlights it but must NOT
// re-anchor the grid. It still needs an initial render (open / first display)
// when no tiles exist yet; later selections only update the highlight.
if (Host != null) Host.PageJumpText = (_currentPage + 1).ToString();
// Keep the statusbar counter honest even when the clicked tile is already in
// view (BringIntoView then scrolls nothing, so the scroll-sync never fires).
SetStatus(string.Format(Loc("Str_PageOf"), _currentPage + 1, _doc!.PageCount) + $" - {DisplayZoomPct():F0}%");
if (_pageContentPanel.Children.Count <= 1)
{
PagePreviewPanel.ScrollToTop();
PagePreviewPanel.ScrollToHorizontalOffset(0);
RenderPage(0); // grid primary is always page 0
// Default the grid to a clean 3-columns-across fit. Deferred to Loaded so the
// viewport width is valid (it can still be 0 mid-open, which would fall back
// to a carried-over zoom and show a single large page).
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded,
(Action)(() => SetZoom(GridZoomForN(Math.Min(_doc?.PageCount ?? 1, 3)))));
}
else if (_currentPage < _pageContentPanel.Children.Count
&& _pageContentPanel.Children[_currentPage] is FrameworkElement gridTile)
{
// Scroll the chosen page's tile into view (BringIntoView accounts for the zoom transform).
gridTile.BringIntoView();
}
return;
}
// Clicking either page of the spread that's already shown (or re-selecting the
// current single page) renders the exact same pixels, so skip the re-render and its
// flash - just move the page number. SpreadStart, NOT a local % 2: this was the
// fourth pairing site and the one #193's book layout missed - its stale (0,1)
// pairing matched the rendered cover and swallowed the render of spread (1,2).
int targetPrimary = _currentPage;
if (_viewMode == ViewMode.TwoPage) targetPrimary = SpreadStart(targetPrimary);
if (targetPrimary == _renderedPrimaryPage && Math.Abs(_zoomLevel - _lastRenderZoom) < 0.0001)
{
if (Host != null) Host.PageJumpText = (_currentPage + 1).ToString();
return;
}
PagePreviewPanel.ScrollToTop();
PagePreviewPanel.ScrollToHorizontalOffset(0);
RenderPage(_currentPage);
ApplyZoom();
// Update page jump box
if (Host != null) Host.PageJumpText = (_currentPage + 1).ToString();
// Re-highlight search results on this page if a search is active
if (_searchBar is not null && _searchBar.Visibility == Visibility.Visible
&& Search.HasResults)
HighlightSearchResultsOnCurrentPage();
}
}
private void ShortcutHelp_Click(object sender, RoutedEventArgs e)
{
if (ShortcutOverlay.Visibility == Visibility.Visible) FadeOverlayOut(ShortcutOverlay);
else ShowShortcutsOverlayExclusive();
}
private void ShortcutOverlay_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Click on the dim backdrop closes the overlay.
FadeOverlayOut(ShortcutOverlay);
}
private void ShortcutOverlayCard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Stop the click from bubbling up to the backdrop handler.
e.Handled = true;
}
private void ShortcutOverlayClose_Click(object sender, RoutedEventArgs e)
{
FadeOverlayOut(ShortcutOverlay);
}
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
e.Handled = true;
}
}
}