Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/gen-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,15 @@ const globalPrefs: Field[] = [
),
setVersion(mkField("ScrollbarInSinglePage", Bool, false, "if true, we show scrollbar in single page mode"), "3.6"),
setVersion(mkField("SmoothScroll", Bool, false, "if true, implements smooth scrolling"), "3.6"),
setVersion(
mkField(
"EnableCitationHover",
Bool,
true,
"if true, hovering an internal-document link shows a popup rendering the destination region (citation entry, figure, footnote)",
),
"3.7",
),
setVersion(
mkField(
"FastScrollOverScrollbar",
Expand Down
5 changes: 5 additions & 0 deletions docs/md/Advanced-options-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ ScrollbarInSinglePage = false
; if true, implements smooth scrolling (introduced in version 3.6)
SmoothScroll = false

; if true, hovering an internal-document link shows a popup rendering the
; destination region (citation entry, figure, footnote) (introduced in version
; 3.7)
EnableCitationHover = true

; if true, mouse wheel scrolling is faster when mouse is over a scrollbar
; (introduced in version 3.6)
FastScrollOverScrollbar = false
Expand Down
1 change: 1 addition & 0 deletions docs/md/Version-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Available in [pre-release](https://www.sumatrapdfreader.org/prerelease) builds.
- fix Edit Annotations window not restoring to the correct monitor in multi-monitor setups
- use `GetFileAttributesEx` instead of opening files for change detection on network drives, avoiding Windows Defender re-scans
- fix toolbar page number misalignment when `PrinterAccess` is revoked in `sumatrapdfrestrict.ini`
- add citation/reference hover preview: hovering an internal-document link (e.g. a `[1]` citation, figure reference, or footnote marker) now shows a small popup rendering the destination region, so you can see the bibliography entry / figure / footnote without leaving the current page. Toggle with the `EnableCitationHover` advanced setting (fixes [#128](https://github.com/sumatrapdfreader/sumatrapdf/issues/128), [#4221](https://github.com/sumatrapdfreader/sumatrapdf/issues/4221))

## 3.6.1 (2026-04-06)

Expand Down
71 changes: 71 additions & 0 deletions src/Canvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
#include "Toolbar.h"
#include "Translations.h"

#include "RefHover.h"

#include "utils/Log.h"

// if set instead of trying to render pages we don't have, we simply do nothing
Expand Down Expand Up @@ -793,6 +795,24 @@ static bool gShowAnnotationNotification = true;
// Forward declaration
static RectF CalculateResizedRect(MainWindow* win, int x, int y);

// Returns true when el is an internal-document link (not an external URL or
// file launch). Used as a heuristic for "this is probably a citation link".
static bool IsCitationLink(IPageElement* el) {
if (!el || !el->Is(kindPageElementDest)) {
return false;
}
IPageDestination* dest = el->AsLink();
if (!dest) {
return false;
}
Kind k = dest->GetKind();
if (k == kindDestinationLaunchURL || k == kindDestinationLaunchFile) {
return false;
}
int destPage = PageDestGetPageNo(dest);
return destPage > 0;
}

static void OnMouseMove(MainWindow* win, int x, int y, WPARAM) {
DisplayModel* dm = win->AsFixed();
// ReportIf(!dm); // can happen if reload fails, we delete DisplayModel
Expand Down Expand Up @@ -902,6 +922,30 @@ static void OnMouseMove(MainWindow* win, int x, int y, WPARAM) {
RemoveNotificationsForGroup(win->hwndCanvas, kNotifAnnotation);
}
win->annotationUnderCursor = annot;

// Citation hover: render the destination region of an internal
// link (typically the bibliography entry) into a popup.
if (gGlobalPrefs->enableCitationHover) {
if (!win->refHover) {
win->refHover = RefHoverCreate(win->hwndCanvas);
}
IPageElement* el = dm->GetElementAtPos(pos, nullptr);
if (win->refHover && IsCitationLink(el)) {
IPageDestination* dest = el->AsLink();
int destPage = PageDestGetPageNo(dest);
RectF destPt = PageDestGetDestPoint(dest);
Point screenPt = {x, y};
ClientToScreen(win->hwndCanvas, (POINT*)&screenPt);
int srcPage = el->GetPageNo();
RectF srcRect = el->GetRect();
RefHoverSchedule(win->refHover, win->hwndCanvas, screenPt, destPage, destPt.x, destPt.y, srcPage,
srcRect);
} else if (win->refHover) {
RefHoverHide(win->refHover, win->hwndCanvas);
}
} else if (win->refHover) {
RefHoverHide(win->refHover, win->hwndCanvas);
}
break;
}

Expand Down Expand Up @@ -2093,6 +2137,22 @@ static LRESULT CanvasOnMouseWheel(MainWindow* win, UINT msg, WPARAM wp, LPARAM l
return res;
}

// Wheel-zoom the citation-hover popup while the cursor is still on the
// citation link that opened it. Avoids moving the cursor onto the popup
// (which would dismiss the hover).
if (win->refHover && win->refHover->hwndPopup && IsWindowVisible(win->refHover->hwndPopup)) {
DisplayModel* dmHover = win->AsFixed();
if (dmHover) {
Point pt = HwndGetCursorPos(win->hwndCanvas);
IPageElement* elHover = dmHover->GetElementAtPos(pt, nullptr);
if (IsCitationLink(elHover)) {
short delta = GET_WHEEL_DELTA_WPARAM(wp);
RefHoverWheelZoom(win->refHover, dmHover->GetEngine(), delta);
return 0;
}
}
}

DisplayModel* dm = win->AsFixed();

// Note: not all mouse drivers correctly report the Ctrl key's state
Expand Down Expand Up @@ -2848,6 +2908,17 @@ static void OnTimer(MainWindow* win, HWND hwnd, WPARAM timerId) {
}
break;

case kRefHoverTimerID: {
DisplayModel* dm = win->AsFixed();
EngineBase* engine = dm ? dm->GetEngine() : nullptr;
float pageZoom = 1.f;
if (dm && win->refHover && win->refHover->pendingDestPage > 0) {
pageZoom = dm->GetZoomReal(win->refHover->pendingDestPage);
}
RefHoverOnTimer(win->refHover, hwnd, engine, pageZoom);
break;
}

case HIDE_FWDSRCHMARK_TIMER_ID:
win->fwdSearchMark.hideStep++;
if (1 == win->fwdSearchMark.hideStep) {
Expand Down
12 changes: 12 additions & 0 deletions src/EngineBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ struct IPageDestination : KindBase {
virtual RectF GetRect2() { return rect; }
// optional zoom level on the above returned page
virtual float GetZoom2() { return zoom; }
// anchor point (x, y) on the destination page; rect's dx/dy may be 0.
// Default falls back to GetRect2 (callers should still tolerate (0,0)).
virtual RectF GetDestPoint2() { return GetRect2(); }

// string value associated with the destination (e.g. a path or a URL)
virtual char* GetValue2() { return nullptr; }
Expand Down Expand Up @@ -119,6 +122,15 @@ static inline RectF PageDestGetRect(IPageDestination* dest) {
return dest->GetRect2();
}

// anchor point on the destination page (x, y in user-space). Returns {0,0,0,0}
// when the destination has no specific anchor.
static inline RectF PageDestGetDestPoint(IPageDestination* dest) {
if (!dest) {
return {};
}
return dest->GetDestPoint2();
}

// optional zoom level on the above returned page
static inline float PageDestGetZoom(IPageDestination* dest) {
return dest->GetZoom2();
Expand Down
23 changes: 22 additions & 1 deletion src/EngineMupdf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ struct PageDestinationMupdf : IPageDestination {
char* value = nullptr;
char* name = nullptr;

// anchor (x, y) on the destination page resolved from the link URI;
// -1 means "not resolved" (e.g. external URL or file launch).
float destX = -1.f;
float destY = -1.f;

PageDestinationMupdf(fz_link* l, fz_outline* o) {
// exactly one must be provided
kind = kindDestinationMupdf;
Expand All @@ -107,6 +112,16 @@ struct PageDestinationMupdf : IPageDestination {
}
return rect;
}

RectF GetDestPoint2() override {
if (outline) {
return RectF{outline->x, outline->y, 0, 0};
}
if (destY >= 0.f) {
return RectF{destX, destY, 0, 0};
}
return {};
}
~PageDestinationMupdf() override {
str::Free(value);
str::Free(name);
Expand Down Expand Up @@ -223,7 +238,13 @@ static IPageDestination* NewPageDestinationMupdf(fz_context* ctx, fz_document* d

auto dest = new PageDestinationMupdf(link, outline);
dest->rect = FzGetRectF(link, outline);
dest->pageNo = FzGetPageNo(ctx, doc, link, outline);
{
float x = 0, y = 0;
const char* destUri = link ? link->uri : (outline ? outline->uri : nullptr);
dest->pageNo = ResolveLink(ctx, doc, destUri, &x, &y);
dest->destX = x;
dest->destY = y;
}
return dest;
}

Expand Down
2 changes: 2 additions & 0 deletions src/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "OverlayScrollbar.h"
#include "SumatraPDF.h"
#include "MainWindow.h"
#include "RefHover.h"
#include "WindowTab.h"
#include "TableOfContents.h"
#include "resource.h"
Expand Down Expand Up @@ -109,6 +110,7 @@ void CreateMovePatternLazy(MainWindow* win) {

MainWindow::~MainWindow() {
KillTimer(hwndCanvas, kSmoothScrollTimerID);
RefHoverDestroy(refHover);
FinishStressTest(this);

ReportIf(TabCount() > 0);
Expand Down
2 changes: 2 additions & 0 deletions src/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ struct WindowTab;

struct Annotation;
struct ILinkHandler;
struct RefHoverState;

// Current action being performed with a mouse
enum class MouseAction {
Expand Down Expand Up @@ -268,6 +269,7 @@ struct MainWindow {
IPageElement* linkOnLastButtonDown = nullptr;
AutoFreeStr urlOnLastButtonDown;
Annotation* annotationUnderCursor = nullptr;
RefHoverState* refHover = nullptr;
// highlight rectangle for element under cursor during context menu (in page coordinates)
RectF contextMenuHighlightRect{};
int contextMenuHighlightPageNo = 0;
Expand Down
Loading