sidebar: Create a Settable Icon widget

This widget allows users to change the displayed image by selecting a
new one from the filesystem.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-08-18 11:16:53 -04:00
parent 0584a2398a
commit 2542a6cbd7
2 changed files with 147 additions and 0 deletions

View File

@ -1,10 +1,20 @@
# Copyright 2022 (c) Anna Schumaker.
"""Custom icon widgets for playlist rows."""
import pathlib
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import Adw
IMAGE_FILTERS = Gio.ListStore()
IMAGE_FILTERS.append(Gtk.FileFilter(name="Image Files",
mime_types=["inode/directory"]))
IMAGE_FILTERS[0].add_pixbuf_formats()
class Icon(Adw.Bin):
"""A custom Adw.Avatar that can load images from a file path."""
@ -32,3 +42,35 @@ class Icon(Adw.Bin):
else:
texture = Gdk.Texture.new_from_filename(str(self.filepath))
self._icon.set_custom_image(texture)
class Settable(Icon):
"""A custom Avatar with an attached popover for selecting an image."""
settable = GObject.Property(type=bool, default=False)
def __init__(self):
"""Initialize our settable icon."""
super().__init__()
self._dialog = Gtk.FileDialog(title="Pick an Image",
filters=IMAGE_FILTERS)
self._long_press = Gtk.GestureLongPress()
self._long_press.connect("pressed", self.__long_press)
self.add_controller(self._long_press)
def __async_ready(self, dialog: Gtk.FileDialog, task: Gio.Task) -> None:
try:
file = dialog.open_finish(task)
self.filepath = pathlib.Path(file.get_path())
except GLib.Error:
self.filepath = None
def __long_press(self, gesture: Gtk.GestureLongPress,
x: float, y: float) -> None:
"""Handle a click event."""
if self.settable:
if self.filepath is not None:
file = Gio.File.new_for_path(str(self.filepath))
self._dialog.set_initial_file(file)
self._dialog.open(self.get_ancestor(Gtk.Window), None,
self.__async_ready)

View File

@ -3,10 +3,31 @@
import unittest
import emmental.sidebar.icon
import tests.util
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import Adw
class TestImageFilters(unittest.TestCase):
"""Test that the global File Filters have been set up properly."""
def test_filters(self):
"""Test the global Filter list."""
self.assertIsInstance(emmental.sidebar.icon.IMAGE_FILTERS,
Gio.ListStore)
filter = emmental.sidebar.icon.IMAGE_FILTERS[0]
self.assertIsInstance(filter, Gtk.FileFilter)
self.assertEqual(filter.get_name(), "Image Files")
(name, mime_types) = filter.to_gvariant()
self.assertEqual(name, "Image Files")
self.assertTupleEqual(mime_types[0], (1, "inode/directory"))
self.assertGreater(len(mime_types), 1)
class TestIcon(unittest.TestCase):
"""Test our icon that can also be set from filepath."""
@ -60,3 +81,87 @@ class TestIcon(unittest.TestCase):
self.icon.filepath = None
self.assertIsNone(self.icon._icon.get_custom_image())
mock_new.assert_not_called()
class TestSettable(unittest.TestCase):
"""Test our icon that can be set by the user."""
def setUp(self):
"""Set up common variables."""
self.icon = emmental.sidebar.icon.Settable()
def test_init(self):
"""Test that the icon is set up properly."""
self.assertIsInstance(self.icon, emmental.sidebar.icon.Icon)
def test_dialog(self):
"""Test that the dialog is set up properly."""
self.assertIsInstance(self.icon._dialog, Gtk.FileDialog)
self.assertEqual(self.icon._dialog.get_title(), "Pick an Image")
self.assertEqual(self.icon._dialog.get_filters(),
emmental.sidebar.icon.IMAGE_FILTERS)
def test_setting(self):
"""Test setting the icon through the FileDialog."""
self.assertIsInstance(self.icon._long_press, Gtk.GestureLongPress)
self.assertIn(self.icon._long_press, self.icon.observe_controllers())
self.assertFalse(self.icon.settable)
mock_set_initial_file = unittest.mock.Mock()
self.icon._dialog.set_initial_file = mock_set_initial_file
self.icon.settable = True
with unittest.mock.patch.object(self.icon._dialog,
"open") as mock_open:
self.icon._long_press.emit("pressed", 0, 0)
mock_set_initial_file.assert_not_called()
mock_open.assert_called_with(None, None,
self.icon._Settable__async_ready)
with unittest.mock.patch.object(self.icon._dialog,
"open_finish") as mock_finish:
task = Gio.Task()
cover_path = str(tests.util.COVER_JPG)
mock_finish.return_value = Gio.File.new_for_path(cover_path)
self.icon._Settable__async_ready(self.icon._dialog, task)
mock_finish.assert_called_with(task)
self.assertEqual(self.icon.filepath, tests.util.COVER_JPG)
def test_clearing(self):
"""Test clearing the icon by canceling the FileDialog."""
mock_set_initial_file = unittest.mock.Mock()
self.icon._dialog.set_initial_file = mock_set_initial_file
self.icon.filepath = tests.util.COVER_JPG
self.icon.settable = True
with unittest.mock.patch.object(self.icon._dialog,
"open") as mock_open:
self.icon._long_press.emit("pressed", 0, 0)
mock_set_initial_file.assert_called()
mock_open.assert_called()
cover_path = str(tests.util.COVER_JPG)
call_args = mock_set_initial_file.call_args.args
self.assertIsInstance(call_args[0], Gio.File)
self.assertEqual(call_args[0].get_path(), cover_path)
with unittest.mock.patch.object(self.icon._dialog, "open_finish",
side_effect=GLib.Error) as mock_finish:
task = Gio.Task()
mock_finish.return_value = None
self.icon._Settable__async_ready(self.icon._dialog, task)
mock_finish.assert_called_with(task)
self.assertIsNone(self.icon.filepath)
def test_not_settable(self):
"""Test when the icon isn't settable."""
mock_set_initial_file = unittest.mock.Mock()
self.icon._dialog.set_initial_file = mock_set_initial_file
with unittest.mock.patch.object(self.icon._dialog,
"open") as mock_open:
self.icon._long_press.emit("pressed", 0, 0)
mock_set_initial_file.assert_not_called()
mock_open.assert_not_called()