diff --git a/app/musicmuster.py b/app/musicmuster.py
index fee5d00..8711bf6 100755
--- a/app/musicmuster.py
+++ b/app/musicmuster.py
@@ -16,7 +16,7 @@ from PyQt5.QtWidgets import (
QFileDialog,
QInputDialog,
QLabel,
- # QLineEdit,
+ QLineEdit,
QListWidgetItem,
QMainWindow,
# QMessageBox,
@@ -77,9 +77,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.set_main_window_size()
self.lblSumPlaytime = QLabel("")
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
-# self.txtSearch = QLineEdit()
-# self.statusbar.addWidget(self.txtSearch)
-# self.txtSearch.setHidden(True)
+ self.txtSearch = QLineEdit()
+ self.statusbar.addWidget(self.txtSearch)
+ self.txtSearch.setHidden(True)
self.hide_played_tracks = False
self.visible_playlist_tab: Callable[[], PlaylistTab] = \
@@ -101,8 +101,12 @@ class Window(QMainWindow, Ui_MainWindow):
def clear_selection(self) -> None:
""" Clear selected row"""
+ # Unselect any selected rows
if self.visible_playlist_tab():
self.visible_playlist_tab().clear_selection()
+ # Clear the search bar
+ self.search_playlist_clear()
+
def closeEvent(self, event: QEvent) -> None:
"""Handle attempt to close main window"""
@@ -148,6 +152,10 @@ class Window(QMainWindow, Ui_MainWindow):
event.accept()
def connect_signals_slots(self) -> None:
+ self.actionFind_next.triggered.connect(
+ lambda: self.tabPlaylist.currentWidget().search_next())
+ self.actionFind_previous.triggered.connect(
+ lambda: self.tabPlaylist.currentWidget().search_previous())
self.actionInsertSectionHeader.triggered.connect(self.insert_header)
self.action_Clear_selection.triggered.connect(self.clear_selection)
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
@@ -162,7 +170,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionNewPlaylist.triggered.connect(self.create_playlist)
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next)
-# self.actionSearch.triggered.connect(self.search_playlist)
+ self.actionSearch.triggered.connect(self.search_playlist)
self.actionInsertTrack.triggered.connect(self.insert_track)
self.actionSelect_next_track.triggered.connect(self.select_next_row)
# self.actionSelect_played_tracks.triggered.connect(self.select_played)
@@ -179,9 +187,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnFade.clicked.connect(self.fade)
self.btnStop.clicked.connect(self.stop)
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
-# self.txtSearch.returnPressed.connect(self.search_playlist_return)
-# self.txtSearch.textChanged.connect(self.search_playlist_update)
-#
+ self.txtSearch.returnPressed.connect(self.search_playlist_return)
+
self.timer.timeout.connect(self.tick)
def close_playlist_tab(self) -> None:
@@ -623,26 +630,35 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session:
dlg = DbDialog(self, session)
dlg.exec()
-#
-# def search_playlist(self):
-# """Show text box to search playlist"""
-#
-# self.disable_play_next_controls()
-# self.txtSearch.setHidden(False)
-# self.txtSearch.setFocus()
-#
-# def search_playlist_return(self):
-# """Close off search box when return pressed"""
-#
-# self.txtSearch.setText("")
-# self.txtSearch.setHidden(True)
-# self.enable_play_next_controls()
-# self.visible_playlist_tab().set_filter("")
-#
-# def search_playlist_update(self):
-# """Update search when search string changes"""
-#
-# self.visible_playlist_tab().set_filter(self.txtSearch.text())
+
+ def search_playlist(self) -> None:
+ """Show text box to search playlist"""
+
+ # Disable play controls so that 'return' in search box doesn't
+ # play next track
+ self.disable_play_next_controls()
+ self.txtSearch.setHidden(False)
+ self.txtSearch.setFocus()
+
+ def search_playlist_clear(self) -> None:
+ """Tidy up and reset search bar"""
+
+ # Clear the search text
+ self.visible_playlist_tab().search("")
+ # Clean up search bar
+ self.txtSearch.setText("")
+ self.txtSearch.setHidden(True)
+
+ def search_playlist_return(self) -> None:
+ """Initiate search when return pressed"""
+
+ self.visible_playlist_tab().search(self.txtSearch.text())
+ self.enable_play_next_controls()
+
+ # def search_playlist_update(self):
+ # """Update search when search string changes"""
+
+ # self.visible_playlist_tab().set_filter(self.txtSearch.text())
def open_playlist(self):
with Session() as session:
diff --git a/app/playlists.py b/app/playlists.py
index 8c0c5bc..e5d8bae 100644
--- a/app/playlists.py
+++ b/app/playlists.py
@@ -146,7 +146,7 @@ class PlaylistTab(QTableWidget):
self.itemSelectionChanged.connect(self._select_event)
- self.row_filter: Optional[str] = None
+ self.search_text: str = ""
self.edit_cell_type = None
self.selecting_in_progress = False
# Connect signals
@@ -775,78 +775,109 @@ class PlaylistTab(QTableWidget):
session.commit()
PlaylistRows.delete_higher_rows(session, self.playlist_id, row)
-# def save_playlist(self, session) -> None:
-# """
-# Save playlist to database.
-#
-# For notes: check the database entry is correct and update it if
-# necessary. Playlists:Note is one:many, so each note may only appear
-# in one playlist.
-#
-# For tracks: erase the playlist tracks and recreate. This is much
-# simpler than trying to implement any Playlists:Tracks many:many
-# changes.
-# """
-#
-# playlist = Playlists.get_by_id(session, self.playlist_id)
-#
-# # Notes first
-# # Create dictionaries indexed by note_id
-# playlist_notes: Dict[int, Notes] = {}
-# database_notes: Dict[int, Notes] = {}
-# notes_rows: List[int] = self._get_notes_rows()
-#
-# # PlaylistTab
-# for row in notes_rows:
-# note: Notes = self._get_row_notes_object(row, session)
-# session.add(note)
-# playlist_notes[note.id] = note
-# if row != note.row:
-# log.debug(f"Updating: {playlist.name=}, {row=}, {note.row=}")
-# note.update(session=session, row=row)
-#
-# # Database
-# for note in playlist.notes:
-# database_notes[note.id] = note
-#
-# # We don't need to check for notes to add to the database as
-# # they can't exist in the playlist without being in the database
-# # and pointing at this playlist.
-#
-# # Notes to remove from database
-# for note_id in set(database_notes.keys()) - set(playlist_notes.keys()):
-# log.debug(
-# "_save_playlist(): "
-# f"Delete {note_id=} from {self=} in database"
-# )
-# database_notes[note_id].delete_note(session)
-#
-# # Note rows to update in playlist database
-# for note_id in set(playlist_notes.keys()) & set(database_notes.keys()):
-# if playlist_notes[note_id].row != database_notes[note_id].row:
-# log.debug(
-# f"_save_playlist(): Update notes row in database "
-# f"from {database_notes[note_id]=} "
-# f"to {playlist_notes[note_id]=}"
-# )
-# database_notes[note_id].update(
-# session, row=playlist_notes[note_id].row)
-#
-# # Tracks
-# # Remove all tracks from this playlist
-# playlist.remove_all_tracks(session)
-# # Iterate on-screen playlist and add tracks back in
-# for row in range(self.rowCount()):
-# if row in notes_rows:
-# continue
-# track_id: int = self.item(
-# row, FIXUP.COL_USERDATA).data(self.CONTENT_OBJECT)
-# playlist.add_track(session, track_id, row)
-# session.commit()
+ def search(self, text: str) -> None:
+ """Set search text and find first match"""
+
+ self.search_text = text
+ if not text:
+ # Search string has been reset
+ return
+ self.search_next()
+
+ def search_next(self) -> None:
+ """
+ Select next row containg self.search_string. Start from
+ top selected row if there is one, else from top.
+
+ Wrap at last row.
+ """
+
+ if not self.search_text:
+ return
+
+ selected_row = self._get_selected_row()
+ if selected_row and selected_row < self.rowCount() - 1:
+ starting_row = selected_row + 1
+ else:
+ starting_row = 0
+
+ wrapped = False
+ match_row = None
+ row = starting_row
+ needle = self.search_text.lower()
+ while True:
+ # Check for match in title, artist or notes
+ title = self._get_row_title(row)
+ if title and needle in title.lower():
+ match_row = row
+ break
+ artist = self._get_row_title(row)
+ if artist and needle in artist.lower():
+ match_row = row
+ break
+ note = self._get_row_note(row)
+ if note and needle in note.lower():
+ match_row = row
+ break
+ row += 1
+ if wrapped and row >= starting_row:
+ break
+ if row >= self.rowCount():
+ row = 0
+ wrapped = True
+
+ if match_row:
+ self.selectRow(row)
+
+ def search_previous(self) -> None:
+ """
+ Select previous row containg self.search_string. Start from
+ top selected row if there is one, else from top.
+
+ Wrap at last row.
+ """
+
+ if not self.search_text:
+ return
+
+ selected_row = self._get_selected_row()
+ if selected_row and selected_row > 0:
+ starting_row = selected_row - 1
+ else:
+ starting_row = self.rowCount() - 1
+
+ wrapped = False
+ match_row = None
+ row = starting_row
+ needle = self.search_text.lower()
+ while True:
+ # Check for match in title, artist or notes
+ title = self._get_row_title(row)
+ if title and needle in title.lower():
+ match_row = row
+ break
+ artist = self._get_row_title(row)
+ if artist and needle in artist.lower():
+ match_row = row
+ break
+ note = self._get_row_note(row)
+ if note and needle in note.lower():
+ match_row = row
+ break
+ row -= 1
+ if wrapped and row <= starting_row:
+ break
+ if row < 0:
+ row = self.rowCount() - 1
+ wrapped = True
+
+ if match_row:
+ self.selectRow(row)
def select_next_row(self) -> None:
"""
Select next or first row. Don't select section headers.
+
Wrap at last row.
"""
@@ -938,13 +969,12 @@ class PlaylistTab(QTableWidget):
# finally:
# self.selecting_in_progress = False
# self._select_event()
-#
-# def set_filter(self, text: Optional[str]) -> None:
-# """Filter rows to only show those containing text"""
-#
-# self.row_filter = text
-# with Session() as session:
-# self.update_display(session)
+
+ def set_searchtext(self, text: Optional[str]) -> None:
+ """Set the search text and find first match"""
+
+ self.search_text = text
+ self._find_next_match()
def set_selected_as_next(self) -> None:
"""Sets the select track as next to play"""
@@ -980,10 +1010,6 @@ class PlaylistTab(QTableWidget):
]
unreadable: List[int] = self._get_unreadable_track_rows()
- if self.row_filter:
- filter_text = self.row_filter.lower()
- else:
- filter_text = None
next_start_time = None
section_start_plr = None
section_time = 0
@@ -1023,22 +1049,6 @@ class PlaylistTab(QTableWidget):
if section_start_plr is not None:
section_time += track.duration
- # If filtering, only show matching tracks
- if filter_text:
- try:
- if (track.title
- and filter_text not in track.title.lower()
- and track.artist
- and filter_text not in track.artist.lower()):
- self.hideRow(row)
- continue
- else:
- self.showRow(row)
- except TypeError:
- print(f"TypeError: {track=}")
- else:
- self.showRow(row)
-
# Colour any note
if note_text:
(self.item(row, columns['row_notes'].idx)
@@ -1119,14 +1129,6 @@ class PlaylistTab(QTableWidget):
continue
# No track associated, so this row is a section header
- if filter_text:
- if filter_text not in note_text.lower():
- self.hideRow(row)
- continue
- else:
- self.showRow(row)
- else:
- self.showRow(row)
# Does the note have a start time?
row_time = self._get_note_text_time(note_text)
if row_time:
@@ -1264,11 +1266,18 @@ class PlaylistTab(QTableWidget):
return (index.row() + 1 if self._is_below(event.pos(), index)
else index.row())
-# def _get_notes_rows(self) -> List[int]:
-# """Return rows marked as notes, or None"""
-#
-# return self._meta_search(RowMeta.NOTE, one=False)
-#
+ def _find_next_match(self) -> None:
+ """
+ Find next match of search_text. Start at first highlighted row
+ if there is one, else from top of playlist.
+ """
+
+ start_row = self._get_selected_row()
+ if start_row is None:
+ start_row = 0
+
+
+
def _find_next_track_row(self, session: Session,
starting_row: int = None) -> Optional[int]:
@@ -1340,6 +1349,16 @@ class PlaylistTab(QTableWidget):
return playlistrow_id
+ def _get_row_artist(self, row: int) -> Optional[str]:
+ """Return artist on this row or None if none"""
+
+ track_id = self._get_row_track_id(row)
+ if not track_id:
+ return None
+
+ item_artist = self.item(row, columns['artist'].idx)
+ return item_artist.text()
+
def _get_row_duration(self, row: int) -> int:
"""Return duration associated with this row"""
@@ -1350,46 +1369,15 @@ class PlaylistTab(QTableWidget):
else:
return 0
- def _get_row_track_id(self, row: int) -> int:
- """Return the track_id associated with this row or None"""
+ def _get_row_note(self, row: int) -> Optional[str]:
+ """Return note on this row or None if none"""
- track_id = (self.item(row, columns['userdata'].idx)
- .data(self.ROW_TRACK_ID))
-
- return track_id
-
-#
-# def _get_row_end_time(self, row) -> Optional[datetime]:
-# """
-# Return row end time as string
-# """
-#
-# try:
-# if self.item(row, FIXUP.COL_END_TIME):
-# return datetime.strptime(self.item(
-# row, FIXUP.COL_END_TIME).text(),
-# Config.NOTE_TIME_FORMAT
-# )
-# else:
-# return None
-# except ValueError:
-# return None
-#
-# def _get_row_notes_object(self, row: int, session: Session) \
-# -> Optional[Notes]:
-# """Return note associated with this row"""
-#
-# note_id = self.item(row, FIXUP.COL_USERDATA).data(self.CONTENT_OBJECT)
-# note = Notes.get_by_id(session, note_id)
-# return note
-#
-# def _get_unplayed_track_rows(self) -> Optional[List[int]]:
-# """Return rows marked as unplayed, or None"""
-#
-# unplayed_rows: Set[int] = set(self._meta_notset(RowMeta.PLAYED))
-# notes_rows: Set[int] = set(self._get_notes_rows())
-#
-# return list(unplayed_rows - notes_rows)
+ track_id = self._get_row_track_id(row)
+ if track_id:
+ item_note = self.item(row, columns['row_notes'].idx)
+ else:
+ item_note = self.item(row, 1)
+ return item_note.text()
def _get_row_start_time(self, row: int) -> Optional[datetime]:
try:
@@ -1403,6 +1391,35 @@ class PlaylistTab(QTableWidget):
except ValueError:
return None
+ def _get_row_title(self, row: int) -> Optional[str]:
+ """Return title on this row or None if none"""
+
+ track_id = self._get_row_track_id(row)
+ if not track_id:
+ return None
+
+ item_title = self.item(row, columns['title'].idx)
+ return item_title.text()
+
+ def _get_row_track_id(self, row: int) -> int:
+ """Return the track_id associated with this row or None"""
+
+ try:
+ track_id = (self.item(row, columns['userdata'].idx)
+ .data(self.ROW_TRACK_ID))
+ except AttributeError:
+ return None
+
+ return track_id
+
+# def _get_unplayed_track_rows(self) -> Optional[List[int]]:
+# """Return rows marked as unplayed, or None"""
+#
+# unplayed_rows: Set[int] = set(self._meta_notset(RowMeta.PLAYED))
+# notes_rows: Set[int] = set(self._get_notes_rows())
+#
+# return list(unplayed_rows - notes_rows)
+
def _get_selected_row(self) -> Optional[int]:
"""Return row number of first selected row, or None if none selected"""
@@ -1410,19 +1427,6 @@ class PlaylistTab(QTableWidget):
return None
else:
return self.selectionModel().selectedRows()[0].row()
-#
-# def _get_row_track_object(self, row: int, session: Session) \
-# -> Optional[Tracks]:
-# """Return track associated with this row"""
-#
-# track_id = self.item(row, FIXUP.COL_USERDATA).data(self.CONTENT_OBJECT)
-# track = Tracks.get_by_id(session, track_id)
-# return track
-#
-# def _get_track_rows(self) -> List[int]:
-# """Return rows marked as tracks, or None"""
-#
-# return self._meta_notset(RowMeta.NOTE)
def _get_unreadable_track_rows(self) -> List[int]:
"""Return rows marked as unreadable, or None"""
diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui
index 130ac5c..e9a86e3 100644
--- a/app/ui/main_window.ui
+++ b/app/ui/main_window.ui
@@ -784,17 +784,26 @@ border: 1px solid rgb(85, 87, 83);
+
+
+
+
+
@@ -1043,6 +1052,22 @@ border: 1px solid rgb(85, 87, 83);
&Remove track
+
+
+ Find next
+
+
+ N
+
+
+
+
+ Find previous
+
+
+ P
+
+
diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py
index 1b19c02..b827b64 100644
--- a/app/ui/main_window_ui.py
+++ b/app/ui/main_window_ui.py
@@ -350,6 +350,8 @@ class Ui_MainWindow(object):
self.menuFile.setObjectName("menuFile")
self.menuPlaylist = QtWidgets.QMenu(self.menubar)
self.menuPlaylist.setObjectName("menuPlaylist")
+ self.menuSearc_h = QtWidgets.QMenu(self.menubar)
+ self.menuSearc_h.setObjectName("menuSearc_h")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setEnabled(True)
@@ -444,6 +446,10 @@ class Ui_MainWindow(object):
self.actionInsertSectionHeader.setObjectName("actionInsertSectionHeader")
self.actionRemove = QtWidgets.QAction(MainWindow)
self.actionRemove.setObjectName("actionRemove")
+ self.actionFind_next = QtWidgets.QAction(MainWindow)
+ self.actionFind_next.setObjectName("actionFind_next")
+ self.actionFind_previous = QtWidgets.QAction(MainWindow)
+ self.actionFind_previous.setObjectName("actionFind_previous")
self.menuFile.addAction(self.actionNewPlaylist)
self.menuFile.addAction(self.actionOpenPlaylist)
self.menuFile.addAction(self.actionClosePlaylist)
@@ -468,16 +474,20 @@ class Ui_MainWindow(object):
self.menuPlaylist.addAction(self.actionInsertTrack)
self.menuPlaylist.addAction(self.actionRemove)
self.menuPlaylist.addAction(self.actionImport)
+ self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSetNext)
self.menuPlaylist.addAction(self.action_Clear_selection)
self.menuPlaylist.addSeparator()
- self.menuPlaylist.addAction(self.actionSearch)
- self.menuPlaylist.addAction(self.actionSelect_next_track)
- self.menuPlaylist.addAction(self.actionSelect_previous_track)
- self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionEnable_controls)
+ self.menuSearc_h.addAction(self.actionSearch)
+ self.menuSearc_h.addAction(self.actionFind_next)
+ self.menuSearc_h.addAction(self.actionFind_previous)
+ self.menuSearc_h.addSeparator()
+ self.menuSearc_h.addAction(self.actionSelect_next_track)
+ self.menuSearc_h.addAction(self.actionSelect_previous_track)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuPlaylist.menuAction())
+ self.menubar.addAction(self.menuSearc_h.menuAction())
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
@@ -514,6 +524,7 @@ class Ui_MainWindow(object):
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.menuFile.setTitle(_translate("MainWindow", "P&laylists"))
self.menuPlaylist.setTitle(_translate("MainWindow", "Sho&wtime"))
+ self.menuSearc_h.setTitle(_translate("MainWindow", "Searc&h"))
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next"))
@@ -560,5 +571,9 @@ class Ui_MainWindow(object):
self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert §ion header..."))
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
+ self.actionFind_next.setText(_translate("MainWindow", "Find next"))
+ self.actionFind_next.setShortcut(_translate("MainWindow", "N"))
+ self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
+ self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
from infotabs import InfoTabs
import icons_rc