WIP V3: context menu started

Sort by title implemented
This commit is contained in:
Keith Edmunds 2023-11-14 23:45:47 +00:00
parent 1b4411d7e5
commit d5871fe77f
2 changed files with 203 additions and 143 deletions

View File

@ -17,7 +17,13 @@ from PyQt6.QtGui import (
from classes import track_sequence, MusicMusterSignals, PlaylistTrack
from config import Config
from dbconfig import scoped_session, Session
from helpers import file_is_unreadable, get_embedded_time, ms_to_mmss
from helpers import (
file_is_unreadable,
get_embedded_time,
open_in_audacity,
ms_to_mmss,
set_track_metadata,
)
from log import log
from models import Playdates, PlaylistRows, Tracks
@ -57,6 +63,7 @@ class PlaylistRowData:
self.plrid: int = plr.id
self.plr_rownum: int = plr.plr_rownum
self.note: str = plr.note
self.track_id = plr.track_id
if plr.track:
self.start_gap = plr.track.start_gap
self.title = plr.track.title
@ -328,6 +335,14 @@ class PlaylistModel(QAbstractTableModel):
# Fall through to no-op
return QVariant()
def delete_rows(self, row_numbers: List[int]) -> None:
"""
Delete passed rows from model
"""
# TODO
print(f"Delete rows {row_numbers=}")
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
"""
Return text for display
@ -517,8 +532,9 @@ class PlaylistModel(QAbstractTableModel):
section_end_time = track_sequence.now.end_time + timedelta(
milliseconds=duration
)
end_time_str = ", section end time " + section_end_time.strftime(
Config.TRACK_TIME_FORMAT
end_time_str = (
", section end time "
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT)
)
stripped_note = prd.note[:-1].strip()
if stripped_note:
@ -547,6 +563,13 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number].path == ""
def is_unplayed_row(self, row_number: int) -> bool:
"""
Return True if row is an unplayed track row, else False
"""
return self.playlist_rows[row_number].played
def insert_header_row(self, row_number: Optional[int], text: str) -> None:
"""
Insert a header row.
@ -624,6 +647,14 @@ class PlaylistModel(QAbstractTableModel):
for modified_row in modified_rows:
self.invalidate_row(modified_row)
def mark_unplayed(self, row_number: int) -> None:
"""
Mark row as unplayed
"""
self.playlist_rows[row_number].played = False
self.invalidate_row(row_number)
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
"""
Move the playlist rows given to to_row and below.
@ -677,6 +708,15 @@ class PlaylistModel(QAbstractTableModel):
# Update display
self.invalidate_rows(list(row_map.keys()))
def open_in_audacity(self, row_number: int) -> None:
"""
Open track at passed row number in Audacity
"""
path = self.playlist_rows[row_number].path
if path:
open_in_audacity(path)
def previous_track_ended(self) -> None:
"""
Notification from musicmuster that the previous track has ended.
@ -718,16 +758,68 @@ class PlaylistModel(QAbstractTableModel):
p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
self.playlist_rows[row_number] = PlaylistRowData(p)
def remove_track(self, row_number: int) -> None:
"""
Remove track from row
"""
# TODO
print(f"remove_track({row_number=})")
def rescan_track(self, row_number: int) -> None:
"""
Rescan track at passed row number
"""
track_id = self.playlist_rows[row_number].track_id
if track_id:
with Session() as session:
track = session.get(Tracks, track_id)
set_track_metadata(track)
self.refresh_row(session, row_number)
self.invalidate_row(row_number)
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
"""Standard function for view"""
return len(self.playlist_rows)
def set_next_row(self, row_number: int) -> None:
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
"""
Set row_number as next track
Return True if the selection is sortable. That means:
- at least two rows selected
- selected rows are contiguous
- selected rows do not include any header rows
"""
# at least two rows selected
if len(row_numbers) < 2:
return False
# selected rows are contiguous
if sorted(row_numbers) != list(range(min(row_numbers), max(row_numbers) + 1)):
return False
# selected rows do not include any header rows
for row_number in row_numbers:
if self.is_header_row(row_number):
return False
return True
def set_next_row(self, row_number: Optional[int]) -> None:
"""
Set row_number as next track. If row_number is None, clear next track.
"""
if row_number is None:
next_row_was = track_sequence.next.plr_rownum
if next_row_was is None:
return
track_sequence.next = PlaylistTrack()
self.invalidate_row(next_row_was)
return
# Update playing_trtack
with Session() as session:
track_sequence.next = PlaylistTrack()
@ -800,6 +892,40 @@ class PlaylistModel(QAbstractTableModel):
return False
def sort_by_artist(self, row_numbers: List[int]) -> None:
"""
Sort selected rows by artist
"""
pass
def sort_by_duration(self, row_numbers: List[int]) -> None:
"""
Sort selected rows by duration
"""
pass
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
"""
Sort selected rows by lastplayed
"""
pass
def sort_by_title(self, row_numbers: List[int]) -> None:
"""
Sort selected rows by title
"""
# Create a subset of playlist_rows with the rows we are
# interested in
shortlist_rows = {k: self.playlist_rows[k] for k in row_numbers}
sorted_list = [
k for k, v in sorted(shortlist_rows.items(), key=lambda item: item[1].title)
]
self.move_rows(sorted_list, min(sorted_list))
def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction

View File

@ -37,7 +37,7 @@ from PyQt6.QtWidgets import (
from dbconfig import Session, scoped_session
from dialogs import TrackSelectDialog
from classes import MusicMusterSignals
from classes import MusicMusterSignals, track_sequence
from config import Config
from helpers import (
ask_yes_no,
@ -1058,114 +1058,94 @@ class PlaylistTab(QTableView):
"""Used to process context (right-click) menu, which is defined here"""
self.menu.clear()
row_number = item.row()
# track_id = self._get_row_track_id(row_number)
# track_row = bool(track_id)
header_row = False
model = cast(PlaylistModel, self.model())
if model:
header_row = model.is_header_row(row_number)
# current = row_number == self._get_current_track_row_number()
# next_row = row_number == self._get_next_track_row_number()
if not model:
return
# # Play with mplayer
# if track_row and not current:
# self._add_context_menu(
# "Play with mplayer", lambda: self._mplayer_play(row_number)
# )
row_number = item.row()
header_row = model.is_header_row(row_number)
track_row = not header_row
current_row = row_number == track_sequence.now.plr_rownum
next_row = row_number == track_sequence.next.plr_rownum
# # Paste
# self._add_context_menu(
# "Paste",
# lambda: self.musicmuster.paste_rows(),
# self.musicmuster.selected_plrs is None,
# )
# Open in Audacity
if track_row and not current_row:
self._add_context_menu(
"Open in Audacity", lambda: model.open_in_audacity(row_number)
)
# # Open in Audacity
# if track_row and not current:
# self._add_context_menu(
# "Open in Audacity", lambda: self._open_in_audacity(row_number)
# )
# Rescan
if track_row and not current_row:
self._add_context_menu("Rescan track", lambda: self._rescan(row_number))
# # Rescan
# if track_row and not current:
# self._add_context_menu(
# "Rescan track", lambda: self._rescan(row_number, track_id)
# )
# # ----------------------
# ----------------------
self.menu.addSeparator()
# # Remove row
# if not current and not next_row:
# self._add_context_menu("Delete row", self._delete_rows)
# Delete row
if not current_row and not next_row:
self._add_context_menu(
"Delete row", lambda: model.delete_rows(self._get_selected_rows())
)
# # Move to playlist
# if not current and not next_row:
# self._add_context_menu(
# "Move to playlist...", self.musicmuster.move_selected
# )
# # ----------------------
# self.menu.addSeparator()
# # Remove track from row
# if track_row and not current and not next_row:
# self._add_context_menu(
# "Remove track from row", lambda: self._remove_track(row_number)
# )
# Remove track from row
if track_row and not current_row and not next_row:
self._add_context_menu(
"Remove track from row", lambda: model.remove_track(row_number)
)
# Add track to section header (ie, make this a track row)
# TODO
if header_row:
self._add_context_menu("Add a track", lambda: self._add_track(row_number))
# # Mark unplayed
# if self._get_row_userdata(row_number, self.PLAYED):
# self._add_context_menu("Mark unplayed", self._mark_unplayed)
# # Unmark as next
# if next_row:
# self._add_context_menu("Unmark as next track", self.clear_next)
self._add_context_menu("Add a track", lambda: print("Add a track"))
# # ----------------------
self.menu.addSeparator()
# # Sort
# sort_menu = self.menu.addMenu("Sort")
# self._add_context_menu(
# "by title", lambda: self._sort_selection(TITLE), parent_menu=sort_menu
# )
# self._add_context_menu(
# "by artist", lambda: self._sort_selection(ARTIST), parent_menu=sort_menu
# )
# self._add_context_menu(
# "by duration", lambda: self._sort_selection(DURATION), parent_menu=sort_menu
# )
# self._add_context_menu(
# "by last played",
# lambda: self._sort_selection(LASTPLAYED),
# parent_menu=sort_menu,
# )
# if sort_menu:
# sort_menu.setEnabled(self._sortable())
# self._add_context_menu("Undo sort", self._sort_undo, not bool(self.sort_undo))
# Mark unplayed
if track_row and model.is_unplayed_row(row_number):
self._add_context_menu("Mark unplayed", lambda: model.mark_unplayed(row_number))
# # Build submenu
# Unmark as next
if next_row:
self._add_context_menu("Unmark as next track", lambda: model.set_next_row(None))
# # ----------------------
# self.menu.addSeparator()
# ----------------------
self.menu.addSeparator()
# # Info
# if track_row:
# self._add_context_menu("Info", lambda: self._info_row(track_id))
# Sort
sort_menu = self.menu.addMenu("Sort")
self._add_context_menu(
"by title",
lambda: model.sort_by_title(self._get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by artist",
lambda: model.sort_by_artist(self._get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by duration",
lambda: model.sort_by_duration(self._get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by last played",
lambda: model.sort_by_lastplayed(self._get_selected_rows()),
parent_menu=sort_menu,
)
if sort_menu:
sort_menu.setEnabled(model.selection_is_sortable(self._get_selected_rows()))
self._add_context_menu("Undo sort", self._sort_undo, not bool(self.sort_undo))
# # Track path
# if track_row:
# self._add_context_menu(
# "Copy track path", lambda: self._copy_path(row_number)
# )
# Info TODO
if track_row:
self._add_context_menu("Info", lambda: print("Track info"))
# # return super(PlaylistTab, self).eventFilter(source, event)
# Track path TODO
if track_row:
self._add_context_menu(
"Copy track path", lambda: print("Track path"))
def _calculate_end_time(
self, start: Optional[datetime], duration: int
@ -1739,30 +1719,11 @@ class PlaylistTab(QTableView):
for i in reversed(sorted(source_row_numbers)):
self.removeRow(i)
def _rescan(self, row_number: int, track_id: int) -> None:
def _rescan(self, row_number: int) -> None:
"""Rescan track"""
with Session() as session:
track = session.get(Tracks, track_id)
if track:
if file_is_unreadable(track.path):
self._set_row_colour_unreadable(row_number)
else:
self._set_row_colour_default(row_number)
set_track_metadata(track)
self._update_row_track_info(session, row_number, track)
else:
_ = self._set_row_track_id(row_number, 0)
note_text = self._get_row_note(row_number)
if note_text is None:
note_text = ""
else:
note_text += f"{track_id=} not found"
self._set_row_header_text(session, row_number, note_text)
log.error(f"playlists._rescan({track_id=}): " "Track not found")
self._set_row_colour_unreadable(row_number)
self._update_start_end_times(session)
model = cast(PlaylistModel, self.model())
model.rescan_track(row_number)
self.clear_selection()
def _reset_next(self, old_plrid: int, new_plrid: int) -> None:
@ -2299,33 +2260,6 @@ class PlaylistTab(QTableView):
return item
def _sortable(self) -> bool:
"""
Return True if the selection is sortable. That means:
- at least two rows selected
- selected rows are contiguous
- selected rows do not include any header rows
"""
selectionModel = self.selectionModel()
if not selectionModel:
return False
source_rows = selectionModel.selectedRows()
if len(source_rows) < 2:
return False
sorted_source_rows = sorted([a.row() for a in source_rows])
if sorted_source_rows != list(
range(min(sorted_source_rows), max(sorted_source_rows) + 1)
):
return False
for row in sorted_source_rows:
if self._get_row_track_id(row) == 0:
return False
return True
def _sort_selection(self, sort_column: int) -> None:
"""
Algorithm: