WIP V3: context menu started
Sort by title implemented
This commit is contained in:
parent
1b4411d7e5
commit
d5871fe77f
@ -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
|
||||
|
||||
|
||||
210
app/playlists.py
210
app/playlists.py
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user