Compare commits

...

11 Commits

Author SHA1 Message Date
Anna Schumaker 1397e6e9e3 Emmental 3.0.6
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker 717fdf39cd tracklist: Add keyboard shortcuts
The following shortcuts are implemented:

- Escape to unselect any selected tracks
- Delete to remove selected tracks from the current playlist
- <Control>/ to focus the "filter tracks" entry
- <Control>l to cycle the loop state of the current playlist
- <Control>s to toggle the shuffle state of the current playlist
- <Control>Up to move the selected track up one position
- <Control>Down to move the selected track down one position

I also change the volume up and down shortcuts to use the <Shift>
modifier. This matches how other Header shortcuts are triggered, and
frees up the non-shifted versions to use here.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker 9cf980d967 emmental: Adjust active-row styling
Make the active row background color a little more transparent.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker 87d8a2ae3a tracklist: Add tooltips to tracklist buttons
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker ddfd37130b tracklist: Move the OSD out of the TrackView
This simplifies the code a lot by letting the TrackList directly call
OSD functions without going through the TrackView. I can also simplify
the TrackView to just contain our columnview.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker 5011db344e tracklist: Rework the Add Tracks to Playlist button to use a ListBox
I also convert my PlaylistRowWidget into a Gtk.ListBoxRow that has the
same functionality. This looks a little nicer, and lets us keep the same
style as the rest of the app.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker 9f240bbc8b tracklist: Rework the Sort Order button to use a ListBox
I convert my SortRow widget into a Gtk.ListBoxRow that has the same
functionality. The main benefit is that it looks nicer in the
Gtk.Popover compared to the Gtk.ListView that I had been using.

I also connect to the listbox "row-activated" signal so I can handle
clicking a specific sort row in the list. Clicking a disabled sort row
will enable it, and clicking an enabled one will reverse the sort order.
I think this is what feels the most natural to the user.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:22 -04:00
Anna Schumaker f6481f0182 tracklist: Rework the VisibleColumns button to use a ListBox
I create a custom Gtk.ListBoxRow for displaying each individual column
name and visibility status. I then bind it to a listbox placed as the
popover button's popover child. This lets me set the 'boxed-list' style
class on the listbox to give it a nicer appearance, and clicking the
label will also toggle column visibility.

Implements: #57 ("Rework visible columns button")
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-24 09:28:06 -04:00
Anna Schumaker 3d6350d7bd tracklist: Give the Top Box the "toolbar" style class
And adjust widget spacing for to keep our nice look.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-22 13:29:53 -04:00
Anna Schumaker eb6b4d8ef4 buttons: Watch for ImageToggle tooltip text changes
If the application changes the active or inactive tooltip text, then we
want to apply that to the button depending on what state it currently
has.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-22 13:29:53 -04:00
Anna Schumaker f7349cd864 factory: Don't change Gtk ListRowWidget state flags
I was using this to set some custom styling for the active playlist and
track inside a ListView. I can accomplish the same thing by adding and
removing a style class from the ListRowWidget, and this doesn't break
Gtk internal stuff that changed in the 4.12 release.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-08-22 12:09:20 -04:00
19 changed files with 663 additions and 509 deletions

View File

@ -21,7 +21,7 @@ from gi.repository import Adw
MAJOR_VERSION = 3
MINOR_VERSION = 0
MICRO_VERSION = 5
MICRO_VERSION = 6
VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}"
VERSION_STRING = f"Emmental {VERSION_NUMBER}{gsetup.DEBUG_STR}"
@ -195,6 +195,8 @@ class Application(Adw.Application):
self.db.settings.bind_setting(f"tracklist.{name}.visible",
column, "visible")
self.factory.bind_property("visible-playlist", track_list, "playlist")
self.__add_accelerators(track_list.accelerators)
return track_list
def build_window(self) -> window.Window:

View File

@ -119,6 +119,13 @@ class ImageToggle(Button):
inactive_tooltip_text=inactive_tooltip_text,
tooltip_text=inactive_tooltip_text,
active=active, **kwargs)
self.connect("notify", self.__notify)
def __notify(self, toggle: Button, param: GObject.ParamSpec) -> None:
match (param.name, self.active):
case ("active-tooltip-text", True) | \
("inactive-tooltip-text", False):
self.set_tooltip_text(self.get_property(param.name))
def do_clicked(self) -> None:
"""Handle a click event."""

View File

@ -21,29 +21,29 @@ box.emmental-splitbutton>menubutton>button {
padding: 6px;
}
listview > row:checked {
row.emmental-active-row {
font-weight: bold;
background-color: alpha(@accent_color, 0.2);
background-color: alpha(@accent_color, 0.15);
}
listview > row:checked:hover {
background-color: alpha(@accent_color, 0.27);
row.emmental-active-row:hover {
background-color: alpha(@accent_color, 0.22);
}
listview > row:checked:active {
background-color: alpha(@accent_color, 0.36);
row.emmental-active-row:active {
background-color: alpha(@accent_color, 0.31);
}
listview > row:checked:selected {
background-color: alpha(@accent_color, 0.3);
row.emmental-active-row:selected {
background-color: alpha(@accent_color, 0.25);
}
listview > row:checked:selected:hover {
background-color: alpha(@accent_color, 0.33);
row.emmental-active-row:selected:hover {
background-color: alpha(@accent_color, 0.28);
}
listview > row:checked:selected:active {
background-color: alpha(@accent_color, 0.39);
row.emmental-active-row:selected:active {
background-color: alpha(@accent_color, 0.34);
}
image.emmental-sidebar-arrow {
@ -70,6 +70,14 @@ button.emmental-stop>image {
color: @red_3;
}
columnview.emmental-track-list > header {
background-color: @card_bg_color;
}
columnview.emmental-track-list > listview {
background-color: @card_bg_color;
}
columnview.emmental-track-list > listview > row > cell {
padding: 0px 2px;
min-height: 40px;

View File

@ -61,17 +61,17 @@ class ListRow(GObject.GObject):
@GObject.Property(type=bool, default=False)
def active(self) -> bool:
"""Get the active state of this Row."""
if parent := self.listitem.get_child().get_parent():
return parent.get_state_flags() & Gtk.StateFlags.CHECKED
if self.listrow is not None:
return self.listrow.has_css_class("emmental-active-row")
return False
@active.setter
def active(self, newval: bool) -> None:
if parent := self.listitem.get_child().get_parent():
if self.listrow is not None:
if newval:
parent.set_state_flags(Gtk.StateFlags.CHECKED, False)
self.listrow.add_css_class("emmental-active-row")
else:
parent.unset_state_flags(Gtk.StateFlags.CHECKED)
self.listrow.remove_css_class("emmental-active-row")
@GObject.Property(type=Gtk.Widget)
def child(self) -> Gtk.Widget | None:
@ -87,6 +87,11 @@ class ListRow(GObject.GObject):
"""Get the list item for this Row."""
return self.listitem.get_item()
@GObject.Property(type=Gtk.Widget)
def listrow(self) -> Gtk.Widget:
"""Get the listrow widget that our child widget is contained in."""
return self.listitem.props.child.props.parent
class InscriptionRow(ListRow):
"""A ListRow for displaying Gtk.Inscription widgets."""

View File

@ -124,9 +124,9 @@ class Header(Gtk.HeaderBar):
"""Get a list of accelerators for the Header."""
res = [ActionEntry("open-file", self._open.activate, "<Control>o"),
ActionEntry("decrease-volume", self._volume.decrement,
"<Control>Down"),
"<Shift><Control>Down"),
ActionEntry("increase-volume", self._volume.increment,
"<Control>Up"),
"<Shift><Control>Up"),
ActionEntry("toggle-bg-mode", self._background.activate,
"<Shift><Control>b")]
if __debug__:

View File

@ -4,12 +4,14 @@ from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gtk
from ..action import ActionEntry
from ..playlist.playlist import Playlist
from ..playlist.previous import Previous
from .. import db
from .. import entry
from . import buttons
from . import footer
from . import selection
from . import trackview
@ -21,23 +23,24 @@ class Card(Gtk.Box):
def __init__(self, sql: db.Connection, **kwargs):
"""Set up the Tracklist widget."""
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
spacing=6, **kwargs)
**kwargs)
self._top_left = Gtk.Box()
self._top_right = Gtk.Box(sensitive=False)
self._top_box = Gtk.CenterBox(margin_top=6, margin_start=6,
margin_end=6)
self._top_box = Gtk.CenterBox(margin_start=6, margin_end=6)
self._filter = entry.Filter("tracks", hexpand=True,
margin_start=100, margin_end=100)
self._trackview = trackview.TrackView(sql, margin_start=6,
margin_end=6)
self._trackview = trackview.TrackView(sql)
self._osd = selection.OSD(sql, self._trackview.selection_model,
child=self._trackview)
self._visible_cols = buttons.VisibleColumns(self._trackview.columns)
self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic",
tooltip_text="unselect all tracks",
has_frame=False, sensitive=False)
self._loop = buttons.LoopButton()
self._shuffle = buttons.ShuffleButton()
self._sort = buttons.SortButton()
self._footer = footer.Footer(margin_start=6, margin_end=6,
margin_bottom=6)
margin_top=6, margin_bottom=6)
self._top_left.append(self._visible_cols)
self._top_left.append(self._unselect)
@ -51,9 +54,14 @@ class Card(Gtk.Box):
self._top_box.set_end_widget(self._top_right)
self.append(self._top_box)
self.append(self._trackview)
self.append(Gtk.Separator())
self.append(self._osd)
self.append(Gtk.Separator())
self.append(self._footer)
self._osd.bind_property("have-selected", self._trackview,
"have-selected")
self._osd.bind_property("n-selected", self._trackview, "n-selected")
self._trackview.bind_property("n-tracks", self._footer, "count")
self._trackview.bind_property("n-selected", self._footer, "selected")
self._trackview.bind_property("runtime", self._footer, "runtime")
@ -62,16 +70,14 @@ class Card(Gtk.Box):
"sensitive")
self._filter.connect("search-changed", self.__search_changed)
self._unselect.connect("clicked", self.__clear_selection)
self._unselect.connect("clicked", self._osd.clear_selection)
self._loop.connect("notify::state", self.__update_loop_state)
self._shuffle.connect("notify::active", self.__update_shuffle_state)
self._sort.connect("notify::sort-order", self.__update_sort_order)
self._top_box.add_css_class("toolbar")
self.add_css_class("card")
def __clear_selection(self, unselect: Gtk.Button) -> None:
self._trackview.clear_selected_tracks()
def __playlist_notify(self, playlist: Playlist, param) -> None:
match param.name:
case "loop":
@ -89,7 +95,7 @@ class Card(Gtk.Box):
self._loop.state = self.playlist.loop
self._shuffle.active = self.playlist.shuffle
self._sort.set_sort_order(self.playlist.sort_order)
self._trackview.reset_osd()
self._osd.reset()
def __update_loop_state(self, loop: buttons.LoopButton, param) -> None:
if self.playlist.loop != loop.state:
@ -132,7 +138,24 @@ class Card(Gtk.Box):
self._trackview.playlist.disconnect_by_func(self.__playlist_notify)
self._trackview.playlist = newval
self._osd.playlist = newval
if newval is not None:
self._top_right.set_sensitive(not isinstance(newval, Previous))
self.__set_button_state()
newval.connect("notify", self.__playlist_notify)
@property
def accelerators(self) -> list[ActionEntry]:
"""Get a list of accelerators for the Tracklist."""
return [ActionEntry("focus-search-track", self._filter.grab_focus,
"<Control>slash"),
ActionEntry("clear-selected-tracks", self._unselect.activate,
"Escape", enabled=(self._unselect, "sensitive")),
ActionEntry("cycle-loop", self._loop.activate,
"<Control>l", enabled=(self._top_right,
"sensitive")),
ActionEntry("toggle-shuffle", self._shuffle.activate,
"<Control>s", enabled=(self._top_right,
"sensitive"))] + \
self._osd.accelerators

View File

@ -5,19 +5,28 @@ from gi.repository import Gio
from gi.repository import Gtk
from . import sorter
from .. import buttons
from .. import factory
class VisibleSwitch(factory.ListRow):
"""A list row containing a Gtk.Switch."""
class VisibleRow(Gtk.ListBoxRow):
"""A ListBoxRow containing a Gtk.Switch and a title Label."""
def __init__(self, listitem: Gtk.ListItem):
"""Initialize a VisibleSwitch ListRow."""
super().__init__(listitem=listitem, child=Gtk.Switch())
active = GObject.Property(type=bool, default=True)
title = GObject.Property(type=str)
def do_bind(self) -> None:
"""Bind the visible property to the switch active property."""
self.bind_and_set_property("visible", "active", bidirectional=True)
def __init__(self, title: str, active: bool):
"""Initialize a VisibleRow ListBoxRow."""
super().__init__(title=title, active=active,
child=Gtk.Box(margin_start=6, margin_end=6,
margin_top=6, margin_bottom=6,
spacing=6))
self._switch = Gtk.Switch(active=active)
self._label = Gtk.Label.new(title)
self.bind_property("active", self._switch, "active",
GObject.BindingFlags.BIDIRECTIONAL)
self.props.child.append(self._switch)
self.props.child.append(self._label)
class VisibleColumns(buttons.PopoverButton):
@ -28,21 +37,21 @@ class VisibleColumns(buttons.PopoverButton):
def __init__(self, columns: Gio.ListModel, **kwargs):
"""Initialize the VisibleColumns button."""
super().__init__(columns=columns, icon_name="columns-symbolic",
tooltip_text="configure visible columns",
has_frame=False, **kwargs)
self._selection = Gtk.NoSelection(model=self.columns)
self.popover_child = Gtk.ColumnView(model=self._selection,
show_row_separators=True)
self.__append_column(factory.InscriptionFactory("title"),
"Column", width=125)
self.__append_column(factory.Factory(row_type=VisibleSwitch),
"Visible")
self.popover_child.add_css_class("data-table")
self.popover_child = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self.popover_child.bind_model(columns, self.__create_func)
self.popover_child.connect("row-activated", self.__row_activated)
self.popover_child.add_css_class("boxed-list")
def __append_column(self, factory: factory.Factory,
title: str, *, width: int = -1) -> None:
column = Gtk.ColumnViewColumn(factory=factory, title=title,
fixed_width=width)
self.popover_child.append_column(column)
def __create_func(self, column: Gtk.ColumnViewColumn) -> VisibleRow:
row = VisibleRow(column.get_title(), column.get_visible())
row.bind_property("active", column, "visible",
GObject.BindingFlags.BIDIRECTIONAL)
return row
def __row_activated(self, box: Gtk.ListBox, row: Gtk.ListBoxRow) -> None:
row.active = not row.active
class LoopButton(buttons.ImageToggle):
@ -53,7 +62,9 @@ class LoopButton(buttons.ImageToggle):
def __init__(self, **kwargs):
"""Initialize a Loop Button."""
super().__init__(active_icon_name="media-playlist-repeat-song",
active_tooltip_text="loop: track",
inactive_icon_name="media-playlist-repeat",
inactive_tooltip_text="loop: disabled",
large_icon=False, state="None",
has_frame=False, **kwargs)
@ -79,9 +90,11 @@ class LoopButton(buttons.ImageToggle):
case ("None", True):
self.active = False
self.icon_opacity = 0.5
self.inactive_tooltip_text = "loop: disabled"
case ("Playlist", _):
self.active = False
self.icon_opacity = 1.0
self.inactive_tooltip_text = "loop: playlist"
case ("Track", _):
self.active = True
self.icon_opacity = 1.0
@ -93,7 +106,9 @@ class ShuffleButton(buttons.ImageToggle):
def __init__(self, **kwargs):
"""Initialize a Shuffle Button."""
super().__init__(active_icon_name="media-playlist-shuffle",
active_tooltip_text="shuffle: enabled",
inactive_icon_name="media-playlist-consecutive",
inactive_tooltip_text="shuffle: disabled",
large_icon=False, icon_opacity=0.5,
has_frame=False, **kwargs)
@ -102,81 +117,57 @@ class ShuffleButton(buttons.ImageToggle):
self.icon_opacity = 1.0 if self.active else 0.5
class SortFieldWidget(Gtk.Box):
"""A Widget to display in the Sort Order button popover."""
class SortRow(Gtk.ListBoxRow):
"""A ListBoxRow for managing Sort Order."""
active = GObject.Property(type=bool, default=False)
sort_field = GObject.Property(type=sorter.SortField)
def __init__(self) -> None:
"""Initialize a SortField Widget."""
super().__init__(spacing=6)
self._enabled = Gtk.Switch(valign=Gtk.Align.CENTER)
self._name = Gtk.Label(hexpand=True, sensitive=False)
def __init__(self, sort_field: sorter.SortField):
"""Initialize a Sort Row."""
super().__init__(sort_field=sort_field, active=sort_field.enabled,
child=Gtk.Box(margin_start=6, margin_end=6,
margin_top=6, margin_bottom=6,
spacing=6))
self._switch = Gtk.Switch(active=self.active, valign=Gtk.Align.CENTER)
self._label = Gtk.Label(label=sort_field.name, hexpand=True,
sensitive=self.active, xalign=0.0)
self._reverse = buttons.ImageToggle("arrow1-up", "arrow1-down",
large_icon=False, sensitive=False)
self._box = Gtk.Box(sensitive=False)
active=sort_field.reversed,
sensitive=self.active,
has_frame=False)
self._move_box = Gtk.Box(sensitive=self.active)
self._move_up = Gtk.Button(icon_name="go-up-symbolic")
self._move_down = Gtk.Button(icon_name="go-down-symbolic")
self._enabled.bind_property("active", self._name, "sensitive")
self._enabled.bind_property("active", self._reverse, "sensitive")
self._enabled.bind_property("active", self._box, "sensitive")
self._switch.connect("notify::active", self.__toggle_enabled)
self._reverse.connect("toggled", self.__reverse)
self._move_up.connect("clicked", self.__move_up)
self._move_down.connect("clicked", self.__move_down)
self._enabled.connect("notify::active", self.__notify_enabled)
self._reverse.connect("clicked", self.__reverse)
self._move_up.connect("clicked", self.__move_item_up)
self._move_down.connect("clicked", self.__move_item_down)
self.props.child.append(self._switch)
self.props.child.append(self._label)
self.props.child.append(self._reverse)
self.props.child.append(self._move_box)
self.append(self._enabled)
self.append(self._name)
self.append(self._reverse)
self.append(self._box)
self._move_box.append(self._move_up)
self._move_box.append(self._move_down)
self._move_box.add_css_class("linked")
self._box.append(self._move_up)
self._box.append(self._move_down)
self._box.add_css_class("linked")
def __move_item_down(self, button: Gtk.Button) -> None:
if self.sort_field is not None:
self.sort_field.move_down()
def __move_item_up(self, button: Gtk.Button) -> None:
if self.sort_field is not None:
self.sort_field.move_up()
def __notify_enabled(self, switch: Gtk.Switch, param) -> None:
if self.sort_field is not None:
if switch.get_active():
self.sort_field.enable()
else:
self.sort_field.disable()
def __toggle_enabled(self, switch: Gtk.Switch, param) -> None:
if switch.props.active:
self.sort_field.enable()
else:
self.sort_field.disable()
def __reverse(self, button: buttons.ImageToggle) -> None:
if self.sort_field is not None:
self.sort_field.reverse()
self.sort_field.reverse()
def set_sort_field(self, field: sorter.SortField | None) -> None:
"""Set the Sort Field displayed by this Widget."""
self.sort_field = field
self._name.set_text(field.name if field is not None else "")
self._enabled.set_active(field is not None and field.enabled)
self._reverse.active = field is not None and field.reversed
def __move_up(self, button: Gtk.Button) -> None:
self.sort_field.move_up()
class SortRow(factory.ListRow):
"""A row for managing Sort Order."""
def __init__(self, listitem: Gtk.ListItem):
"""Initialize a Sort Row."""
super().__init__(listitem=listitem, child=SortFieldWidget())
def do_bind(self) -> None:
"""Bind Sort Field properties to the Widget."""
self.child.set_sort_field(self.item)
def do_unbind(self) -> None:
"""Unbind properties from the widget."""
self.child.set_sort_field(None)
def __move_down(self, button: Gtk.Button) -> None:
self.sort_field.move_down()
class SortButton(buttons.PopoverButton):
@ -188,14 +179,24 @@ class SortButton(buttons.PopoverButton):
def __init__(self, **kwargs):
"""Initialize the Sort button."""
super().__init__(has_frame=False, model=sorter.SortOrderModel(),
tooltip_text="configure playlist sort order",
icon_name="view-list-ordered-symbolic", **kwargs)
self._selection = Gtk.NoSelection(model=self.model)
self._factory = factory.Factory(row_type=SortRow)
self.popover_child = Gtk.ListView(model=self._selection,
factory=self._factory,
show_separators=True)
self.popover_child = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self.popover_child.bind_model(self.model, self.__create_func)
self.popover_child.connect("row-activated", self.__row_activated)
self.popover_child.add_css_class("boxed-list")
self.model.bind_property("sort-order", self, "sort-order")
def __create_func(self, sort_field: sorter.SortField) -> SortRow:
return SortRow(sort_field)
def __row_activated(self, box: Gtk.ListBox, row: SortRow) -> None:
if row.active:
row._reverse.active = not row.sort_field.reversed
else:
row.sort_field.enable()
def set_sort_order(self, newval: str) -> None:
"""Directly set the sort order."""
self.model.set_sort_order(newval)

View File

@ -63,23 +63,6 @@ class TrackRow(factory.ListRow):
else:
self.bind_album(child_prop)
@GObject.Property(type=bool, default=False)
def active(self) -> bool:
"""Get the active state of this Row."""
if parent := self.listitem.get_child().get_parent():
if parent := parent.get_parent():
return parent.get_state_flags() & Gtk.StateFlags.CHECKED
return False
@active.setter
def active(self, newval: bool) -> None:
if parent := self.listitem.get_child().get_parent():
if parent := parent.get_parent():
if newval:
parent.set_state_flags(Gtk.StateFlags.CHECKED, False)
else:
parent.unset_state_flags(Gtk.StateFlags.CHECKED)
@GObject.Property(type=bool, default=True)
def online(self) -> bool:
"""Get the online state of this Row."""
@ -90,6 +73,14 @@ class TrackRow(factory.ListRow):
self.listitem.set_activatable(newval)
self.child.set_sensitive(newval)
@GObject.Property(type=Gtk.Widget)
def listrow(self) -> Gtk.Widget:
"""Test property for active track styling."""
if child := self.listitem.props.child:
if cell := child.props.parent:
return cell.props.parent
return None
class InscriptionRow(TrackRow):
"""Base class for Track Rows displaying a Gtk.Inscription."""

View File

@ -4,40 +4,39 @@ from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import Adw
from ..action import ActionEntry
from ..buttons import PopoverButton
from .. import db
from .. import factory
from .. import playlist
class PlaylistRowWidget(Gtk.Box):
"""A row widget for Playlists."""
class PlaylistRow(Gtk.ListBoxRow):
"""A ListBoxRow widget for Playlists."""
name = GObject.Property(type=str)
image = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self):
"""Initialize a PlaylistRowWidget."""
super().__init__()
self._icon = Adw.Avatar(size=32)
self._label = Gtk.Label(xalign=0.0)
def __init__(self, name: str, image: GObject.TYPE_PYOBJECT):
"""Initialize a PlaylistRow."""
super().__init__(child=Gtk.Box(margin_start=6, margin_end=6,
margin_top=6, margin_bottom=6,
spacing=6), name=name)
match name:
case "Favorite Tracks": icon_name = "heart-filled-symbolic"
case "Queued Tracks": icon_name = "music-queue-symbolic"
case _: icon_name = "playlist2-symbolic"
self._icon = Adw.Avatar(size=32, text=name, icon_name=icon_name)
self._label = Gtk.Label.new(name)
self.bind_property("name", self._label, "label")
self.bind_property("name", self._icon, "text")
self.connect("notify::name", self.__name_changed)
self.connect("notify::image", self.__image_changed)
self.image = image
self.append(self._icon)
self.append(self._label)
self.props.child.append(self._icon)
self.props.child.append(self._label)
def __name_changed(self, row: Gtk.Box, param) -> None:
match self.name:
case "Favorite Tracks": icon = "heart-filled-symbolic"
case "Queued Tracks": icon = "music-queue-symbolic"
case _: icon = "playlist2-symbolic"
self._icon.set_icon_name(icon)
def __image_changed(self, row: Gtk.Box, param) -> None:
def __image_changed(self, row: Gtk.ListBoxRow,
param: GObject.ParamSpec) -> None:
if self.image is not None and self.image.is_file():
texture = Gdk.Texture.new_from_filename(str(self.image))
else:
@ -45,20 +44,6 @@ class PlaylistRowWidget(Gtk.Box):
self._icon.set_custom_image(texture)
class PlaylistRow(factory.ListRow):
"""A list row for displaying Playlists."""
def __init__(self, listitem: Gtk.ListItem):
"""Initialize a PlaylistRow."""
super().__init__(listitem)
self.child = PlaylistRowWidget()
def do_bind(self):
"""Bind a Playlist to this Row."""
self.bind_and_set_property("name", "name")
self.bind_and_set_property("image", "image")
class UserTracksFilter(Gtk.Filter):
"""Filters for tracks with user-tracks set to True."""
@ -77,28 +62,28 @@ class UserTracksFilter(Gtk.Filter):
return playlist.user_tracks and playlist != self.playlist
class PlaylistView(Gtk.ListView):
class PlaylistView(Gtk.ListBox):
"""A ListView for selecting Playlists."""
playlist = GObject.Property(type=db.playlist.Playlist)
def __init__(self, sql: db.Connection):
"""Initialize the PlaylistView."""
super().__init__(show_separators=True, single_click_activate=True)
super().__init__(selection_mode=Gtk.SelectionMode.NONE)
self._filtered = Gtk.FilterListModel(model=sql.playlists,
filter=UserTracksFilter())
self._selection = Gtk.NoSelection(model=self._filtered)
self._factory = factory.Factory(PlaylistRow)
self.connect("activate", self.__playlist_activated)
self.bind_property("playlist", self._filtered.get_filter(), "playlist")
self.add_css_class("rich-list")
self.bind_model(self._filtered, self.__create_func)
self.connect("row-activated", self.__row_activated)
self.add_css_class("boxed-list")
self.set_model(self._selection)
self.set_factory(self._factory)
def __row_activated(self, box: Gtk.ListBox, row: PlaylistRow) -> None:
self.emit("playlist-selected", self._filtered[row.get_index()])
def __playlist_activated(self, view: Gtk.ListView, position: int) -> None:
self.emit("playlist-selected", self._selection[position])
def __create_func(self, playlist: db.playlist.Playlist) -> PlaylistRow:
row = PlaylistRow(playlist.name, playlist.image)
playlist.bind_property("image", row, "image")
return row
@GObject.Signal(arg_types=(db.playlists.Playlist,))
def playlist_selected(self, playlist: db.playlists.Playlist) -> None:
@ -115,8 +100,10 @@ class MoveButtons(Gtk.Box):
"""Initialize the Move Buttons."""
super().__init__(**kwargs)
self._down = Gtk.Button(icon_name="go-down-symbolic",
tooltip_text="move selected track down",
hexpand=True, sensitive=False)
self._up = Gtk.Button(icon_name="go-up-symbolic",
tooltip_text="move selected track up",
hexpand=True, sensitive=False)
self.bind_property("can-move-down", self._down, "sensitive")
@ -165,12 +152,15 @@ class OSD(Gtk.Overlay):
super().__init__(sql=sql, selection=selection, **kwargs)
self._add = PopoverButton(child=Adw.ButtonContent(label="Add",
icon_name="list-add-symbolic"),
tooltip_text="add selected tracks "
"to a playlist",
halign=Gtk.Align.START, valign=Gtk.Align.END,
margin_start=16, margin_bottom=16,
direction=Gtk.ArrowType.UP, visible=False,
popover_child=PlaylistView(sql))
self._remove = Gtk.Button(child=Adw.ButtonContent(label="Remove",
icon_name="list-remove-symbolic"),
tooltip_text="remove selected tracks",
halign=Gtk.Align.END, valign=Gtk.Align.END,
margin_end=16, margin_bottom=16,
visible=False)
@ -268,3 +258,15 @@ class OSD(Gtk.Overlay):
self.__selection_changed(self.selection, 0, 0)
if self.playlist is not None:
self._add.popover_child.playlist = self.playlist.playlist
@property
def accelerators(self) -> list[ActionEntry]:
"""Get a list of accelerators for the OSD."""
return [ActionEntry("remove-selected-tracks", self._remove.activate,
"Delete", enabled=(self._remove, "visible")),
ActionEntry("move-track-up", self._move._up.activate,
"<Control>Up",
enabled=(self._move, "can-move-up")),
ActionEntry("move-track-down", self._move._down.activate,
"<Control>Down",
enabled=(self._move, "can-move-down"))]

View File

@ -7,10 +7,9 @@ from .. import db
from .. import factory
from .. import playlist
from . import row
from . import selection
class TrackView(Gtk.Frame):
class TrackView(Gtk.ScrolledWindow):
"""A Gtk.ColumnView that has been configured to show Tracks."""
playlist = GObject.Property(type=playlist.playlist.Playlist)
@ -30,8 +29,6 @@ class TrackView(Gtk.Frame):
show_row_separators=True,
enable_rubberband=True,
model=self._selection)
self._scrollwin = Gtk.ScrolledWindow(child=self._columnview)
self._osd = selection.OSD(sql, self._selection, child=self._scrollwin)
self.__append_column("Art", "cover", row.AlbumCover, resizable=False)
self.__append_column("Fav", "favorite", row.FavoriteButton,
@ -57,16 +54,13 @@ class TrackView(Gtk.Frame):
self.__append_column("Filepath", "path", row.PathString, visible=False)
self.bind_property("playlist", self._filtermodel, "model")
self.bind_property("playlist", self._osd, "playlist")
self._osd.bind_property("have-selected", self, "have-selected")
self._osd.bind_property("n-selected", self, "n-selected")
self._selection.bind_property("n-items", self, "n-tracks")
self._selection.connect("items-changed", self.__runtime_changed)
self._columnview.connect("activate", self.__track_activated)
self._columnview.add_css_class("emmental-track-list")
self.set_child(self._osd)
self.set_child(self._columnview)
def __append_column(self, title: str, property: str, row_type: type,
*, width: int = -1, visible: bool = True,
@ -95,15 +89,12 @@ class TrackView(Gtk.Frame):
pos = max(i - 3, 0) * adjustment.get_upper()
adjustment.set_value(pos / self._selection.get_n_items())
def clear_selected_tracks(self) -> None:
"""Clear the currently selected tracks."""
self._osd.clear_selection()
def reset_osd(self) -> None:
"""Reset the OSD."""
self._osd.reset()
@GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel:
"""Get the ListModel for the columns."""
return self._columnview.get_columns()
@GObject.Property(type=Gio.ListModel)
def selection_model(self) -> Gio.ListModel:
"""Get the SelectionModel for the ColumnView."""
return self._columnview.get_model()

View File

@ -185,9 +185,9 @@ class TestHeader(tests.util.TestCase):
"""Check that the accelerators list is set up properly."""
entries = [("open-file", self.header._open.activate, "<Control>o"),
("decrease-volume", self.header._volume.decrement,
"<Control>Down"),
"<Shift><Control>Down"),
("increase-volume", self.header._volume.increment,
"<Control>Up"),
"<Shift><Control>Up"),
("toggle-bg-mode", self.header._background.activate,
"<Shift><Control>b"),
("edit-settings", self.header._settings.activate,

View File

@ -211,6 +211,17 @@ class TestImageToggle(unittest.TestCase):
button2.active = False
self.assertEqual(button2.get_tooltip_text(), "inactive tooltip text")
def test_changing_tooltip_text(self):
"""Test changing the tooltip text for the current state."""
self.assertEqual(self.button.props.tooltip_text, None)
self.button.inactive_tooltip_text = "inactive tooltip"
self.assertEqual(self.button.props.tooltip_text, "inactive tooltip")
self.button.active = True
self.assertEqual(self.button.props.tooltip_text, None)
self.button.active_tooltip_text = "active tooltip"
self.assertEqual(self.button.props.tooltip_text, "active tooltip")
def test_toggle(self):
"""Test the toggle signal."""
toggled = unittest.mock.Mock()

View File

@ -22,9 +22,9 @@ class TestEmmental(unittest.TestCase):
"""Check that version constants have been set properly."""
self.assertEqual(emmental.MAJOR_VERSION, 3)
self.assertEqual(emmental.MINOR_VERSION, 0)
self.assertEqual(emmental.MICRO_VERSION, 5)
self.assertEqual(emmental.VERSION_NUMBER, "3.0.5")
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.5-debug")
self.assertEqual(emmental.MICRO_VERSION, 6)
self.assertEqual(emmental.VERSION_NUMBER, "3.0.6")
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.6-debug")
def test_application(self):
"""Check that the application instance is initialized properly."""
@ -63,7 +63,7 @@ class TestEmmental(unittest.TestCase):
mock_startup.assert_called()
mock_load.assert_called()
mock_add_window.assert_called_with(self.application.win)
mock_set_useragent.assert_called_with("emmental-debug", "3.0.5")
mock_set_useragent.assert_called_with("emmental-debug", "3.0.6")
@unittest.mock.patch("sys.stdout")
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
@ -128,8 +128,8 @@ class TestEmmental(unittest.TestCase):
self.application.build_window()
for action, accel in [("app.open-file", "<Control>o"),
("app.decrease-volume", "<Control>Down"),
("app.increase-volume", "<Control>Up"),
("app.decrease-volume", "<Shift><Control>Down"),
("app.increase-volume", "<Shift><Control>Up"),
("app.toggle-bg-mode", "<Shift><Control>b"),
("app.edit-settings", "<Shift><Control>s")]:
self.assertEqual(self.application.get_accels_for_action(action),
@ -227,6 +227,16 @@ class TestEmmental(unittest.TestCase):
self.application.player = emmental.audio.Player()
win = self.application.build_window()
for action, accel in [("app.focus-search-track", "<Control>slash"),
("app.clear-selected-tracks", "Escape"),
("app.cycle-loop", "<Control>l"),
("app.toggle-shuffle", "<Control>s"),
("app.remove-selected-tracks", "Delete"),
("app.move-track-up", "<Control>Up"),
("app.move-track-down", "<Control>Down")]:
self.assertEqual(self.application.get_accels_for_action(action),
[accel])
self.assertEqual(win.tracklist.sql, self.application.db)
playlist = self.application.db.playlists.create("Test Playlist")

View File

@ -39,16 +39,21 @@ class TestListRow(unittest.TestCase):
def test_bind_active(self):
"""Test binding a property to the Row's active property."""
self.assertIsNone(self.row.listrow)
self.row.active = True
self.assertFalse(self.row.active)
parent = Gtk.Box()
parent.append(self.row.child)
self.assertEqual(self.row.listrow, parent)
self.row.bind_active("sensitive")
self.assertEqual(len(self.row.bindings), 1)
self.assertTrue(parent.get_state_flags() & Gtk.StateFlags.CHECKED)
self.assertTrue(parent.has_css_class("emmental-active-row"))
self.assertTrue(self.row.active)
self.item.set_sensitive(False)
self.assertFalse(parent.get_state_flags() & Gtk.StateFlags.CHECKED)
self.assertFalse(parent.has_css_class("emmental-active-row"))
self.assertFalse(self.row.active)
def test_bind_and_set_property(self):

View File

@ -7,12 +7,62 @@ from gi.repository import Gio
from gi.repository import Gtk
class TestVisibleColumnRow(unittest.TestCase):
"""Test the Visible Column ListBoxRow."""
def setUp(self):
"""Set up common variables."""
self.row = emmental.tracklist.buttons.VisibleRow("title", True)
def test_init(self):
"""Test that the VisibleRow was set up properly."""
self.assertIsInstance(self.row, Gtk.ListBoxRow)
self.assertIsInstance(self.row.props.child, Gtk.Box)
self.assertEqual(self.row.title, "title")
self.assertTrue(self.row.active)
self.assertEqual(self.row.props.child.props.margin_start, 6)
self.assertEqual(self.row.props.child.props.margin_end, 6)
self.assertEqual(self.row.props.child.props.margin_top, 6)
self.assertEqual(self.row.props.child.props.margin_bottom, 6)
self.assertEqual(self.row.props.child.props.spacing, 6)
row2 = emmental.tracklist.buttons.VisibleRow("title2", False)
self.assertEqual(row2.title, "title2")
self.assertFalse(row2.active)
def test_switch(self):
"""Test the VisibleRow switch."""
self.assertIsInstance(self.row._switch, Gtk.Switch)
self.assertEqual(self.row._switch.props.parent, self.row.props.child)
self.assertTrue(self.row._switch.props.active)
self.row.active = False
self.assertFalse(self.row._switch.props.active)
self.row._switch.props.active = True
self.assertTrue(self.row.active)
row2 = emmental.tracklist.buttons.VisibleRow("title2", False)
self.assertFalse(row2._switch.props.active)
def test_label(self):
"""Test the VisibleRow title label."""
self.assertIsInstance(self.row._label, Gtk.Label)
self.assertEqual(self.row._label.props.label, "title")
self.assertEqual(self.row._switch.get_next_sibling(),
self.row._label)
class TestVisibleColumns(unittest.TestCase):
"""Test the Visible Columns button."""
def setUp(self):
"""Set up common variables."""
self.columns = Gio.ListStore()
self.columns.append(Gtk.ColumnViewColumn(title="title", visible=True))
self.columns.append(Gtk.ColumnViewColumn(title="title2",
visible=False))
self.button = emmental.tracklist.buttons.VisibleColumns(self.columns)
def test_init(self):
@ -20,55 +70,43 @@ class TestVisibleColumns(unittest.TestCase):
self.assertIsInstance(self.button, emmental.buttons.PopoverButton)
self.assertFalse(self.button.get_has_frame())
self.assertEqual(self.button.get_icon_name(), "columns-symbolic")
self.assertEqual(self.button.get_tooltip_text(),
"configure visible columns")
self.assertEqual(self.button.columns, self.columns)
def test_popover_child(self):
"""Test that the popover_child was set up properly."""
self.assertIsInstance(self.button.popover_child, Gtk.ColumnView)
self.assertIsInstance(self.button._selection, Gtk.NoSelection)
self.assertTrue(self.button.popover_child.get_show_row_separators())
self.assertTrue(self.button.popover_child.has_css_class("data-table"))
self.assertIsInstance(self.button.popover_child, Gtk.ListBox)
self.assertEqual(self.button.popover_child.props.selection_mode,
Gtk.SelectionMode.NONE)
self.assertTrue(self.button.popover_child.has_css_class("boxed-list"))
self.assertEqual(self.button.popover_child.get_model(),
self.button._selection)
self.assertEqual(self.button._selection.get_model(),
self.button.columns)
def test_create_func(self):
"""Test that the Gtk.ListBox creates VisibleRows correctly."""
row = self.button.popover_child.get_row_at_index(0)
self.assertIsInstance(row, emmental.tracklist.buttons.VisibleRow)
self.assertEqual(row.title, "title")
self.assertTrue(row.active)
def test_columns(self):
"""Test the popover_child columns."""
columns = self.button.popover_child.get_columns()
self.assertEqual(len(columns), 2)
row.active = False
self.assertFalse(self.columns[0].get_visible())
row.active = True
self.assertTrue(self.columns[0].get_visible())
self.columns[0].set_visible(False)
self.assertFalse(row.active)
self.assertIsInstance(columns[0].get_factory(),
emmental.factory.InscriptionFactory)
self.assertEqual(columns[0].get_title(), "Column")
self.assertEqual(columns[0].get_fixed_width(), 125)
row = self.button.popover_child.get_row_at_index(1)
self.assertIsInstance(row, emmental.tracklist.buttons.VisibleRow)
self.assertEqual(row.title, "title2")
self.assertFalse(row.active)
self.assertIsInstance(columns[1].get_factory(),
emmental.factory.Factory)
self.assertEqual(columns[1].get_factory().row_type,
emmental.tracklist.buttons.VisibleSwitch)
self.assertEqual(columns[1].get_title(), "Visible")
self.assertEqual(columns[1].get_fixed_width(), -1)
def test_visible_switch(self):
"""Test the visible switch widget."""
item = Gtk.Label()
listitem = Gtk.ListItem()
listitem.get_item = unittest.mock.Mock(return_value=item)
switch = emmental.tracklist.buttons.VisibleSwitch(listitem)
self.assertIsInstance(switch, emmental.factory.ListRow)
self.assertIsInstance(switch.child, Gtk.Switch)
switch.bind()
self.assertEqual(len(switch.bindings), 1)
self.assertTrue(switch.child.get_active())
item.set_visible(False)
self.assertFalse(switch.child.get_active())
switch.child.set_active(True)
item.set_visible(True)
def test_activate(self):
"""Test activating a Gtk.ListBox row."""
row = self.button.popover_child.get_row_at_index(0)
self.button.popover_child.emit("row-activated", row)
self.assertFalse(row.active)
self.button.popover_child.emit("row-activated", row)
self.assertTrue(row.active)
class TestLoopButton(unittest.TestCase):
@ -83,7 +121,9 @@ class TestLoopButton(unittest.TestCase):
self.assertIsInstance(self.loop, emmental.buttons.ImageToggle)
self.assertEqual(self.loop.active_icon_name,
"media-playlist-repeat-song")
self.assertEqual(self.loop.active_tooltip_text, "loop: track")
self.assertEqual(self.loop.inactive_icon_name, "media-playlist-repeat")
self.assertEqual(self.loop.inactive_tooltip_text, "loop: disabled")
self.assertFalse(self.loop.large_icon)
self.assertFalse(self.loop.get_has_frame())
@ -93,26 +133,31 @@ class TestLoopButton(unittest.TestCase):
self.assertEqual(self.loop.state, "None")
self.assertAlmostEqual(self.loop.icon_opacity, 0.5, delta=0.005)
self.assertEqual(self.loop.props.tooltip_text, "loop: disabled")
self.assertFalse(self.loop.active)
self.loop.state = "Playlist"
self.assertEqual(self.loop.state, "Playlist")
self.assertEqual(self.loop.icon_opacity, 1.0)
self.assertEqual(self.loop.props.tooltip_text, "loop: playlist")
self.assertFalse(self.loop.active)
self.loop.state = "Track"
self.assertEqual(self.loop.state, "Track")
self.assertEqual(self.loop.icon_opacity, 1.0)
self.assertEqual(self.loop.props.tooltip_text, "loop: track")
self.assertTrue(self.loop.active)
self.loop.can_disable = False
self.loop.state = "None"
self.assertEqual(self.loop.state, "Track")
self.assertEqual(self.loop.props.tooltip_text, "loop: track")
self.assertTrue(self.loop.active)
self.loop.can_disable = True
self.loop.state = "None"
self.assertAlmostEqual(self.loop.icon_opacity, 0.5, delta=0.005)
self.assertEqual(self.loop.inactive_tooltip_text, "loop: disabled")
self.assertFalse(self.loop.active)
def test_click(self):
@ -143,14 +188,20 @@ class TestShuffleButtons(unittest.TestCase):
def test_init(self):
"""Test that the shuffle button is configured correctly."""
self.assertIsInstance(self.shuffle, emmental.buttons.ImageToggle)
self.assertEqual(self.shuffle.active_icon_name,
"media-playlist-shuffle")
self.assertEqual(self.shuffle.inactive_icon_name,
"media-playlist-consecutive")
self.assertAlmostEqual(self.shuffle.icon_opacity, 0.5, delta=0.005)
self.assertFalse(self.shuffle.large_icon)
self.assertFalse(self.shuffle.get_has_frame())
self.assertEqual(self.shuffle.active_icon_name,
"media-playlist-shuffle")
self.assertEqual(self.shuffle.active_tooltip_text, "shuffle: enabled")
self.assertEqual(self.shuffle.inactive_icon_name,
"media-playlist-consecutive")
self.assertEqual(self.shuffle.inactive_tooltip_text,
"shuffle: disabled")
self.assertAlmostEqual(self.shuffle.icon_opacity, 0.5, delta=0.005)
def test_opacity(self):
"""Test adjusting the opacity based on active state."""
self.shuffle.active = True
@ -159,123 +210,123 @@ class TestShuffleButtons(unittest.TestCase):
self.assertEqual(self.shuffle.icon_opacity, 0.5)
class TestSortFieldWidget(unittest.TestCase):
"""Test the Sort Field widget."""
class TestSortRow(unittest.TestCase):
"""Test the Sort Row ListBoxRow."""
def setUp(self):
"""Set up common variables."""
self.sort = emmental.tracklist.buttons.SortFieldWidget()
self.model = emmental.tracklist.sorter.SortOrderModel()
self.model[0].enable()
self.model[0].reverse()
self.model[1].reverse()
self.row1 = emmental.tracklist.buttons.SortRow(self.model[0])
self.row2 = emmental.tracklist.buttons.SortRow(self.model[1])
def test_init(self):
"""Test that the Sort Field Widget is configured correctly."""
self.assertIsInstance(self.sort, Gtk.Box)
self.assertIsInstance(self.sort._box, Gtk.Box)
self.assertIsInstance(self.sort._name, Gtk.Label)
self.assertIsInstance(self.row1, Gtk.ListBoxRow)
self.assertIsInstance(self.row1.props.child, Gtk.Box)
self.assertTrue(self.sort._name.get_hexpand())
self.assertEqual(self.row1.props.child.props.margin_start, 6)
self.assertEqual(self.row1.props.child.props.margin_end, 6)
self.assertEqual(self.row1.props.child.props.margin_top, 6)
self.assertEqual(self.row1.props.child.props.margin_bottom, 6)
self.assertEqual(self.row1.props.child.props.spacing, 6)
self.assertEqual(self.sort.get_spacing(), 6)
self.assertEqual(self.sort._enabled.get_next_sibling(),
self.sort._name)
self.assertEqual(self.sort._reverse.get_next_sibling(),
self.sort._box)
self.assertTrue(self.sort._box.has_css_class("linked"))
self.assertEqual(self.row1.sort_field, self.model[0])
self.assertTrue(self.row1.active)
def test_set_sort_field(self):
"""Test setting a sort field to the Sort Field Widget."""
self.assertIsNone(self.sort.sort_field)
self.assertEqual(self.row2.sort_field, self.model[1])
self.assertFalse(self.row2.active)
self.sort.set_sort_field(self.model[0])
self.assertEqual(self.sort.sort_field, self.model[0])
self.assertEqual(self.sort._name.get_text(), self.model[0].name)
self.assertTrue(self.sort._enabled.get_active())
self.assertTrue(self.sort._reverse.active)
def test_switch(self):
"""Test the SortRow switch."""
self.assertIsInstance(self.row1._switch, Gtk.Switch)
self.assertEqual(self.row1._switch.props.valign, Gtk.Align.CENTER)
self.assertEqual(self.row1._switch.props.parent, self.row1.props.child)
self.sort.set_sort_field(None)
self.assertIsNone(self.sort.sort_field)
self.assertEqual(self.sort._name.get_text(), "")
self.assertFalse(self.sort._enabled.get_active())
self.assertFalse(self.sort._reverse.active)
self.assertTrue(self.row1._switch.props.active)
self.assertFalse(self.row2._switch.props.active)
def test_enabled(self):
"""Test enabling and disabling a sort field."""
self.assertIsInstance(self.sort._enabled, Gtk.Switch)
self.assertEqual(self.sort._enabled.get_valign(), Gtk.Align.CENTER)
self.assertEqual(self.sort.get_first_child(), self.sort._enabled)
with unittest.mock.patch.object(self.model[0],
"disable") as mock_disable:
self.row1._switch.props.active = False
mock_disable.assert_called()
self.sort._enabled.set_active(True)
with unittest.mock.patch.object(self.model[0],
"enable") as mock_enable:
self.row1._switch.props.active = True
mock_enable.assert_called()
self.sort.set_sort_field(self.model[1])
self.assertFalse(self.sort._name.get_sensitive())
self.assertFalse(self.sort._box.get_sensitive())
self.assertFalse(self.sort._reverse.get_sensitive())
def test_label(self):
"""Test the SortRow title label."""
self.assertIsInstance(self.row1._label, Gtk.Label)
self.assertEqual(self.row1._switch.get_next_sibling(),
self.row1._label)
self.sort._enabled.set_active(True)
self.assertTrue(self.model[1].enabled)
self.assertTrue(self.sort._name.get_sensitive())
self.assertTrue(self.sort._box.get_sensitive())
self.assertTrue(self.sort._reverse.get_sensitive())
self.assertEqual(self.row1._label.props.label, self.model[0].name)
self.assertEqual(self.row1._label.props.xalign, 0.0)
self.assertTrue(self.row1._label.props.hexpand)
self.sort._enabled.set_active(False)
self.assertFalse(self.model[1].enabled)
self.assertFalse(self.sort._name.get_sensitive())
self.assertFalse(self.sort._box.get_sensitive())
self.assertFalse(self.sort._reverse.get_sensitive())
def test_move_down(self):
"""Test the moving a sort field down."""
self.assertIsInstance(self.sort._move_down, Gtk.Button)
self.assertEqual(self.sort._move_down.get_icon_name(),
"go-down-symbolic")
self.assertEqual(self.sort._move_up.get_next_sibling(),
self.sort._move_down)
self.sort._move_down.emit("clicked")
(field := self.model[0]).enable()
self.model[1].enable()
self.sort.set_sort_field(field)
self.sort._move_down.emit("clicked")
self.assertEqual(self.model.index(field), 1)
def test_move_up(self):
"""Test the moving a sort field."""
self.assertIsInstance(self.sort._move_up, Gtk.Button)
self.assertEqual(self.sort._move_up.get_icon_name(), "go-up-symbolic")
self.assertEqual(self.sort._box.get_first_child(),
self.sort._move_up)
self.sort._move_up.emit("clicked")
self.model[0].enable()
(field := self.model[1]).enable()
self.sort.set_sort_field(field)
self.sort._move_up.emit("clicked")
self.assertEqual(self.model.index(field), 0)
self.assertTrue(self.row1._label.props.sensitive)
self.assertFalse(self.row2._label.props.sensitive)
def test_reverse(self):
"""Test reversing a sort field."""
self.assertIsInstance(self.sort._reverse, emmental.buttons.ImageToggle)
self.assertEqual(self.sort._name.get_next_sibling(),
self.sort._reverse)
"""Test the SortRow reverse button."""
self.assertIsInstance(self.row1._reverse, emmental.buttons.ImageToggle)
self.assertEqual(self.row1._label.get_next_sibling(),
self.row1._reverse)
self.assertEqual(self.sort._reverse.active_icon_name, "arrow1-up")
self.assertEqual(self.sort._reverse.inactive_icon_name, "arrow1-down")
self.assertFalse(self.sort._reverse.large_icon)
self.assertEqual(self.row1._reverse.active_icon_name, "arrow1-up")
self.assertEqual(self.row1._reverse.inactive_icon_name, "arrow1-down")
self.assertFalse(self.row1._reverse.props.has_frame)
self.assertFalse(self.row1._reverse.large_icon)
self.sort._reverse.emit("clicked")
self.assertFalse(self.row1._reverse.props.active)
self.assertTrue(self.row1._reverse.props.sensitive)
self.sort.set_sort_field(self.model[0])
self.sort._reverse.emit("clicked")
self.assertFalse(self.model[0].reversed)
self.sort._reverse.emit("clicked")
self.assertTrue(self.model[0].reversed)
self.assertTrue(self.row2._reverse.props.active)
self.assertFalse(self.row2._reverse.props.sensitive)
with unittest.mock.patch.object(self.model[0],
"reverse") as mock_reverse:
self.row1._reverse.emit("toggled")
mock_reverse.assert_called()
def test_move_box(self):
"""Test the box containing the move up & down buttons."""
self.assertIsInstance(self.row1._move_box, Gtk.Box)
self.assertEqual(self.row1._reverse.get_next_sibling(),
self.row1._move_box)
self.assertTrue(self.row1._move_box.has_css_class("linked"))
self.assertTrue(self.row1._move_box.props.sensitive)
self.assertFalse(self.row2._move_box.props.sensitive)
def test_move_up(self):
"""Test the move up button."""
self.assertIsInstance(self.row1._move_up, Gtk.Button)
self.assertEqual(self.row1._move_up.get_icon_name(),
"go-up-symbolic")
self.assertEqual(self.row1._move_up.props.parent,
self.row1._move_box)
with unittest.mock.patch.object(self.model[0],
"move_up") as mock_move_up:
self.row1._move_up.emit("clicked")
mock_move_up.assert_called()
def test_move_down(self):
"""Test the move down button."""
self.assertIsInstance(self.row1._move_down, Gtk.Button)
self.assertEqual(self.row1._move_down.get_icon_name(),
"go-down-symbolic")
self.assertEqual(self.row1._move_up.get_next_sibling(),
self.row1._move_down)
with unittest.mock.patch.object(self.model[0],
"move_down") as mock_move_down:
self.row1._move_down.emit("clicked")
mock_move_down.assert_called()
class TestSortButton(unittest.TestCase):
@ -288,45 +339,44 @@ class TestSortButton(unittest.TestCase):
def test_init(self):
"""Test that the Sort button is configured correctly."""
self.assertIsInstance(self.sort, emmental.buttons.PopoverButton)
self.assertEqual(self.sort.get_icon_name(),
"view-list-ordered-symbolic")
self.assertEqual(self.sort.get_tooltip_text(),
"configure playlist sort order")
self.assertFalse(self.sort.get_has_frame())
def test_popover_child(self):
"""Test that the popover_child is configured correctly."""
self.assertIsInstance(self.sort.popover_child, Gtk.ListView)
self.assertIsInstance(self.sort.model,
emmental.tracklist.sorter.SortOrderModel)
self.assertIsInstance(self.sort._selection, Gtk.NoSelection)
self.assertIsInstance(self.sort._factory, emmental.factory.Factory)
self.assertIsInstance(self.sort.popover_child, Gtk.ListBox)
self.assertEqual(self.sort.popover_child.props.selection_mode,
Gtk.SelectionMode.NONE)
self.assertTrue(self.sort.popover_child.has_css_class("boxed-list"))
self.assertTrue(self.sort.popover_child.get_show_separators())
def test_create_func(self):
"""Test that the Gtk.ListBox creates SortRows correctly."""
row = self.sort.popover_child.get_row_at_index(0)
self.assertIsInstance(row, emmental.tracklist.buttons.SortRow)
self.assertEqual(row.sort_field, self.sort.model[0])
self.assertEqual(self.sort.popover_child.get_model(),
self.sort._selection)
self.assertEqual(self.sort._selection.get_model(), self.sort.model)
self.assertEqual(self.sort.popover_child.get_factory(),
self.sort._factory)
self.assertEqual(self.sort._factory.row_type,
emmental.tracklist.buttons.SortRow)
def test_activate(self):
"""Test activating a Gtk.ListBox sort row."""
row = self.sort.popover_child.get_row_at_index(0)
field = row.sort_field
self.assertFalse(field.enabled)
self.assertFalse(field.reversed)
def test_sort_row(self):
"""Test the Sort Row object."""
(field := self.sort.model[0]).enable()
listitem = Gtk.ListItem()
listitem.get_item = lambda: field
self.sort.model[1].enable()
with unittest.mock.patch.object(field, "enable") as mock_enable:
self.sort.popover_child.emit("row-activated", row)
mock_enable.assert_called()
row = emmental.tracklist.buttons.SortRow(listitem)
self.assertIsInstance(row, emmental.factory.ListRow)
self.assertIsInstance(row.child,
emmental.tracklist.buttons.SortFieldWidget)
mock_enable.reset_mock()
row.active = True
row.bind()
self.assertEqual(row.child.sort_field, field)
row.unbind()
self.assertIsNone(row.child.sort_field)
with unittest.mock.patch.object(field, "reverse") as mock_reverse:
self.sort.popover_child.emit("row-activated", row)
self.assertTrue(row._reverse.active)
mock_enable.assert_not_called()
mock_reverse.assert_called()
def test_sort_order(self):
"""Test the sort-order property."""

View File

@ -41,11 +41,13 @@ class TestTrackRowWidgets(tests.util.TestCase):
row = emmental.tracklist.row.TrackRow(self.listitem, "property")
self.assertIsInstance(row, emmental.factory.ListRow)
self.assertIsNone(row.album_binding)
self.assertIsNone(row.listrow)
self.assertEqual(row.property, "property")
self.assertEqual(row.mediumid, 0)
row.child = Gtk.Label()
self.columnrow.set_child(row.child)
self.assertEqual(row.listrow, self.listrow)
self.library.online = False
self.track.active = True
@ -54,8 +56,7 @@ class TestTrackRowWidgets(tests.util.TestCase):
self.assertFalse(self.listitem.get_activatable())
self.assertFalse(row.child.get_sensitive())
self.assertTrue(row.active)
self.assertTrue(self.listrow.get_state_flags() &
Gtk.StateFlags.CHECKED)
self.assertTrue(self.listrow.has_css_class("emmental-active-row"))
self.library.online = True
self.track.active = False
@ -63,8 +64,7 @@ class TestTrackRowWidgets(tests.util.TestCase):
self.assertTrue(self.listitem.get_activatable())
self.assertTrue(row.child.get_sensitive())
self.assertFalse(row.active)
self.assertFalse(self.listrow.get_state_flags() &
Gtk.StateFlags.CHECKED)
self.assertFalse(self.listrow.has_css_class("emmental-active-row"))
def test_inscription_row(self):
"""Test the base Inscription Row."""

View File

@ -10,83 +10,65 @@ from gi.repository import Gtk
from gi.repository import Adw
class TestPlaylistRowWidget(unittest.TestCase):
"""Test the Playlist Row Widget."""
class TestPlaylistRow(unittest.TestCase):
"""Test the PlaylistRow Widget."""
def setUp(self):
"""Set up common variables."""
self.widget = emmental.tracklist.selection.PlaylistRowWidget()
cover = tests.util.COVER_JPG
self.row = emmental.tracklist.selection.PlaylistRow("name", cover)
def test_init(self):
"""Test that the Playlist Row Widget is set up properly."""
self.assertIsInstance(self.widget, Gtk.Box)
"""Test that the PlaylistRow Widget is set up properly."""
self.assertIsInstance(self.row, Gtk.ListBoxRow)
self.assertIsInstance(self.row.props.child, Gtk.Box)
self.assertEqual(self.row.name, "name")
self.assertEqual(self.row.image, tests.util.COVER_JPG)
self.assertEqual(self.row.props.child.props.margin_start, 6)
self.assertEqual(self.row.props.child.props.margin_end, 6)
self.assertEqual(self.row.props.child.props.margin_top, 6)
self.assertEqual(self.row.props.child.props.margin_bottom, 6)
self.assertEqual(self.row.props.child.props.spacing, 6)
def test_label(self):
"""Test the Playlist Row Widget label."""
self.widget.name = "Test Playlist Name"
self.assertIsInstance(self.widget._label, Gtk.Label)
self.assertEqual(self.widget._label.get_text(), "Test Playlist Name")
self.assertEqual(self.widget._label.get_xalign(), 0.0)
self.assertEqual(self.widget._icon.get_next_sibling(),
self.widget._label)
"""Test the PlaylistRow Widget label."""
self.assertIsInstance(self.row._label, Gtk.Label)
self.assertEqual(self.row._label.props.label, "name")
self.assertEqual(self.row._icon.get_next_sibling(), self.row._label)
def test_icon(self):
"""Test the Playlist Row Widget icon."""
self.assertIsInstance(self.widget._icon, Adw.Avatar)
self.assertEqual(self.widget.get_first_child(), self.widget._icon)
self.assertEqual(self.widget._icon.get_size(), 32)
self.widget.name = "Favorite Tracks"
self.assertEqual(self.widget._icon.get_icon_name(),
"heart-filled-symbolic")
self.assertEqual(self.widget._icon.get_text(), "Favorite Tracks")
self.widget.name = "Queued Tracks"
self.assertEqual(self.widget._icon.get_icon_name(),
"music-queue-symbolic")
self.assertEqual(self.widget._icon.get_text(), "Queued Tracks")
self.widget.name = "Any Other Name"
self.assertEqual(self.widget._icon.get_icon_name(),
"""Test the PlaylistRow Widget icon."""
self.assertIsInstance(self.row._icon, Adw.Avatar)
self.assertEqual(self.row.props.child.get_first_child(),
self.row._icon)
self.assertEqual(self.row._icon.get_size(), 32)
self.assertEqual(self.row._icon.get_text(), "name")
self.assertEqual(self.row._icon.get_icon_name(),
"playlist2-symbolic")
self.assertEqual(self.widget._icon.get_text(), "Any Other Name")
fav = emmental.tracklist.selection.PlaylistRow("Favorite Tracks", None)
self.assertEqual(fav._icon.props.icon_name, "heart-filled-symbolic")
queue = emmental.tracklist.selection.PlaylistRow("Queued Tracks", None)
self.assertEqual(queue._icon.props.icon_name, "music-queue-symbolic")
def test_image(self):
"""Test the Playlist Row Widget image."""
self.assertIsNone(self.widget.image)
self.widget.image = tests.util.COVER_JPG
self.assertIsNotNone(self.widget._icon.get_custom_image())
self.widget.image = None
self.assertIsNone(self.widget._icon.get_custom_image())
self.widget.image = pathlib.Path("/a/b/c.jpg")
self.assertIsNone(self.widget._icon.get_custom_image())
"""Test the PlaylistRow Widget image."""
self.assertIsNotNone(self.row._icon.props.custom_image)
none = emmental.tracklist.selection.PlaylistRow("none", None)
self.assertIsNone(none.image)
self.assertIsNone(none._icon.props.custom_image)
class TestPlaylistRow(tests.util.TestCase):
"""Test the PlaylistRow widget."""
later = emmental.tracklist.selection.PlaylistRow("later", None)
later.image = tests.util.COVER_JPG
self.assertIsNotNone(later._icon.props.custom_image)
def setUp(self):
"""Set up common variables."""
super().setUp()
self.playlist = self.sql.playlists.create("Test Playlist")
self.listitem = Gtk.ListItem()
self.listitem.get_item = unittest.mock.Mock(return_value=self.playlist)
self.row = emmental.tracklist.selection.PlaylistRow(self.listitem)
def test_init(self):
"""Test that the PlaylistRow is initialized properly."""
self.assertIsInstance(self.row, emmental.factory.ListRow)
self.assertIsInstance(self.row.child,
emmental.tracklist.selection.PlaylistRowWidget)
def test_bind(self):
"""Test binding the PlaylistRow."""
self.row.bind()
self.assertEqual(self.row.child.name, "Test Playlist")
self.assertIsNone(self.row.child.image)
self.playlist.image = pathlib.Path("/a/b/c.jpg")
self.assertEqual(self.row.child.image, pathlib.Path("/a/b/c.jpg"))
path = pathlib.Path("/a/b/c.jpg")
inval = emmental.tracklist.selection.PlaylistRow("inval", path)
self.assertIsNone(inval._icon.props.custom_image)
class TestUserTracksFilter(tests.util.TestCase):
@ -134,35 +116,38 @@ class TestPlaylistView(tests.util.TestCase):
def test_init(self):
"""Test that the Playlist View is set up properly."""
self.assertIsInstance(self.view, Gtk.ListView)
self.assertIsInstance(self.view, Gtk.ListBox)
self.assertEqual(self.view.props.selection_mode,
Gtk.SelectionMode.NONE)
self.assertTrue(self.view.has_css_class("boxed-list"))
self.assertTrue(self.view.get_show_separators())
self.assertTrue(self.view.get_single_click_activate())
self.assertTrue(self.view.has_css_class("rich-list"))
def test_models(self):
"""Test that the models have been connected correctly."""
self.assertIsInstance(self.view._selection, Gtk.NoSelection)
def test_filter_model(self):
"""Test that the filter model has been connected correctly."""
self.assertIsInstance(self.view._filtered, Gtk.FilterListModel)
self.assertIsInstance(self.view._filtered.get_filter(),
emmental.tracklist.selection.UserTracksFilter)
self.assertEqual(self.view.get_model(), self.view._selection)
self.assertEqual(self.view._selection.get_model(), self.view._filtered)
self.assertEqual(self.view._filtered.get_model(), self.sql.playlists)
def test_factory(self):
"""Test that the factory has been configured correctly."""
self.assertIsInstance(self.view._factory, emmental.factory.Factory)
self.assertEqual(self.view.get_factory(), self.view._factory)
self.assertEqual(self.view._factory.row_type,
emmental.tracklist.selection.PlaylistRow)
self.view.playlist = self.sql.playlists.collection
self.assertEqual(self.view._filtered.get_filter().playlist,
self.sql.playlists.collection)
def test_create_func(self):
"""Test that the PlaylistView creates PlaylistRows correctly."""
row = self.view.get_row_at_index(0)
self.assertIsInstance(row, emmental.tracklist.selection.PlaylistRow)
self.assertEqual(row.name, "Favorite Tracks")
self.assertEqual(row.image, None)
self.sql.playlists.favorites.image = tests.util.COVER_JPG
self.assertEqual(row.image, tests.util.COVER_JPG)
def test_activate(self):
"""Test activating a Playlist Row for adding tracks."""
selected = unittest.mock.Mock()
self.view.connect("playlist-selected", selected)
self.view.emit("activate", 0)
self.view.emit("row-activated", self.view.get_row_at_index(0))
selected.assert_called_with(self.view, self.sql.playlists.favorites)
@ -184,6 +169,8 @@ class TestMoveButtons(unittest.TestCase):
"""Test the move down button."""
self.assertIsInstance(self.move._down, Gtk.Button)
self.assertEqual(self.move._down.get_icon_name(), "go-down-symbolic")
self.assertEqual(self.move._down.get_tooltip_text(),
"move selected track down")
self.assertTrue(self.move._down.has_css_class("opaque"))
self.assertTrue(self.move._down.has_css_class("pill"))
self.assertTrue(self.move._down.get_hexpand())
@ -209,6 +196,8 @@ class TestMoveButtons(unittest.TestCase):
"""Test the move up button."""
self.assertIsInstance(self.move._up, Gtk.Button)
self.assertEqual(self.move._up.get_icon_name(), "go-up-symbolic")
self.assertEqual(self.move._up.get_tooltip_text(),
"move selected track up")
self.assertTrue(self.move._up.has_css_class("opaque"))
self.assertTrue(self.move._up.has_css_class("pill"))
self.assertTrue(self.move._up.get_hexpand())
@ -264,6 +253,8 @@ class TestOsd(tests.util.TestCase):
self.assertEqual(self.osd._add.get_child().get_icon_name(),
"list-add-symbolic")
self.assertEqual(self.osd._add.get_child().get_label(), "Add")
self.assertEqual(self.osd._add.get_tooltip_text(),
"add selected tracks to a playlist")
self.assertEqual(self.osd._add.get_halign(), Gtk.Align.START)
self.assertEqual(self.osd._add.get_valign(), Gtk.Align.END)
self.assertEqual(self.osd._add.get_margin_start(), 16)
@ -313,6 +304,8 @@ class TestOsd(tests.util.TestCase):
self.assertEqual(self.osd._remove.get_child().get_icon_name(),
"list-remove-symbolic")
self.assertEqual(self.osd._remove.get_child().get_label(), "Remove")
self.assertEqual(self.osd._remove.get_tooltip_text(),
"remove selected tracks")
self.assertEqual(self.osd._remove.get_halign(), Gtk.Align.END)
self.assertEqual(self.osd._remove.get_valign(), Gtk.Align.END)
self.assertEqual(self.osd._remove.get_margin_end(), 16)
@ -463,3 +456,28 @@ class TestOsd(tests.util.TestCase):
self.osd.reset()
mock_unselect.assert_called()
self.assertFalse(self.osd.have_selected)
def test_accelerators(self):
"""Test that the accelerators list is set up properly."""
entries = [("remove-selected-tracks", self.osd._remove.activate,
["Delete"], self.osd._remove, "visible"),
("move-track-up", self.osd._move._up.activate,
["<Control>Up"], self.osd._move, "can-move-up"),
("move-track-down", self.osd._move._down.activate,
["<Control>Down"], self.osd._move, "can-move-down")]
accels = self.osd.accelerators
self.assertIsInstance(accels, list)
for i, (name, func, accel, gobject, prop) in enumerate(entries):
with self.subTest(action=name):
self.assertIsInstance(accels[i], emmental.action.ActionEntry)
self.assertEqual(accels[i].name, name)
self.assertEqual(accels[i].func, func)
self.assertEqual(accels[i].accels, accel)
if gobject and prop:
enabled = gobject.get_property(prop)
self.assertEqual(accels[i].enabled, enabled)
gobject.set_property(prop, not enabled)
self.assertEqual(accels[i].enabled, not enabled)

View File

@ -27,11 +27,9 @@ class TestTracklist(tests.util.TestCase):
self.assertIsInstance(self.tracklist._top_right, Gtk.Box)
self.assertEqual(self.tracklist.sql, self.sql)
self.assertEqual(self.tracklist.get_spacing(), 6)
self.assertEqual(self.tracklist.get_orientation(),
Gtk.Orientation.VERTICAL)
self.assertEqual(self.tracklist._top_box.get_margin_top(), 6)
self.assertEqual(self.tracklist._top_box.get_margin_start(), 6)
self.assertEqual(self.tracklist._top_box.get_margin_end(), 6)
self.assertEqual(self.tracklist._top_box.get_orientation(),
@ -44,6 +42,7 @@ class TestTracklist(tests.util.TestCase):
self.assertEqual(self.tracklist._top_box.get_end_widget(),
self.tracklist._top_right)
self.assertTrue(self.tracklist._top_box.has_css_class("toolbar"))
self.assertTrue(self.tracklist.has_css_class("card"))
def test_visible_columns(self):
@ -61,14 +60,16 @@ class TestTracklist(tests.util.TestCase):
self.assertIsInstance(self.tracklist._unselect, Gtk.Button)
self.assertEqual(self.tracklist._unselect.get_icon_name(),
"edit-select-none-symbolic")
self.assertEqual(self.tracklist._unselect.get_tooltip_text(),
"unselect all tracks")
self.assertFalse(self.tracklist._unselect.get_has_frame())
self.assertFalse(self.tracklist._unselect.get_sensitive())
self.tracklist._trackview.have_selected = True
self.assertTrue(self.tracklist._unselect.get_sensitive())
with unittest.mock.patch.object(self.tracklist._trackview,
"clear_selected_tracks") as mock_clear:
with unittest.mock.patch.object(self.tracklist._osd.selection,
"unselect_all") as mock_clear:
self.tracklist._unselect.emit("clicked")
mock_clear.assert_called()
@ -161,23 +162,45 @@ class TestTracklist(tests.util.TestCase):
"""Test the Trackview widget."""
self.assertIsInstance(self.tracklist._trackview,
emmental.tracklist.trackview.TrackView)
self.assertEqual(self.tracklist._top_box.get_next_sibling(),
self.tracklist._trackview)
self.assertEqual(self.tracklist._trackview.get_margin_start(), 6)
self.assertEqual(self.tracklist._trackview.get_margin_end(), 6)
self.assertEqual(self.tracklist.columns,
self.tracklist._trackview.columns)
sep = self.tracklist._top_box.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertEqual(sep.get_orientation(), Gtk.Orientation.HORIZONTAL)
self.assertEqual(sep.get_next_sibling(), self.tracklist._osd)
self.assertEqual(self.tracklist._osd.get_child(),
self.tracklist._trackview)
def test_osd(self):
"""Test the OSD widget."""
self.assertIsInstance(self.tracklist._osd,
emmental.tracklist.selection.OSD)
self.assertEqual(self.tracklist._osd.sql, self.sql)
self.assertEqual(self.tracklist._osd.selection,
self.tracklist._trackview.selection_model)
self.assertFalse(self.tracklist._trackview.have_selected)
self.tracklist._osd.have_selected = True
self.assertTrue(self.tracklist._trackview.have_selected)
self.assertEqual(self.tracklist._trackview.n_selected, 0)
self.tracklist._osd.n_selected = 4
self.assertEqual(self.tracklist._trackview.n_selected, 4)
def test_footer(self):
"""Test that the footer is wired up properly."""
self.assertIsInstance(self.tracklist._footer,
emmental.tracklist.footer.Footer)
self.assertEqual(self.tracklist._footer.get_margin_start(), 6)
self.assertEqual(self.tracklist._footer.get_margin_end(), 6)
self.assertEqual(self.tracklist._footer.get_margin_top(), 6)
self.assertEqual(self.tracklist._footer.get_margin_bottom(), 6)
self.assertEqual(self.tracklist._trackview.get_next_sibling(),
self.tracklist._footer)
sep = self.tracklist._osd.get_next_sibling()
self.assertIsInstance(sep, Gtk.Separator)
self.assertEqual(sep.get_orientation(), Gtk.Orientation.HORIZONTAL)
self.assertEqual(sep.get_next_sibling(), self.tracklist._footer)
self.tracklist._trackview.n_tracks = 5
self.tracklist._trackview.n_selected = 3
@ -192,11 +215,12 @@ class TestTracklist(tests.util.TestCase):
self.assertIsNone(self.tracklist.playlist)
self.assertFalse(self.tracklist._top_right.get_sensitive())
with unittest.mock.patch.object(self.tracklist._trackview,
"reset_osd") as mock_reset_osd:
with unittest.mock.patch.object(self.tracklist._osd,
"reset") as mock_reset_osd:
self.tracklist.playlist = self.playlist
self.assertEqual(self.tracklist.playlist, self.playlist)
self.assertEqual(self.tracklist._trackview.playlist, self.playlist)
self.assertEqual(self.tracklist._osd.playlist, self.playlist)
self.assertTrue(self.tracklist._top_right.get_sensitive())
mock_reset_osd.assert_called()
@ -226,3 +250,40 @@ class TestTracklist(tests.util.TestCase):
self.assertEqual(self.tracklist._Card__scroll_idle(None),
GLib.SOURCE_REMOVE)
mock_scroll.assert_called_with(None)
def test_accelerators(self):
"""Check that the accelerators list is set up properly."""
entries = [("focus-search-track", self.tracklist._filter.grab_focus,
["<Control>slash"], None, None),
("clear-selected-tracks", self.tracklist._unselect.activate,
["Escape"], self.tracklist._unselect, "sensitive"),
("cycle-loop", self.tracklist._loop.activate,
["<Control>l"], self.tracklist._top_right, "sensitive"),
("toggle-shuffle", self.tracklist._shuffle.activate,
["<Control>s"], self.tracklist._top_right, "sensitive")]
accels = self.tracklist.accelerators
self.assertIsInstance(accels, list)
for i, (name, func, accel, gobject, prop) in enumerate(entries):
with self.subTest(action=name):
self.assertIsInstance(accels[i], emmental.action.ActionEntry)
self.assertEqual(accels[i].name, name)
self.assertEqual(accels[i].func, func)
self.assertListEqual(accels[i].accels, accel)
if gobject and prop:
enabled = gobject.get_property(prop)
self.assertEqual(accels[i].enabled, enabled)
gobject.set_property(prop, not enabled)
self.assertEqual(accels[i].enabled, not enabled)
start = len(entries)
osd_accels = self.tracklist._osd.accelerators
for i, accel in enumerate(osd_accels):
with self.subTest(name=accel.name):
self.assertIsInstance(accels[start + i],
emmental.action.ActionEntry)
self.assertEqual(accels[start + i].name, accel.name)
self.assertEqual(accels[start + i].func, accel.func)
self.assertListEqual(accels[start + i].accels, accel.accels)

View File

@ -28,15 +28,8 @@ class TestTrackView(tests.util.TestCase):
def test_init(self):
"""Test that the TrackView is initialized properly."""
self.assertIsInstance(self.trackview, Gtk.Frame)
self.assertIsInstance(self.trackview._scrollwin, Gtk.ScrolledWindow)
self.assertIsInstance(self.trackview._osd,
emmental.tracklist.selection.OSD)
self.assertEqual(self.trackview.get_child(), self.trackview._osd)
self.assertEqual(self.trackview._osd.get_child(),
self.trackview._scrollwin)
self.assertEqual(self.trackview._scrollwin.get_child(),
self.assertIsInstance(self.trackview, Gtk.ScrolledWindow)
self.assertEqual(self.trackview.get_child(),
self.trackview._columnview)
def test_list_models(self):
@ -50,6 +43,8 @@ class TestTrackView(tests.util.TestCase):
self.assertEqual(self.trackview._selection.get_model(),
self.trackview._filtermodel)
self.assertEqual(self.trackview.selection_model,
self.trackview._selection)
def test_columnview(self):
"""Test the columnview."""
@ -80,27 +75,12 @@ class TestTrackView(tests.util.TestCase):
requested.assert_called_with(self.playlist, self.track)
mock_unselect.assert_called()
def test_clear_selected_tracks(self):
"""Test the clear_selected_tracks() function."""
with unittest.mock.patch.object(self.trackview._osd,
"clear_selection") as mock_clear:
self.trackview.clear_selected_tracks()
mock_clear.assert_called()
def test_reset_osd(self):
"""Test the reset_osd() function."""
with unittest.mock.patch.object(self.trackview._osd,
"reset") as mock_reset:
self.trackview.reset_osd()
mock_reset.assert_called()
def test_playlist(self):
"""Test the playlist property."""
self.assertIsNone(self.trackview.playlist)
self.trackview.playlist = self.playlist
self.assertEqual(self.trackview._filtermodel.get_model(),
self.playlist)
self.assertEqual(self.trackview._osd.playlist, self.playlist)
def test_n_tracks(self):
"""Test the n-tracks property."""
@ -116,17 +96,6 @@ class TestTrackView(tests.util.TestCase):
self.db_plist.add_track(self.track)
self.assertEqual(self.trackview.runtime, 10.0)
def test_n_selected(self):
"""Test the n-selected and have-selected properties."""
self.assertEqual(self.trackview.n_selected, 0)
self.assertFalse(self.trackview.have_selected)
self.trackview._osd.n_selected = 4
self.trackview._osd.have_selected = True
self.assertEqual(self.trackview.n_selected, 4)
self.assertTrue(self.trackview.have_selected)
class TestTrackViewColumns(tests.util.TestCase):
"""Test the TrackView Columns."""