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: