From dcd63015b89a75abf026cdea3c5bc16fd9f6f67f Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Mon, 12 Sep 2022 14:03:00 -0400 Subject: [PATCH] sidebar: Create a PlaylistRow This row is configured for showing user and system playlists. This means we use a SettableIcon, and EditableTitle, and an extra button for deleting playlists. I use the big-x-symbolic icon from the gnome icon library for deleting playlists. Signed-off-by: Anna Schumaker --- emmental/emmental.css | 4 ++ emmental/sidebar/row.py | 48 +++++++++++++ icons/scalable/actions/big-x-symbolic.svg | 2 + tests/sidebar/test_row.py | 84 +++++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 icons/scalable/actions/big-x-symbolic.svg diff --git a/emmental/emmental.css b/emmental/emmental.css index 8e80582..ee9beb0 100644 --- a/emmental/emmental.css +++ b/emmental/emmental.css @@ -56,3 +56,7 @@ image.emmental-sidebar-arrow:checked { transform: rotate(-180deg); color: @accent_color; } + +button.emmental-delete>image { + color: @destructive_color; +} diff --git a/emmental/sidebar/row.py b/emmental/sidebar/row.py index 198380d..e1574f1 100644 --- a/emmental/sidebar/row.py +++ b/emmental/sidebar/row.py @@ -3,7 +3,9 @@ from gi.repository import GObject from gi.repository import Gtk from .title import PlaylistTitle +from .title import EditableTitle from .icon import Icon +from .icon import Settable from .. import db @@ -45,3 +47,49 @@ class Row(BaseRow): digits = str(year) self._year = year self._icon.text = f"{digits[-2]} {digits[-1]}" + + +class PlaylistRow(BaseRow): + """An advanced playlist row with extra actions for users.""" + + icon_name = GObject.Property(type=str) + filepath = GObject.Property(type=GObject.TYPE_PYOBJECT) + modifiable = GObject.Property(type=bool, default=False) + + def __init__(self, **kwargs): + """Initialize a PlaylistRow.""" + super().__init__(**kwargs) + self._icon = Settable() + self._title = EditableTitle(margin_start=12, margin_end=12) + self._delete = Gtk.Button(icon_name="big-x-symbolic", + valign=Gtk.Align.CENTER, + has_frame=False, visible=False) + + self.bind_property("name", self._icon, "text") + self.bind_property("icon-name", self._icon, "icon-name") + self.bind_property("filepath", self._icon, "filepath", + GObject.BindingFlags.BIDIRECTIONAL) + self.bind_property("modifiable", self._icon, "settable") + + self.bind_property("name", self._title, "title") + self.bind_property("count", self._title, "count") + self.bind_property("modifiable", self._title, "editable") + + self.bind_property("modifiable", self._delete, "visible") + + self._title.connect("request-rename", self.__on_rename) + self._delete.connect("clicked", self.__on_delete) + + self._delete.add_css_class("emmental-delete") + + self.append(self._icon) + self.append(self._title) + self.append(self._delete) + + def __on_rename(self, title: EditableTitle, new_name: str) -> None: + if self.playlist is not None: + self.playlist.rename(new_name) + + def __on_delete(self, button: Gtk.Button) -> None: + if self.playlist is not None: + self.playlist.delete() diff --git a/icons/scalable/actions/big-x-symbolic.svg b/icons/scalable/actions/big-x-symbolic.svg new file mode 100644 index 0000000..3b9f2b9 --- /dev/null +++ b/icons/scalable/actions/big-x-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/tests/sidebar/test_row.py b/tests/sidebar/test_row.py index b2cfcf6..1eecfec 100644 --- a/tests/sidebar/test_row.py +++ b/tests/sidebar/test_row.py @@ -93,3 +93,87 @@ class TestRow(unittest.TestCase): self.assertEqual(self.row._icon.text, "8 8") self.row.year = 2000 self.assertEqual(self.row._icon.text, "0 0") + + +class TestPlaylistRow(unittest.TestCase): + """Test our playlist row with extra buttons.""" + + def setUp(self): + """Set up common variables.""" + self.row = emmental.sidebar.row.PlaylistRow() + self.playlist = emmental.db.playlists.Playlist(table=Gio.ListStore(), + propertyid=12345, + playlistid=67890, + name="Test Playlist") + + def test_init(self): + """Test that the playlist row is configured properly.""" + self.assertIsInstance(self.row, emmental.sidebar.row.BaseRow) + + def test_icon(self): + """Test the settable icon widget and properties.""" + self.assertIsInstance(self.row._icon, emmental.sidebar.icon.Settable) + self.assertEqual(self.row.get_first_child(), self.row._icon) + + self.row.name = "Test Playlist" + self.row.icon_name = "image-missing" + self.row.filepath = tests.util.COVER_JPG + + self.assertEqual(self.row._icon.text, "Test Playlist") + self.assertEqual(self.row._icon.icon_name, "image-missing") + self.assertEqual(self.row._icon.filepath, tests.util.COVER_JPG) + + self.row._icon.filepath = None + self.assertIsNone(self.row.filepath) + + def test_title(self): + """Test the editable title widget and properties.""" + self.assertIsInstance(self.row._title, + emmental.sidebar.title.EditableTitle) + self.assertEqual(self.row._icon.get_next_sibling(), self.row._title) + + self.assertEqual(self.row._title.get_margin_start(), 12) + self.assertEqual(self.row._title.get_margin_end(), 12) + + self.row.name = "Test Playlist" + self.row.count = 42 + self.assertEqual(self.row._title.title, "Test Playlist") + self.assertEqual(self.row._title.count, 42) + + def test_delete(self): + """Test the delete button.""" + self.assertIsInstance(self.row._delete, Gtk.Button) + self.assertEqual(self.row._title.get_next_sibling(), self.row._delete) + + 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.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_modifiable(self): + """Test the modifiable property.""" + self.assertFalse(self.row.modifiable) + self.assertFalse(self.row._icon.settable) + self.assertFalse(self.row._title.editable) + self.assertFalse(self.row._delete.get_visible()) + + self.row.modifiable = True + self.assertTrue(self.row._icon.settable) + self.assertTrue(self.row._title.editable) + self.assertTrue(self.row._delete.get_visible()) + + def test_rename(self): + """Test responding to the request-rename signal.""" + self.row._title.emit("request-rename", "No Error Expected") + + self.playlist.rename = unittest.mock.Mock() + self.row.playlist = self.playlist + self.row._title.emit("request-rename", "New Name") + self.playlist.rename.assert_called_with("New Name")