import os import tempfile from logging import debug from PyQt5.QtWidgets import ( QAbstractScrollArea, QGraphicsPixmapItem, QGraphicsScene, QGraphicsView, QMenu, QAction, ) from PyQt5.QtCore import QEvent, QMimeData, Qt, pyqtSignal, QUrl, QPoint from PyQt5.QtGui import ( QDragEnterEvent, QDragMoveEvent, QDropEvent, QClipboard, QPixmap, ) class AlbumArtGraphicsView(QGraphicsView): """ Displays the album art of the currently playing song """ albumArtDropped = pyqtSignal(str) albumArtDeleted = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self.setAcceptDrops(True) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.setMinimumSize(200, 200) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored) self.setInteractive(False) self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter) self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate) self.album_art_scene: QGraphicsScene = QGraphicsScene() self.customContextMenuRequested.connect(self.showContextMenu) def dragEnterEvent(self, event: QDragEnterEvent | None): if not event: return if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dragMoveEvent(self, event: QDragMoveEvent | None): if event is None: return if event.mimeData().hasUrls(): event.accept() else: event.ignore() def dropEvent(self, event: QDropEvent | None): """Handles drag and drop pic onto view to add album art to songs""" if event is None: return urls = event.mimeData().urls() if urls: first_url = urls[0].toLocalFile() if first_url.lower().endswith((".png", ".jpg", ".jpeg")): debug(f"dropped {first_url}") self.albumArtDropped.emit( first_url ) # emit signal that album art was dropped event.accept() return event.ignore() def showContextMenu(self, position: QPoint): """Handles showing a context menu when right clicking the album art""" contextMenu = QMenu(self) # Create the menu copyAction = QAction("Copy", self) # Add the actions pasteAction = QAction("Paste", self) deleteAction = QAction("Delete", self) copyAction.triggered.connect( self.copy_album_art_to_clipboard ) # Add the signal triggers pasteAction.triggered.connect(self.paste_album_art_from_clipboard) deleteAction.triggered.connect(self.delete_album_art) contextMenu.addAction(copyAction) # Add actions to the menu contextMenu.addAction(pasteAction) contextMenu.addAction(deleteAction) # DO contextMenu.exec_(self.mapToGlobal(position)) # Show the menu def load_album_art(self, album_art_data: bytes) -> None: """Displays the album art for the currently playing track in the GraphicsView""" if album_art_data: # Clear the scene try: self.album_art_scene.clear() except Exception: pass # Reset the scene self.setScene(self.album_art_scene) # Create pixmap for album art pixmap = QPixmap() pixmap.loadFromData(album_art_data) # Create a QGraphicsPixmapItem for more control over pic pixmap_item = QGraphicsPixmapItem(pixmap) pixmap_item.setTransformationMode( Qt.TransformationMode.SmoothTransformation ) # For better quality scaling # Add pixmap item to the scene self.album_art_scene.addItem(pixmap_item) # Set the scene self.setScene(self.album_art_scene) # Adjust the album art scaling self.adjust_pixmap_scaling(pixmap_item) def copy_album_art_to_clipboard(self): """Copies album art to the clipboard""" if not self.scene().items(): return # dont care if no pic clipboard = self.qapp.clipboard pixmap_item = self.scene().items()[0] if hasattr(pixmap_item, "pixmap"): clipboard.setPixmap(pixmap_item.pixmap()) def paste_album_art_from_clipboard(self): """Handles pasting album art into a song via system clipboard""" clipboard = self.qapp.clipboard mime_data = clipboard.mimeData() # Check if clipboard data is raw data or filepath pixmap = None if mime_data.hasUrls(): for url in mime_data.urls(): file_path = url.toLocalFile() if os.path.isfile(file_path): pixmap = QPixmap(file_path) else: pixmap = clipboard.pixmap() # Put image on screen and emit signal for ID3 tags to be updated if pixmap is not None: # Add pixmap raw data image # try: # self.scene().clear() # except Exception: # pass self.scene().addPixmap(pixmap) # Create temp file for pic temp_file, file_path = tempfile.mkstemp(suffix=".jpg") os.close(temp_file) # close the file pixmap.save(file_path, "JPEG") self.albumArtDropped.emit( file_path ) # emit signal that album art was pasted def delete_album_art(self): """Emits a signal for the album art of the current song to be deleted""" self.albumArtDeleted.emit() def adjust_pixmap_scaling(self, pixmap_item) -> None: """Adjust the scaling of the pixmap item to fit the QGraphicsView, maintaining aspect ratio""" viewWidth = self.width() viewHeight = self.height() pixmapSize = pixmap_item.pixmap().size() # Calculate scaling factor while maintaining aspect ratio scaleX = viewWidth / pixmapSize.width() scaleY = viewHeight / pixmapSize.height() scaleFactor = min(scaleX, scaleY) # Apply scaling to the pixmap item pixmap_item.setScale(scaleFactor) def load_qapp(self, qapp): """Necessary for talking between components...""" self.qapp = qapp