From 60bafb14eeedd4e897bfe2fcd34e7db23d0dd3f0 Mon Sep 17 00:00:00 2001 From: httpsrichard <33163707+httpsrichard@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:40:41 +0300 Subject: [PATCH] Add scroll margin to TOC keyboard navigation (toc_scroll_margin) --- pdf_viewer/config.cpp | 2 ++ pdf_viewer/prefs.config | 5 +++++ pdf_viewer/ui.h | 48 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pdf_viewer/config.cpp b/pdf_viewer/config.cpp index a76c8d689..e188e5bc1 100644 --- a/pdf_viewer/config.cpp +++ b/pdf_viewer/config.cpp @@ -199,6 +199,7 @@ int SINGLE_MAIN_WINDOW_MOVE[2] = { -1, -1 }; bool ENABLE_EXPERIMENTAL_FEATURES = false; bool CREATE_TABLE_OF_CONTENTS_IF_NOT_EXISTS = true; int MAX_CREATED_TABLE_OF_CONTENTS_SIZE = 5000; +int TOC_SCROLL_MARGIN = 3; bool FORCE_CUSTOM_LINE_ALGORITHM = false; float OVERVIEW_SIZE[2] = { 0.8f, 0.4f }; float OVERVIEW_OFFSET[2] = { 0.0f, 0.0f }; @@ -1130,6 +1131,7 @@ ConfigManager::ConfigManager(const Path& default_path, const Path& auto_path, co add_int(L"status_bar_font_size", &STATUS_BAR_FONT_SIZE, IntExtras{1, 100}); add_int(L"text_summary_context_size", &TEXT_SUMMARY_CONTEXT_SIZE, IntExtras{1, 100}); add_int(L"max_created_toc_size", &MAX_CREATED_TABLE_OF_CONTENTS_SIZE, IntExtras{1, 100000}); + add_int(L"toc_scroll_margin", &TOC_SCROLL_MARGIN, IntExtras{0, 100}); add_int(L"prerendered_page_count", &PRERENDERED_PAGE_COUNT, IntExtras{0, 10}); add_int(L"reload_interval_miliseconds", &RELOAD_INTERVAL_MILISECONDS, IntExtras{0, 10000}); add_ivec2(L"main_window_size", MAIN_WINDOW_SIZE); diff --git a/pdf_viewer/prefs.config b/pdf_viewer/prefs.config index 661ec549a..9302104ef 100644 --- a/pdf_viewer/prefs.config +++ b/pdf_viewer/prefs.config @@ -179,6 +179,11 @@ create_table_of_contents_if_not_exists 1 # Limits the maximum size of created table of contents max_created_toc_size 5000 +# Number of rows of context to keep visible above/below the current selection +# when navigating the table of contents with the keyboard. Set to 0 to restore +# the old behavior (scroll the bare minimum needed to keep the selection visible). +toc_scroll_margin 3 + # Warn the user on the command line only when redefining keys inside # the same file. When set to 1, sioyek will warn when redefining keys # from other files also diff --git a/pdf_viewer/ui.h b/pdf_viewer/ui.h index 2ab1f08b0..e1527ec39 100644 --- a/pdf_viewer/ui.h +++ b/pdf_viewer/ui.h @@ -79,6 +79,7 @@ const int max_select_size = 100; extern bool SMALL_TOC; extern bool MULTILINE_MENUS; extern bool TOUCH_MODE; +extern int TOC_SCROLL_MARGIN; class HierarchialSortFilterProxyModel : public QSortFilterProxyModel { @@ -173,6 +174,51 @@ class BaseSelectorWidget : public QWidget { }; +// QTreeView (and QAbstractItemView in general) only scrolls the bare minimum +// needed to bring the current row into view (ScrollHint::EnsureVisible with no +// margin). That means keyboard navigation can push the selection all the way to +// the last visible row with no look-ahead, and when reversing direction the +// viewport doesn't budge until the selection reaches the edge of whatever is +// already on screen. +// +// This subclass keeps exactly `TOC_SCROLL_MARGIN` rows of context visible +// above/below the current row at all times, by additionally requesting +// visibility for an index that many steps above/below the target before +// scrolling to the target itself. Because this re-asserts the same fixed +// margin on every single step, continuous movement toward an edge ends up +// scrolling the view exactly one row per keypress -- the selection stays +// pinned at a fixed screen offset while the list moves underneath it. The +// margin size itself never varies, unlike a recentering/chunked approach. +class SioyekTreeView : public QTreeView { +public: + using QTreeView::QTreeView; + +protected: + QModelIndex step_index(const QModelIndex& from, int n, bool down) const { + QModelIndex cur = from; + for (int i = 0; i < n; i++) { + QModelIndex next = down ? indexBelow(cur) : indexAbove(cur); + if (!next.isValid()) break; + cur = next; + } + return cur; + } + + void scrollTo(const QModelIndex& index, ScrollHint hint = EnsureVisible) override { + if (!index.isValid() || TOC_SCROLL_MARGIN <= 0) { + QTreeView::scrollTo(index, hint); + return; + } + + QModelIndex above = step_index(index, TOC_SCROLL_MARGIN, false); + QModelIndex below = step_index(index, TOC_SCROLL_MARGIN, true); + + QTreeView::scrollTo(below, EnsureVisible); + QTreeView::scrollTo(above, EnsureVisible); + QTreeView::scrollTo(index, hint); + } +}; + template class FilteredTreeSelect : public BaseSelectorWidget { private: @@ -189,7 +235,7 @@ class FilteredTreeSelect : public BaseSelectorWidget { FilteredTreeSelect(bool fuzzy, QStandardItemModel* item_model, std::function&)> on_done, MainWidget* parent, - std::vector selected_index) : BaseSelectorWidget(new QTreeView(), fuzzy, item_model, parent), + std::vector selected_index) : BaseSelectorWidget(new SioyekTreeView(), fuzzy, item_model, parent), on_done(on_done) { auto index = QModelIndex();