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

View File

@ -2,7 +2,6 @@
"""A sidebar Header attached to a hidden ListView for selecting playlists.""" """A sidebar Header attached to a hidden ListView for selecting playlists."""
import typing import typing
from gi.repository import GObject from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gtk from gi.repository import Gtk
from .. import db from .. import db
from .. import factory from .. import factory
@ -86,9 +85,7 @@ class Section(header.Header):
def select_playlist(self, playlist: db.playlist.Playlist) -> None: def select_playlist(self, playlist: db.playlist.Playlist) -> None:
"""Select the requested playlist.""" """Select the requested playlist."""
if (index := self.playlist_index(playlist)) is not None: if (index := self.playlist_index(playlist)) is not None:
self._selection.select_item(index, True) self._listview.scroll_to(index, Gtk.ListScrollFlags.SELECT)
self._listview.activate_action("list.scroll-to-item",
GLib.Variant.new_uint32(index))
@GObject.Signal(arg_types=(db.playlist.Playlist,)) @GObject.Signal(arg_types=(db.playlist.Playlist,))
def playlist_activated(self, playlist: 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: def scroll_to_track(self, track: db.tracks.Track) -> None:
"""Scroll to the requested Track.""" """Scroll to the requested Track."""
# This is a workaround until the ColumnView has better scrolling for i in range(self._selection.props.n_items):
# support, which seems to be targeted for Gtk 4.10. if self._selection[i] == track:
adjustment = self._scrollwin.get_vadjustment() self._columnview.scroll_to(i, None, Gtk.ListScrollFlags.NONE)
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())
@GObject.Property(type=Gio.ListModel) @GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel: def columns(self) -> Gio.ListModel:

View File

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

View File

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