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 <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-08-06 16:39:25 -04:00
parent 2542a6cbd7
commit b25ca24dc3
3 changed files with 259 additions and 0 deletions

View File

@ -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;
}

View File

@ -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)

View File

@ -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)