WIP re editing

This commit is contained in:
Keith Edmunds 2022-08-13 21:13:03 +01:00
parent 4d9bf9a36b
commit cee84563fb

View File

@ -6,7 +6,7 @@ from collections import namedtuple
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import List, Optional
from PyQt5.QtCore import QEvent, Qt, pyqtSignal from PyQt5.QtCore import QEvent, QModelIndex, Qt, pyqtSignal
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QBrush, QBrush,
QColor, QColor,
@ -14,16 +14,18 @@ from PyQt5.QtGui import (
QDropEvent QDropEvent
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
# QInputDialog, # QInputDialog,
# QLineEdit, QLineEdit,
QMainWindow, QMainWindow,
QMenu, QMenu,
# QStyledItemDelegate, QStyledItemDelegate,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QWidget
) )
from config import Config 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) columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES)
# class NoSelectDelegate(QStyledItemDelegate): class NoSelectDelegate(QStyledItemDelegate):
# """https://stackoverflow.com/questions/72790705/dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962""" """
# https://stackoverflow.com/questions/72790705/
# def createEditor(self, parent, option, index): dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962
# editor = super().createEditor(parent, option, index) """
# if isinstance(editor, QLineEdit):
# def deselect(): def createEditor(self, parent, option, index):
# # Important! First disconnect, otherwise editor.deselect() editor = super().createEditor(parent, option, index)
# # will call again this function if isinstance(editor, QLineEdit):
# editor.selectionChanged.disconnect(deselect) def deselect():
# editor.deselect() # Important! First disconnect, otherwise editor.deselect()
# editor.selectionChanged.connect(deselect) # will call again this function
# return editor editor.selectionChanged.disconnect(deselect)
editor.deselect()
editor.selectionChanged.connect(deselect)
return editor
class PlaylistTab(QTableWidget): class PlaylistTab(QTableWidget):
# cellEditingStarted = QtCore.pyqtSignal(int, int) # Custom signals
# cellEditingEnded = QtCore.pyqtSignal() cellEditingStarted = pyqtSignal(int, int)
cellEditingEnded = pyqtSignal()
# Qt.UserRoles # Qt.UserRoles
ROW_FLAGS = Qt.UserRole ROW_FLAGS = Qt.UserRole
@ -103,12 +109,13 @@ class PlaylistTab(QTableWidget):
self.menu: Optional[QMenu] = None self.menu: Optional[QMenu] = None
self.current_track_start_time: Optional[datetime] = None self.current_track_start_time: Optional[datetime] = None
#
# # Don't select text on edit # Don't select text on edit
# self.setItemDelegate(NoSelectDelegate(self)) self.setItemDelegate(NoSelectDelegate(self))
#
# Set up widget # Set up widget
# self.setEditTriggers(QAbstractItemView.AllEditTriggers) # self.setEditTriggers(QAbstractItemView.DoubleClicked)
self.setEditTriggers(QAbstractItemView.AllEditTriggers)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
@ -146,13 +153,13 @@ class PlaylistTab(QTableWidget):
self.itemSelectionChanged.connect(self._select_event) self.itemSelectionChanged.connect(self._select_event)
self.row_filter: Optional[str] = None 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 self.selecting_in_progress = False
# Connect signals # Connect signals
# self.cellChanged.connect(self._cell_changed) # self.cellClicked.connect(self._edit_note_cell)
# self.cellClicked.connect(self._edit_note_cell) self.cellEditingEnded.connect(self._cell_edit_ended)
# self.cellEditingEnded.connect(self._cell_edit_ended) self.cellEditingStarted.connect(self._cell_edit_started)
# self.cellEditingStarted.connect(self._cell_edit_started)
# self.doubleClicked.connect(self._edit_cell) # self.doubleClicked.connect(self._edit_cell)
self.horizontalHeader().sectionResized.connect(self._column_resize) self.horizontalHeader().sectionResized.connect(self._column_resize)
@ -161,12 +168,8 @@ class PlaylistTab(QTableWidget):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}" return f"<PlaylistTab(id={self.playlist_id}"
#
# # ########## Events ########## # ########## Events other than cell editing ##########
#
# def closeEditor(self, editor, hint): # review
# super(PlaylistTab, self).closeEditor(editor, hint)
# self.cellEditingEnded.emit()
def closeEvent(self, event) -> None: def closeEvent(self, event) -> None:
"""Handle closing playist tab""" """Handle closing playist tab"""
@ -230,12 +233,6 @@ class PlaylistTab(QTableWidget):
self.save_playlist(session) self.save_playlist(session)
self.update_display(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): def eventFilter(self, source, event):
"""Used to process context (right-click) menu, which is defined here""" """Used to process context (right-click) menu, which is defined here"""
@ -327,6 +324,206 @@ class PlaylistTab(QTableWidget):
self.setDragEnabled(False) self.setDragEnabled(False)
super().mouseReleaseEvent(event) 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 ########## # # ########## Externally called functions ##########
def clear_next(self, session) -> None: def clear_next(self, session) -> None:
@ -1150,86 +1347,6 @@ class PlaylistTab(QTableWidget):
cb = QApplication.clipboard() cb = QApplication.clipboard()
cb.clear(mode=cb.Clipboard) cb.clear(mode=cb.Clipboard)
cb.setText(pathqs, 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: # def _clear_played_row_status(self, row: int) -> None:
# """Clear played status on row""" # """Clear played status on row"""
@ -1268,23 +1385,6 @@ class PlaylistTab(QTableWidget):
return (index.row() + 1 if self._is_below(event.pos(), index) return (index.row() + 1 if self._is_below(event.pos(), index)
else index.row()) 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]: # def _get_notes_rows(self) -> List[int]:
# """Return rows marked as notes, or None""" # """Return rows marked as notes, or None"""
# #