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 MAJOR_VERSION = 3
MINOR_VERSION = 0 MINOR_VERSION = 0
MICRO_VERSION = 5 MICRO_VERSION = 6
VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}" VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}"
VERSION_STRING = f"Emmental {VERSION_NUMBER}{gsetup.DEBUG_STR}" 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", self.db.settings.bind_setting(f"tracklist.{name}.visible",
column, "visible") column, "visible")
self.factory.bind_property("visible-playlist", track_list, "playlist") self.factory.bind_property("visible-playlist", track_list, "playlist")
self.__add_accelerators(track_list.accelerators)
return track_list return track_list
def build_window(self) -> window.Window: def build_window(self) -> window.Window:

View File

@ -119,6 +119,13 @@ class ImageToggle(Button):
inactive_tooltip_text=inactive_tooltip_text, inactive_tooltip_text=inactive_tooltip_text,
tooltip_text=inactive_tooltip_text, tooltip_text=inactive_tooltip_text,
active=active, **kwargs) 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: def do_clicked(self) -> None:
"""Handle a click event.""" """Handle a click event."""

View File

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

View File

@ -61,17 +61,17 @@ class ListRow(GObject.GObject):
@GObject.Property(type=bool, default=False) @GObject.Property(type=bool, default=False)
def active(self) -> bool: def active(self) -> bool:
"""Get the active state of this Row.""" """Get the active state of this Row."""
if parent := self.listitem.get_child().get_parent(): if self.listrow is not None:
return parent.get_state_flags() & Gtk.StateFlags.CHECKED return self.listrow.has_css_class("emmental-active-row")
return False return False
@active.setter @active.setter
def active(self, newval: bool) -> None: def active(self, newval: bool) -> None:
if parent := self.listitem.get_child().get_parent(): if self.listrow is not None:
if newval: if newval:
parent.set_state_flags(Gtk.StateFlags.CHECKED, False) self.listrow.add_css_class("emmental-active-row")
else: else:
parent.unset_state_flags(Gtk.StateFlags.CHECKED) self.listrow.remove_css_class("emmental-active-row")
@GObject.Property(type=Gtk.Widget) @GObject.Property(type=Gtk.Widget)
def child(self) -> Gtk.Widget | None: def child(self) -> Gtk.Widget | None:
@ -87,6 +87,11 @@ class ListRow(GObject.GObject):
"""Get the list item for this Row.""" """Get the list item for this Row."""
return self.listitem.get_item() 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): class InscriptionRow(ListRow):
"""A ListRow for displaying Gtk.Inscription widgets.""" """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.""" """Get a list of accelerators for the Header."""
res = [ActionEntry("open-file", self._open.activate, "<Control>o"), res = [ActionEntry("open-file", self._open.activate, "<Control>o"),
ActionEntry("decrease-volume", self._volume.decrement, ActionEntry("decrease-volume", self._volume.decrement,
"<Control>Down"), "<Shift><Control>Down"),
ActionEntry("increase-volume", self._volume.increment, ActionEntry("increase-volume", self._volume.increment,
"<Control>Up"), "<Shift><Control>Up"),
ActionEntry("toggle-bg-mode", self._background.activate, ActionEntry("toggle-bg-mode", self._background.activate,
"<Shift><Control>b")] "<Shift><Control>b")]
if __debug__: if __debug__:

View File

@ -4,12 +4,14 @@ from gi.repository import GObject
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gio from gi.repository import Gio
from gi.repository import Gtk from gi.repository import Gtk
from ..action import ActionEntry
from ..playlist.playlist import Playlist from ..playlist.playlist import Playlist
from ..playlist.previous import Previous from ..playlist.previous import Previous
from .. import db from .. import db
from .. import entry from .. import entry
from . import buttons from . import buttons
from . import footer from . import footer
from . import selection
from . import trackview from . import trackview
@ -21,23 +23,24 @@ class Card(Gtk.Box):
def __init__(self, sql: db.Connection, **kwargs): def __init__(self, sql: db.Connection, **kwargs):
"""Set up the Tracklist widget.""" """Set up the Tracklist widget."""
super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL, super().__init__(sql=sql, orientation=Gtk.Orientation.VERTICAL,
spacing=6, **kwargs) **kwargs)
self._top_left = Gtk.Box() self._top_left = Gtk.Box()
self._top_right = Gtk.Box(sensitive=False) self._top_right = Gtk.Box(sensitive=False)
self._top_box = Gtk.CenterBox(margin_top=6, margin_start=6, self._top_box = Gtk.CenterBox(margin_start=6, margin_end=6)
margin_end=6)
self._filter = entry.Filter("tracks", hexpand=True, self._filter = entry.Filter("tracks", hexpand=True,
margin_start=100, margin_end=100) margin_start=100, margin_end=100)
self._trackview = trackview.TrackView(sql, margin_start=6, self._trackview = trackview.TrackView(sql)
margin_end=6) self._osd = selection.OSD(sql, self._trackview.selection_model,
child=self._trackview)
self._visible_cols = buttons.VisibleColumns(self._trackview.columns) self._visible_cols = buttons.VisibleColumns(self._trackview.columns)
self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic", self._unselect = Gtk.Button(icon_name="edit-select-none-symbolic",
tooltip_text="unselect all tracks",
has_frame=False, sensitive=False) has_frame=False, sensitive=False)
self._loop = buttons.LoopButton() self._loop = buttons.LoopButton()
self._shuffle = buttons.ShuffleButton() self._shuffle = buttons.ShuffleButton()
self._sort = buttons.SortButton() self._sort = buttons.SortButton()
self._footer = footer.Footer(margin_start=6, margin_end=6, 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._visible_cols)
self._top_left.append(self._unselect) self._top_left.append(self._unselect)
@ -51,9 +54,14 @@ class Card(Gtk.Box):
self._top_box.set_end_widget(self._top_right) self._top_box.set_end_widget(self._top_right)
self.append(self._top_box) 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.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-tracks", self._footer, "count")
self._trackview.bind_property("n-selected", self._footer, "selected") self._trackview.bind_property("n-selected", self._footer, "selected")
self._trackview.bind_property("runtime", self._footer, "runtime") self._trackview.bind_property("runtime", self._footer, "runtime")
@ -62,16 +70,14 @@ class Card(Gtk.Box):
"sensitive") "sensitive")
self._filter.connect("search-changed", self.__search_changed) 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._loop.connect("notify::state", self.__update_loop_state)
self._shuffle.connect("notify::active", self.__update_shuffle_state) self._shuffle.connect("notify::active", self.__update_shuffle_state)
self._sort.connect("notify::sort-order", self.__update_sort_order) self._sort.connect("notify::sort-order", self.__update_sort_order)
self._top_box.add_css_class("toolbar")
self.add_css_class("card") 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: def __playlist_notify(self, playlist: Playlist, param) -> None:
match param.name: match param.name:
case "loop": case "loop":
@ -89,7 +95,7 @@ class Card(Gtk.Box):
self._loop.state = self.playlist.loop self._loop.state = self.playlist.loop
self._shuffle.active = self.playlist.shuffle self._shuffle.active = self.playlist.shuffle
self._sort.set_sort_order(self.playlist.sort_order) 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: def __update_loop_state(self, loop: buttons.LoopButton, param) -> None:
if self.playlist.loop != loop.state: 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.disconnect_by_func(self.__playlist_notify)
self._trackview.playlist = newval self._trackview.playlist = newval
self._osd.playlist = newval
if newval is not None: if newval is not None:
self._top_right.set_sensitive(not isinstance(newval, Previous)) self._top_right.set_sensitive(not isinstance(newval, Previous))
self.__set_button_state() self.__set_button_state()
newval.connect("notify", self.__playlist_notify) 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 gi.repository import Gtk
from . import sorter from . import sorter
from .. import buttons from .. import buttons
from .. import factory
class VisibleSwitch(factory.ListRow): class VisibleRow(Gtk.ListBoxRow):
"""A list row containing a Gtk.Switch.""" """A ListBoxRow containing a Gtk.Switch and a title Label."""
def __init__(self, listitem: Gtk.ListItem): active = GObject.Property(type=bool, default=True)
"""Initialize a VisibleSwitch ListRow.""" title = GObject.Property(type=str)
super().__init__(listitem=listitem, child=Gtk.Switch())
def do_bind(self) -> None: def __init__(self, title: str, active: bool):
"""Bind the visible property to the switch active property.""" """Initialize a VisibleRow ListBoxRow."""
self.bind_and_set_property("visible", "active", bidirectional=True) 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): class VisibleColumns(buttons.PopoverButton):
@ -28,21 +37,21 @@ class VisibleColumns(buttons.PopoverButton):
def __init__(self, columns: Gio.ListModel, **kwargs): def __init__(self, columns: Gio.ListModel, **kwargs):
"""Initialize the VisibleColumns button.""" """Initialize the VisibleColumns button."""
super().__init__(columns=columns, icon_name="columns-symbolic", super().__init__(columns=columns, icon_name="columns-symbolic",
tooltip_text="configure visible columns",
has_frame=False, **kwargs) has_frame=False, **kwargs)
self._selection = Gtk.NoSelection(model=self.columns) self.popover_child = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self.popover_child = Gtk.ColumnView(model=self._selection, self.popover_child.bind_model(columns, self.__create_func)
show_row_separators=True) self.popover_child.connect("row-activated", self.__row_activated)
self.__append_column(factory.InscriptionFactory("title"), self.popover_child.add_css_class("boxed-list")
"Column", width=125)
self.__append_column(factory.Factory(row_type=VisibleSwitch),
"Visible")
self.popover_child.add_css_class("data-table")
def __append_column(self, factory: factory.Factory, def __create_func(self, column: Gtk.ColumnViewColumn) -> VisibleRow:
title: str, *, width: int = -1) -> None: row = VisibleRow(column.get_title(), column.get_visible())
column = Gtk.ColumnViewColumn(factory=factory, title=title, row.bind_property("active", column, "visible",
fixed_width=width) GObject.BindingFlags.BIDIRECTIONAL)
self.popover_child.append_column(column) return row
def __row_activated(self, box: Gtk.ListBox, row: Gtk.ListBoxRow) -> None:
row.active = not row.active
class LoopButton(buttons.ImageToggle): class LoopButton(buttons.ImageToggle):
@ -53,7 +62,9 @@ class LoopButton(buttons.ImageToggle):
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize a Loop Button.""" """Initialize a Loop Button."""
super().__init__(active_icon_name="media-playlist-repeat-song", super().__init__(active_icon_name="media-playlist-repeat-song",
active_tooltip_text="loop: track",
inactive_icon_name="media-playlist-repeat", inactive_icon_name="media-playlist-repeat",
inactive_tooltip_text="loop: disabled",
large_icon=False, state="None", large_icon=False, state="None",
has_frame=False, **kwargs) has_frame=False, **kwargs)
@ -79,9 +90,11 @@ class LoopButton(buttons.ImageToggle):
case ("None", True): case ("None", True):
self.active = False self.active = False
self.icon_opacity = 0.5 self.icon_opacity = 0.5
self.inactive_tooltip_text = "loop: disabled"
case ("Playlist", _): case ("Playlist", _):
self.active = False self.active = False
self.icon_opacity = 1.0 self.icon_opacity = 1.0
self.inactive_tooltip_text = "loop: playlist"
case ("Track", _): case ("Track", _):
self.active = True self.active = True
self.icon_opacity = 1.0 self.icon_opacity = 1.0
@ -93,7 +106,9 @@ class ShuffleButton(buttons.ImageToggle):
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize a Shuffle Button.""" """Initialize a Shuffle Button."""
super().__init__(active_icon_name="media-playlist-shuffle", super().__init__(active_icon_name="media-playlist-shuffle",
active_tooltip_text="shuffle: enabled",
inactive_icon_name="media-playlist-consecutive", inactive_icon_name="media-playlist-consecutive",
inactive_tooltip_text="shuffle: disabled",
large_icon=False, icon_opacity=0.5, large_icon=False, icon_opacity=0.5,
has_frame=False, **kwargs) has_frame=False, **kwargs)
@ -102,81 +117,57 @@ class ShuffleButton(buttons.ImageToggle):
self.icon_opacity = 1.0 if self.active else 0.5 self.icon_opacity = 1.0 if self.active else 0.5
class SortFieldWidget(Gtk.Box): class SortRow(Gtk.ListBoxRow):
"""A Widget to display in the Sort Order button popover.""" """A ListBoxRow for managing Sort Order."""
active = GObject.Property(type=bool, default=False)
sort_field = GObject.Property(type=sorter.SortField) sort_field = GObject.Property(type=sorter.SortField)
def __init__(self) -> None: def __init__(self, sort_field: sorter.SortField):
"""Initialize a SortField Widget.""" """Initialize a Sort Row."""
super().__init__(spacing=6) super().__init__(sort_field=sort_field, active=sort_field.enabled,
self._enabled = Gtk.Switch(valign=Gtk.Align.CENTER) child=Gtk.Box(margin_start=6, margin_end=6,
self._name = Gtk.Label(hexpand=True, sensitive=False) 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", self._reverse = buttons.ImageToggle("arrow1-up", "arrow1-down",
large_icon=False, sensitive=False) active=sort_field.reversed,
self._box = Gtk.Box(sensitive=False) 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_up = Gtk.Button(icon_name="go-up-symbolic")
self._move_down = Gtk.Button(icon_name="go-down-symbolic") self._move_down = Gtk.Button(icon_name="go-down-symbolic")
self._enabled.bind_property("active", self._name, "sensitive") self._switch.connect("notify::active", self.__toggle_enabled)
self._enabled.bind_property("active", self._reverse, "sensitive") self._reverse.connect("toggled", self.__reverse)
self._enabled.bind_property("active", self._box, "sensitive") 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.props.child.append(self._switch)
self._reverse.connect("clicked", self.__reverse) self.props.child.append(self._label)
self._move_up.connect("clicked", self.__move_item_up) self.props.child.append(self._reverse)
self._move_down.connect("clicked", self.__move_item_down) self.props.child.append(self._move_box)
self.append(self._enabled) self._move_box.append(self._move_up)
self.append(self._name) self._move_box.append(self._move_down)
self.append(self._reverse) self._move_box.add_css_class("linked")
self.append(self._box)
self._box.append(self._move_up) def __toggle_enabled(self, switch: Gtk.Switch, param) -> None:
self._box.append(self._move_down) if switch.props.active:
self._box.add_css_class("linked") self.sort_field.enable()
else:
def __move_item_down(self, button: Gtk.Button) -> None: self.sort_field.disable()
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 __reverse(self, button: buttons.ImageToggle) -> None: 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: def __move_up(self, button: Gtk.Button) -> None:
"""Set the Sort Field displayed by this Widget.""" self.sort_field.move_up()
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_down(self, button: Gtk.Button) -> None:
class SortRow(factory.ListRow): self.sort_field.move_down()
"""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)
class SortButton(buttons.PopoverButton): class SortButton(buttons.PopoverButton):
@ -188,14 +179,24 @@ class SortButton(buttons.PopoverButton):
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize the Sort button.""" """Initialize the Sort button."""
super().__init__(has_frame=False, model=sorter.SortOrderModel(), super().__init__(has_frame=False, model=sorter.SortOrderModel(),
tooltip_text="configure playlist sort order",
icon_name="view-list-ordered-symbolic", **kwargs) icon_name="view-list-ordered-symbolic", **kwargs)
self._selection = Gtk.NoSelection(model=self.model) self.popover_child = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
self._factory = factory.Factory(row_type=SortRow) self.popover_child.bind_model(self.model, self.__create_func)
self.popover_child = Gtk.ListView(model=self._selection, self.popover_child.connect("row-activated", self.__row_activated)
factory=self._factory, self.popover_child.add_css_class("boxed-list")
show_separators=True)
self.model.bind_property("sort-order", self, "sort-order") 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: def set_sort_order(self, newval: str) -> None:
"""Directly set the sort order.""" """Directly set the sort order."""
self.model.set_sort_order(newval) self.model.set_sort_order(newval)

View File

@ -63,23 +63,6 @@ class TrackRow(factory.ListRow):
else: else:
self.bind_album(child_prop) 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) @GObject.Property(type=bool, default=True)
def online(self) -> bool: def online(self) -> bool:
"""Get the online state of this Row.""" """Get the online state of this Row."""
@ -90,6 +73,14 @@ class TrackRow(factory.ListRow):
self.listitem.set_activatable(newval) self.listitem.set_activatable(newval)
self.child.set_sensitive(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): class InscriptionRow(TrackRow):
"""Base class for Track Rows displaying a Gtk.Inscription.""" """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 Gdk
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Adw from gi.repository import Adw
from ..action import ActionEntry
from ..buttons import PopoverButton from ..buttons import PopoverButton
from .. import db from .. import db
from .. import factory
from .. import playlist from .. import playlist
class PlaylistRowWidget(Gtk.Box): class PlaylistRow(Gtk.ListBoxRow):
"""A row widget for Playlists.""" """A ListBoxRow widget for Playlists."""
name = GObject.Property(type=str) name = GObject.Property(type=str)
image = GObject.Property(type=GObject.TYPE_PYOBJECT) image = GObject.Property(type=GObject.TYPE_PYOBJECT)
def __init__(self): def __init__(self, name: str, image: GObject.TYPE_PYOBJECT):
"""Initialize a PlaylistRowWidget.""" """Initialize a PlaylistRow."""
super().__init__() super().__init__(child=Gtk.Box(margin_start=6, margin_end=6,
self._icon = Adw.Avatar(size=32) margin_top=6, margin_bottom=6,
self._label = Gtk.Label(xalign=0.0) 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.connect("notify::image", self.__image_changed)
self.image = image
self.append(self._icon) self.props.child.append(self._icon)
self.append(self._label) self.props.child.append(self._label)
def __name_changed(self, row: Gtk.Box, param) -> None: def __image_changed(self, row: Gtk.ListBoxRow,
match self.name: param: GObject.ParamSpec) -> None:
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:
if self.image is not None and self.image.is_file(): if self.image is not None and self.image.is_file():
texture = Gdk.Texture.new_from_filename(str(self.image)) texture = Gdk.Texture.new_from_filename(str(self.image))
else: else:
@ -45,20 +44,6 @@ class PlaylistRowWidget(Gtk.Box):
self._icon.set_custom_image(texture) 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): class UserTracksFilter(Gtk.Filter):
"""Filters for tracks with user-tracks set to True.""" """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 return playlist.user_tracks and playlist != self.playlist
class PlaylistView(Gtk.ListView): class PlaylistView(Gtk.ListBox):
"""A ListView for selecting Playlists.""" """A ListView for selecting Playlists."""
playlist = GObject.Property(type=db.playlist.Playlist) playlist = GObject.Property(type=db.playlist.Playlist)
def __init__(self, sql: db.Connection): def __init__(self, sql: db.Connection):
"""Initialize the PlaylistView.""" """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, self._filtered = Gtk.FilterListModel(model=sql.playlists,
filter=UserTracksFilter()) 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.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) def __row_activated(self, box: Gtk.ListBox, row: PlaylistRow) -> None:
self.set_factory(self._factory) self.emit("playlist-selected", self._filtered[row.get_index()])
def __playlist_activated(self, view: Gtk.ListView, position: int) -> None: def __create_func(self, playlist: db.playlist.Playlist) -> PlaylistRow:
self.emit("playlist-selected", self._selection[position]) row = PlaylistRow(playlist.name, playlist.image)
playlist.bind_property("image", row, "image")
return row
@GObject.Signal(arg_types=(db.playlists.Playlist,)) @GObject.Signal(arg_types=(db.playlists.Playlist,))
def playlist_selected(self, playlist: db.playlists.Playlist) -> None: def playlist_selected(self, playlist: db.playlists.Playlist) -> None:
@ -115,8 +100,10 @@ class MoveButtons(Gtk.Box):
"""Initialize the Move Buttons.""" """Initialize the Move Buttons."""
super().__init__(**kwargs) super().__init__(**kwargs)
self._down = Gtk.Button(icon_name="go-down-symbolic", self._down = Gtk.Button(icon_name="go-down-symbolic",
tooltip_text="move selected track down",
hexpand=True, sensitive=False) hexpand=True, sensitive=False)
self._up = Gtk.Button(icon_name="go-up-symbolic", self._up = Gtk.Button(icon_name="go-up-symbolic",
tooltip_text="move selected track up",
hexpand=True, sensitive=False) hexpand=True, sensitive=False)
self.bind_property("can-move-down", self._down, "sensitive") self.bind_property("can-move-down", self._down, "sensitive")
@ -165,12 +152,15 @@ class OSD(Gtk.Overlay):
super().__init__(sql=sql, selection=selection, **kwargs) super().__init__(sql=sql, selection=selection, **kwargs)
self._add = PopoverButton(child=Adw.ButtonContent(label="Add", self._add = PopoverButton(child=Adw.ButtonContent(label="Add",
icon_name="list-add-symbolic"), icon_name="list-add-symbolic"),
tooltip_text="add selected tracks "
"to a playlist",
halign=Gtk.Align.START, valign=Gtk.Align.END, halign=Gtk.Align.START, valign=Gtk.Align.END,
margin_start=16, margin_bottom=16, margin_start=16, margin_bottom=16,
direction=Gtk.ArrowType.UP, visible=False, direction=Gtk.ArrowType.UP, visible=False,
popover_child=PlaylistView(sql)) popover_child=PlaylistView(sql))
self._remove = Gtk.Button(child=Adw.ButtonContent(label="Remove", self._remove = Gtk.Button(child=Adw.ButtonContent(label="Remove",
icon_name="list-remove-symbolic"), icon_name="list-remove-symbolic"),
tooltip_text="remove selected tracks",
halign=Gtk.Align.END, valign=Gtk.Align.END, halign=Gtk.Align.END, valign=Gtk.Align.END,
margin_end=16, margin_bottom=16, margin_end=16, margin_bottom=16,
visible=False) visible=False)
@ -268,3 +258,15 @@ class OSD(Gtk.Overlay):
self.__selection_changed(self.selection, 0, 0) self.__selection_changed(self.selection, 0, 0)
if self.playlist is not None: if self.playlist is not None:
self._add.popover_child.playlist = self.playlist.playlist 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 factory
from .. import playlist from .. import playlist
from . import row from . import row
from . import selection
class TrackView(Gtk.Frame): class TrackView(Gtk.ScrolledWindow):
"""A Gtk.ColumnView that has been configured to show Tracks.""" """A Gtk.ColumnView that has been configured to show Tracks."""
playlist = GObject.Property(type=playlist.playlist.Playlist) playlist = GObject.Property(type=playlist.playlist.Playlist)
@ -30,8 +29,6 @@ class TrackView(Gtk.Frame):
show_row_separators=True, show_row_separators=True,
enable_rubberband=True, enable_rubberband=True,
model=self._selection) 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("Art", "cover", row.AlbumCover, resizable=False)
self.__append_column("Fav", "favorite", row.FavoriteButton, 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.__append_column("Filepath", "path", row.PathString, visible=False)
self.bind_property("playlist", self._filtermodel, "model") 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.bind_property("n-items", self, "n-tracks")
self._selection.connect("items-changed", self.__runtime_changed) self._selection.connect("items-changed", self.__runtime_changed)
self._columnview.connect("activate", self.__track_activated) self._columnview.connect("activate", self.__track_activated)
self._columnview.add_css_class("emmental-track-list") 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, def __append_column(self, title: str, property: str, row_type: type,
*, width: int = -1, visible: bool = True, *, width: int = -1, visible: bool = True,
@ -95,15 +89,12 @@ class TrackView(Gtk.Frame):
pos = max(i - 3, 0) * adjustment.get_upper() pos = max(i - 3, 0) * adjustment.get_upper()
adjustment.set_value(pos / self._selection.get_n_items()) 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) @GObject.Property(type=Gio.ListModel)
def columns(self) -> Gio.ListModel: def columns(self) -> Gio.ListModel:
"""Get the ListModel for the columns.""" """Get the ListModel for the columns."""
return self._columnview.get_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.""" """Check that the accelerators list is set up properly."""
entries = [("open-file", self.header._open.activate, "<Control>o"), entries = [("open-file", self.header._open.activate, "<Control>o"),
("decrease-volume", self.header._volume.decrement, ("decrease-volume", self.header._volume.decrement,
"<Control>Down"), "<Shift><Control>Down"),
("increase-volume", self.header._volume.increment, ("increase-volume", self.header._volume.increment,
"<Control>Up"), "<Shift><Control>Up"),
("toggle-bg-mode", self.header._background.activate, ("toggle-bg-mode", self.header._background.activate,
"<Shift><Control>b"), "<Shift><Control>b"),
("edit-settings", self.header._settings.activate, ("edit-settings", self.header._settings.activate,

View File

@ -211,6 +211,17 @@ class TestImageToggle(unittest.TestCase):
button2.active = False button2.active = False
self.assertEqual(button2.get_tooltip_text(), "inactive tooltip text") 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): def test_toggle(self):
"""Test the toggle signal.""" """Test the toggle signal."""
toggled = unittest.mock.Mock() toggled = unittest.mock.Mock()

View File

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

View File

@ -39,16 +39,21 @@ class TestListRow(unittest.TestCase):
def test_bind_active(self): def test_bind_active(self):
"""Test binding a property to the Row's active property.""" """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 = Gtk.Box()
parent.append(self.row.child) parent.append(self.row.child)
self.assertEqual(self.row.listrow, parent)
self.row.bind_active("sensitive") self.row.bind_active("sensitive")
self.assertEqual(len(self.row.bindings), 1) 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.assertTrue(self.row.active)
self.item.set_sensitive(False) 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) self.assertFalse(self.row.active)
def test_bind_and_set_property(self): def test_bind_and_set_property(self):

View File

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

View File

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

View File

@ -10,83 +10,65 @@ from gi.repository import Gtk
from gi.repository import Adw from gi.repository import Adw
class TestPlaylistRowWidget(unittest.TestCase): class TestPlaylistRow(unittest.TestCase):
"""Test the Playlist Row Widget.""" """Test the PlaylistRow Widget."""
def setUp(self): def setUp(self):
"""Set up common variables.""" """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): def test_init(self):
"""Test that the Playlist Row Widget is set up properly.""" """Test that the PlaylistRow Widget is set up properly."""
self.assertIsInstance(self.widget, Gtk.Box) 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): def test_label(self):
"""Test the Playlist Row Widget label.""" """Test the PlaylistRow Widget label."""
self.widget.name = "Test Playlist Name" self.assertIsInstance(self.row._label, Gtk.Label)
self.assertIsInstance(self.widget._label, Gtk.Label) self.assertEqual(self.row._label.props.label, "name")
self.assertEqual(self.widget._label.get_text(), "Test Playlist Name") self.assertEqual(self.row._icon.get_next_sibling(), self.row._label)
self.assertEqual(self.widget._label.get_xalign(), 0.0)
self.assertEqual(self.widget._icon.get_next_sibling(),
self.widget._label)
def test_icon(self): def test_icon(self):
"""Test the Playlist Row Widget icon.""" """Test the PlaylistRow Widget icon."""
self.assertIsInstance(self.widget._icon, Adw.Avatar) self.assertIsInstance(self.row._icon, Adw.Avatar)
self.assertEqual(self.widget.get_first_child(), self.widget._icon) self.assertEqual(self.row.props.child.get_first_child(),
self.assertEqual(self.widget._icon.get_size(), 32) self.row._icon)
self.assertEqual(self.row._icon.get_size(), 32)
self.widget.name = "Favorite Tracks" self.assertEqual(self.row._icon.get_text(), "name")
self.assertEqual(self.widget._icon.get_icon_name(), self.assertEqual(self.row._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(),
"playlist2-symbolic") "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): def test_image(self):
"""Test the Playlist Row Widget image.""" """Test the PlaylistRow Widget image."""
self.assertIsNone(self.widget.image) self.assertIsNotNone(self.row._icon.props.custom_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())
none = emmental.tracklist.selection.PlaylistRow("none", None)
self.assertIsNone(none.image)
self.assertIsNone(none._icon.props.custom_image)
class TestPlaylistRow(tests.util.TestCase): later = emmental.tracklist.selection.PlaylistRow("later", None)
"""Test the PlaylistRow widget.""" later.image = tests.util.COVER_JPG
self.assertIsNotNone(later._icon.props.custom_image)
def setUp(self): path = pathlib.Path("/a/b/c.jpg")
"""Set up common variables.""" inval = emmental.tracklist.selection.PlaylistRow("inval", path)
super().setUp() self.assertIsNone(inval._icon.props.custom_image)
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"))
class TestUserTracksFilter(tests.util.TestCase): class TestUserTracksFilter(tests.util.TestCase):
@ -134,35 +116,38 @@ class TestPlaylistView(tests.util.TestCase):
def test_init(self): def test_init(self):
"""Test that the Playlist View is set up properly.""" """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()) def test_filter_model(self):
self.assertTrue(self.view.get_single_click_activate()) """Test that the filter model has been connected correctly."""
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)
self.assertIsInstance(self.view._filtered, Gtk.FilterListModel) self.assertIsInstance(self.view._filtered, Gtk.FilterListModel)
self.assertIsInstance(self.view._filtered.get_filter(), self.assertIsInstance(self.view._filtered.get_filter(),
emmental.tracklist.selection.UserTracksFilter) 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) self.assertEqual(self.view._filtered.get_model(), self.sql.playlists)
def test_factory(self): self.view.playlist = self.sql.playlists.collection
"""Test that the factory has been configured correctly.""" self.assertEqual(self.view._filtered.get_filter().playlist,
self.assertIsInstance(self.view._factory, emmental.factory.Factory) self.sql.playlists.collection)
self.assertEqual(self.view.get_factory(), self.view._factory)
self.assertEqual(self.view._factory.row_type, def test_create_func(self):
emmental.tracklist.selection.PlaylistRow) """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): def test_activate(self):
"""Test activating a Playlist Row for adding tracks.""" """Test activating a Playlist Row for adding tracks."""
selected = unittest.mock.Mock() selected = unittest.mock.Mock()
self.view.connect("playlist-selected", selected) 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) selected.assert_called_with(self.view, self.sql.playlists.favorites)
@ -184,6 +169,8 @@ class TestMoveButtons(unittest.TestCase):
"""Test the move down button.""" """Test the move down button."""
self.assertIsInstance(self.move._down, Gtk.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_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("opaque"))
self.assertTrue(self.move._down.has_css_class("pill")) self.assertTrue(self.move._down.has_css_class("pill"))
self.assertTrue(self.move._down.get_hexpand()) self.assertTrue(self.move._down.get_hexpand())
@ -209,6 +196,8 @@ class TestMoveButtons(unittest.TestCase):
"""Test the move up button.""" """Test the move up button."""
self.assertIsInstance(self.move._up, Gtk.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_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("opaque"))
self.assertTrue(self.move._up.has_css_class("pill")) self.assertTrue(self.move._up.has_css_class("pill"))
self.assertTrue(self.move._up.get_hexpand()) 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(), self.assertEqual(self.osd._add.get_child().get_icon_name(),
"list-add-symbolic") "list-add-symbolic")
self.assertEqual(self.osd._add.get_child().get_label(), "Add") 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_halign(), Gtk.Align.START)
self.assertEqual(self.osd._add.get_valign(), Gtk.Align.END) self.assertEqual(self.osd._add.get_valign(), Gtk.Align.END)
self.assertEqual(self.osd._add.get_margin_start(), 16) 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(), self.assertEqual(self.osd._remove.get_child().get_icon_name(),
"list-remove-symbolic") "list-remove-symbolic")
self.assertEqual(self.osd._remove.get_child().get_label(), "Remove") 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_halign(), Gtk.Align.END)
self.assertEqual(self.osd._remove.get_valign(), Gtk.Align.END) self.assertEqual(self.osd._remove.get_valign(), Gtk.Align.END)
self.assertEqual(self.osd._remove.get_margin_end(), 16) self.assertEqual(self.osd._remove.get_margin_end(), 16)
@ -463,3 +456,28 @@ class TestOsd(tests.util.TestCase):
self.osd.reset() self.osd.reset()
mock_unselect.assert_called() mock_unselect.assert_called()
self.assertFalse(self.osd.have_selected) 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.assertIsInstance(self.tracklist._top_right, Gtk.Box)
self.assertEqual(self.tracklist.sql, self.sql) self.assertEqual(self.tracklist.sql, self.sql)
self.assertEqual(self.tracklist.get_spacing(), 6)
self.assertEqual(self.tracklist.get_orientation(), self.assertEqual(self.tracklist.get_orientation(),
Gtk.Orientation.VERTICAL) 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_start(), 6)
self.assertEqual(self.tracklist._top_box.get_margin_end(), 6) self.assertEqual(self.tracklist._top_box.get_margin_end(), 6)
self.assertEqual(self.tracklist._top_box.get_orientation(), 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.assertEqual(self.tracklist._top_box.get_end_widget(),
self.tracklist._top_right) self.tracklist._top_right)
self.assertTrue(self.tracklist._top_box.has_css_class("toolbar"))
self.assertTrue(self.tracklist.has_css_class("card")) self.assertTrue(self.tracklist.has_css_class("card"))
def test_visible_columns(self): def test_visible_columns(self):
@ -61,14 +60,16 @@ class TestTracklist(tests.util.TestCase):
self.assertIsInstance(self.tracklist._unselect, Gtk.Button) self.assertIsInstance(self.tracklist._unselect, Gtk.Button)
self.assertEqual(self.tracklist._unselect.get_icon_name(), self.assertEqual(self.tracklist._unselect.get_icon_name(),
"edit-select-none-symbolic") "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_has_frame())
self.assertFalse(self.tracklist._unselect.get_sensitive()) self.assertFalse(self.tracklist._unselect.get_sensitive())
self.tracklist._trackview.have_selected = True self.tracklist._trackview.have_selected = True
self.assertTrue(self.tracklist._unselect.get_sensitive()) self.assertTrue(self.tracklist._unselect.get_sensitive())
with unittest.mock.patch.object(self.tracklist._trackview, with unittest.mock.patch.object(self.tracklist._osd.selection,
"clear_selected_tracks") as mock_clear: "unselect_all") as mock_clear:
self.tracklist._unselect.emit("clicked") self.tracklist._unselect.emit("clicked")
mock_clear.assert_called() mock_clear.assert_called()
@ -161,23 +162,45 @@ class TestTracklist(tests.util.TestCase):
"""Test the Trackview widget.""" """Test the Trackview widget."""
self.assertIsInstance(self.tracklist._trackview, self.assertIsInstance(self.tracklist._trackview,
emmental.tracklist.trackview.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.assertEqual(self.tracklist.columns,
self.tracklist._trackview.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): def test_footer(self):
"""Test that the footer is wired up properly.""" """Test that the footer is wired up properly."""
self.assertIsInstance(self.tracklist._footer, self.assertIsInstance(self.tracklist._footer,
emmental.tracklist.footer.Footer) emmental.tracklist.footer.Footer)
self.assertEqual(self.tracklist._footer.get_margin_start(), 6) 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_end(), 6)
self.assertEqual(self.tracklist._footer.get_margin_top(), 6)
self.assertEqual(self.tracklist._footer.get_margin_bottom(), 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_tracks = 5
self.tracklist._trackview.n_selected = 3 self.tracklist._trackview.n_selected = 3
@ -192,11 +215,12 @@ class TestTracklist(tests.util.TestCase):
self.assertIsNone(self.tracklist.playlist) self.assertIsNone(self.tracklist.playlist)
self.assertFalse(self.tracklist._top_right.get_sensitive()) self.assertFalse(self.tracklist._top_right.get_sensitive())
with unittest.mock.patch.object(self.tracklist._trackview, with unittest.mock.patch.object(self.tracklist._osd,
"reset_osd") as mock_reset_osd: "reset") as mock_reset_osd:
self.tracklist.playlist = self.playlist self.tracklist.playlist = self.playlist
self.assertEqual(self.tracklist.playlist, self.playlist) self.assertEqual(self.tracklist.playlist, self.playlist)
self.assertEqual(self.tracklist._trackview.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()) self.assertTrue(self.tracklist._top_right.get_sensitive())
mock_reset_osd.assert_called() mock_reset_osd.assert_called()
@ -226,3 +250,40 @@ class TestTracklist(tests.util.TestCase):
self.assertEqual(self.tracklist._Card__scroll_idle(None), self.assertEqual(self.tracklist._Card__scroll_idle(None),
GLib.SOURCE_REMOVE) GLib.SOURCE_REMOVE)
mock_scroll.assert_called_with(None) 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): def test_init(self):
"""Test that the TrackView is initialized properly.""" """Test that the TrackView is initialized properly."""
self.assertIsInstance(self.trackview, Gtk.Frame) self.assertIsInstance(self.trackview, Gtk.ScrolledWindow)
self.assertIsInstance(self.trackview._scrollwin, Gtk.ScrolledWindow) self.assertEqual(self.trackview.get_child(),
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.trackview._columnview) self.trackview._columnview)
def test_list_models(self): def test_list_models(self):
@ -50,6 +43,8 @@ class TestTrackView(tests.util.TestCase):
self.assertEqual(self.trackview._selection.get_model(), self.assertEqual(self.trackview._selection.get_model(),
self.trackview._filtermodel) self.trackview._filtermodel)
self.assertEqual(self.trackview.selection_model,
self.trackview._selection)
def test_columnview(self): def test_columnview(self):
"""Test the columnview.""" """Test the columnview."""
@ -80,27 +75,12 @@ class TestTrackView(tests.util.TestCase):
requested.assert_called_with(self.playlist, self.track) requested.assert_called_with(self.playlist, self.track)
mock_unselect.assert_called() 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): def test_playlist(self):
"""Test the playlist property.""" """Test the playlist property."""
self.assertIsNone(self.trackview.playlist) self.assertIsNone(self.trackview.playlist)
self.trackview.playlist = self.playlist self.trackview.playlist = self.playlist
self.assertEqual(self.trackview._filtermodel.get_model(), self.assertEqual(self.trackview._filtermodel.get_model(),
self.playlist) self.playlist)
self.assertEqual(self.trackview._osd.playlist, self.playlist)
def test_n_tracks(self): def test_n_tracks(self):
"""Test the n-tracks property.""" """Test the n-tracks property."""
@ -116,17 +96,6 @@ class TestTrackView(tests.util.TestCase):
self.db_plist.add_track(self.track) self.db_plist.add_track(self.track)
self.assertEqual(self.trackview.runtime, 10.0) 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): class TestTrackViewColumns(tests.util.TestCase):
"""Test the TrackView Columns.""" """Test the TrackView Columns."""