diff --git a/app/playlists.py b/app/playlists.py index 5c1de4c..0a6d20b 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -270,13 +270,22 @@ class PlaylistTab(QTableWidget): self.hide_or_show_played_tracks() def _add_context_menu( - self, text: str, action: Callable, disabled: bool = False - ) -> QAction: + self, + text: str, + action: Callable, + disabled: bool = False, + parent_menu: Optional[QMenu] = None, + ) -> Optional[QAction]: """ Add item to self.menu """ - menu_item = self.menu.addAction(text) + if parent_menu is None: + parent_menu = self.menu + + menu_item = parent_menu.addAction(text) + if not menu_item: + return None menu_item.setDisabled(disabled) menu_item.triggered.connect(action) @@ -1073,6 +1082,30 @@ class PlaylistTab(QTableWidget): # ---------------------- 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()) + + # Build submenu + + # ---------------------- + self.menu.addSeparator() + # Info if track_row: self._add_context_menu("Info", lambda: self._info_row(track_id)) @@ -2184,6 +2217,82 @@ class PlaylistTab(QTableWidget): 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() + sorted_source_rows = sorted([a.row() for a in source_rows]) + if len(sorted_source_rows) < 2: + return False + + 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: + - check row selection is contiguous; return if not + - copy (row-number, sort-field) to a list + - sort the list by sort-field + - create a new row after the selection + - iterate the list and move items to new row + - create another new row and repeat until all rows moved + - delete old rows + """ + + # Check selection is contiguous + selectionModel = self.selectionModel() + if not selectionModel: + return + source_rows = selectionModel.selectedRows() + if not self._sortable(): + return + + # Copy (row-number, sort-field) to a list + sorted_rows = [] + for index in source_rows: + sort_item = self.item(index.row(), sort_column) + if sort_item: + sorted_rows.append((index.row(), sort_item.text())) + print(f"{sort_item.text()=}") + + # Sort the list + sorted_rows.sort(key=lambda row: row[1]) + + # Move rows + source_row_numbers = [a[0] for a in sorted_rows] + next_row = max(source_row_numbers) + 1 + for source_row_number in source_row_numbers: + self.insertRow(next_row) + for column in range(self.columnCount()): + self.setItem(next_row, column, self.takeItem(source_row_number, column)) + next_row += 1 + + # Remove source rows + for i in reversed(source_rows): + self.removeRow(i.row()) + + # Save playlist + # with Session() as session: + # self.save_playlist(session) + # self._update_start_end_times(session) + def _track_time_between_rows( self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows ) -> int: