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. /// Records the CURRENT page onto the back stack. Call BEFORE performing a jump. 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; } } /// 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. 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 { } } } }