diff --git a/app/playlistmodel.py b/app/playlistmodel.py index dd8b0b6..87d9e7a 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -394,7 +394,7 @@ class PlaylistModel(QAbstractTableModel): if rat.intro: return QVariant(f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}") else: - return QVariant() + return QVariant("") dispatch_table = { Col.ARTIST.value: QVariant(rat.artist), diff --git a/app/playlists.py b/app/playlists.py index e622aca..7f56d9b 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,5 +1,5 @@ # Standard library imports -from typing import Callable, cast, List, Optional, TYPE_CHECKING +from typing import Any, Callable, cast, List, Optional, TYPE_CHECKING # PyQt imports from PyQt6.QtCore import ( @@ -12,17 +12,15 @@ from PyQt6.QtCore import ( Qt, QTimer, ) -from PyQt6.QtGui import QAction, QKeyEvent, QTextDocument +from PyQt6.QtGui import QAction, QDropEvent, QKeyEvent, QTextDocument from PyQt6.QtWidgets import ( QAbstractItemDelegate, QAbstractItemView, QApplication, QDoubleSpinBox, QFrame, - QHeaderView, QMenu, QMessageBox, - QPlainTextEdit, QProxyStyle, QStyle, QStyledItemDelegate, @@ -55,12 +53,12 @@ if TYPE_CHECKING: from musicmuster import Window -class EscapeDelegate(QStyledItemDelegate): +class TextDelegate(QStyledItemDelegate): """ - - increases the height of a row when editing to make editing easier - closes the edit on control-return - checks with user before abandoning edit on Escape - positions cursor where double-click occurs + - expands edit box and parent table row as text is added Parts inspired by https://stackoverflow.com/questions/69113867/ make-row-of-qtableview-expand-as-editor-grows-in-height @@ -86,85 +84,38 @@ class EscapeDelegate(QStyledItemDelegate): super().__init__(parent) self.source_model = source_model self.signals = MusicMusterSignals() - self.click_position = None # Store the mouse click position - self.current_editor = None + self.click_position = None + self.current_editor: Optional[Any] = None def createEditor( self, parent: Optional[QWidget], option: QStyleOptionViewItem, index: QModelIndex, - ) -> Optional[QDoubleSpinBox | QTextEdit]: + ) -> Optional[QTextEdit]: """ Intercept createEditor call and make row just a little bit taller """ - # editor: QDoubleSpinBox | QTextEdit - class Editor(QTextEdit): def resizeEvent(self, event): super().resizeEvent(event) parent.parent().resizeRowToContents(index.row()) - def keyPressEvent(self, event): - if event.modifiers() == Qt.KeyboardModifier.ControlModifier: - if event.key() == Qt.Key.Key_Return: - self.commitData.emit(editor) - # self.closeEditor.emit(editor) - return - # elif event.key() == Qt.Key.Key_Escape: - # # Close editor if no changes have been made - # data_modified = False - # if isinstance(editor, QPlainTextEdit): - # data_modified = self.original_model_data != editor.toPlainText() - # elif isinstance(editor, QDoubleSpinBox): - # data_modified = ( - # self.original_model_data != int(editor.value()) * 1000 - # ) - # if not data_modified: - # self.closeEditor.emit(editor) - # return True - - # discard_edits = QMessageBox.question( - # cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" - # ) - # if discard_edits == QMessageBox.StandardButton.Yes: - # self.closeEditor.emit(editor) - # return True - - super().keyPressEvent(event) - self.signals = MusicMusterSignals() self.signals.enable_escape_signal.emit(False) - # if isinstance(self.parent(), PlaylistTab): - # p = cast(PlaylistTab, self.parent()) - - # if index.column() == Col.INTRO.value: - # editor = QDoubleSpinBox(parent) - # editor.setDecimals(1) - # editor.setSingleStep(0.1) - # return editor - # elif isinstance(index.data(), str): if self.current_editor: editor = self.current_editor else: editor = Editor(parent) - editor.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) + editor.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) editor.setFrameShape(QFrame.Shape.NoFrame) self.current_editor = editor - # # editor.setGeometry(option.rect) # Match the cell geometry - # row = index.row() - # row_height = p.rowHeight(row) - # p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT) - EscapeDelegate.EditorDocument(editor) + TextDelegate.EditorDocument(editor) return editor - # return super().createEditor(parent, option, index) - def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None: """ Intercept editor destroyment @@ -172,23 +123,31 @@ class EscapeDelegate(QStyledItemDelegate): super().destroyEditor(editor, index) self.current_editor = None - self.parent().resizeRowToContents(index.row()) + # Funky mypy dancing: + parent = self.parent() + if parent and hasattr(parent, "resizeRowToContents"): + parent.resizeRowToContents(index.row()) self.signals.enable_escape_signal.emit(True) def editorEvent( - self, event: QEvent, model: QAbstractItemModel, option, index: QModelIndex + self, + event: Optional[QEvent], + model: Optional[QAbstractItemModel], + option: QStyleOptionViewItem, + index: QModelIndex, ) -> bool: """Capture mouse click position.""" - if event.type() == QEvent.Type.MouseButtonPress: - self.click_position = event.pos() + if event and event.type() == QEvent.Type.MouseButtonPress: + if hasattr(event, "pos"): + self.click_position = event.pos() return super().editorEvent(event, model, option, index) def eventFilter(self, editor: Optional[QObject], event: Optional[QEvent]) -> bool: """By default, QPlainTextEdit doesn't handle enter or return""" if editor is None or event is None: - return super().eventFilter(editor, event) + return False if event.type() == QEvent.Type.Show: if self.click_position and isinstance(editor, QTextEdit): @@ -202,7 +161,33 @@ class EscapeDelegate(QStyledItemDelegate): # Reset click position self.click_position = None - return super().eventFilter(editor, event) + return False + + elif event.type() == QEvent.Type.KeyPress: + key_event = cast(QKeyEvent, event) + key = key_event.key() + if key == Qt.Key.Key_Return: + if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier): + self.commitData.emit(editor) + self.closeEditor.emit(editor) + return True + + elif key == Qt.Key.Key_Escape: + # Close editor if no changes have been made + if hasattr(editor, "toPlainText"): + data_modified = ( + self.original_model_data.value() != editor.toPlainText() + ) + if not data_modified: + self.closeEditor.emit(editor) + return True + + discard_edits = QMessageBox.question( + cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" + ) + if discard_edits == QMessageBox.StandardButton.Yes: + self.closeEditor.emit(editor) + return True return False @@ -244,6 +229,229 @@ class EscapeDelegate(QStyledItemDelegate): editor.setGeometry(option.rect) +# Will be int delegate +# class EscapeDelegate(QStyledItemDelegate): +# """ +# - increases the height of a row when editing to make editing easier +# - closes the edit on control-return +# - checks with user before abandoning edit on Escape +# - positions cursor where double-click occurs + +# Parts inspired by https://stackoverflow.com/questions/69113867/ +# make-row-of-qtableview-expand-as-editor-grows-in-height +# """ + +# class EditorDocument(QTextDocument): +# def __init__(self, parent): +# super().__init__(parent) +# self.setDocumentMargin(0) +# self.contentsChange.connect(self.contents_change) +# self.height = None +# parent.setDocument(self) + +# def contents_change(self, position, chars_removed, chars_added): +# def resize_func(): +# if self.size().height() != self.height: +# doc_size = self.size() +# self.parent().resize(int(doc_size.width()), int(doc_size.height())) + +# QTimer.singleShot(0, resize_func) + +# def __init__(self, parent: QWidget, source_model: PlaylistModel) -> None: +# super().__init__(parent) +# self.source_model = source_model +# self.signals = MusicMusterSignals() +# self.click_position = None # Store the mouse click position +# self.current_editor = None + +# def createEditor( +# self, +# parent: Optional[QWidget], +# option: QStyleOptionViewItem, +# index: QModelIndex, +# ) -> Optional[QDoubleSpinBox | QTextEdit]: +# """ +# Intercept createEditor call and make row just a little bit taller +# """ + +# # editor: QDoubleSpinBox | QTextEdit + +# class Editor(QTextEdit): +# def resizeEvent(self, event): +# super().resizeEvent(event) +# parent.parent().resizeRowToContents(index.row()) + +# def keyPressEvent(self, event): +# if event.modifiers() == Qt.KeyboardModifier.ControlModifier: +# if event.key() == Qt.Key.Key_Return: +# print("control-return") +# import pdb; pdb.set_trace() +# self.commit.emit(self) +# # self.closeEditor.emit(editor) +# return +# elif event.key() == Qt.Key.Key_Escape: +# # Close editor if no changes have been made +# print("escape") +# data_modified = False +# if isinstance(editor, QPlainTextEdit): +# data_modified = self.original_model_data != editor.toPlainText() +# elif isinstance(editor, QDoubleSpinBox): +# data_modified = ( +# self.original_model_data != int(editor.value()) * 1000 +# ) +# if not data_modified: +# self.closeEditor.emit(editor) +# return True + +# discard_edits = QMessageBox.question( +# cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" +# ) +# if discard_edits == QMessageBox.StandardButton.Yes: +# self.closeEditor.emit(editor) +# return True + +# super().keyPressEvent(event) + +# self.signals = MusicMusterSignals() +# self.signals.enable_escape_signal.emit(False) + +# # if isinstance(self.parent(), PlaylistTab): +# # p = cast(PlaylistTab, self.parent()) + +# # if index.column() == Col.INTRO.value: +# # editor = QDoubleSpinBox(parent) +# # editor.setDecimals(1) +# # editor.setSingleStep(0.1) +# # return editor +# # elif isinstance(index.data(), str): +# if self.current_editor: +# editor = self.current_editor +# else: +# editor = Editor(parent) +# editor.setVerticalScrollBarPolicy( +# Qt.ScrollBarPolicy.ScrollBarAlwaysOff +# ) +# editor.setFrameShape(QFrame.Shape.NoFrame) +# self.current_editor = editor +# # # editor.setGeometry(option.rect) # Match the cell geometry +# # row = index.row() +# # row_height = p.rowHeight(row) +# # p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT) + +# EscapeDelegate.EditorDocument(editor) +# return editor + +# # return super().createEditor(parent, option, index) + +# def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None: +# """ +# Intercept editor destroyment +# """ + +# super().destroyEditor(editor, index) +# self.current_editor = None +# self.parent().resizeRowToContents(index.row()) +# self.signals.enable_escape_signal.emit(True) + +# def editorEvent( +# self, event: QEvent, model: QAbstractItemModel, option, index: QModelIndex +# ) -> bool: +# """Capture mouse click position.""" + +# if event.type() == QEvent.Type.MouseButtonPress: +# self.click_position = event.pos() +# return super().editorEvent(event, model, option, index) + +# def eventFilter(self, editor: Optional[QObject], event: Optional[QEvent]) -> bool: +# """By default, QPlainTextEdit doesn't handle enter or return""" + +# if editor is None or event is None: +# return False + +# if event.type() == QEvent.Type.Show: +# if self.click_position and isinstance(editor, QTextEdit): +# # Map click position to editor's local space +# local_click_position = editor.mapFromParent(self.click_position) + +# # Move cursor to the calculated position +# cursor = editor.cursorForPosition(local_click_position) +# editor.setTextCursor(cursor) + +# # Reset click position +# self.click_position = None + +# return False + +# elif event.type() == QEvent.Type.KeyPress: +# key_event = cast(QKeyEvent, event) +# key = key_event.key() +# if key == Qt.Key.Key_Return: +# if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier): +# self.commitData.emit(editor) +# self.closeEditor.emit(editor) +# return True + +# elif key == Qt.Key.Key_Escape: +# # Close editor if no changes have been made +# if hasattr(editor, "toPlainText"): +# data_modified = self.original_model_data.value() != editor.toPlainText() +# # if isinstance(editor, QPlainTextEdit): +# # data_modified = self.original_model_data.value() != editor.toPlainText() +# # elif isinstance(editor, QDoubleSpinBox): +# # data_modified = ( +# # self.original_model_data != int(editor.value()) * 1000 +# # ) +# if not data_modified: +# self.closeEditor.emit(editor) +# return True + +# discard_edits = QMessageBox.question( +# cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" +# ) +# if discard_edits == QMessageBox.StandardButton.Yes: +# self.closeEditor.emit(editor) +# return True + +# return False + +# def sizeHint(self, option, index): +# self.initStyleOption(option, index) +# if self.current_editor: +# doc = self.current_editor.document() +# else: +# doc = QTextDocument() +# doc.setTextWidth(option.rect.width()) +# doc.setDefaultFont(option.font) +# doc.setDocumentMargin(0) +# doc.setHtml(option.text) +# return QSize(int(doc.idealWidth()), int(doc.size().height())) + +# def setEditorData(self, editor, index): +# proxy_model = index.model() +# edit_index = proxy_model.mapToSource(index) + +# self.original_model_data = self.source_model.data( +# edit_index, Qt.ItemDataRole.EditRole +# ) +# if index.column() == Col.INTRO.value: +# editor.setValue(self.original_model_data.value() / 1000) +# else: +# editor.setPlainText(self.original_model_data.value()) + +# def setModelData(self, editor, model, index): +# proxy_model = index.model() +# edit_index = proxy_model.mapToSource(index) + +# if isinstance(editor, QTextEdit): +# value = editor.toPlainText().strip() +# elif isinstance(editor, QDoubleSpinBox): +# value = editor.value() +# self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole) + +# def updateEditorGeometry(self, editor, option, index): +# editor.setGeometry(option.rect) + + class PlaylistStyle(QProxyStyle): def drawPrimitive(self, element, option, painter, widget=None): """ @@ -282,7 +490,7 @@ class PlaylistTab(QTableView): # Set up widget self.source_model = PlaylistModel(playlist_id) self.proxy_model = PlaylistProxyModel(self.source_model) - self.setItemDelegate(EscapeDelegate(self, self.source_model)) + self.setItemDelegate(TextDelegate(self, self.source_model)) self.setAlternatingRowColors(True) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) @@ -360,10 +568,12 @@ class PlaylistTab(QTableView): # Deselect edited line self.clear_selection() - def dropEvent(self, event): + def dropEvent(self, event: Optional[QDropEvent]) -> None: + if not event: + return if event.source() is not self or ( event.dropAction() != Qt.DropAction.MoveAction - and self.dragDropMode() != QAbstractItemView.InternalMove + and self.dragDropMode() != QAbstractItemView.DragDropMode.InternalMove ): super().dropEvent(event) @@ -776,6 +986,8 @@ class PlaylistTab(QTableView): Import current Audacity track to passed row """ + if not self.ac: + return try: self.ac.export() self._rescan(row_number)