diff --git a/emmental/header/__init__.py b/emmental/header/__init__.py index 90b076a..dd0df9d 100644 --- a/emmental/header/__init__.py +++ b/emmental/header/__init__.py @@ -1,10 +1,12 @@ # Copyright 2022 (c) Anna Schumaker. """A custom Gtk.HeaderBar configured for our application.""" +import pathlib from gi.repository import GObject from gi.repository import Gtk from gi.repository import Adw from .. import db from .. import buttons +from . import open from . import replaygain from . import volume if __debug__: @@ -36,6 +38,7 @@ class Header(Gtk.HeaderBar): def __init__(self, sql: db.Connection, title: str): """Initialize the HeaderBar.""" super().__init__(title=title, subtitle=SUBTITLE, sql=sql) + self._open = open.Button() self._title = Adw.WindowTitle(title=self.title, subtitle=self.subtitle) self._volume = volume.Controls() self._replaygain = replaygain.Selector() @@ -58,6 +61,7 @@ class Header(Gtk.HeaderBar): self.bind_property("volume", self._volume, "volume", GObject.BindingFlags.BIDIRECTIONAL) + self.pack_start(self._open) if __debug__: self._window = settings.Window(sql) self._settings = Gtk.Button.new_from_icon_name("settings-symbolic") @@ -66,6 +70,8 @@ class Header(Gtk.HeaderBar): self.pack_end(self._button) self.set_title_widget(self._title) + + self._open.connect("track-requested", self.__track_requested) self.connect("notify::volume", self.__notify_volume) def __run_settings(self, button: Gtk.Button) -> None: @@ -74,3 +80,11 @@ class Header(Gtk.HeaderBar): def __notify_volume(self, header, param) -> None: self._button.set_icon_name(_volume_icon(self.volume)) + + def __track_requested(self, button: open.Button, + path: pathlib.Path) -> None: + self.emit("track-requested", path) + + @GObject.Signal(arg_types=(GObject.TYPE_PYOBJECT,)) + def track_requested(self, path: pathlib.Path) -> None: + """Signal that a track has been requested.""" diff --git a/emmental/header/open.py b/emmental/header/open.py new file mode 100644 index 0000000..abdc558 --- /dev/null +++ b/emmental/header/open.py @@ -0,0 +1,39 @@ +# Copyright 2023 (c) Anna Schumaker. +"""A custom Button that opens a FileDialog to select a file for playback.""" +import pathlib +from gi.repository import GObject +from gi.repository import GLib +from gi.repository import Gio +from gi.repository import Gtk + + +class Button(Gtk.Button): + """Our pre-configured open button.""" + + def __init__(self): + """Initialize our open button.""" + super().__init__(icon_name="document-open-symbolic") + self._filters = Gio.ListStore() + self._filter = Gtk.FileFilter(name="Audio Files", + mime_types=["inode/directory", + "audio/*"]) + self._dialog = Gtk.FileDialog(filters=self._filters, + title="Pick a Track") + + self._filters.append(self._filter) + + def __async_ready(self, dialog: Gtk.FileDialog, task: Gio.Task) -> None: + try: + file = dialog.open_finish(task) + self.emit("track-requested", pathlib.Path(file.get_path())) + except GLib.Error: + pass + + def do_clicked(self) -> None: + """Handle a click event.""" + self._dialog.open(self.get_ancestor(Gtk.Window), None, + self.__async_ready) + + @GObject.Signal(arg_types=(GObject.TYPE_PYOBJECT,)) + def track_requested(self, file: pathlib.Path) -> None: + """Signal that a track has been requested.""" diff --git a/tests/header/test_header.py b/tests/header/test_header.py index 9c5f953..733e6d0 100644 --- a/tests/header/test_header.py +++ b/tests/header/test_header.py @@ -2,6 +2,7 @@ """Tests our application header.""" import emmental import tests.util +import pathlib import unittest.mock from gi.repository import Gtk from gi.repository import Adw @@ -32,6 +33,15 @@ class TestHeader(tests.util.TestCase): self.assertEqual(self.header._title.get_subtitle(), emmental.header.SUBTITLE) + def test_open(self): + """Check that the Open button works as expected.""" + self.assertIsInstance(self.header._open, emmental.header.open.Button) + + signal = unittest.mock.Mock() + self.header.connect("track-requested", signal) + self.header._open.emit("track-requested", pathlib.Path("/a/b/c/1.ogg")) + signal.assert_called_with(self.header, pathlib.Path("/a/b/c/1.ogg")) + def test_settings(self): """Check that the Settings window is set up correctly.""" self.assertIsInstance(self.header._settings, Gtk.Button) diff --git a/tests/header/test_open.py b/tests/header/test_open.py new file mode 100644 index 0000000..4184938 --- /dev/null +++ b/tests/header/test_open.py @@ -0,0 +1,56 @@ +# Copyright 2023 (c) Anna Schumaker. +"""Tests our Open button.""" +import emmental.header.open +import pathlib +import unittest +from gi.repository import Gio +from gi.repository import Gtk + + +class TestButton(unittest.TestCase): + """Test the Open button.""" + + def setUp(self): + """Set up common variables.""" + self.button = emmental.header.open.Button() + + def test_button(self): + """Check that the button was set up properly.""" + self.assertIsInstance(self.button, Gtk.Button) + self.assertEqual(self.button.get_icon_name(), "document-open-symbolic") + + def test_filter(self): + """Check that the file filter is set up properly.""" + self.assertIsInstance(self.button._filter, Gtk.FileFilter) + self.assertIsInstance(self.button._filters, Gio.ListStore) + + self.assertEqual(self.button._filter.get_name(), "Audio Files") + self.assertEqual(self.button._filters[0], self.button._filter) + + def test_dialog(self): + """Check that the file dialog is set up properly.""" + self.assertIsInstance(self.button._dialog, Gtk.FileDialog) + self.assertEqual(self.button._dialog.get_title(), "Pick a Track") + self.assertEqual(self.button._dialog.get_filters(), + self.button._filters) + self.assertTrue(self.button._dialog.get_modal()) + + def test_clicked(self): + """Test clicking on the button.""" + with unittest.mock.patch.object(self.button._dialog, + "open") as mock_open: + self.button.emit("clicked") + mock_open.assert_called_with(None, None, + self.button._Button__async_ready) + + with unittest.mock.patch.object(self.button._dialog, + "open_finish") as mock_finish: + task = Gio.Task() + signal = unittest.mock.Mock() + mock_finish.return_value = Gio.File.new_for_path("/a/b/c/1.ogg") + self.button.connect("track-requested", signal) + + self.button._Button__async_ready(self.button._dialog, task) + mock_finish.assert_called_with(task) + signal.assert_called_with(self.button, + pathlib.Path("/a/b/c/1.ogg"))