From 9d3e4b8d0c00849af956fb93a645d0fcf924a0ea Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 22 Oct 2023 22:53:59 +0100 Subject: [PATCH] V3 WIP Drag and drop partly implemented UI works but outputs model changes needed to stdout --- app/playlistmodel.py | 25 ++++----- app/playlists.py | 122 +++++++++++++++++++++++-------------------- poetry.lock | 46 +++++++++++++++- pyproject.toml | 1 + 4 files changed, 121 insertions(+), 73 deletions(-) diff --git a/app/playlistmodel.py b/app/playlistmodel.py index fd653c3..4b5bbf8 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -166,20 +166,6 @@ class PlaylistModel(QAbstractTableModel): # Fall through to no-op return QVariant() - def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: - """ - Return text for editing - """ - - if column == Col.TITLE.value: - return QVariant(prd.title) - if column == Col.ARTIST.value: - return QVariant(prd.artist) - if column == Col.NOTE.value: - return QVariant(prd.note) - - return QVariant() - def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: """ Return text for display @@ -241,9 +227,13 @@ class PlaylistModel(QAbstractTableModel): """ if not index.isValid(): - return Qt.ItemFlag.ItemIsEnabled + return Qt.ItemFlag.ItemIsDropEnabled - default = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable + default = ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + ) if index.column() in [Col.TITLE.value, Col.ARTIST.value, Col.NOTE.value]: return default | Qt.ItemFlag.ItemIsEditable @@ -353,3 +343,6 @@ class PlaylistModel(QAbstractTableModel): return True return False + + def supportedDropActions(self): + return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction diff --git a/app/playlists.py b/app/playlists.py index c790685..b38dfe2 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -31,6 +31,9 @@ from PyQt6.QtWidgets import ( QTableView, QTableWidgetItem, QWidget, + QProxyStyle, + QStyle, + QStyleOption, ) from config import Config @@ -45,6 +48,7 @@ from helpers import ( set_track_metadata, ) from log import log + from models import Playlists, PlaylistRows, Settings, Tracks, NoteColours from playlistmodel import PlaylistModel @@ -52,37 +56,8 @@ from playlistmodel import PlaylistModel if TYPE_CHECKING: from musicmuster import Window, MusicMusterSignals -# scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") -# section_header_cleanup_re = re.compile(r"(@\d\d:\d\d:\d\d.*)?(\+)?") -# start_time_re = re.compile(r"@\d\d:\d\d:\d\d") - HEADER_NOTES_COLUMN = 2 -# # Columns -# Column = namedtuple("Column", ["idx", "heading"]) -# columns = {} -# columns["userdata"] = Column(idx=0, heading=Config.COLUMN_NAME_AUTOPLAY) -# columns["start_gap"] = Column(idx=1, heading=Config.COLUMN_NAME_LEADING_SILENCE) -# columns["title"] = Column(idx=2, heading=Config.COLUMN_NAME_TITLE) -# columns["artist"] = Column(idx=3, heading=Config.COLUMN_NAME_ARTIST) -# columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH) -# columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME) -# columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME) -# columns["lastplayed"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED) -# columns["bitrate"] = Column(idx=8, heading=Config.COLUMN_NAME_BITRATE) -# columns["row_notes"] = Column(idx=9, heading=Config.COLUMN_NAME_NOTES) - -# USERDATA = columns["userdata"].idx -# START_GAP = columns["start_gap"].idx -# TITLE = columns["title"].idx -# ARTIST = columns["artist"].idx -# DURATION = columns["duration"].idx -# START_TIME = columns["start_time"].idx -# END_TIME = columns["end_time"].idx -# LASTPLAYED = columns["lastplayed"].idx -# BITRATE = columns["bitrate"].idx -# ROW_NOTES = columns["row_notes"].idx - class EscapeDelegate(QStyledItemDelegate): """ @@ -157,6 +132,25 @@ class EscapeDelegate(QStyledItemDelegate): editor.setGeometry(option.rect) +class PlaylistStyle(QProxyStyle): + def drawPrimitive(self, element, option, painter, widget=None): + """ + Draw a line across the entire row rather than just the column + we're hovering over. This may not always work depending on global + style - for instance I think it won't work on OSX. + """ + if ( + element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop + and not option.rect.isNull() + ): + option_new = QStyleOption(option) + option_new.rect.setLeft(0) + if widget: + option_new.rect.setRight(widget.width()) + option = option_new + super().drawPrimitive(element, option, painter, widget) + + class PlaylistTab(QTableView): def __init__( self, @@ -178,17 +172,15 @@ class PlaylistTab(QTableView): self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) - # This dancing is to satisfy mypy - # Drag and drop setup - # self.setAcceptDrops(True) - # viewport = self.viewport() - # if viewport: - # viewport.setAcceptDrops(True) - # self.setDragDropOverwriteMode(False) - # self.setDropIndicatorShown(True) - # self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) - # self.setDragEnabled(False) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDragDropOverwriteMode(False) + self.setAcceptDrops(True) + # Set our custom style - this draws the drop indicator across the whole row + self.setStyle(PlaylistStyle()) + # TODO: change this later to only enable drags when multiple + # rows selected + self.setDragEnabled(True) # Prepare for context menu # self.menu = QMenu() # self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) @@ -221,6 +213,24 @@ class PlaylistTab(QTableView): # ########## Events other than cell editing ########## + def dropEvent(self, event): + if event.source() is not self or ( + event.dropAction() != Qt.DropAction.MoveAction + and self.dragDropMode() != QAbstractItemView.InternalMove + ): + super().dropEvent(event) + + from_rows = list(set([a.row() for a in self.selectedIndexes()])) + to_row = self.indexAt(event.position().toPoint()).row() + if ( + 0 <= min(from_rows) <= self.model().rowCount() + and 0 <= max(from_rows) <= self.model().rowCount() + and 0 <= to_row <= self.model().rowCount() + ): + print(f"move_rows({from_rows=}, {to_row=})") + event.accept() + super().dropEvent(event) + # def dropEvent(self, event: Optional[QDropEvent]) -> None: # """ # Handle drag/drop of rows @@ -510,7 +520,7 @@ class PlaylistTab(QTableView): """Unselect all tracks and reset drag mode""" self.clearSelection() - self.setDragEnabled(False) + # self.setDragEnabled(False) # def get_new_row_number(self) -> int: # """ @@ -1246,7 +1256,7 @@ class PlaylistTab(QTableView): self.save_playlist(session) # Reset drag mode - self.setDragEnabled(False) + # self.setDragEnabled(False) self._update_start_end_times(session) @@ -1304,21 +1314,21 @@ class PlaylistTab(QTableView): return self._plrid_to_row_number(next_track.plr_id) - @staticmethod - def _get_note_text_time(text: str) -> Optional[datetime]: - """Return datetime specified as @hh:mm:ss in text""" + # @staticmethod + # def _get_note_text_time(text: str) -> Optional[datetime]: + # """Return datetime specified as @hh:mm:ss in text""" - try: - match = start_time_re.search(text) - except TypeError: - return None - if not match: - return None + # try: + # match = start_time_re.search(text) + # except TypeError: + # return None + # if not match: + # return None - try: - return datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT) - except ValueError: - return None + # try: + # return datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT) + # except ValueError: + # return None def _get_played_rows(self, session: scoped_session) -> List[int]: """ @@ -2361,7 +2371,7 @@ class PlaylistTab(QTableView): self._reorder_rows(new_order) # Reset drag mode to allow row selection by dragging - self.setDragEnabled(False) + # self.setDragEnabled(False) # Save playlist with Session() as session: @@ -2384,7 +2394,7 @@ class PlaylistTab(QTableView): ] # Reset drag mode to allow row selection by dragging - self.setDragEnabled(False) + # self.setDragEnabled(False) # Save playlist with Session() as session: diff --git a/poetry.lock b/poetry.lock index 3092b61..5fd9edf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1030,6 +1030,23 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] +[[package]] +name = "pdbp" +version = "1.5.0" +description = "pdbp (Pdb+): A drop-in replacement for pdb and pdbpp." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pdbp-1.5.0-py3-none-any.whl", hash = "sha256:7640598c336ec3e3e0b2aeec71d20a1e810ba49e3e1b3effac5b862a798dea7d"}, + {file = "pdbp-1.5.0.tar.gz", hash = "sha256:23e03897fe950794a487238b64d8b0cec66760083c4697e3b7bc5ca0fae617ea"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.6", markers = "platform_system == \"Windows\""} +pygments = ">=2.16.1" +tabcompleter = ">=1.3.0" + [[package]] name = "pexpect" version = "4.8.0" @@ -1452,6 +1469,18 @@ files = [ [package.dependencies] numpy = ">=1.20.0" +[[package]] +name = "pyreadline3" +version = "3.4.1" +description = "A python implementation of GNU readline." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, + {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, +] + [[package]] name = "pytest" version = "7.4.2" @@ -1915,6 +1944,21 @@ files = [ {file = "stackprinter-0.2.10.tar.gz", hash = "sha256:99d1ea6b91ffad96b28241edd7bcf071752b0cf694cab58d2335df5353acd086"}, ] +[[package]] +name = "tabcompleter" +version = "1.3.0" +description = "tabcompleter --- Autocompletion in the Python console." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabcompleter-1.3.0-py3-none-any.whl", hash = "sha256:59dfe825f4d88a51d486c0a513763eca6224f2146518d185ee2ebfc4f2398b80"}, + {file = "tabcompleter-1.3.0.tar.gz", hash = "sha256:47b9d4f783d14ebca5c66223c7f82cc1ef89f7313ba9ea0ce75265670178bb6e"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "text-unidecode" version = "1.3" @@ -2148,4 +2192,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "97f122b0c15850e806e764ab7d3df23ce115e8aa9cc7a775c64834b18beef664" +content-hash = "514b699dbd1e579adcad3ee6112c632bbd01bc801377b27a5cbe7cebd35d5995" diff --git a/pyproject.toml b/pyproject.toml index 0b05ce8..9c86b8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ furo = "^2023.5.20" black = "^23.3.0" flakehell = "^0.9.0" mypy = "^1.6.0" +pdbp = "^1.5.0" [build-system] requires = ["poetry-core>=1.0.0"]