Compare commits

...

4 Commits

Author SHA1 Message Date
Keith Edmunds
bdf7b0979d Cell editing rewrite
Simplied, commented, no longer using custom signals, all functions
have type information.
2022-08-13 22:12:22 +01:00
Keith Edmunds
cee84563fb WIP re editing 2022-08-13 21:13:03 +01:00
Keith Edmunds
4d9bf9a36b Hide/show played tracks button working 2022-08-13 16:32:37 +01:00
Keith Edmunds
ce0c3de40d 3dB drop button working 2022-08-13 16:11:55 +01:00
3 changed files with 217 additions and 175 deletions

View File

@ -127,18 +127,17 @@ class Music:
# #
# with lock: # with lock:
# return self.player.set_time(ms) # return self.player.set_time(ms)
#
# def set_volume(self, volume, set_default=True): def set_volume(self, volume, set_default=True):
# """Set maximum volume used for player""" """Set maximum volume used for player"""
#
# with lock: if not self.player:
# if not self.player: return
# return
# if set_default:
# if set_default: self.max_volume = volume
# self.max_volume = volume
# self.player.audio_set_volume(volume)
# self.player.audio_set_volume(volume)
def stop(self) -> float: def stop(self) -> float:
"""Immediately stop playing""" """Immediately stop playing"""

View File

@ -174,9 +174,8 @@ class Window(QMainWindow, Ui_MainWindow):
lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
self.actionSkipToNext.triggered.connect(self.play_next) self.actionSkipToNext.triggered.connect(self.play_next)
self.actionStop.triggered.connect(self.stop) self.actionStop.triggered.connect(self.stop)
# ***kae self.btnDrop3db.clicked.connect(self.drop3db)
# self.btnDrop3db.clicked.connect(self.drop3db) self.btnHidePlayed.clicked.connect(self.hide_played)
# self.btnHidePlayed.clicked.connect(self.hide_played)
self.btnFade.clicked.connect(self.fade) self.btnFade.clicked.connect(self.fade)
self.btnStop.clicked.connect(self.stop) self.btnStop.clicked.connect(self.stop)
self.tabPlaylist.tabCloseRequested.connect(self.close_tab) self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
@ -282,14 +281,14 @@ class Window(QMainWindow, Ui_MainWindow):
f.write( f.write(
f"{playdate.track.artist},{playdate.track.title}\n" f"{playdate.track.artist},{playdate.track.title}\n"
) )
#
# def drop3db(self) -> None: def drop3db(self) -> None:
# """Drop music level by 3db if button checked""" """Drop music level by 3db if button checked"""
#
# if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
# self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False) self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False)
# else: else:
# self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False) self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False)
def enable_play_next_controls(self) -> None: def enable_play_next_controls(self) -> None:
""" """
@ -389,19 +388,21 @@ class Window(QMainWindow, Ui_MainWindow):
"""Fade currently playing track""" """Fade currently playing track"""
self.stop_playing(fade=True) self.stop_playing(fade=True)
#
# def hide_played(self): def hide_played(self):
# """Toggle hide played tracks""" """Toggle hide played tracks"""
#
# if self.hide_played_tracks: if self.hide_played_tracks:
# self.hide_played_tracks = False self.hide_played_tracks = False
# self.btnHidePlayed.setText("Hide played") self.btnHidePlayed.setText("Hide played")
# else: else:
# self.hide_played_tracks = True self.hide_played_tracks = True
# self.btnHidePlayed.setText("Show played") self.btnHidePlayed.setText("Show played")
# if self.current_track_playlist_tab:
# with Session() as session: # Update all displayed playlists
# self.current_track_playlist_tab.update_display(session) with Session() as session:
for i in range(self.tabPlaylist.count()):
self.tabPlaylist.widget(i).update_display(session)
# #
# def import_track(self) -> None: # def import_track(self) -> None:
# """Import track file""" # """Import track file"""

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,25 +72,25 @@ 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)
# cellEditingEnded = QtCore.pyqtSignal()
# Qt.UserRoles # Qt.UserRoles
ROW_FLAGS = Qt.UserRole ROW_FLAGS = Qt.UserRole
ROW_TRACK_ID = Qt.UserRole + 1 ROW_TRACK_ID = Qt.UserRole + 1
@ -103,12 +105,12 @@ 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.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
@ -146,14 +148,9 @@ 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.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.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) self.horizontalHeader().sectionResized.connect(self._column_resize)
# Now load our tracks and notes # Now load our tracks and notes
@ -161,12 +158,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 +223,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 +314,158 @@ 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
#
# 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"""
# Disable cell changed signal connection as note updates will
# change cell again (metadata)
self.cellChanged.disconnect(self._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)
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 closeEditor(self,
editor: QWidget,
hint: QAbstractItemDelegate.EndEditHint) -> None:
"""
Override QAbstractItemView.closeEditor to enable play controls
and update display.
"""
# 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()
super(PlaylistTab, self).closeEditor(editor, hint)
def edit(self, index: QModelIndex,
trigger: QAbstractItemView.EditTrigger,
event: QEvent) -> bool:
"""
Override QAbstractItemView.edit to catch when editing starts
"""
self.edit_cell_type = None
result = super(PlaylistTab, self).edit(index, trigger, event)
if result:
row = index.row()
column = index.column()
# 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)
# 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:
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)
return result
# # ########## Externally called functions ########## # # ########## Externally called functions ##########
def clear_next(self, session) -> None: def clear_next(self, session) -> None:
@ -1150,86 +1289,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 +1327,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"""
# #