From cee84563fb79c25f934343e1087dfdfd6a6b53c4 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sat, 13 Aug 2022 21:13:03 +0100 Subject: [PATCH] WIP re editing --- app/playlists.py | 374 ++++++++++++++++++++++++++++++----------------- 1 file changed, 237 insertions(+), 137 deletions(-) diff --git a/app/playlists.py b/app/playlists.py index 5a1a697..3cd5d8d 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -6,7 +6,7 @@ from collections import namedtuple from datetime import datetime, timedelta from typing import List, Optional -from PyQt5.QtCore import QEvent, Qt, pyqtSignal +from PyQt5.QtCore import QEvent, QModelIndex, Qt, pyqtSignal from PyQt5.QtGui import ( QBrush, QColor, @@ -14,16 +14,18 @@ from PyQt5.QtGui import ( QDropEvent ) from PyQt5.QtWidgets import ( + QAbstractItemDelegate, QAbstractItemView, QApplication, # QInputDialog, - # QLineEdit, + QLineEdit, QMainWindow, QMenu, - # QStyledItemDelegate, + QStyledItemDelegate, QMessageBox, QTableWidget, QTableWidgetItem, + QWidget ) from config import Config @@ -70,24 +72,28 @@ columns["lastplayed"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED) columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES) -# class NoSelectDelegate(QStyledItemDelegate): -# """https://stackoverflow.com/questions/72790705/dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962""" -# -# def createEditor(self, parent, option, index): -# editor = super().createEditor(parent, option, index) -# if isinstance(editor, QLineEdit): -# def deselect(): -# # Important! First disconnect, otherwise editor.deselect() -# # will call again this function -# editor.selectionChanged.disconnect(deselect) -# editor.deselect() -# editor.selectionChanged.connect(deselect) -# return editor +class NoSelectDelegate(QStyledItemDelegate): + """ + https://stackoverflow.com/questions/72790705/ + dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962 + """ + + def createEditor(self, parent, option, index): + editor = super().createEditor(parent, option, index) + if isinstance(editor, QLineEdit): + def deselect(): + # Important! First disconnect, otherwise editor.deselect() + # will call again this function + editor.selectionChanged.disconnect(deselect) + editor.deselect() + editor.selectionChanged.connect(deselect) + return editor class PlaylistTab(QTableWidget): - # cellEditingStarted = QtCore.pyqtSignal(int, int) - # cellEditingEnded = QtCore.pyqtSignal() + # Custom signals + cellEditingStarted = pyqtSignal(int, int) + cellEditingEnded = pyqtSignal() # Qt.UserRoles ROW_FLAGS = Qt.UserRole @@ -103,12 +109,13 @@ class PlaylistTab(QTableWidget): self.menu: Optional[QMenu] = None self.current_track_start_time: Optional[datetime] = None -# -# # Don't select text on edit -# self.setItemDelegate(NoSelectDelegate(self)) -# + + # Don't select text on edit + self.setItemDelegate(NoSelectDelegate(self)) + # Set up widget -# self.setEditTriggers(QAbstractItemView.AllEditTriggers) + # self.setEditTriggers(QAbstractItemView.DoubleClicked) + self.setEditTriggers(QAbstractItemView.AllEditTriggers) self.setAlternatingRowColors(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -146,13 +153,13 @@ class PlaylistTab(QTableWidget): self.itemSelectionChanged.connect(self._select_event) self.row_filter: Optional[str] = None -# self.editing_cell: bool = False + # self.editing_cell: bool = False + self.edit_cell_type = None self.selecting_in_progress = False # Connect signals -# self.cellChanged.connect(self._cell_changed) -# self.cellClicked.connect(self._edit_note_cell) -# self.cellEditingEnded.connect(self._cell_edit_ended) -# self.cellEditingStarted.connect(self._cell_edit_started) + # self.cellClicked.connect(self._edit_note_cell) + self.cellEditingEnded.connect(self._cell_edit_ended) + self.cellEditingStarted.connect(self._cell_edit_started) # self.doubleClicked.connect(self._edit_cell) self.horizontalHeader().sectionResized.connect(self._column_resize) @@ -161,12 +168,8 @@ class PlaylistTab(QTableWidget): def __repr__(self) -> str: return f" None: """Handle closing playist tab""" @@ -230,12 +233,6 @@ class PlaylistTab(QTableWidget): self.save_playlist(session) self.update_display(session) -# def edit(self, index, trigger, event): -# result = super(PlaylistTab, self).edit(index, trigger, event) -# if result: -# self.cellEditingStarted.emit(index.row(), index.column()) -# return result - def eventFilter(self, source, event): """Used to process context (right-click) menu, which is defined here""" @@ -327,6 +324,206 @@ class PlaylistTab(QTableWidget): self.setDragEnabled(False) super().mouseReleaseEvent(event) + # ########## Cell editing ########## + # + # We only want to allow cell editing on tracks, artists and notes, + # although notes may be section headers. + # + # Once editing starts, we need to disable play controls so that a + # 'return' doesn't play the next track. + # + # Earlier in this file: + # - self.setEditTriggers(QAbstractItemView.DoubleClicked) - triggers + # editing on double-click + # - self.setItemDelegate(NoSelectDelegate(self)) and associated class + # ensure that the text is not selected when editing starts + # - cellEditingStarted and cellEditingEnded: custom signals, used + # below + # + # Call sequences: + # Start editing: + # edit() (called twice; not sure why) + # _cell_edit_started() + # End editing: + # _cell_changed() (only if changes made) + # closeEditor() + # _cell_edit_ended() + + + # def _edit_note_cell(self, row: int, column: int): # review + # """Called when table is single-clicked""" + + # print(f"_edit_note_cell({row=}, {column=}") + # # if column in [FIXUP.COL_ROW_NOTES]: + # # item = self.item(row, column) + # # self.editItem(item) + + def _cell_changed(self, row: int, column: int) -> None: + """Called when cell content has changed""" + + print("KAE _cell_changed()") + + new_text = self.item(row, column).text() + track_id = self._get_row_track_id(row) + + # Determin cell type changed + with Session() as session: + if self.edit_cell_type == "row_notes": + # Get playlistrow object + plr_id = self._get_playlistrow_id(row) + plr_item = session.get(PlaylistRows, plr_id) + plr_item.note = new_text + + # Set/clear row start time accordingly + start_time = self._get_note_text_time(new_text) + if start_time: + self._set_row_start_time(row, start_time) + else: + self._set_row_start_time(row, None) + + if row in self._get_notes_rows(): + # Save change to database + note: Notes = self._get_row_notes_object(row, session) + note.update(session, row, new_text) + else: + track = None + if track_id: + track = session.get(Tracks, track_id) + if track: + if self.edit_cell_type == "title": + track.title = new_text + elif self.edit_cell_type == "artist": + track.artist = new_text + # Headers will be incorrect if the edited track is + # previous / current / next TODO: this will require + # the stored data in musicmuster to be updated, + # which currently it isn't). + self.musicmuster.update_headers() + + self.edit_cell_type = None + + def _cell_edit_ended(self) -> None: + """ + Called by cellEditingEnded signal + + Enable play controls. + """ + + print("KAE _cell_edit_ended()") + # self.editing_cell = False + # Disable cell changed signal connection + self.cellChanged.disconnect(self._cell_changed) + + # update_display to update start times, such as when a note has + # been edited + with Session() as session: + self.update_display(session) + + self.musicmuster.enable_play_next_controls() + + def _cell_edit_started(self, row: int, column: int) -> None: + """ + Called by cellEditingStarted signal. + + Disable play controls so that keys work during edit. + """ + + print("KAE _cell_edit_started()") + # Is this a track row? + track_row = self._get_row_track_id(row) + + note_column = 0 + if track_row: + # If a track row, we only allow editing of title, artist and + # note. Check that this column is one of those. + self.edit_cell_type = None + if column == columns['title'].idx: + self.edit_cell_type = "title" + elif column == columns['artist'].idx: + self.edit_cell_type = "artist" + elif column == columns['row_notes'].idx: + self.edit_cell_type = "row_notes" + else: + # Can't edit other columns + return + + # Check whether we're editing a notes row for later + if self.edit_cell_type == "row_notes": + note_column = columns['row_notes'].idx + else: + # This is a section header. Text is always in row 1. + if column != 1: + return + note_column = 1 + self.edit_cell_type = "row_notes" + + # Connect signal so we know when cell has changed. + self.cellChanged.connect(self._cell_changed) + # self.editing_cell = True + # Disable play controls so that keyboard input doesn't disturb playing + self.musicmuster.disable_play_next_controls() + + # If this is a note cell, we need to remove any existing section + # timing so user can't edit that. Keep it simple: refresh text + # from database. Note column will only be non-zero if we are + # editing a note. + + if note_column: + with Session() as session: + print(" KAE editing a note") + plr_id = self._get_playlistrow_id(row) + plr_item = session.get(PlaylistRows, plr_id) + item = self.item(row, note_column) + item.setText(plr_item.note) + else: + print(" KAE NOT editing a note") + + print(f" KAE _cell_edit_started(), {note_column=}") + return + + def closeEditor(self, + editor: QWidget, + hint: QAbstractItemDelegate.EndEditHint) -> None: + """ + Override QAbstractItemView.closeEditor to emit signal when + editing ends. + """ + + print("KAE closeEditor()") + super(PlaylistTab, self).closeEditor(editor, hint) + self.cellEditingEnded.emit() + + def edit(self, index: QModelIndex, + trigger: QAbstractItemView.EditTrigger, + event: QEvent) -> bool: + """ + Override QAbstractItemView.edit to catch when editing starts + """ + + print("KAE edit()") + self.edit_cell_type = None + result = super(PlaylistTab, self).edit(index, trigger, event) + if result: + self.cellEditingStarted.emit(index.row(), index.column()) + return result + + # def _edit_cell(self, mi): # review + # """ + # Called when table is double-clicked + + # Signal comes from QAbstractItemView.doubleClicked() + # """ + + # print("KAE _edit_cell()") + # row = mi.row() + # column = mi.column() + # item = self.item(row, column) + + # if column in [FIXUP.COL_TITLE, FIXUP.COL_ARTIST]: + # self.editItem(item) + + + # # ########## Externally called functions ########## def clear_next(self, session) -> None: @@ -1150,86 +1347,6 @@ class PlaylistTab(QTableWidget): cb = QApplication.clipboard() cb.clear(mode=cb.Clipboard) cb.setText(pathqs, mode=cb.Clipboard) -# -# def _cell_changed(self, row: int, column: int) -> None: -# """Called when cell content has changed""" -# -# if not self.editing_cell: -# return -# if column not in [FIXUP.COL_TITLE, FIXUP.COL_ARTIST]: -# return -# -# new_text: str = self.item(row, column).text() -# log.debug(f"_cell_changed({row=}, {column=}, {new_text=}") -# -# with Session() as session: -# if row in self._get_notes_rows(): -# # Save change to database -# note: Notes = self._get_row_notes_object(row, session) -# note.update(session, row, new_text) -# # Set/clear row start time accordingly -# start_time = self._get_note_text_time(new_text) -# if start_time: -# self._set_row_start_time(row, start_time) -# log.debug( -# f"_cell_changed:Note {new_text} contains valid " -# f"{start_time=}" -# ) -# else: -# # Reset row start time in case it used to have one -# self._set_row_start_time(row, None) -# log.debug( -# f"_cell_changed:Note {new_text} does not contain " -# "start time" -# ) -# else: -# track: Tracks = self._get_row_track_object(row, session) -# if column == FIXUP.COL_ARTIST: -# track.update_artist(session, artist=new_text) -# elif column == FIXUP.COL_TITLE: -# track.update_title(session, title=new_text) -# else: -# log.error("_cell_changed(): unrecognised column") -# -# def _cell_edit_ended(self) -> None: -# """Called when cell edit ends""" -# -# log.debug("_cell_edit_ended()") -# -# self.editing_cell = False -# -# # update_display to update start times, such as when a note has -# # been edited -# with Session() as session: -# self.update_display(session) -# -# self.musicmuster.enable_play_next_controls() -# -# def _cell_edit_started(self, row: int, column: int) -> None: -# """ -# Called when cell editing started. Disable play controls so -# that keys work during edit. -# """ -# -# log.debug(f"_cell_edit_started({row=}, {column=})") -# -# self.editing_cell = True -# # Disable play controls so that keyboard input doesn't disturb playing -# self.musicmuster.disable_play_next_controls() -# -# # If this is a note cell and it's a section start, we need to -# # remove any existing section timing so user can't edit that. -# # Section timing is only in display of item, not in note text in -# # database. Keep it simple: if this is a note, pull text from -# # database. -# -# if self._is_note_row(row): -# item = self.item(row, FIXUP.COL_TITLE) -# with Session() as session: -# note_object = self._get_row_notes_object(row, session) -# if note_object: -# item.setText(note_object.note) -# return # def _clear_played_row_status(self, row: int) -> None: # """Clear played status on row""" @@ -1268,23 +1385,6 @@ class PlaylistTab(QTableWidget): return (index.row() + 1 if self._is_below(event.pos(), index) else index.row()) -# def _edit_note_cell(self, row, column): # review -# """Called when table is single-clicked""" -# -# if column in [FIXUP.COL_ROW_NOTES]: -# item = self.item(row, column) -# self.editItem(item) -# -# def _edit_cell(self, mi): # review -# """Called when table is double-clicked""" -# -# row = mi.row() -# column = mi.column() -# item = self.item(row, column) -# -# if column in [FIXUP.COL_TITLE, FIXUP.COL_ARTIST]: -# self.editItem(item) -# # def _get_notes_rows(self) -> List[int]: # """Return rows marked as notes, or None""" #