Compare commits

...

3 Commits

Author SHA1 Message Date
Anna Schumaker 70d7f5fa70 tracklist: Use the Gtk.ColumnView.scroll_to() function for scrolling
Rather than trying to implement this myself through manually moving the
scrolled window. It's much easier to simply let Gtk do the work for us.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-30 13:40:22 -04:00
Anna Schumaker 2504f4b91d sidebar: Add a timeout when selecting playlists on load
I've found that during startup, we sometimes try to select the current
playlist before the Gtk sidebar widgets are completely loaded. This
results showing the right section, but not actually selecting a
playlist. We can fix this by selecting the playlist after a short
timeout to give everything a chance to load.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-30 13:22:28 -04:00
Anna Schumaker 7358183fef sidebar: Use the Gtk.ListView.scroll_to() function for scrolling
Rather than activating an action through a GLib.Variant, we can use the
newly added scroll_to() function to do most of the work for us.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-30 11:39:52 -04:00
5 changed files with 78 additions and 74 deletions

View File

@ -1,6 +1,7 @@
# Copyright 2022 (c) Anna Schumaker.
"""A card for displaying the list of playlists."""
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gtk
from . import artist
from . import decade
@ -66,27 +67,37 @@ class Card(Gtk.Box):
if self.get_sensitive() is False:
if False not in {tbl.loaded for tbl in sql.playlist_tables()}:
self.set_sensitive(True)
self.select_playlist(sql.active_playlist)
self.select_playlist(sql.active_playlist, 150)
if len(sql.libraries) == 0:
self._libraries.extra_widget.emit("clicked")
def select_playlist(self, playlist: db.playlist.Playlist) -> None:
"""Set the current active playlist."""
def __select_playlist(self, playlist: db.playlist.Playlist) -> bool:
if playlist is not None:
match playlist.table:
case self.sql.playlists:
section = self._playlists
case self.sql.artists | self.sql.albums | self.sql.media:
section = self._artists
case self.sql.genres:
section = self._genres
case self.sql.decades | self.sql.years:
section = self._decades
case self.sql.libraries:
section = self._libraries
section.active = True
section = self.table_section(playlist.table)
if not section.active:
section.active = True
return GLib.SOURCE_CONTINUE
section.select_playlist(playlist)
return GLib.SOURCE_REMOVE
def select_playlist(self, playlist: db.playlist.Playlist,
timeout: int = 0) -> None:
"""Set the current active playlist."""
GLib.timeout_add(timeout, self.__select_playlist, playlist)
def table_section(self, table: db.playlist.Table) -> section.Section:
"""Get the Section associated with a specific Playlist Table."""
match table:
case self.sql.playlists:
return self._playlists
case self.sql.artists | self.sql.albums | self.sql.media:
return self._artists
case self.sql.genres:
return self._genres
case self.sql.decades | self.sql.years:
return self._decades
case self.sql.libraries:
return self._libraries
@property
def accelerators(self) -> list[ActionEntry]:

View File

@ -2,7 +2,6 @@
"""A sidebar Header attached to a hidden ListView for selecting playlists."""
import typing
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gtk
from .. import db
from .. import factory
@ -86,9 +85,7 @@ class Section(header.Header):
def select_playlist(self, playlist: db.playlist.Playlist) -> None:
"""Select the requested playlist."""
if (index := self.playlist_index(playlist)) is not None:
self._selection.select_item(index, True)
self._listview.activate_action("list.scroll-to-item",
GLib.Variant.new_uint32(index))
self._listview.scroll_to(index, Gtk.ListScrollFlags.SELECT)
@GObject.Signal(arg_types=(db.playlist.Playlist,))
def playlist_activated(self, playlist: db.playlist.Playlist):

View File

@ -81,13 +81,9 @@ class TrackView(Gtk.ScrolledWindow):
def scroll_to_track(self, track: db.tracks.Track) -> None:
"""Scroll to the requested Track."""
# This is a workaround until the ColumnView has better scrolling
# support, which seems to be targeted for Gtk 4.10.
adjustment = self._scrollwin.get_vadjustment()
for (i, t) in enumerate(self._selection):
if t == track:
pos = max(i - 3, 0) * adjustment.get_upper()
adjustment.set_value(pos / self._selection.get_n_items())
for i in range(self._selection.props.n_items):
if self._selection[i] == track:
self._columnview.scroll_to(i, None, Gtk.ListScrollFlags.NONE)
@GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel:

View File

@ -4,7 +4,6 @@ import emmental.db
import emmental.sidebar.section
import tests.util
import unittest.mock
from gi.repository import GLib
from gi.repository import Gtk
@ -105,18 +104,12 @@ class TestSection(tests.util.TestCase):
def test_select_playlist(self):
"""Test selecting a specific playlist."""
self.section.do_get_subtitle = unittest.mock.Mock(return_value="")
playlist_selected = unittest.mock.Mock()
self.section.connect("playlist-selected", playlist_selected)
playlist = self.table.create("Test Playlist")
playlist_selected.assert_not_called()
with unittest.mock.patch.object(self.section._listview,
"activate_action") as mock_action:
"scroll_to") as mock_scroll_to:
self.section.select_playlist(playlist)
playlist_selected.assert_called_with(self.section, playlist)
mock_action.assert_called_with("list.scroll-to-item",
GLib.Variant.new_uint32(0))
mock_scroll_to.assert_called_with(0, Gtk.ListScrollFlags.SELECT)
def test_playlist_selected(self):
"""Test selecting a playlist in the list."""

View File

@ -3,6 +3,7 @@
import emmental.sidebar
import tests.util
import unittest.mock
from gi.repository import GLib
from gi.repository import Gtk
@ -73,10 +74,13 @@ class TestSidebar(tests.util.TestCase):
self.assertFalse(self.sidebar.get_sensitive())
self.sidebar.select_playlist.assert_not_called()
self.sidebar._libraries.extra_widget.emit.assert_not_called()
self.sql.emit("table-loaded", table)
self.assertTrue(self.sidebar.get_sensitive())
self.sidebar.select_playlist.assert_called()
table.load(now=True)
self.assertEqual(self.sidebar.get_sensitive(),
table == tables[-1])
playlist = self.sql.playlists.collection
self.sidebar.select_playlist.assert_called_with(playlist, 150)
self.sidebar._libraries.extra_widget.emit.assert_called_with("clicked")
self.sidebar.select_playlist.reset_mock()
@ -147,45 +151,48 @@ class TestSidebar(tests.util.TestCase):
def test_select_playlist(self):
"""Test setting the active playlist."""
self.assertEqual(self.sidebar._Card__select_playlist(None),
GLib.SOURCE_REMOVE)
playlist = self.sql.playlists.create("Test Playlist")
self.sidebar.select_playlist(playlist)
self.assertTrue(self.sidebar._playlists.active)
self.assertEqual(self.sidebar.selected_playlist, playlist)
with unittest.mock.patch.object(self.sidebar._playlists,
"select_playlist") as mock_select:
self.assertEqual(self.sidebar._Card__select_playlist(playlist),
GLib.SOURCE_CONTINUE)
self.assertTrue(self.sidebar._playlists.active)
mock_select.assert_not_called()
artist = self.sql.artists.create("Test Artist")
album = self.sql.albums.create("Test Album", "Test Artist", "2023")
medium = self.sql.media.create(album, "Test Medium", number=1)
self.assertEqual(self.sidebar._Card__select_playlist(playlist),
GLib.SOURCE_REMOVE)
mock_select.assert_called_with(playlist)
self.sidebar._artists.select_playlist = unittest.mock.Mock()
for plist in [artist, album, medium]:
self.sidebar._artists.select_playlist.reset_mock()
self.sidebar._artists.active = False
with unittest.mock.patch.object(GLib, "timeout_add") as mock_to:
self.sidebar.select_playlist(playlist)
mock_to.assert_called_with(0, self.sidebar._Card__select_playlist,
playlist)
self.sidebar.select_playlist(playlist, 42)
mock_to.assert_called_with(42, self.sidebar._Card__select_playlist,
playlist)
self.sidebar.select_playlist(plist)
self.assertTrue(self.sidebar._artists.active)
self.sidebar._artists.select_playlist.assert_called_with(plist)
genre = self.sql.genres.create("Test Genre")
self.sidebar.select_playlist(genre)
self.assertTrue(self.sidebar._genres.active)
self.assertEqual(self.sidebar.selected_playlist, genre)
decade = self.sql.decades.create(1990)
year = self.sql.years.create(1990)
self.sidebar._decades.select_playlist = unittest.mock.Mock()
for plist in [decade, year]:
self.sidebar._decades.select_playlist.reset_mock()
self.sidebar._decades.active = False
self.sidebar.select_playlist(plist)
self.assertTrue(self.sidebar._decades.active)
self.sidebar._decades.select_playlist.assert_called_with(plist)
library = self.sql.libraries.create("/a/b/c")
self.sidebar.select_playlist(library)
self.assertTrue(self.sidebar._libraries.active)
self.assertEqual(self.sidebar.selected_playlist, library)
def test_table_section(self):
"""Test converting a Playlist database table into a Section."""
self.assertEqual(self.sidebar.table_section(self.sql.playlists),
self.sidebar._playlists)
self.assertEqual(self.sidebar.table_section(self.sql.artists),
self.sidebar._artists)
self.assertEqual(self.sidebar.table_section(self.sql.albums),
self.sidebar._artists)
self.assertEqual(self.sidebar.table_section(self.sql.media),
self.sidebar._artists)
self.assertEqual(self.sidebar.table_section(self.sql.genres),
self.sidebar._genres)
self.assertEqual(self.sidebar.table_section(self.sql.decades),
self.sidebar._decades)
self.assertEqual(self.sidebar.table_section(self.sql.years),
self.sidebar._decades)
self.assertEqual(self.sidebar.table_section(self.sql.libraries),
self.sidebar._libraries)
self.assertIsNone(self.sidebar.table_section(None))
def test_accelerators(self):
"""Check that the accelerators list is set up properly."""