706 lines
36 KiB
C#
706 lines
36 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 KillerPDF.Services;
|
|
using PdfPigDoc = UglyToad.PdfPig.PdfDocument;
|
|
|
|
namespace KillerPDF
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
// ============================================================
|
|
// Keyboard shortcuts
|
|
// ============================================================
|
|
|
|
protected override void OnPreviewKeyDown(KeyEventArgs e)
|
|
{
|
|
base.OnPreviewKeyDown(e);
|
|
|
|
// Keyboard view of the shortcuts overlay: holding Ctrl / Shift / Alt previews that layer.
|
|
KbSyncLayerFromModifiers();
|
|
|
|
// Bold / italic / underline while a text annotation is being edited. This has to come
|
|
// BEFORE the early return below, which hands every other key to the edit box: that
|
|
// return is exactly why Ctrl+B never reached the text tool and collapsed the sidebar
|
|
// instead. The two meanings of Ctrl+B cannot collide, because the sidebar binding is
|
|
// only reachable when no edit box has focus and this one only when it does.
|
|
if (_activeTextBox is not null && _activeTextBox.IsFocused &&
|
|
Keyboard.Modifiers == ModifierKeys.Control &&
|
|
e.Key is Key.B or Key.I or Key.U)
|
|
{
|
|
switch (e.Key)
|
|
{
|
|
case Key.B: _textBold = !_textBold; break;
|
|
case Key.I: _textItalic = !_textItalic; break;
|
|
case Key.U: _textUnderline = !_textUnderline; break;
|
|
}
|
|
ApplyTextStyleToSelection(); // TextSettingsBar.cs, same pair the toggle buttons call
|
|
ShowTextSettings();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// Don't intercept keys when typing in an editable TextBox (typewriter tool or form field).
|
|
// The zoom ComboBox is editable-but-read-only; after using it, focus parks on its inner
|
|
// TextBox and would otherwise swallow every shortcut (e.g. Ctrl+F) until the user clicked away.
|
|
if (e.OriginalSource is TextBox tbSrc && !tbSrc.IsReadOnly) return;
|
|
if (_activeTextBox is not null && _activeTextBox.IsFocused) return;
|
|
|
|
if (e.Key == Key.C && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
// An annotation selection copies the annotation(s); otherwise copy page text.
|
|
if (_selectedAnnotation is not null || _selectedSet.Count > 0) CopySelectedAnnotations();
|
|
else CopySelectedText();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.V && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
// Internal annotation clipboard takes priority over an OS-clipboard image paste.
|
|
if (_annotationClipboard.Count > 0) PasteAnnotations(PageList.SelectedIndex);
|
|
else PasteFromClipboard();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.A && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
// Prefer selecting all annotations (shows where everything is, makes stacked annotations
|
|
// editable); fall back to selecting page text when there are none on screen.
|
|
if (!SelectAllAnnotations()) SelectAllText();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
ToggleSearchBar();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F3 && (Keyboard.Modifiers == ModifierKeys.None || Keyboard.Modifiers == ModifierKeys.Shift))
|
|
{
|
|
// F3 / Shift+F3 - next / previous search match, the Find Next convention (Acrobat and
|
|
// Sumatra do the same). With the search bar closed, F3 opens it like Ctrl+F.
|
|
if (_searchBar is null || _searchBar.Visibility != Visibility.Visible)
|
|
ToggleSearchBar();
|
|
else if (Search.HasResults)
|
|
{
|
|
if (Keyboard.Modifiers == ModifierKeys.Shift) SearchPrevResult();
|
|
else SearchNextResult();
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && _shapePolyPoints.Count > 0)
|
|
{
|
|
// Shapes tool (#127 Phase 3): abandon the in-progress polygon.
|
|
CancelShapePolygon();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Back && _shapePolyPoints.Count > 0)
|
|
{
|
|
// Remove the last placed polygon vertex.
|
|
ShapePolyBackspace();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Enter && _shapePolyPoints.Count >= 3)
|
|
{
|
|
// Close the polygon from the keyboard.
|
|
CommitShapePolygon();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Enter && _currentTool == EditTool.Crop && _cropConfirmBar is not null)
|
|
{
|
|
ApplyCrop([PageList.SelectedIndex]);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && _currentTool == EditTool.Crop && _cropConfirmBar is not null)
|
|
{
|
|
HideCropConfirmBar();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && ShortcutOverlay.Visibility == Visibility.Visible)
|
|
{
|
|
FadeOverlayOut(ShortcutOverlay);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && AboutOverlay.Visibility == Visibility.Visible)
|
|
{
|
|
CloseAboutOverlay();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && _searchBar is not null && _searchBar.Visibility == Visibility.Visible)
|
|
{
|
|
CloseSearchBar();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && _busyCts is not null)
|
|
{
|
|
// A cancellable long operation (OCR, repair) is running behind the busy overlay - offer to
|
|
// cancel it instead of letting Escape fall through to the app-exit handler below.
|
|
if (KillerDialog.Show(this, string.Format(Loc("Str_Dlg_CancelBusy"), _busyOpLabel), "KillerPDF",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
|
_busyCts?.Cancel();
|
|
e.Handled = true;
|
|
}
|
|
// #153: matched by the character the key TYPES, not its position. On a German layout
|
|
// "?" is Shift+ss, so the US-positional OemQuestion check never fired (and the exact
|
|
// modifier equality failed too, since typing "?" holds Shift). The VK test stays as a
|
|
// fast path for layouts where it already worked.
|
|
else if (Services.KeyLayout.IsCtrlChar(e.Key, '?')
|
|
|| (e.Key == Key.OemQuestion && Keyboard.Modifiers == ModifierKeys.Control))
|
|
{
|
|
if (ShortcutOverlay.Visibility == Visibility.Visible) FadeOverlayOut(ShortcutOverlay);
|
|
else ShowShortcutsOverlayExclusive();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F1)
|
|
{
|
|
// Toggle the shortcuts overlay (conventional Help key, alongside Ctrl+?).
|
|
if (ShortcutOverlay.Visibility == Visibility.Visible) FadeOverlayOut(ShortcutOverlay);
|
|
else ShowShortcutsOverlayExclusive();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F12)
|
|
{
|
|
// Toggle the About dialog. (Moved off F2, which now belongs to rename-bookmark in the
|
|
// outline panel, #133 - Windows convention. Document Info moved to F4 / Ctrl+D below.)
|
|
if (AboutOverlay.Visibility == Visibility.Visible) CloseAboutOverlay();
|
|
else ShowAboutOverlay();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F4 && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
// Document Info - the advertised single-key shortcut (F1 help, F4 info, F5-F8 views,
|
|
// F11 full screen, F12 about). Looked it up on the shortcuts overlay and pressed it?
|
|
// The cheat sheet gets out of the way first.
|
|
if (ShortcutOverlay.Visibility == Visibility.Visible) FadeOverlayOut(ShortcutOverlay);
|
|
OpenDocumentInfo();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.D && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
// Compatibility alias: Ctrl+D is Document Properties in Acrobat, Foxit, and SumatraPDF.
|
|
if (ShortcutOverlay.Visibility == Visibility.Visible) FadeOverlayOut(ShortcutOverlay);
|
|
OpenDocumentInfo();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F5) { SetViewMode(ViewMode.Continuous); e.Handled = true; }
|
|
else if (e.Key == Key.F6) { SetViewMode(ViewMode.Single); e.Handled = true; }
|
|
else if (e.Key == Key.F7) { SetViewMode(ViewMode.TwoPage); e.Handled = true; }
|
|
else if (e.Key == Key.F8) { SetViewMode(ViewMode.Grid); e.Handled = true; }
|
|
// F10 = split pane. Matched as Key.System + SystemKey == Key.F10, NOT e.Key == Key.F10:
|
|
// F10 is a system key in WPF (it activates the menu bar), so it never arrives as
|
|
// Key.F10 and a plain check silently never fires. The Shift+F10 branch further down has
|
|
// the same shape; F11 below does not need it, because F11 is not a system key.
|
|
// Shift is excluded here because Shift+F10 is the context menu - without that guard the
|
|
// two would both fire off one press.
|
|
else if (e.Key == Key.System && e.SystemKey == Key.F10
|
|
&& !Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
|
|
{
|
|
ToggleSplit();
|
|
e.Handled = true;
|
|
}
|
|
// #215: reading view - toolbar off. Alt+M arrives as a system key (Alt+letter), same
|
|
// shape as the F10 branches above.
|
|
else if (e.Key == Key.System && e.SystemKey == Key.M && Keyboard.Modifiers == ModifierKeys.Alt)
|
|
{ ToggleToolbarHidden(); e.Handled = true; }
|
|
else if (e.Key == Key.F11) { ToggleFullScreen(); e.Handled = true; }
|
|
else if (e.Key == Key.Escape && _fullScreen) { ToggleFullScreen(); e.Handled = true; }
|
|
else if (e.Key == Key.F4 && Keyboard.Modifiers == ModifierKeys.Shift && _doc is not null)
|
|
{
|
|
ShowCurrentFileSize();
|
|
e.Handled = true;
|
|
}
|
|
// PgDn / PgUp navigate to the next / previous page - they never reorder pages (that's the
|
|
// toolbar Move Up/Down buttons). Handled at the window level with e.Handled so it behaves the
|
|
// same whether the page canvas or a sidebar thumbnail has focus; without this, a focused
|
|
// PageList (ListBox) would page its own selection instead. The TextBox guard at the top of this
|
|
// handler already exempts typing in a form field / typewriter box.
|
|
else if (e.Key == Key.PageDown && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
NavigatePageStep(1); // one page; one SPREAD in Two-Page mode (#120)
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.PageUp && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
NavigatePageStep(-1);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.P && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
Print_Click(this, e);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Delete && (_selectedAnnotation is not null || _selectedSet.Count > 0))
|
|
{
|
|
DeleteSelected();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Z && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
if (!e.IsRepeat) Undo_Click(this, e); // ignore key auto-repeat so one press = one undo
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.S && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
SaveAs_Click(this, e);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.S && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
SaveInPlace();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.W && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
CloseTab(_active);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.W && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
CloseOtherTabs(_active);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Q && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
CloseAllTabs();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Tab && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
CycleTab(-1);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Tab && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
CycleTab(1);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.O && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
Open_Click(this, e);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.O && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
OcrPageToClipboard(PageList.SelectedIndex);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.I && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
BeginOcrRegion();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.B && Keyboard.Modifiers == ModifierKeys.None && _doc is not null
|
|
&& _viewMode == ViewMode.TwoPage
|
|
&& ShortcutOverlay.Visibility != Visibility.Visible
|
|
&& AboutOverlay.Visibility != Visibility.Visible)
|
|
{
|
|
// #193: bare B = book layout toggle, only while Two-Page is active (single-key
|
|
// house style; the sidebar is F9). Same guards as the bare-key switches.
|
|
ToggleBookMode();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.N && Keyboard.Modifiers == ModifierKeys.None && _doc is not null
|
|
&& ShortcutOverlay.Visibility != Visibility.Visible
|
|
&& AboutOverlay.Visibility != Visibility.Visible)
|
|
{
|
|
// Bare N = invert document colors (night mode), #135. Moved off Ctrl+I in 1.6.6 so
|
|
// the conventional italic chord is free while editing text; single-key house style.
|
|
// Same guards as the bare-key tool switches below (doc open, no overlay, not typing).
|
|
ToggleDocInvert(!ActiveViewer.DocInvert); // per pane: flips the focused pane only
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.N && Keyboard.Modifiers == ModifierKeys.Shift && _doc is not null
|
|
&& ShortcutOverlay.Visibility != Visibility.Visible
|
|
&& AboutOverlay.Visibility != Visibility.Visible)
|
|
{
|
|
// Shift+N pairs with bare N: toggles whether night mode inverts pictures too
|
|
// (the moon's right-click option). Same guards as N.
|
|
ToggleInvertImages(!BitmapHelpers.DocInvertImages);
|
|
e.Handled = true;
|
|
}
|
|
// App-wide accessibility size (AppScale.cs), distinct from the Ctrl+wheel page zoom:
|
|
// Ctrl+Shift +/- steps the whole-app size, Ctrl+Shift+0 resets it. Works with no
|
|
// mouse; the wheel-over-logo gesture drives the same scale.
|
|
else if ((e.Key == Key.OemPlus || e.Key == Key.Add) && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
ApplyAppScale(_appScale + AppScaleStep, persist: true);
|
|
e.Handled = true;
|
|
}
|
|
else if ((e.Key == Key.OemMinus || e.Key == Key.Subtract) && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
ApplyAppScale(_appScale - AppScaleStep, persist: true);
|
|
e.Handled = true;
|
|
}
|
|
else if ((e.Key == Key.D0 || e.Key == Key.NumPad0) && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
ApplyAppScale(1.0, persist: true);
|
|
e.Handled = true;
|
|
}
|
|
// Toolbar appearance, mirroring the bar's right-click menu top to bottom: Ctrl+Shift+1/2
|
|
// pick the icon size, Ctrl+Shift+3..6 pick where the text goes. Number row only - the
|
|
// numpad digits stay clear of the tool shortcuts' numpad mappings.
|
|
else if (e.Key is >= Key.D1 and <= Key.D6 && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
switch (e.Key)
|
|
{
|
|
case Key.D1: SetToolbarIconSize(ToolbarIconSize.Small); break;
|
|
case Key.D2: SetToolbarIconSize(ToolbarIconSize.Large); break;
|
|
case Key.D3: SetToolbarLabelMode(ToolbarLabelMode.None); break;
|
|
case Key.D4: SetToolbarLabelMode(ToolbarLabelMode.Beside);break;
|
|
case Key.D5: SetToolbarLabelMode(ToolbarLabelMode.Under); break;
|
|
case Key.D6: SetToolbarLabelMode(ToolbarLabelMode.Only); break;
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F9 && Keyboard.Modifiers == ModifierKeys.Shift)
|
|
{
|
|
ToggleSidebarSide(); // left/right, pairing with F9's collapse toggle
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.N && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
NewDocument();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.F9 && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
// The sidebar gets the single key, house style. F9 used to jog the view mode, which
|
|
// was the one redundant F-key: F5-F8 already jump straight to all four modes and the
|
|
// wheel over the view still cycles them. There is deliberately no Ctrl+B alias:
|
|
// Ctrl+B is bold, which is what users pressing it were expecting all along.
|
|
SidebarToggle_Click(this, e);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Home && Keyboard.Modifiers == ModifierKeys.None && _doc is not null)
|
|
{
|
|
// First / last page (the Acrobat / Sumatra convention).
|
|
RecordNavJump();
|
|
PageList.SelectedIndex = 0;
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.End && Keyboard.Modifiers == ModifierKeys.None && _doc is not null)
|
|
{
|
|
RecordNavJump();
|
|
PageList.SelectedIndex = _doc.PageCount - 1;
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.D1 && Keyboard.Modifiers == ModifierKeys.Control && _doc is not null)
|
|
{
|
|
_fitMode = FitMode.None;
|
|
SetTrueZoom(1.0); // actual size (Acrobat Ctrl+1); Ctrl+0 stays the 100% reset
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.D2 && Keyboard.Modifiers == ModifierKeys.Control && _doc is not null)
|
|
{
|
|
App.SetSetting("DefaultFitMode", FitMode.Width.ToString());
|
|
FitToWidth(); // (Acrobat Ctrl+2)
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.D3 && Keyboard.Modifiers == ModifierKeys.Control && _doc is not null)
|
|
{
|
|
App.SetSetting("DefaultFitMode", FitMode.Page.ToString());
|
|
FitToPage();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Y && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
if (!e.IsRepeat) Redo_Click(this, e);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Z && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))
|
|
{
|
|
if (!e.IsRepeat) Redo_Click(this, e); // Ctrl+Shift+Z, the editor-style redo
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.System && e.SystemKey == Key.Left && Keyboard.Modifiers == ModifierKeys.Alt)
|
|
{
|
|
NavHistoryGo(back: true); // retrace bookmark / link / jump-box jumps
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.System && e.SystemKey == Key.Right && Keyboard.Modifiers == ModifierKeys.Alt)
|
|
{
|
|
NavHistoryGo(back: false);
|
|
e.Handled = true;
|
|
}
|
|
else if ((e.Key == Key.Apps
|
|
|| (e.Key == Key.System && e.SystemKey == Key.F10 && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)))
|
|
&& _doc is not null)
|
|
{
|
|
// Windows accessibility convention: the Menu key / Shift+F10 opens the context menu
|
|
// at the current selection without the mouse.
|
|
OpenContextMenuAtSelection();
|
|
e.Handled = true;
|
|
}
|
|
// Bare-key tool switches. Only when a document is open, no modifier is held, and no
|
|
// overlay is up (and not while typing - guarded at the top of this handler).
|
|
else if (Keyboard.Modifiers == ModifierKeys.None && _doc is not null
|
|
&& ShortcutOverlay.Visibility != Visibility.Visible
|
|
&& AboutOverlay.Visibility != Visibility.Visible
|
|
&& TryToolShortcut(e.Key))
|
|
{
|
|
e.Handled = true;
|
|
}
|
|
// Left/Right move one page - one two-page SPREAD in Two-Page mode (#120), so a press
|
|
// always changes what's on screen instead of stepping through both pages of a spread.
|
|
else if (e.Key == Key.Left && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
if (NavigatePageStep(-1)) e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Right && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
if (NavigatePageStep(1)) e.Handled = true;
|
|
}
|
|
// Up/Down scroll the view like the mouse wheel instead of jumping a page; at the top/
|
|
// bottom edge (or when the page fits the viewport, where there's nothing to scroll)
|
|
// they flip to the previous/next page, so at fit-to-page zoom this behaves exactly
|
|
// like the old page navigation. Left/Right and PgUp/PgDn stay hard page jumps.
|
|
// Handled at the window level so a focused sidebar thumbnail doesn't move its own
|
|
// selection instead (same reasoning as PgUp/PgDn above).
|
|
else if ((e.Key == Key.Up || e.Key == Key.Down) && Keyboard.Modifiers == ModifierKeys.None)
|
|
{
|
|
if (_doc is not null)
|
|
{
|
|
ScrollOrFlipByKey(up: e.Key == Key.Up);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
// #153: "the key that types + or =", whatever position that is on this layout. The
|
|
// Ctrl+Shift app-scale chords above are tested FIRST, so ignoring Shift here cannot
|
|
// swallow them - which is why this ordering matters and must not be rearranged.
|
|
else if (Services.KeyLayout.IsCtrlChar(e.Key, '+', '=')
|
|
|| ((e.Key == Key.OemPlus || e.Key == Key.Add) && Keyboard.Modifiers == ModifierKeys.Control))
|
|
{
|
|
if (_viewMode == ViewMode.Grid) GridZoomStep(false); else SetZoom(_zoomLevel + ZoomStep);
|
|
e.Handled = true;
|
|
}
|
|
else if (Services.KeyLayout.IsCtrlChar(e.Key, '-')
|
|
|| ((e.Key == Key.OemMinus || e.Key == Key.Subtract) && Keyboard.Modifiers == ModifierKeys.Control))
|
|
{
|
|
if (_viewMode == ViewMode.Grid) GridZoomStep(true); else SetZoom(_zoomLevel - ZoomStep);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.D0 && Keyboard.Modifiers == ModifierKeys.Control)
|
|
{
|
|
SetTrueZoom(1.0);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape && _doc is not null && _currentTool != EditTool.Select)
|
|
{
|
|
// 1.6.6 (#127 Phase 3): Esc with nothing left to cancel drops back to the Select
|
|
// tool, Acrobat-style. With Select already active it keeps falling through to the
|
|
// app-exit handler below, unchanged.
|
|
SetTool(EditTool.Select);
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Escape)
|
|
{
|
|
// No overlay active - ESC exits the app
|
|
Close();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == Key.Space && !_spaceHeld)
|
|
{
|
|
_spaceHeld = true;
|
|
PagePreviewPanel.Cursor = Cursors.Hand;
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
// Full-window overlays are mutually exclusive: opening the shortcuts overlay dismisses
|
|
// About instead of stacking - ShowAboutOverlay does the converse.
|
|
private void ShowShortcutsOverlayExclusive()
|
|
{
|
|
if (AboutOverlay.Visibility == Visibility.Visible) FadeOverlayOut(AboutOverlay);
|
|
ApplyPersistedShortcutView();
|
|
FadeOverlayIn(ShortcutOverlay);
|
|
}
|
|
|
|
// ── Jump history (Alt+Left / Alt+Right / mouse back-forward buttons) ─────────────────────
|
|
// Page-granular: recorded at the long-jump sites (bookmark click, internal link, the page
|
|
// jump box, Home/End) so a reader thrown 30 pages by a bookmark can retrace the hop.
|
|
|
|
/// <summary>Records the CURRENT page onto the back stack. Call BEFORE performing a jump.</summary>
|
|
private void RecordNavJump()
|
|
{
|
|
if (_doc is null) return;
|
|
int cur = Math.Max(0, PageList.SelectedIndex);
|
|
if (_navBack.Count > 0 && _navBack.Peek() == cur) { _navForward.Clear(); return; }
|
|
_navBack.Push(cur);
|
|
_navForward.Clear(); // a fresh jump invalidates the forward chain, like a browser
|
|
}
|
|
|
|
private void NavHistoryGo(bool back)
|
|
{
|
|
if (_doc is null) return;
|
|
var from = back ? _navBack : _navForward;
|
|
var to = back ? _navForward : _navBack;
|
|
if (from.Count == 0) return;
|
|
int cur = Math.Max(0, PageList.SelectedIndex);
|
|
int target = from.Pop();
|
|
to.Push(cur);
|
|
if (target >= 0 && target < _doc.PageCount)
|
|
PageList.SelectedIndex = target;
|
|
}
|
|
|
|
// Mouse back / forward buttons (XButton1 / XButton2) retrace the same history, like a
|
|
// browser. Registered on the window in the MainWindow constructor.
|
|
private void NavHistory_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.ChangedButton == MouseButton.XButton1) { NavHistoryGo(back: true); e.Handled = true; }
|
|
else if (e.ChangedButton == MouseButton.XButton2) { NavHistoryGo(back: false); e.Handled = true; }
|
|
}
|
|
|
|
/// <summary>Keyboard access to the right-click menu (Menu key / Shift+F10): the selected
|
|
/// annotation's menu at its bounds, or the page-level menu centered on the current page's
|
|
/// canvas. Placement is set to Center just for this open and restored on close, so the
|
|
/// mouse path keeps its open-at-cursor behavior.</summary>
|
|
private void OpenContextMenuAtSelection()
|
|
{
|
|
if (_doc is null || _annotationCanvas.ContextMenu is not ContextMenu cm) return;
|
|
int pg = Math.Max(0, PageList.SelectedIndex);
|
|
Point pt;
|
|
if (_selectedAnnotation is not null)
|
|
{
|
|
pg = _selectedAnnotation.PageIndex;
|
|
var b = AnnotBounds(_selectedAnnotation);
|
|
pt = new Point(b.X + b.Width / 2, b.Y + b.Height / 2);
|
|
}
|
|
else
|
|
pt = _renderDims.TryGetValue(pg, out var rd)
|
|
? new Point(rd.w / 2.0, rd.h / 2.0)
|
|
: new Point(0, 0);
|
|
var canvas = VisibleCanvasForPage(pg) ?? CanvasForPage(pg);
|
|
if (canvas is null) return;
|
|
_activeCanvas = canvas;
|
|
PopulateContextMenu(pt, pg);
|
|
cm.PlacementTarget = canvas;
|
|
var prevPlacement = cm.Placement;
|
|
cm.Placement = System.Windows.Controls.Primitives.PlacementMode.Center;
|
|
void Restore(object? s, RoutedEventArgs a) { cm.Placement = prevPlacement; cm.Closed -= Restore; }
|
|
cm.Closed += Restore;
|
|
cm.IsOpen = true;
|
|
}
|
|
|
|
// One Up/Down arrow press scrolls this many DIP; key auto-repeat makes holding the key a
|
|
// smooth continuous scroll. Kept smaller than a wheel notch (144 DIP, see WheelScrollFactor)
|
|
// for fine reading control.
|
|
private const double ArrowScrollStep = 48.0;
|
|
|
|
// Up/Down arrow behavior, mirroring PagePreview_PreviewMouseWheel exactly: Grid and
|
|
// Continuous are one scroll over the whole document, so the keys always scroll; Single/
|
|
// Two-Page scroll within the page and flip to the previous/next page at the edges.
|
|
private void ScrollOrFlipByKey(bool up)
|
|
{
|
|
double step = up ? -ArrowScrollStep : ArrowScrollStep;
|
|
if (_viewMode == ViewMode.Grid || _viewMode == ViewMode.Continuous)
|
|
{
|
|
PagePreviewPanel.ScrollToVerticalOffset(PagePreviewPanel.VerticalOffset + step);
|
|
return;
|
|
}
|
|
// NavigatePageByWheel treats positive delta as "previous page" (wheel-up), so reuse
|
|
// the same convention here.
|
|
if (PagePreviewPanel.ScrollableHeight <= 0)
|
|
{
|
|
NavigatePageByWheel(up ? 120 : -120);
|
|
return;
|
|
}
|
|
bool atTop = PagePreviewPanel.VerticalOffset <= 0;
|
|
bool atBottom = PagePreviewPanel.VerticalOffset >= PagePreviewPanel.ScrollableHeight - 1;
|
|
if ((atTop && up) || (atBottom && !up))
|
|
{
|
|
NavigatePageByWheel(up ? 120 : -120);
|
|
return;
|
|
}
|
|
PagePreviewPanel.ScrollToVerticalOffset(PagePreviewPanel.VerticalOffset + step);
|
|
}
|
|
|
|
protected override void OnPreviewKeyUp(KeyEventArgs e)
|
|
{
|
|
base.OnPreviewKeyUp(e);
|
|
KbSyncLayerFromModifiers(); // releasing a modifier drops the keyboard view back a layer
|
|
if (e.Key == Key.Space && _spaceHeld)
|
|
{
|
|
_spaceHeld = false;
|
|
if (!ActiveViewer.IsPanning)
|
|
PagePreviewPanel.Cursor = Cursors.Arrow;
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
// Maps a bare key to an editing tool. Returns false for any other key so the caller's
|
|
// shortcut chain continues. Mirrors the toolbar tool buttons exactly - Signature routes
|
|
// through its button handler so the signature picker opens (a bare SetTool would only arm
|
|
// the tool without showing the menu).
|
|
private bool TryToolShortcut(Key key)
|
|
{
|
|
switch (key)
|
|
{
|
|
// Tools are reachable by their toolbar position (digits 1-9, 0 mirror the toolbar,
|
|
// left to right); the original letter keys stay as fallbacks. Both the number-row and
|
|
// numpad digits map. 1.6.6 remap (#127 Phase 3): Select is V-only (the Photoshop /
|
|
// Illustrator / Figma convention) - its digit went to Text so Shapes could take 4 and
|
|
// the digits keep mirroring the toolbar order.
|
|
case Key.V: SetTool(EditTool.Select); return true;
|
|
case Key.T: case Key.D1: case Key.NumPad1: SetTool(EditTool.Text); return true;
|
|
case Key.H: case Key.D2: case Key.NumPad2: SetTool(EditTool.Highlight); return true;
|
|
case Key.L: case Key.U: case Key.D3: case Key.NumPad3: SetTool(EditTool.Line); return true;
|
|
case Key.D4: case Key.NumPad4: SetTool(EditTool.Shape); return true;
|
|
case Key.D: case Key.D5: case Key.NumPad5: SetTool(EditTool.Draw); return true;
|
|
case Key.I: case Key.D6: case Key.NumPad6: SetTool(EditTool.Image); return true;
|
|
case Key.G: case Key.D7: case Key.NumPad7: ToolSignature_Click(this, new RoutedEventArgs()); return true;
|
|
case Key.C: case Key.D8: case Key.NumPad8: SetTool(EditTool.Crop); return true;
|
|
case Key.R: case Key.D9: case Key.NumPad9: ToolRotate_Click(this, new RoutedEventArgs()); return true;
|
|
case Key.S: case Key.D0: case Key.NumPad0: ToolStamp_Click(this, new RoutedEventArgs()); return true;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
// Appends each tool's key to its tooltip, e.g. "Highlight (2)". Digits mirror the toolbar
|
|
// position (1-9, 0); Select shows its letter (V) since the 1.6.6 remap made it V-only.
|
|
// Re-resolves the localized base text so a language switch keeps the right wording (from SelectLocale).
|
|
private void ApplyToolNumberTooltips()
|
|
{
|
|
// n null or empty = no key to advertise, so the tooltip is just the localized text
|
|
// rather than a stray empty bracket pair.
|
|
void Set(System.Windows.Controls.Button btn, string key, string? n)
|
|
{
|
|
if (btn == null || TryFindResource(key) is not string s) return;
|
|
btn.ToolTip = string.IsNullOrEmpty(n) ? s : $"{s} ({n})";
|
|
}
|
|
Set(ToolSelectBtn, "Str_TT_SelectTool", "V");
|
|
Set(ToolTextBtn, "Str_TT_TextTool", "1");
|
|
Set(ToolHighlightBtn, "Str_TT_HighlightTool", "2");
|
|
Set(ToolUnderlineBtn, "Str_TT_LineTool", "3"); // repurposed to the Line tool
|
|
Set(ToolShapeBtn, "Str_TT_ShapeTool", "4");
|
|
Set(ToolDrawBtn, "Str_TT_DrawTool", "5");
|
|
Set(ToolImageBtn, "Str_TT_ImageTool", "6");
|
|
Set(ToolSignatureBtn, "Str_TT_SignatureTool", "7");
|
|
Set(ToolCropBtn, "Str_TT_CropTool", "8");
|
|
Set(_toolRotateBtn, "Str_TT_RotateTool", "9");
|
|
// Not a tool, but the same treatment. Cycling has no key any more: F9 went to the
|
|
// sidebar, so the wheel over the view (or the button itself) is how you jog modes,
|
|
// and advertising a dead key here is worse than advertising none.
|
|
Set(ViewModeBtn, "Str_TT_ViewMode", null);
|
|
}
|
|
|
|
// Opens the online help / how-to page in the user's default browser.
|
|
private void OnlineHelp_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try { Process.Start(new ProcessStartInfo("https://killerpdf.net/help.html") { UseShellExecute = true }); }
|
|
catch { }
|
|
}
|
|
}
|
|
}
|