Skip to content

Commit 4831e1f

Browse files
kjkclaude
andcommitted
Add CmdExpandToCurrentPage to expand TOC to current page (fixes #1998)
New command that expands the Bookmarks (table of contents) tree down to the entry matching the current page and selects/scrolls to it, like Explorer's "Expand to current folder". Available from the TOC sidebar right-click menu and the Ctrl+K command palette. Implemented as ExpandTocToCurrentPage() in TableOfContents.cpp: finds the best-matching TocItem for the current page (TreeItemForPageNo), then uses TreeView_EnsureVisible to expand collapsed ancestors and scroll into view, and selects the item. Includes a GUI test (tests/issue-1998.ts) that drives the app via WM_COMMAND and reads the TOC tree state with cross-process TreeView messages: collapses the tree, invokes the command, and asserts the tree expands to the current page with an item selected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3138b98 commit 4831e1f

11 files changed

Lines changed: 264 additions & 1 deletion

cmd/gen-commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export const commands = [
236236
"CmdCommandPaletteTOC", "Command Palette: Table Of Contents",
237237
"CmdDebugToggleRenderInfo", "Debug: Toggle Render Queue Info",
238238
"CmdConvertImageToPdf", "Convert Image To PDF",
239+
"CmdExpandToCurrentPage", "Expand TOC to Current Page",
239240
"CmdNone", "Do nothing",
240241
];
241242

docs/md/Commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ CmdToggleBookmarks,F12,Toggle Bookmarks,
3939
CmdToggleTableOfContents,,Toggle Table Of Contents,ver 3.6+
4040
CmdCollapseAll,,Collapse All,
4141
CmdExpandAll,,Expand All,
42+
CmdExpandToCurrentPage,,Expand TOC to Current Page,"In the Bookmarks (table of contents) sidebar, expand the tree down to the entry for the current page and select it, ver 3.7+"
4243
CmdOpenEmbeddedPDF,,Open Embedded PDF,
4344
CmdSaveEmbeddedFile,,Save Embedded File...,
4445
CmdCreateShortcutToFile,,Create .lnk Shortcut,

docs/md/Version-history.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
Available in [pre-release](https://www.sumatrapdfreader.org/prerelease) builds.
66

7+
- add `CmdExpandToCurrentPage` (`Expand TOC to Current Page`, in the Bookmarks sidebar right-click menu and the `Ctrl + k` command palette) to expand the table of contents tree down to the current page's entry and select it, like Explorer's "Expand to current folder" (fixes #1998)
78
- Save As now warns instead of failing silently when a file can't be written (e.g. the destination path exceeds the Windows `MAX_PATH` limit); previously there was no way to tell the save hadn't happened (fixes #1016)
89
- can convert an image to a PDF: right-click an image (or an open image document) and choose `Image / Convert to PDF`, or pick `PDF` in the format drop-down of the Save Image dialog. The new PDF gets `CreationDate`/`ModDate` metadata with the current time and time zone (fixes #949)
910
- in the Favorites pane and menu, a favorite for a file with a long name now shows your favorite's name first, then the file name, so the name you gave it is no longer pushed out of view (fixes #829, #2236)

src/Commands.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ static SeqStrings gCommandNames =
246246
"CmdCommandPaletteTOC\0"
247247
"CmdDebugToggleRenderInfo\0"
248248
"CmdConvertImageToPdf\0"
249+
"CmdExpandToCurrentPage\0"
249250
"CmdNone\0"
250251
"\0";
251252

@@ -482,6 +483,7 @@ static i32 gCommandIds[] = {
482483
CmdCommandPaletteTOC,
483484
CmdDebugToggleRenderInfo,
484485
CmdConvertImageToPdf,
486+
CmdExpandToCurrentPage,
485487
CmdNone,
486488
};
487489

@@ -718,6 +720,7 @@ SeqStrings gCommandDescriptions =
718720
"Command Palette: Table Of Contents\0"
719721
"Debug: Toggle Render Queue Info\0"
720722
"Convert Image To PDF\0"
723+
"Expand TOC to Current Page\0"
721724
"Do nothing\0"
722725
"\0";
723726
// clang-format on

src/Commands.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ enum {
241241
CmdCommandPaletteTOC = 430,
242242
CmdDebugToggleRenderInfo = 431,
243243
CmdConvertImageToPdf = 432,
244-
CmdNone = 433,
244+
CmdExpandToCurrentPage = 433,
245+
CmdNone = 434,
245246

246247
/* range for file history */
247248
CmdFileHistoryFirst,

src/SumatraPDF.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7042,6 +7042,10 @@ static LRESULT FrameOnCommand(MainWindow* win, HWND hwnd, UINT msg, WPARAM wp, L
70427042
}
70437043
break;
70447044

7045+
case CmdExpandToCurrentPage:
7046+
ExpandTocToCurrentPage(win);
7047+
break;
7048+
70457049
case CmdScrollUpHalfPage: {
70467050
if (win->IsCurrentTabAbout()) {
70477051
HomePageOnVScroll(win, SB_PAGEUP);

src/TableOfContents.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,37 @@ void UpdateTocSelection(MainWindow* win, int currPageNo) {
367367
treeView->SelectItem(toSelect);
368368
}
369369

370+
// expand the table of contents tree down to the entry matching the current
371+
// page, then select and scroll to it (issue #1998, like Explorer's
372+
// "Expand to current folder")
373+
void ExpandTocToCurrentPage(MainWindow* win) {
374+
if (!win || !win->IsDocLoaded()) {
375+
return;
376+
}
377+
// make sure the bookmarks (table of contents) sidebar is visible
378+
if (!win->tocVisible) {
379+
SetSidebarVisibility(win, true, gGlobalPrefs->showFavorites);
380+
}
381+
if (!win->tocLoaded || !win->tocVisible) {
382+
return;
383+
}
384+
TreeView* treeView = win->tocTreeView;
385+
int currPageNo = win->ctrl->CurrentPageNo();
386+
TocItem* item = TreeItemForPageNo(treeView, currPageNo);
387+
if (!item) {
388+
return;
389+
}
390+
HTREEITEM hi = treeView->GetHandleByTreeItem((TreeItem)item);
391+
if (!hi) {
392+
return;
393+
}
394+
// TreeView_EnsureVisible expands any collapsed ancestors and scrolls the
395+
// item into view, which is exactly the "expand to current page" behavior
396+
TreeView_EnsureVisible(treeView->hwnd, hi);
397+
treeView->SelectItem((TreeItem)item);
398+
HwndSetFocus(treeView->hwnd);
399+
}
400+
370401
static void UpdateDocTocExpansionStateRecur(TreeView* treeView, Vec<int>& tocState, TocItem* tocItem) {
371402
while (tocItem) {
372403
// items without children cannot be toggled
@@ -538,6 +569,10 @@ static MenuDef menuDefContextToc[] = {
538569
_TRN("Collapse All"),
539570
CmdCollapseAll,
540571
},
572+
{
573+
_TRN("Expand to Current Page"),
574+
CmdExpandToCurrentPage,
575+
},
541576
{
542577
kMenuSeparator,
543578
0,
@@ -673,6 +708,9 @@ static void TocContextMenu(ContextMenuEvent* ev) {
673708
case CmdCollapseAll:
674709
win->tocTreeView->CollapseAll();
675710
break;
711+
case CmdExpandToCurrentPage:
712+
ExpandTocToCurrentPage(win);
713+
break;
676714
case CmdFavoriteAdd:
677715
AddFavoriteFromToc(win, dti);
678716
break;

src/TableOfContents.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ void ClearTocBox(MainWindow*);
66
void ToggleTocBox(MainWindow*);
77
void LoadTocTree(MainWindow*);
88
void UpdateTocSelection(MainWindow*, int currPageNo);
9+
void ExpandTocToCurrentPage(MainWindow*);
910
void UpdateTocExpansionState(Vec<int>& tocState, TreeView*, TocTree*);
1011
void UnsubclassToc(MainWindow*);
1112
void TocFilterChanged(MainWindow*);

tests/all.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// and register it in the `tests` array below.
1111

1212
import { buildApp } from "./util.ts";
13+
import { testit as issue1998 } from "./issue-1998.ts";
1314
import { testit as issue906 } from "./issue-906.ts";
1415
import { testit as issue933 } from "./issue-933.ts";
1516
import { testit as issue4967 } from "./issue-4967.ts";
@@ -24,6 +25,7 @@ import { testit as issue5681 } from "./issue-5681.ts";
2425
import { testit as issueChmLzx } from "./issue-chm-lzx.ts";
2526

2627
const tests: [string, () => void | Promise<void>][] = [
28+
["issue-1998", issue1998],
2729
["issue-906", issue906],
2830
["issue-933", issue933],
2931
["issue-4967", issue4967],

tests/issue-1998.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Test for issue #1998: "Expand to current page in toc tree".
2+
//
3+
// CmdExpandToCurrentPage expands the table-of-contents tree down to the entry
4+
// matching the current page and selects it (like Explorer's "Expand to current
5+
// folder"). Implemented in src/TableOfContents.cpp (ExpandTocToCurrentPage),
6+
// wired to the TOC context menu and the global command dispatcher.
7+
//
8+
// The test opens a PDF with a nested TOC, navigates to the last page, collapses
9+
// the whole tree, then invokes the command via WM_COMMAND and reads the tree
10+
// state with cross-process TreeView messages. After the command, the path to the
11+
// (deeply nested) last-page entry must be expanded -- so more tree rows are
12+
// visible than when fully collapsed -- and an item must be selected. Reverting
13+
// the fix leaves the tree collapsed with no expansion, so the test fails.
14+
//
15+
// Needs a PDF with a multi-level outline; uses one from the local bugs folder
16+
// and skips cleanly if it isn't present (so tests/all.ts keeps going).
17+
18+
import { spawnSync } from "node:child_process";
19+
import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
20+
import { join } from "node:path";
21+
import { EXE, ROOT, tmpPath } from "./util";
22+
23+
// a PDF with a nested (multi-level) table of contents
24+
const TOC_PDF = "C:\\Users\\kjk\\OneDrive\\!sumatra\\bugs\\bug-1352-merged_manuals-1.4.2.pdf";
25+
26+
const SETTINGS = [
27+
`ShowToc = true`,
28+
`ShowFavorites = false`,
29+
`DefaultDisplayMode = continuous`,
30+
`DefaultZoom = fit page`,
31+
`Scrollbars = windows`,
32+
`RestoreSession = false`,
33+
`ShowStartPage = false`,
34+
`CheckForUpdates = false`,
35+
``,
36+
].join("\n");
37+
38+
export async function testit(): Promise<void> {
39+
if (!existsSync(TOC_PDF)) {
40+
console.log(` SKIP: TOC test PDF not found: ${TOC_PDF}`);
41+
return;
42+
}
43+
44+
// copy the PDF out of OneDrive to a local path -- opening a cloud-backed file
45+
// can block/delay window creation (hydration), which breaks the automation
46+
const pdf = tmpPath("issue-1998.pdf");
47+
copyFileSync(TOC_PDF, pdf);
48+
49+
const appdata = tmpPath("issue-1998-appdata");
50+
rmSync(appdata, { recursive: true, force: true });
51+
mkdirSync(appdata, { recursive: true });
52+
writeFileSync(join(appdata, "SumatraPDF-settings.txt"), SETTINGS);
53+
54+
const ps1 = join(ROOT, "tests", "issue-1998.verify.ps1");
55+
const r = spawnSync(
56+
"powershell.exe",
57+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1, "-Exe", EXE, "-Pdf", pdf, "-AppData", appdata],
58+
{ encoding: "utf8", timeout: 90_000 },
59+
);
60+
const out = (r.stdout || "") + (r.stderr || "");
61+
const m = out.match(/RESULT collapsed=(\d+) after=(\d+) hasSelection=(\d+)/);
62+
if (!m) {
63+
throw new Error(`could not read TOC tree state; output:\n${out}`);
64+
}
65+
const collapsed = parseInt(m[1], 10);
66+
const after = parseInt(m[2], 10);
67+
const hasSelection = m[3] === "1";
68+
console.log(` collapsed rows=${collapsed}, after-expand rows=${after}, hasSelection=${hasSelection}`);
69+
70+
if (collapsed <= 0) {
71+
throw new Error(`baseline collapsed tree has no rows (${collapsed}) -- test setup wrong`);
72+
}
73+
if (!hasSelection) {
74+
throw new Error(`CmdExpandToCurrentPage did not select a TOC entry`);
75+
}
76+
if (after <= collapsed) {
77+
throw new Error(
78+
`CmdExpandToCurrentPage did not expand the tree to the current page ` +
79+
`(visible rows ${collapsed} -> ${after}; expected an increase)`,
80+
);
81+
}
82+
console.log(` expanded TOC to current page: ${collapsed} -> ${after} visible rows ✓`);
83+
}
84+
85+
if (import.meta.main) {
86+
const { runStandalone } = await import("./util");
87+
await runStandalone(testit);
88+
}

0 commit comments

Comments
 (0)