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 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"<PlaylistTab(id={self.playlist_id}"
#
# # ########## Events ##########
#
# def closeEditor(self, editor, hint): # review
# super(PlaylistTab, self).closeEditor(editor, hint)
# self.cellEditingEnded.emit()
# ########## Events other than cell editing ##########
def closeEvent(self, event) -> 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"""
#