Much improved search now working

This commit is contained in:
Keith Edmunds 2022-08-14 22:19:15 +01:00
parent b7c0fa94dd
commit ebdb0d0a82
4 changed files with 255 additions and 195 deletions

View File

@ -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:

View File

@ -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"""

View File

@ -784,17 +784,26 @@ border: 1px solid rgb(85, 87, 83);</string>
<addaction name="actionInsertTrack"/>
<addaction name="actionRemove"/>
<addaction name="actionImport"/>
<addaction name="separator"/>
<addaction name="actionSetNext"/>
<addaction name="action_Clear_selection"/>
<addaction name="separator"/>
<addaction name="actionEnable_controls"/>
</widget>
<widget class="QMenu" name="menuSearc_h">
<property name="title">
<string>Searc&amp;h</string>
</property>
<addaction name="actionSearch"/>
<addaction name="actionFind_next"/>
<addaction name="actionFind_previous"/>
<addaction name="separator"/>
<addaction name="actionSelect_next_track"/>
<addaction name="actionSelect_previous_track"/>
<addaction name="separator"/>
<addaction name="actionEnable_controls"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuPlaylist"/>
<addaction name="menuSearc_h"/>
</widget>
<widget class="QStatusBar" name="statusbar">
<property name="enabled">
@ -1043,6 +1052,22 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>&amp;Remove track</string>
</property>
</action>
<action name="actionFind_next">
<property name="text">
<string>Find next</string>
</property>
<property name="shortcut">
<string>N</string>
</property>
</action>
<action name="actionFind_previous">
<property name="text">
<string>Find previous</string>
</property>
<property name="shortcut">
<string>P</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -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 &section 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