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:
parent
0584a2398a
commit
2542a6cbd7
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue