From 4860c9f1885f96b7bf54670cf6335c96a93e436f Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 11 Dec 2024 15:34:48 +0000 Subject: [PATCH 1/4] Expang edit box working, code untidy --- app/playlists.py | 170 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 119 insertions(+), 51 deletions(-) diff --git a/app/playlists.py b/app/playlists.py index f816660..e622aca 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -8,15 +8,17 @@ from PyQt6.QtCore import ( QModelIndex, QObject, QItemSelection, + QSize, Qt, QTimer, ) -from PyQt6.QtGui import QAction, QKeyEvent +from PyQt6.QtGui import QAction, QKeyEvent, QTextDocument from PyQt6.QtWidgets import ( QAbstractItemDelegate, QAbstractItemView, QApplication, QDoubleSpinBox, + QFrame, QHeaderView, QMenu, QMessageBox, @@ -28,6 +30,7 @@ from PyQt6.QtWidgets import ( QStyleOptionViewItem, QTableView, QTableWidgetItem, + QTextEdit, QWidget, ) @@ -58,54 +61,119 @@ class EscapeDelegate(QStyledItemDelegate): - 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 | QPlainTextEdit]: + ) -> Optional[QDoubleSpinBox | QTextEdit]: """ Intercept createEditor call and make row just a little bit taller """ - editor: QDoubleSpinBox | QPlainTextEdit + # 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 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): - editor = QPlainTextEdit(parent) - 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) - return editor + # 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) - return super().createEditor(parent, option, index) + 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) - return super().destroyEditor(editor, index) def editorEvent( self, event: QEvent, model: QAbstractItemModel, option, index: QModelIndex @@ -123,7 +191,7 @@ class EscapeDelegate(QStyledItemDelegate): return super().eventFilter(editor, event) if event.type() == QEvent.Type.Show: - if self.click_position and isinstance(editor, QPlainTextEdit): + if self.click_position and isinstance(editor, QTextEdit): # Map click position to editor's local space local_click_position = editor.mapFromParent(self.click_position) @@ -136,36 +204,20 @@ class EscapeDelegate(QStyledItemDelegate): return super().eventFilter(editor, event) - elif event.type() == QEvent.Type.KeyPress: - key_event = cast(QKeyEvent, event) - if key_event.key() == Qt.Key.Key_Return: - if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier): - self.commitData.emit(editor) - self.closeEditor.emit(editor) - return True - - elif key_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 - 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) @@ -182,7 +234,7 @@ class EscapeDelegate(QStyledItemDelegate): proxy_model = index.model() edit_index = proxy_model.mapToSource(index) - if isinstance(editor, QPlainTextEdit): + if isinstance(editor, QTextEdit): value = editor.toPlainText().strip() elif isinstance(editor, QDoubleSpinBox): value = editor.value() @@ -264,7 +316,7 @@ class PlaylistTab(QTableView): # Set up for Audacity try: - self.ac = AudacityController() + self.ac: Optional[AudacityController] = AudacityController() except ApplicationError as e: self.ac = None show_warning(self.musicmuster, "Audacity error", str(e)) @@ -272,9 +324,15 @@ class PlaylistTab(QTableView): # Stretch last column *after* setting column widths which is # *much* faster h_header = self.horizontalHeader() - if isinstance(h_header, QHeaderView): + if h_header: h_header.sectionResized.connect(self._column_resize) h_header.setStretchLastSection(True) + # Resize on vertical header click + v_header = self.verticalHeader() + if v_header: + v_header.setMinimumSectionSize(5) + v_header.sectionHandleDoubleClicked.disconnect() + v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents) # Setting ResizeToContents causes screen flash on load self.resize_rows() @@ -370,6 +428,16 @@ class PlaylistTab(QTableView): self.reset() super().mouseReleaseEvent(event) + def resizeRowToContents(self, row): + super().resizeRowToContents(row) + self.verticalHeader().resizeSection(row, self.sizeHintForRow(row)) + + def resizeRowsToContents(self): + header = self.verticalHeader() + for row in range(self.model().rowCount()): + hint = self.sizeHintForRow(row) + header.resizeSection(row, hint) + def selectionChanged( self, selected: QItemSelection, deselected: QItemSelection ) -> None: From 07d8ce9c41cc49cb7e65d1ef80524e6587ede73f Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 11 Dec 2024 22:35:11 +0000 Subject: [PATCH 2/4] Add type hints for profiling calls --- app/models.py | 2 +- app/musicmuster.py | 2 +- app/playlistmodel.py | 15 +++++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/models.py b/app/models.py index 7cf73fc..d46e5d5 100644 --- a/app/models.py +++ b/app/models.py @@ -567,7 +567,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): session: Session, playlist_id: int, sqla_map: List[dict[str, int]], - dummy_for_profiling=None, + dummy_for_profiling: Optional[int] = None, ) -> None: """ Take a {plrid: row_number} dictionary and update the row numbers accordingly diff --git a/app/musicmuster.py b/app/musicmuster.py index d72be6b..955c6e9 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1066,7 +1066,7 @@ class Window(QMainWindow, Ui_MainWindow): webbrowser.get("browser").open_new_tab(url) @line_profiler.profile - def paste_rows(self, dummy_for_profiling=None) -> None: + def paste_rows(self, dummy_for_profiling: Optional[int] = None) -> None: """ Paste earlier cut rows. """ diff --git a/app/playlistmodel.py b/app/playlistmodel.py index afdf28b..dd8b0b6 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -747,7 +747,10 @@ class PlaylistModel(QAbstractTableModel): @line_profiler.profile def move_rows( - self, from_rows: list[int], to_row_number: int, dummy_for_profiling=None + self, + from_rows: list[int], + to_row_number: int, + dummy_for_profiling: Optional[int] = None, ) -> None: """ Move the playlist rows given to to_row and below. @@ -819,7 +822,7 @@ class PlaylistModel(QAbstractTableModel): from_rows: list[int], to_row_number: int, to_playlist_id: int, - dummy_for_profiling=None, + dummy_for_profiling: Optional[int] = None, ) -> None: """ Move the playlist rows given to to_row and below of to_playlist. @@ -992,7 +995,9 @@ class PlaylistModel(QAbstractTableModel): self.invalidate_row(track_sequence.previous.row_number) @line_profiler.profile - def refresh_data(self, session: db.session, dummy_for_profiling=None) -> None: + def refresh_data( + self, session: db.session, dummy_for_profiling: Optional[int] = None + ) -> None: """Populate self.playlist_rows with playlist data""" # We used to clear self.playlist_rows each time but that's @@ -1019,7 +1024,9 @@ class PlaylistModel(QAbstractTableModel): # Same as refresh data, but only used when creating playslit. # Distinguishes profile time between initial load and other # refreshes. - def load_data(self, session: db.session, dummy_for_profiling=None) -> None: + def load_data( + self, session: db.session, dummy_for_profiling: Optional[int] = None + ) -> None: """Populate self.playlist_rows with playlist data""" # We used to clear self.playlist_rows each time but that's From 0b30a02dde697b8ce1c2b2b247c77b429a797038 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 11 Dec 2024 22:37:39 +0000 Subject: [PATCH 3/4] Row resizing WIP Resizing works, code is clean, rows not too tall, IntegerDelegate to be provided still. --- app/playlistmodel.py | 2 +- app/playlists.py | 346 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 280 insertions(+), 68 deletions(-) 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) From e29c7ed0fffbef559a46ccd13e575f0198835913 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Thu, 12 Dec 2024 18:02:58 +0000 Subject: [PATCH 4/4] Add in delegate for spinbox --- app/playlists.py | 258 +++++------------------------------------------ 1 file changed, 23 insertions(+), 235 deletions(-) diff --git a/app/playlists.py b/app/playlists.py index 7f56d9b..1090720 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -53,7 +53,7 @@ if TYPE_CHECKING: from musicmuster import Window -class TextDelegate(QStyledItemDelegate): +class PlaylistDelegate(QStyledItemDelegate): """ - closes the edit on control-return - checks with user before abandoning edit on Escape @@ -92,11 +92,13 @@ class TextDelegate(QStyledItemDelegate): parent: Optional[QWidget], option: QStyleOptionViewItem, index: QModelIndex, - ) -> Optional[QTextEdit]: + ) -> 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) @@ -108,12 +110,17 @@ class TextDelegate(QStyledItemDelegate): 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 - - TextDelegate.EditorDocument(editor) + if index.column() == Col.INTRO.value: + editor = QDoubleSpinBox(parent) + editor.setDecimals(1) + editor.setSingleStep(0.1) + return editor + elif isinstance(index.data(), str): + editor = Editor(parent) + editor.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + editor.setFrameShape(QFrame.Shape.NoFrame) + self.current_editor = editor + PlaylistDelegate.EditorDocument(editor) return editor def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None: @@ -174,9 +181,12 @@ class TextDelegate(QStyledItemDelegate): elif key == Qt.Key.Key_Escape: # Close editor if no changes have been made - if hasattr(editor, "toPlainText"): + data_modified = False + if isinstance(editor, QTextEdit): + data_modified = self.original_model_data.value() != editor.toPlainText() + elif isinstance(editor, QDoubleSpinBox): data_modified = ( - self.original_model_data.value() != editor.toPlainText() + self.original_model_data.value() != int(editor.value()) * 1000 ) if not data_modified: self.closeEditor.emit(editor) @@ -211,7 +221,8 @@ class TextDelegate(QStyledItemDelegate): edit_index, Qt.ItemDataRole.EditRole ) if index.column() == Col.INTRO.value: - editor.setValue(self.original_model_data.value() / 1000) + if self.original_model_data.value(): + editor.setValue(self.original_model_data.value() / 1000) else: editor.setPlainText(self.original_model_data.value()) @@ -229,229 +240,6 @@ class TextDelegate(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): """ @@ -490,7 +278,7 @@ class PlaylistTab(QTableView): # Set up widget self.source_model = PlaylistModel(playlist_id) self.proxy_model = PlaylistProxyModel(self.source_model) - self.setItemDelegate(TextDelegate(self, self.source_model)) + self.setItemDelegate(PlaylistDelegate(self, self.source_model)) self.setAlternatingRowColors(True) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)