diff --git a/app/dbconfig.py b/app/dbconfig.py index ee6949a..d1c1f3c 100644 --- a/app/dbconfig.py +++ b/app/dbconfig.py @@ -31,11 +31,10 @@ def Session() -> Generator[scoped_session, None, None]: function = frame.function lineno = frame.lineno Session = scoped_session(sessionmaker(bind=engine)) - log.debug(f"SqlA: session acquired [{hex(id(Session))}]") log.debug( - f"Session acquisition: {file}:{function}:{lineno} " f"[{hex(id(Session))}]" + f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]" ) yield Session - log.debug(f" SqlA: session released [{hex(id(Session))}]") + log.debug(f" Session released [{hex(id(Session))}]") Session.commit() Session.close() diff --git a/app/musicmuster.py b/app/musicmuster.py index 0a810b5..cac1a2a 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -71,7 +71,7 @@ from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tr from config import Config from playlists import PlaylistTab from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore -from ui.dlg_search_database_ui import Ui_Dialog # type: ignore +from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from ui.main_window_ui import Ui_MainWindow # type: ignore @@ -246,9 +246,10 @@ class MusicMusterSignals(QObject): emit-a-signal-from-another-class-to-main-class """ + enable_escape_signal = pyqtSignal(bool) set_next_track_signal = pyqtSignal(int, int) span_cells_signal = pyqtSignal(int, int, int, int) - enable_escape_signal = pyqtSignal(bool) + add_track_to_playlist_signal = pyqtSignal(int, int, str) class PlaylistTrack: @@ -745,9 +746,7 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: # Save the selected PlaylistRows items ready for a later # paste - self.selected_plrs = self.active_tab().get_selected_playlistrows( - session - ) + self.selected_plrs = self.active_tab().get_selected_playlistrows(session) def debug(self): """Invoke debugger""" @@ -928,7 +927,10 @@ class Window(QMainWindow, Ui_MainWindow): def get_one_track(self, session: scoped_session) -> Optional[Tracks]: """Show dialog box to select one track and return it to caller""" - dlg = DbDialog(self, session, get_one_track=True) + dlg = TrackSelectDialog(self, session) + dlg.ui.txtNote.hide() + dlg.ui.lblNote.hide() + if dlg.exec(): return dlg.track else: @@ -1054,15 +1056,18 @@ class Window(QMainWindow, Ui_MainWindow): ok = dlg.exec() if ok: model.insert_header_row( - self.active_tab().get_selected_row_number(), - dlg.textValue() + self.active_tab().get_selected_row_number(), dlg.textValue() ) def insert_track(self) -> None: """Show dialog box to select and add track from database""" with Session() as session: - dlg = DbDialog(self, session, get_one_track=False) + dlg = TrackSelectDialog( + session=session, + signals=self.signals, + playlist_id=self.active_tab().playlist_id, + ) dlg.exec() def load_last_playlists(self) -> None: @@ -1159,9 +1164,7 @@ class Window(QMainWindow, Ui_MainWindow): """ with Session() as session: - selected_plrs = self.active_tab().get_selected_playlistrows( - session - ) + selected_plrs = self.active_tab().get_selected_playlistrows(session) if not selected_plrs: return @@ -1914,66 +1917,42 @@ class CartDialog(QDialog): self.ui.lblPath.setText(self.path) -class DbDialog(QDialog): +class TrackSelectDialog(QDialog): """Select track from database""" def __init__( self, - musicmuster: Window, session: scoped_session, - get_one_track: bool = False, + signals: MusicMusterSignals, + playlist_id: int, *args, **kwargs, ) -> None: """ Subclassed QDialog to manage track selection - - If get_one_track is True, return after first track selection - with that track in ui.track. Otherwise, allow multiple tracks - to be added to the playlist. """ super().__init__(*args, **kwargs) - self.musicmuster = musicmuster self.session = session - self.get_one_track = get_one_track + self.signals = signals + self.playlist_id = playlist_id self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.btnAdd.clicked.connect(self.add_selected) self.ui.btnAddClose.clicked.connect(self.add_selected_and_close) self.ui.btnClose.clicked.connect(self.close) - self.ui.matchList.itemDoubleClicked.connect(self.double_click) + self.ui.matchList.itemDoubleClicked.connect(self.add_selected) self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) self.ui.radioTitle.toggled.connect(self.title_artist_toggle) self.ui.searchString.textEdited.connect(self.chars_typed) self.track: Optional[Tracks] = None - if get_one_track: - self.ui.txtNote.hide() - self.ui.lblNote.hide() - record = Settings.get_int_settings(self.session, "dbdialog_width") width = record.f_int or 800 record = Settings.get_int_settings(self.session, "dbdialog_height") height = record.f_int or 600 self.resize(width, height) - def __del__(self) -> None: - """Save dialog size and position""" - - # FIXME: - # if record.f_int != self.height(): - # ^^^^^^^^^^^^^ - # RuntimeError: wrapped C/C++ object of type DbDialog has been deleted - - record = Settings.get_int_settings(self.session, "dbdialog_height") - if record.f_int != self.height(): - record.update(self.session, {"f_int": self.height()}) - - record = Settings.get_int_settings(self.session, "dbdialog_width") - if record.f_int != self.width(): - record.update(self.session, {"f_int": self.width()}) - def add_selected(self) -> None: """Handle Add button""" @@ -1989,7 +1968,13 @@ class DbDialog(QDialog): if not note and not track: return - self.add_track(track, self.ui.txtNote.text()) + self.ui.txtNote.clear() + self.select_searchtext() + + track_id = None + if track: + track_id = track.id + self.signals.add_track_to_playlist_signal.emit(self.playlist_id, track_id, note) def add_selected_and_close(self) -> None: """Handle Add and Close button""" @@ -1997,24 +1982,6 @@ class DbDialog(QDialog): self.add_selected() self.accept() - def add_track(self, track: Optional[Tracks], note: str) -> None: - """Add passed track to playlist on screen""" - - if self.get_one_track: - self.track = track - self.accept() - return - - if track: - self.musicmuster.active_tab().insert_track( - self.session, track, note - ) - else: - self.musicmuster.active_tab().insert_header(self.session, note) - - self.ui.txtNote.clear() - self.select_searchtext() - def chars_typed(self, s: str) -> None: """Handle text typed in search box""" @@ -2042,12 +2009,23 @@ class DbDialog(QDialog): t.setData(Qt.ItemDataRole.UserRole, track) self.ui.matchList.addItem(t) - def double_click(self, entry: QListWidgetItem) -> None: - """Add items that are double-clicked""" + def closeEvent(self, event: Optional[QEvent]) -> None: + """ + Override close and save dialog coordinates + """ - track = entry.data(Qt.ItemDataRole.UserRole) - note = self.ui.txtNote.text() - self.add_track(track, note) + if not event: + return + + record = Settings.get_int_settings(self.session, "dbdialog_height") + if record.f_int != self.height(): + record.update(self.session, {"f_int": self.height()}) + + record = Settings.get_int_settings(self.session, "dbdialog_width") + if record.f_int != self.width(): + record.update(self.session, {"f_int": self.width()}) + + event.accept() def keyPressEvent(self, event): """ @@ -2059,7 +2037,7 @@ class DbDialog(QDialog): self.ui.matchList.clearSelection() return - super(DbDialog, self).keyPressEvent(event) + super(TrackSelectDialog, self).keyPressEvent(event) def select_searchtext(self) -> None: """Select the searchbox""" diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 1494076..30ae274 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -19,11 +19,10 @@ from PyQt6.QtGui import ( ) from config import Config - +from playlists import PlaylistTab from helpers import ( file_is_unreadable, ) - from models import PlaylistRows, Tracks if TYPE_CHECKING: @@ -98,14 +97,22 @@ class PlaylistModel(QAbstractTableModel): """ def __init__( - self, playlist_id: int, signals: "MusicMusterSignals", *args, **kwargs + self, + playlist: PlaylistTab, + playlist_id: int, + signals: "MusicMusterSignals", + *args, + **kwargs, ): + self.playlist = playlist self.playlist_id = playlist_id self.signals = signals super().__init__(*args, **kwargs) self.playlist_rows: dict[int, PlaylistRowData] = {} + self.signals.add_track_to_playlist_signal.connect(self.add_track) + with Session() as session: self.refresh_data(session) @@ -114,6 +121,29 @@ class PlaylistModel(QAbstractTableModel): f"" ) + def add_track( + self, playlist_id: int, track: Optional[Tracks], note: Optional[str] + ) -> None: + """ + Add track if it's for our playlist + """ + + # Ignore if it's not for us + if playlist_id != self.playlist_id: + return + + row_number = self.playlist.get_selected_row_number() + + # Insert track if we have one + if track: + self.insert_track_row(row_number, track, note) + # If we only have a note, add as a header row + elif note: + self.insert_header_row(row_number, note) + else: + # No track, no note, no point + return + def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush: """Return background setting""" @@ -299,7 +329,7 @@ class PlaylistModel(QAbstractTableModel): def insert_header_row(self, row_number: Optional[int], text: str) -> None: """ - Insert a header row. Return row number or None if insertion failed. + Insert a header row. """ with Session() as session: @@ -347,6 +377,24 @@ class PlaylistModel(QAbstractTableModel): # Insert the new row and return it return PlaylistRows(session, self.playlist_id, new_row_number) + def insert_track_row( + self, row_number: Optional[int], track_id: int, text: Optional[str] + ) -> None: + """ + Insert a track row. + """ + + with Session() as session: + plr = self._insert_row(session, row_number) + # Update the PlaylistRows object + plr.track_id = track_id + if text: + plr.note = text + # Repopulate self.playlist_rows + self.refresh_data(session) + # Update the display from the new row onwards + self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows)))) + def invalidate_row(self, modified_row: int) -> None: """ Signal to view to refresh invlidated row @@ -394,7 +442,8 @@ class PlaylistModel(QAbstractTableModel): [y for y in range(len(self.playlist_rows)) if y not in row_map.values()], ): # Optimise: only add to map if there is a change - row_map[old_row] = new_row + if old_row != new_row: + row_map[old_row] = new_row # For SQLAlchemy, build a list of dictionaries that map plrid to # new row number: @@ -404,7 +453,7 @@ class PlaylistModel(QAbstractTableModel): sqla_map.append({"plrid": plrid, "plr_rownum": newrow}) # Update database. Ref: - # https://docs.sqlalchemy.org/en/20/core/sqlelement.html#sqlalchemy.sql.expression.case + # https://docs.sqlalchemy.org/en/20/tutorial/data_update.html#the-update-sql-expression-construct stmt = ( update(PlaylistRows) .where( diff --git a/app/playlists.py b/app/playlists.py index 52f8b76..0b8c97a 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -51,7 +51,7 @@ from log import log from models import Playlists, PlaylistRows, Settings, Tracks, NoteColours -from playlistmodel import PlaylistModel +import playlistmodel if TYPE_CHECKING: from musicmuster import Window, MusicMusterSignals @@ -205,7 +205,7 @@ class PlaylistTab(QTableView): # self.edit_cell_type: Optional[int] # Load playlist rows - self.setModel(PlaylistModel(playlist_id, signals)) + self.setModel(playlistmodel.PlaylistModel(self, playlist_id, signals)) self._set_column_widths() # kae def __repr__(self) -> str: diff --git a/app/ui/dlg_SearchDatabase.ui b/app/ui/dlg_TrackSelect.ui similarity index 100% rename from app/ui/dlg_SearchDatabase.ui rename to app/ui/dlg_TrackSelect.ui diff --git a/app/ui/dlg_search_database_ui.py b/app/ui/dlg_TrackSelect_ui.py similarity index 97% rename from app/ui/dlg_search_database_ui.py rename to app/ui/dlg_TrackSelect_ui.py index 2b15dae..4b7a62e 100644 --- a/app/ui/dlg_search_database_ui.py +++ b/app/ui/dlg_TrackSelect_ui.py @@ -1,6 +1,6 @@ -# Form implementation generated from reading ui file 'app/ui/dlg_SearchDatabase.ui' +# Form implementation generated from reading ui file 'dlg_TrackSelect.ui' # -# Created by: PyQt6 UI code generator 6.5.2 +# Created by: PyQt6 UI code generator 6.5.3 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing.