feat(settings): make the settings category list scrollable (#886)
* feat(settings): make the settings category list scrollable Wrap the sidebar category ListBox in a Gtk.ScrolledWindow (matching the page wrappers) so the dialog can shrink vertically without clipping categories and new pages do not pin the minimum window height. Fixes #678 * fix(settings): reveal the selected sidebar row in the scroller * fix(settings): retry sidebar scroll until the selected row is laid out --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Jatin K Malik
parent
ff108595fd
commit
9e6a03cd20
@@ -2697,7 +2697,15 @@ class SettingsDialog(Gtk.Dialog):
|
||||
for page in self._pages:
|
||||
self.sidebar_listbox.add(self._build_sidebar_row(page))
|
||||
self.sidebar_listbox.connect("row-selected", self._on_sidebar_row_selected)
|
||||
sidebar_box.pack_start(self.sidebar_listbox, True, True, 0)
|
||||
# Same treatment as the pages: without a ScrolledWindow the category
|
||||
# list's natural height pins the dialog's minimum height, so the
|
||||
# window can never shrink past the list and new categories clip.
|
||||
self.sidebar_scroller = Gtk.ScrolledWindow()
|
||||
self.sidebar_scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
self.sidebar_scroller.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
self.sidebar_scroller.add(self.sidebar_listbox)
|
||||
sidebar_scroller = self.sidebar_scroller
|
||||
sidebar_box.pack_start(sidebar_scroller, True, True, 0)
|
||||
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
main_box.pack_start(sidebar_box, False, False, 0)
|
||||
@@ -2749,6 +2757,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.navigate_to_page(self._initial_page)
|
||||
else:
|
||||
self.sidebar_listbox.select_row(self.sidebar_listbox.get_row_at_index(0))
|
||||
self._scroll_sidebar_selection_into_view()
|
||||
|
||||
# Restore the saved mode and point the simple questions at the live model
|
||||
# before the first visibility pass, so nothing flashes the wrong group.
|
||||
@@ -2773,11 +2782,50 @@ class SettingsDialog(Gtk.Dialog):
|
||||
# Navigation: sidebar, stack, and settings search
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _scroll_sidebar_selection_into_view(self) -> None:
|
||||
"""Bring the selected sidebar row into the scroller's viewport."""
|
||||
row = self.sidebar_listbox.get_selected_row()
|
||||
if row is None:
|
||||
return
|
||||
|
||||
def scroll_to_row() -> bool:
|
||||
allocation = row.get_allocation()
|
||||
if allocation.height <= 0:
|
||||
return True # not laid out yet — try again
|
||||
adjustment = self.sidebar_scroller.get_vadjustment()
|
||||
page_size = adjustment.get_page_size()
|
||||
value = adjustment.get_value()
|
||||
if allocation.y < value:
|
||||
adjustment.set_value(max(adjustment.get_lower(), allocation.y))
|
||||
elif allocation.y + allocation.height > value + page_size:
|
||||
adjustment.set_value(
|
||||
min(
|
||||
adjustment.get_upper() - page_size,
|
||||
allocation.y + allocation.height - page_size,
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
# Allocations exist only once the row is mapped, which may be after
|
||||
# one idle turn; retry briefly instead of leaving the row hidden.
|
||||
# A new selection aborts the retry for the previously chosen row.
|
||||
retries_left = 20
|
||||
|
||||
def scroll_once() -> bool:
|
||||
nonlocal retries_left
|
||||
retries_left -= 1
|
||||
if self.sidebar_listbox.get_selected_row() is not row:
|
||||
return False
|
||||
return scroll_to_row() and retries_left > 0
|
||||
|
||||
GLib.idle_add(scroll_once)
|
||||
|
||||
def navigate_to_page(self, page_name: str) -> bool:
|
||||
"""Select a settings page by its internal name (e.g. ``about``)."""
|
||||
for page in self._pages:
|
||||
if page.name == page_name and page.sidebar_row is not None:
|
||||
self.sidebar_listbox.select_row(page.sidebar_row)
|
||||
self._scroll_sidebar_selection_into_view()
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -2932,6 +2980,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self._pages[0],
|
||||
)
|
||||
self.sidebar_listbox.select_row(page.sidebar_row)
|
||||
self._scroll_sidebar_selection_into_view()
|
||||
self.settings_stack.set_visible_child_name(page.name)
|
||||
self._search_previous_page = None
|
||||
|
||||
@@ -2996,6 +3045,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
if first_match_page is not None:
|
||||
self.sidebar_listbox.select_row(first_match_page.sidebar_row)
|
||||
self._scroll_sidebar_selection_into_view()
|
||||
self.settings_stack.set_visible_child_name(first_match_page.name)
|
||||
else:
|
||||
self.sidebar_listbox.unselect_all()
|
||||
|
||||
@@ -751,9 +751,7 @@ class TestSettingsDialogHelperFunctions(unittest.TestCase):
|
||||
|
||||
def test_faster_whisper_recommendation_uses_language_for_english_variant(self):
|
||||
"""English language nudges Faster Whisper recommendations to .en variants."""
|
||||
from vocalinux.ui.settings_dialog import (
|
||||
_recommended_faster_whisper_variant_for_language,
|
||||
)
|
||||
from vocalinux.ui.settings_dialog import _recommended_faster_whisper_variant_for_language
|
||||
|
||||
self.assertEqual(
|
||||
_recommended_faster_whisper_variant_for_language(
|
||||
@@ -1162,6 +1160,20 @@ class TestSettingsNavigation(unittest.TestCase):
|
||||
self.assertIn("self.search_entry.grab_focus()", body)
|
||||
self.assertIn('"escape"', body)
|
||||
|
||||
def test_sidebar_category_list_is_scrollable(self) -> None:
|
||||
"""The category list sits in a ScrolledWindow so the dialog can shrink
|
||||
vertically without clipping categories (#678)."""
|
||||
class_body = self.source_code.split("class SettingsDialog(Gtk.Dialog):")[1]
|
||||
init_body = class_body.split("def __init__")[1].split("\n def ")[0]
|
||||
self.assertIn("sidebar_scroller = Gtk.ScrolledWindow()", init_body)
|
||||
self.assertIn(
|
||||
"sidebar_scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)",
|
||||
init_body,
|
||||
)
|
||||
self.assertIn("sidebar_scroller.set_shadow_type(Gtk.ShadowType.NONE)", init_body)
|
||||
self.assertIn("sidebar_scroller.add(self.sidebar_listbox)", init_body)
|
||||
self.assertIn("sidebar_box.pack_start(sidebar_scroller", init_body)
|
||||
|
||||
|
||||
class TestAboutPage(unittest.TestCase):
|
||||
"""About page: this app, VocaHQ family, talk-to-us. Updater stays."""
|
||||
|
||||
Reference in New Issue
Block a user