sidebar: Create a LibraryRow

This is a preconfigured row for displaying library paths. It includes a
switch to enable & disable the path, buttons for removing and updating a
path, and a progress bar for displaying scan progress.

I use the "update-symbolic" and "stop-sign-large-symbolic" icons from
the gnome icon library for this widget.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-08-11 11:27:37 -04:00
parent dcd63015b8
commit 70799fa50f
5 changed files with 203 additions and 0 deletions

View File

@ -60,3 +60,7 @@ image.emmental-sidebar-arrow:checked {
button.emmental-delete>image {
color: @destructive_color;
}
button.emmental-stop>image {
color: @red_3;
}

View File

@ -93,3 +93,65 @@ class PlaylistRow(BaseRow):
def __on_delete(self, button: Gtk.Button) -> None:
if self.playlist is not None:
self.playlist.delete()
class LibraryRow(BaseRow):
"""An advaced playlist row with extra actions for library management."""
enabled = GObject.Property(type=bool, default=True)
online = GObject.Property(type=bool, default=True)
scanning = GObject.Property(type=bool, default=False)
progress = GObject.Property(type=float)
def __init__(self, **kwargs):
"""Initialize a LibraryRow."""
super().__init__(**kwargs)
self._box = Gtk.Box()
self._overlay = Gtk.Overlay(child=self._box)
self._switch = Gtk.Switch(active=self.enabled, valign=Gtk.Align.CENTER)
self._title = PlaylistTitle(margin_start=12, margin_end=12)
self._scan = Gtk.Button(icon_name="update", has_frame=False,
valign=Gtk.Align.CENTER)
self._stop = Gtk.Button(icon_name="stop-sign-large", has_frame=False,
valign=Gtk.Align.CENTER, visible=False)
self._delete = Gtk.Button(icon_name="big-x-symbolic",
valign=Gtk.Align.CENTER, has_frame=False)
self._progress = Gtk.ProgressBar(valign=Gtk.Align.END, visible=False)
self.bind_property("enabled", self._switch, "active",
GObject.BindingFlags.BIDIRECTIONAL)
self.bind_property("online", self._switch, "sensitive")
self.bind_property("name", self._title, "title")
self.bind_property("count", self._title, "count")
self.bind_property("online", self._title, "sensitive")
self.bind_property("enabled", self._title, "sensitive")
self.bind_property("scanning", self._stop, "visible")
self.bind_property("scanning", self._scan, "visible",
GObject.BindingFlags.INVERT_BOOLEAN)
self.bind_property("scanning", self._progress, "visible")
self.bind_property("progress", self._progress, "fraction")
self._delete.connect("clicked", self.__on_button_press, "delete")
self._scan.connect("clicked", self.__on_button_press, "scan")
self._stop.connect("clicked", self.__on_button_press, "stop")
self._delete.add_css_class("emmental-delete")
self._stop.add_css_class("emmental-stop")
self._progress.add_css_class("osd")
self._box.append(self._switch)
self._box.append(self._title)
self._box.append(self._scan)
self._box.append(self._stop)
self._box.append(self._delete)
self._overlay.add_overlay(self._progress)
self.append(self._overlay)
def __on_button_press(self, button: Gtk.Button, action: str) -> None:
if self.playlist is not None:
match action:
case "delete": self.playlist.delete()
case "scan": self.playlist.scan()
case "stop": self.playlist.stop()

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 4.921875 -0.0078125 l -0.292969 0.2890625 l -4.6210935 4.582031 v 6.167969 l 4.9101565 4.953125 h 6.167969 l 4.910156 -4.953125 v -6.167969 l -4.914063 -4.8710935 z m 0.820313 2.0000005 h 4.519531 l 3.734375 3.707031 v 4.507812 l -3.742188 3.777344 h -4.503906 l -3.742188 -3.777344 v -4.507812 z m 0 0"/><path d="m 6.484375 4.015625 l -2.457031 2.433594 v 3.03125 l 2.457031 2.476562 h 3.03125 l 2.457031 -2.476562 v -3.03125 l -2.457031 -2.433594 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 625 B

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 7.957031 2 c -0.082031 0 -0.164062 0.003906 -0.246093 0.007812 c -0.1875 0.011719 -0.375 0.03125 -0.5625 0.0625 c -1.582032 0.226563 -3.007813 1.070313 -3.96875 2.34375 c -0.804688 1.074219 -1.183594 2.332032 -1.179688 3.585938 h 2.003906 c 0 -0.832031 0.253906 -1.671875 0.796875 -2.398438 c 1.335938 -1.777343 3.820313 -2.113281 5.597657 -0.78125 c 0.429687 0.320313 0.769531 0.734376 1.03125 1.1875 h -1.4375 c -0.550782 0 -1 0.449219 -1 1 v 1 h 6 v -6 h -1 c -0.550782 0 -1 0.449219 -1 1 v 1.6875 c -1.113282 -1.695312 -3.007813 -2.710937 -5.039063 -2.695312 z m 0 0"/><path d="m 8.035156 15.007812 c 0.082032 0 0.164063 -0.003906 0.246094 -0.007812 c 0.1875 -0.011719 0.375 -0.03125 0.5625 -0.0625 c 1.582031 -0.226562 3.007812 -1.066406 3.96875 -2.34375 c 0.804688 -1.074219 1.183594 -2.332031 1.179688 -3.585938 h -2.003907 c -0.003906 0.832032 -0.257812 1.675782 -0.796875 2.398438 c -1.335937 1.777344 -3.820312 2.113281 -5.597656 0.78125 c -0.429688 -0.320312 -0.769531 -0.734375 -1.03125 -1.1875 h 1.4375 c 0.550781 0 1 -0.449219 1 -1 v -1 h -6 v 6 h 1 c 0.550781 0 1 -0.449219 1 -1 v -1.6875 c 1.113281 1.695312 3.007812 2.710938 5.035156 2.695312 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -177,3 +177,136 @@ class TestPlaylistRow(unittest.TestCase):
self.row.playlist = self.playlist
self.row._title.emit("request-rename", "New Name")
self.playlist.rename.assert_called_with("New Name")
class TestLibraryRow(unittest.TestCase):
"""Tests our library row with extra widgets."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.row = emmental.sidebar.row.LibraryRow()
self.playlist = emmental.db.playlist.Playlist(Gio.ListStore(),
propertyid=12345,
name="Test Playlist")
def test_init(self):
"""Test that the library row is configured properly."""
self.assertIsInstance(self.row, emmental.sidebar.row.BaseRow)
self.assertIsInstance(self.row._box, Gtk.Box)
self.assertIsInstance(self.row._overlay, Gtk.Overlay)
self.assertEqual(self.row.get_first_child(), self.row._overlay)
self.assertEqual(self.row._overlay.get_child(), self.row._box)
def test_switch(self):
"""Test the switch widget and enabled property."""
self.assertIsInstance(self.row._switch, Gtk.Switch)
self.assertEqual(self.row._switch.get_valign(), Gtk.Align.CENTER)
self.assertEqual(self.row._box.get_first_child(), self.row._switch)
self.assertTrue(self.row.enabled)
self.assertTrue(self.row._switch.get_active())
self.assertTrue(self.row._title.get_sensitive())
self.row.enabled = False
self.assertFalse(self.row._switch.get_active())
self.assertFalse(self.row._title.get_sensitive())
self.row._switch.set_active(True)
self.assertTrue(self.row.enabled)
self.assertTrue(self.row._title.get_sensitive())
def test_progress(self):
"""Test the progress bar widget and property."""
self.assertIsInstance(self.row._progress, Gtk.ProgressBar)
self.assertEqual(self.row._progress.get_valign(), Gtk.Align.END)
self.assertTrue(self.row._progress.has_css_class("osd"))
self.assertIn(self.row._progress, self.row._overlay)
self.assertEqual(self.row.progress, 0.0)
self.row.progress = 0.42
self.assertEqual(self.row._progress.get_fraction(), 0.42)
def test_title(self):
"""Test the title widget and properties."""
self.assertIsInstance(self.row._title,
emmental.sidebar.title.PlaylistTitle)
self.assertEqual(self.row._title.get_margin_start(), 12)
self.assertEqual(self.row._title.get_margin_end(), 12)
self.assertEqual(self.row._switch.get_next_sibling(), self.row._title)
self.row.name = "/a/b/c"
self.row.count = 42
self.assertEqual(self.row._title.title, "/a/b/c")
self.assertEqual(self.row._title.count, 42)
def test_scan(self):
"""Test the scan button."""
self.assertIsInstance(self.row._scan, Gtk.Button)
self.assertEqual(self.row._scan.get_icon_name(), "update")
self.assertEqual(self.row._scan.get_valign(), Gtk.Align.CENTER)
self.assertFalse(self.row._scan.get_has_frame())
self.assertEqual(self.row._title.get_next_sibling(), self.row._scan)
self.row._scan.emit("clicked")
self.playlist.scan = unittest.mock.Mock()
self.row.playlist = self.playlist
self.row._scan.emit("clicked")
self.playlist.scan.assert_called()
def test_stop(self):
"""Test the stop button."""
self.assertIsInstance(self.row._stop, Gtk.Button)
self.assertEqual(self.row._stop.get_icon_name(), "stop-sign-large")
self.assertEqual(self.row._stop.get_valign(), Gtk.Align.CENTER)
self.assertFalse(self.row._stop.get_has_frame())
self.assertTrue(self.row._stop.has_css_class("emmental-stop"))
self.assertEqual(self.row._scan.get_next_sibling(), self.row._stop)
self.row._stop.emit("clicked")
self.playlist.stop = unittest.mock.Mock()
self.row.playlist = self.playlist
self.row._stop.emit("clicked")
self.playlist.stop.assert_called()
def test_delete(self):
"""Test the delete button."""
self.assertIsInstance(self.row._delete, Gtk.Button)
self.assertEqual(self.row._delete.get_icon_name(), "big-x-symbolic")
self.assertEqual(self.row._delete.get_valign(), Gtk.Align.CENTER)
self.assertFalse(self.row._delete.get_has_frame())
self.assertTrue(self.row._delete.has_css_class("emmental-delete"))
self.assertEqual(self.row._stop.get_next_sibling(), self.row._delete)
self.row._delete.emit("clicked")
self.playlist.delete = unittest.mock.Mock()
self.row.playlist = self.playlist
self.row._delete.emit("clicked")
self.playlist.delete.assert_called()
def test_online(self):
"""Test the online property."""
self.assertTrue(self.row.online)
self.assertTrue(self.row._switch.get_sensitive())
self.assertTrue(self.row._title.get_sensitive())
self.row.online = False
self.assertFalse(self.row._switch.get_sensitive())
self.assertFalse(self.row._title.get_sensitive())
def test_scaning(self):
"""Test the scanning property."""
self.assertFalse(self.row.scanning)
self.assertFalse(self.row._progress.get_visible())
self.assertFalse(self.row._stop.get_visible())
self.assertTrue(self.row._scan.get_visible())
self.row.scanning = True
self.assertTrue(self.row._progress.get_visible())
self.assertTrue(self.row._stop.get_visible())
self.assertFalse(self.row._scan.get_visible())