From b25ca24dc30f2e358104b3a369977797642042bf Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Sat, 6 Aug 2022 16:39:25 -0400 Subject: [PATCH] sidebar: Create a Header class This will be used to display different types of playlists in the sidebar, such as artist or genre. It also has a revealer that shows its child when the header is active. Signed-off-by: Anna Schumaker --- emmental/emmental.css | 11 +++ emmental/sidebar/header.py | 81 +++++++++++++++++ tests/sidebar/test_header.py | 167 +++++++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 emmental/sidebar/header.py create mode 100644 tests/sidebar/test_header.py diff --git a/emmental/emmental.css b/emmental/emmental.css index dac99ba..8e80582 100644 --- a/emmental/emmental.css +++ b/emmental/emmental.css @@ -45,3 +45,14 @@ listview > row:checked:selected:hover { listview > row:checked:selected:active { background-color: alpha(@accent_color, 0.39); } + +image.emmental-sidebar-arrow { + transition: 250ms; + transform: rotate(0deg); +} + +image.emmental-sidebar-arrow:checked { + transition: 250ms; + transform: rotate(-180deg); + color: @accent_color; +} diff --git a/emmental/sidebar/header.py b/emmental/sidebar/header.py new file mode 100644 index 0000000..6b0ab3d --- /dev/null +++ b/emmental/sidebar/header.py @@ -0,0 +1,81 @@ +# Copyright 2022 (c) Anna Schumaker. +"""A sidebar Section Header widget.""" +from gi.repository import GObject +from gi.repository import Gtk +from gi.repository import Adw +from .title import Title + + +class Header(Gtk.Box): + """The Section Header.""" + + icon_name = GObject.Property(type=str) + title = GObject.Property(type=str) + subtitle = GObject.Property(type=str) + pending = GObject.Property(type=bool, default=False) + progress = GObject.Property(type=float) + + active = GObject.Property(type=bool, default=False) + extra_widget = GObject.Property(type=Gtk.Widget) + reveal_widget = GObject.Property(type=Gtk.Widget) + animation = GObject.Property(type=Gtk.RevealerTransitionType, + default=Gtk.RevealerTransitionType.SLIDE_UP) + + def __init__(self, icon_name: str = "image-missing", + title: str = "", subtitle: str = "", + extra_widget: Gtk.Widget = None, **kwargs): + """Initialize a row Header widget.""" + super().__init__(icon_name=icon_name, extra_widget=extra_widget, + title=title, subtitle=subtitle, + orientation=Gtk.Orientation.VERTICAL, **kwargs) + self._overlay = Gtk.Overlay(css_name="button") + self._box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=12) + self._icon = Gtk.Image(icon_name=self.icon_name, + icon_size=Gtk.IconSize.LARGE) + self._title = Title(title=self.title, subtitle=self.subtitle) + self._extra = Adw.Bin(child=self.extra_widget, valign=Gtk.Align.CENTER) + self._arrow = Gtk.Image(icon_name="go-down-symbolic") + self._progress = Gtk.ProgressBar(valign=Gtk.Align.END, + visible=self.pending) + self._revealer = Gtk.Revealer(transition_type=self.animation) + self._clicked = Gtk.GestureClick() + + self._arrow.add_css_class("emmental-sidebar-arrow") + self._progress.add_css_class("osd") + + self.bind_property("icon-name", self._icon, "icon-name") + self.bind_property("title", self._title, "title") + self.bind_property("subtitle", self._title, "subtitle") + self.bind_property("extra-widget", self._extra, "child") + self.bind_property("pending", self._progress, "visible") + self.bind_property("progress", self._progress, "fraction") + + self.bind_property("active", self._revealer, "reveal-child") + self.bind_property("active", self._revealer, "vexpand") + self.bind_property("reveal-widget", self._revealer, "child") + self.bind_property("animation", self._revealer, "transition-type") + + self._clicked.connect("released", self.__clicked) + self.connect("notify::active", self.__notify_active) + + self._box.append(self._icon) + self._box.append(self._title) + self._box.append(self._extra) + self._box.append(self._arrow) + + self._overlay.set_child(self._box) + self._overlay.add_overlay(self._progress) + self._overlay.add_controller(self._clicked) + + self.append(self._overlay) + self.append(self._revealer) + + def __clicked(self, gesture: Gtk.GestureClick, n_press: int, + x: int, y: int) -> None: + self.active = True + + def __notify_active(self, header, param) -> None: + if self.active: + self._arrow.set_state_flags(Gtk.StateFlags.CHECKED, False) + else: + self._arrow.unset_state_flags(Gtk.StateFlags.CHECKED) diff --git a/tests/sidebar/test_header.py b/tests/sidebar/test_header.py new file mode 100644 index 0000000..117c667 --- /dev/null +++ b/tests/sidebar/test_header.py @@ -0,0 +1,167 @@ +# Copyright 2022 (c) Anna Schumaker. +"""Tests our sidebar Section Header widget.""" +import unittest +import emmental.sidebar.header +from gi.repository import Gtk +from gi.repository import Adw + + +class TestHeader(unittest.TestCase): + """Tests the Section Header.""" + + def setUp(self): + """Set up common variables.""" + self.header = emmental.sidebar.header.Header() + + def test_init(self): + """Test that the Header is set up properly.""" + self.assertIsInstance(self.header, Gtk.Box) + self.assertIsInstance(self.header._overlay, Gtk.Overlay) + + self.assertEqual(self.header.get_orientation(), + Gtk.Orientation.VERTICAL) + + self.assertIsInstance(self.header._box, Gtk.Box) + self.assertEqual(self.header._box.get_spacing(), 12) + self.assertEqual(self.header._box.get_orientation(), + Gtk.Orientation.HORIZONTAL) + + self.assertEqual(self.header.get_first_child(), self.header._overlay) + self.assertEqual(self.header._overlay.get_child(), self.header._box) + + self.assertEqual(self.header._overlay.get_css_name(), "button") + + def test_icon(self): + """Test the icon widget and icon-name property.""" + self.assertIsInstance(self.header._icon, Gtk.Image) + self.assertEqual(self.header._box.get_first_child(), self.header._icon) + + self.assertEqual(self.header.icon_name, "image-missing") + self.assertEqual(self.header._icon.get_icon_name(), "image-missing") + self.assertEqual(self.header._icon.get_icon_size(), Gtk.IconSize.LARGE) + + self.header.icon_name = "audio-x-generic" + self.assertEqual(self.header._icon.get_icon_name(), "audio-x-generic") + + header2 = emmental.sidebar.header.Header(icon_name="audio-x-generic") + self.assertEqual(header2._icon.get_icon_name(), "audio-x-generic") + + def test_title(self): + """Test the title widget and property.""" + self.assertIsInstance(self.header._title, emmental.sidebar.title.Title) + self.assertEqual(self.header._icon.get_next_sibling(), + self.header._title) + + self.assertEqual(self.header.title, "") + self.assertEqual(self.header._title.title, "") + + self.header.title = "Test Title" + self.assertEqual(self.header._title.title, "Test Title") + + header2 = emmental.sidebar.header.Header(title="Other Title") + self.assertEqual(header2._title.title, "Other Title") + + def test_subtitle(self): + """Test the subtitle property.""" + self.assertEqual(self.header.subtitle, "") + self.assertEqual(self.header._title.subtitle, "") + + self.header.subtitle = "Test Subtitle" + self.assertEqual(self.header._title.subtitle, "Test Subtitle") + + header2 = emmental.sidebar.header.Header(subtitle="Other Subtitle") + self.assertEqual(header2._title.subtitle, "Other Subtitle") + + def test_extra_widget(self): + """Test setting an extra widget.""" + self.assertIsInstance(self.header._extra, Adw.Bin) + self.assertEqual(self.header._title.get_next_sibling(), + self.header._extra) + + self.assertIsNone(self.header.extra_widget) + self.assertIsNone(self.header._extra.get_child()) + self.assertEqual(self.header._extra.get_valign(), Gtk.Align.CENTER) + + self.header.extra_widget = Gtk.Button() + self.assertEqual(self.header._extra.get_child(), + self.header.extra_widget) + + header2 = emmental.sidebar.header.Header(extra_widget=Gtk.Button()) + self.assertIsInstance(header2._extra.get_child(), Gtk.Button) + + def test_active_arrow(self): + """Test the active property and arrow widget.""" + self.assertIsInstance(self.header._arrow, Gtk.Image) + self.assertEqual(self.header._arrow.get_icon_name(), + "go-down-symbolic") + self.assertEqual(self.header._extra.get_next_sibling(), + self.header._arrow) + + self.assertFalse(self.header.active) + flags = self.header._arrow.get_state_flags() + self.assertFalse(flags & Gtk.StateFlags.CHECKED) + + for active in [True, False]: + with self.subTest(active=active): + self.header.active = active + flags = self.header._arrow.get_state_flags() + self.assertEqual(bool(flags & Gtk.StateFlags.CHECKED), active) + + def test_progress(self): + """Test the progress proprety and progress bar widget.""" + self.assertIsInstance(self.header._progress, Gtk.ProgressBar) + self.assertEqual(self.header._progress.get_valign(), Gtk.Align.END) + self.assertFalse(self.header._progress.get_visible()) + self.assertTrue(self.header._progress.has_css_class("osd")) + self.assertIn(self.header._progress, self.header._overlay) + + self.assertEqual(self.header.progress, 0.0) + self.assertFalse(self.header.pending) + + self.header.pending = True + self.header.progress = 0.42 + self.assertTrue(self.header._progress.get_visible()) + self.assertAlmostEqual(self.header._progress.get_fraction(), 0.42) + + header2 = emmental.sidebar.header.Header(pending=True) + self.assertTrue(header2.pending) + self.assertTrue(header2._progress.get_visible()) + + def test_revealer(self): + """Test the revealer.""" + self.assertIsInstance(self.header._revealer, Gtk.Revealer) + self.assertEqual(self.header._overlay.get_next_sibling(), + self.header._revealer) + + self.assertEqual(self.header.animation, + Gtk.RevealerTransitionType.SLIDE_UP) + self.assertEqual(self.header._revealer.get_transition_type(), + Gtk.RevealerTransitionType.SLIDE_UP) + + self.assertIsNone(self.header.reveal_widget) + self.assertIsNone(self.header._revealer.get_child()) + + self.header.reveal_widget = Gtk.Label() + self.assertEqual(self.header._revealer.get_child(), + self.header.reveal_widget) + + self.header.active = True + self.assertTrue(self.header._revealer.get_reveal_child()) + self.assertTrue(self.header._revealer.get_vexpand()) + + self.header.animation = Gtk.RevealerTransitionType.SLIDE_DOWN + self.assertEqual(self.header._revealer.get_transition_type(), + Gtk.RevealerTransitionType.SLIDE_DOWN) + + def test_clicked(self): + """Test clicking the header.""" + self.assertIsInstance(self.header._clicked, Gtk.GestureClick) + self.assertIn(self.header._clicked, + self.header._overlay.observe_controllers()) + + for i in [1, 2]: + with self.subTest(i=i): + self.header._clicked.emit("released", 1, 42, 42) + self.assertTrue(self.header.active) + flags = self.header._arrow.get_state_flags() + self.assertTrue(flags & Gtk.StateFlags.CHECKED)