From 8219ffc416aa2c2fc31fb3a49eef91afc6d38e5c Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:26:24 -0700 Subject: [PATCH] feat: expanded file deletion/trashing (#409) * feat: send deleted files to system trash This refactors the file deletion code to send files to the system trash instead of performing a hard deletion. It also fixes deleting video files and GIFs loaded in the Preview Panel. * feat(ui): add file deletion confirmation boxes * feat(ui): add delete file menu option + shortcut * ui: update file deletion message boxes * fix(ui): same default confirm button on win/mac - Make "Yes" the default choice in the delete file modal for both Windows and macOS (Linux untested) - Change status messages to be more broad, since they also are displayed when cancelling the operation * ui: show perm deletion warning on all platforms --- requirements.txt | 1 + tagstudio/resources/qt/videos/placeholder.mp4 | Bin 0 -> 2590 bytes tagstudio/src/core/media_types.py | 13 ++ tagstudio/src/qt/helpers/file_deleter.py | 30 ++++ tagstudio/src/qt/resource_manager.py | 14 ++ tagstudio/src/qt/resources.json | 4 + tagstudio/src/qt/ts_qt.py | 156 ++++++++++++++++-- tagstudio/src/qt/widgets/fields.py | 6 +- tagstudio/src/qt/widgets/item_thumb.py | 8 + tagstudio/src/qt/widgets/preview_panel.py | 85 +++++++++- 10 files changed, 298 insertions(+), 19 deletions(-) create mode 100644 tagstudio/resources/qt/videos/placeholder.mp4 create mode 100644 tagstudio/src/qt/helpers/file_deleter.py diff --git a/requirements.txt b/requirements.txt index c2bddc36..e4afacdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ pydub==0.25.1 mutagen==1.47.0 numpy==1.26.4 ffmpeg-python==0.2.0 +Send2Trash==1.8.3 vtf2img==0.1.0 diff --git a/tagstudio/resources/qt/videos/placeholder.mp4 b/tagstudio/resources/qt/videos/placeholder.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..1e22e4c724aeefa2a44b44f3462f075495373b1e GIT binary patch literal 2590 zcmdT`&1)M+6d$c+MRpZejve9mp}-u$ss+Ac30A({p#*$ z69)o0rq>>d>7mECy@gW9C6~}b4~5WMA(wpgKWHiKAw_v_b~KVVA1#Fj^0YJW^Y`BD z%)UW{5ZdBfp%VulAsRsrLuZLu^TI}r5K?SKwoS;`_nyT#=t$Z`8Ri4+^Q(Km*evE7 z6#N9HLhifi;tsg-o%<^{fY%G-r?kJ=g{R?KbVuL#edoQ?jq{Ef1#!^gbfcm#HRw6t z3@kgMo3+YfrA`p`YhnE1Pv6dbz8$YW`1;n5XSNsJKjO$V>a=6%i%ay1m|J0N=#5IX zTBTvjSYD=X;u;OwG<4ehG=$)G5E{!mb*uuCc}I#=Bht~(sC%s?ZxCakTo2bHEWM67~K6_N2 z6P62T$$ga{dvY~j*ku`47KzI5&7ukuDn{lhyrnD>RhY@5if^kJi7HMji$oOJ&- zRg6SU9#9sEntUgVDjiZW5>+~^ED}{ZmPJh+Q85xVbyQg-YU=$gs(f6U|T!}b>4R4!4(BA%&QH8W;ygU7)snT@!i3p z-SsSoflNK?#ryH`58x%_WXEzNA;FY57F>rz+5|n=V7}$r7^Ag4$Hzjmi6@fEH44Cj zmHOtbbTMc|f2kL_G?lgEF>ksc--&rF4k@*h9lVo4MjFj24Pe>;5eL$95vh4(p6mBP zt3cNE9Ngpm$1lClbLu*uF#|X9P4_C~&k<~9QVv-h0>^?l=3*|+0>o4oTMgR*uY+fV zG>F|chq2SC9gao#MQ)+I9z8GXA|vy#R#QmBwc=fd>*nCQCVj}jt{nuv?|&SycV>Xa zfN%{BV7D`UrH%BJ+_(SMSE5Qfy`rz=N=WYAf9fHbuR7X8vIkYEj~vflhd%+sHX=XE zHY)jP8)59fXdkjI+ozFfpM{Z{xQCL`I|2_EDEsX~oY=z`eGixe2Y%Os?*Ts|Y1?&P zm?Y_0oB{VeTW}D`k bool: + """Sends a file to the system trash. + + Args: + path (str | Path): The path of the file to delete. + """ + _path = Path(path) + try: + logging.info(f"[delete_file] Sending to Trash: {_path}") + send2trash(_path) + return True + except PermissionError as e: + logging.error(f"[delete_file][ERROR] PermissionError: {e}") + except FileNotFoundError: + logging.error(f"[delete_file][ERROR] File Not Found: {_path}") + except Exception as e: + logging.error(e) + return False diff --git a/tagstudio/src/qt/resource_manager.py b/tagstudio/src/qt/resource_manager.py index 3c4e8099..c38d382a 100644 --- a/tagstudio/src/qt/resource_manager.py +++ b/tagstudio/src/qt/resource_manager.py @@ -31,6 +31,20 @@ class ResourceManager: ) ResourceManager._initialized = True + @staticmethod + def get_path(id: str) -> Path | None: + """Get a resource's path from the ResourceManager. + Args: + id (str): The name of the resource. + + Returns: + Path: The resource path if found, else None. + """ + res: dict = ResourceManager._map.get(id) + if res: + return Path(__file__).parents[2] / "resources" / res.get("path") + return None + def get(self, id: str) -> Any: """Get a resource from the ResourceManager. This can include resources inside and outside of QResources, and will return diff --git a/tagstudio/src/qt/resources.json b/tagstudio/src/qt/resources.json index ef007b6f..e5857909 100644 --- a/tagstudio/src/qt/resources.json +++ b/tagstudio/src/qt/resources.json @@ -90,5 +90,9 @@ "thumb_loading": { "path": "qt/images/thumb_loading.png", "mode": "pil" + }, + "placeholder_mp4": { + "path": "qt/videos/placeholder.mp4", + "mode": "rb" } } diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 41874188..13cf1cc0 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -12,6 +12,7 @@ import copy import logging import math import os +import platform import sys import time import typing @@ -53,6 +54,7 @@ from PySide6.QtWidgets import ( QMenu, QMenuBar, QComboBox, + QMessageBox, ) from humanfriendly import format_timespan @@ -69,9 +71,11 @@ from src.core.constants import ( TAG_FAVORITE, TAG_ARCHIVED, ) +from src.core.palette import ColorType, get_ui_color from src.core.utils.web import strip_web_protocol from src.qt.flowlayout import FlowLayout from src.qt.main_window import Ui_MainWindow +from src.qt.helpers.file_deleter import delete_file from src.qt.helpers.function_iterator import FunctionIterator from src.qt.helpers.custom_runnable import CustomRunnable from src.qt.resource_manager import ResourceManager @@ -446,6 +450,15 @@ class QtDriver(QObject): edit_menu.addSeparator() + self.delete_file_action = QAction("Delete Selected File(s)", menu_bar) + self.delete_file_action.triggered.connect( + lambda f="": self.delete_files_callback(f) + ) + self.delete_file_action.setShortcut(QtCore.Qt.Key.Key_Delete) + edit_menu.addAction(self.delete_file_action) + + edit_menu.addSeparator() + manage_file_extensions_action = QAction("Manage File Extensions", menu_bar) manage_file_extensions_action.triggered.connect( lambda: self.show_file_extension_modal() @@ -532,13 +545,13 @@ class QtDriver(QObject): folders_to_tags_action.triggered.connect(lambda: ftt_modal.show()) macros_menu.addAction(folders_to_tags_action) - # Help Menu ========================================================== + # Help Menu ============================================================ self.repo_action = QAction("Visit GitHub Repository", menu_bar) self.repo_action.triggered.connect( lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio") ) help_menu.addAction(self.repo_action) - self.set_macro_menu_viability() + self.set_menu_action_viability() self.update_clipboard_actions() @@ -549,6 +562,9 @@ class QtDriver(QObject): menu_bar.addMenu(window_menu) menu_bar.addMenu(help_menu) + # ====================================================================== + + # Preview Panel -------------------------------------------------------- self.preview_panel = PreviewPanel(self.lib, self) l: QHBoxLayout = self.main_window.splitter l.addWidget(self.preview_panel) @@ -758,7 +774,7 @@ class QtDriver(QObject): self.copied_fields.clear() self.is_buffer_merged = False self.update_clipboard_actions() - self.set_macro_menu_viability() + self.set_menu_action_viability() self.preview_panel.update_widgets() self.filter_items() self.main_window.toggle_landing_page(True) @@ -796,7 +812,7 @@ class QtDriver(QObject): self.selected.append((item.mode, item.item_id)) item.thumb_button.set_selected(True) - self.set_macro_menu_viability() + self.set_menu_action_viability() self.preview_panel.update_widgets() def clear_select_action_callback(self): @@ -804,7 +820,7 @@ class QtDriver(QObject): for item in self.item_thumbs: item.thumb_button.set_selected(False) - self.set_macro_menu_viability() + self.set_menu_action_viability() self.preview_panel.update_widgets() def show_tag_database(self): @@ -827,6 +843,114 @@ class QtDriver(QObject): self.modal.saved.connect(lambda: (panel.save(), self.filter_items(""))) self.modal.show() + def delete_files_callback(self, origin_path: str | Path): + """Callback to send on or more files to the system trash. + + If 0-1 items are currently selected, the origin_path is used to delete the file + from the originating context menu item. + If there are currently multiple items selected, + then the selection buffer is used to determine the files to be deleted. + + Args: + origin_path(str): The file path associated with the widget making the call. + May or may not be the file targeted, depending on the selection rules. + """ + entry = None + pending: list[Path] = [] + deleted_count: int = 0 + + if len(self.selected) <= 1 and origin_path: + pending.append(Path(origin_path)) + elif (len(self.selected) > 1) or (len(self.selected) <= 1 and not origin_path): + for i, item_pair in enumerate(self.selected): + if item_pair[0] == ItemType.ENTRY: + entry = self.lib.get_entry(item_pair[1]) + filepath: Path = self.lib.library_dir / entry.path / entry.filename + pending.append(filepath) + + if pending: + return_code = self.delete_file_confirmation(len(pending), pending[0]) + logging.info(return_code) + # If there was a confirmation and not a cancellation + if return_code == 2 and return_code != 3: + for i, f in enumerate(pending): + if (origin_path == f) or (not origin_path): + self.preview_panel.stop_file_use() + if delete_file(f): + self.main_window.statusbar.showMessage( + f'Deleting file [{i}/{len(pending)}]: "{f}"...' + ) + self.main_window.statusbar.repaint() + + entry_id = self.lib.get_entry_id_from_filepath(f) + self.lib.remove_entry(entry_id) + self.purge_item_from_navigation(ItemType.ENTRY, entry_id) + deleted_count += 1 + self.selected.clear() + + if deleted_count > 0: + self.filter_items() + self.preview_panel.update_widgets() + + if len(self.selected) <= 1 and deleted_count == 0: + self.main_window.statusbar.showMessage("No files deleted.") + elif len(self.selected) <= 1 and deleted_count == 1: + self.main_window.statusbar.showMessage(f"Deleted {deleted_count} file!") + elif len(self.selected) > 1 and deleted_count == 0: + self.main_window.statusbar.showMessage("No files deleted.") + elif len(self.selected) > 1 and deleted_count < len(self.selected): + self.main_window.statusbar.showMessage( + f"Only deleted {deleted_count} file{'' if deleted_count == 1 else 's'}! Check if any of the files are currently missing or in use." + ) + elif len(self.selected) > 1 and deleted_count == len(self.selected): + self.main_window.statusbar.showMessage(f"Deleted {deleted_count} files!") + self.main_window.statusbar.repaint() + + def delete_file_confirmation(self, count: int, filename: Path | None = None) -> int: + """A confirmation dialogue box for deleting files. + + Args: + count(int): The number of files to be deleted. + filename(Path | None): The filename to show if only one file is to be deleted. + """ + trash_term: str = "Trash" + if platform.system() == "Windows": + trash_term = "Recycle Bin" + # NOTE: Windows + send2trash will PERMANENTLY delete files which cannot be moved to the Recycle Bin. + # This is done without any warning, so this message is currently the best way I've got to inform the user. + # https://github.com/arsenetar/send2trash/issues/28 + # This warning is applied to all platforms until at least macOS and Linux can be verified to + # not exhibit this same behavior. + perm_warning: str = ( + f"

" + f"WARNING! If this file can't be moved to the {trash_term}, " + f"it will be permanently deleted!

" + ) + + msg = QMessageBox() + msg.setTextFormat(Qt.TextFormat.RichText) + msg.setWindowTitle("Delete File" if count == 1 else "Delete Files") + msg.setIcon(QMessageBox.Icon.Warning) + if count <= 1: + msg.setText( + f"

Are you sure you want to move this file to the {trash_term}?

" + "

This will remove it from TagStudio AND your file system!

" + f"{filename if filename else ''}" + f"{perm_warning}
" + ) + elif count > 1: + msg.setText( + f"

Are you sure you want to move these {count} files to the {trash_term}?

" + "

This will remove them from TagStudio AND your file system!

" + f"{perm_warning}
" + ) + + yes_button: QPushButton = msg.addButton("&Yes", QMessageBox.ButtonRole.YesRole) + msg.addButton("&No", QMessageBox.ButtonRole.NoRole) + msg.setDefaultButton(yes_button) + + return msg.exec() + def add_new_files_callback(self): """Runs when user initiates adding new files to the Library.""" # # if self.lib.files_not_in_library: @@ -1397,17 +1521,19 @@ class QtDriver(QObject): if it.mode == type and it.item_id == id: self.preview_panel.set_tags_updated_slot(it.update_badges) - self.set_macro_menu_viability() + self.set_menu_action_viability() self.update_clipboard_actions() self.preview_panel.update_widgets() - def set_macro_menu_viability(self): + def set_menu_action_viability(self): if len([x[1] for x in self.selected if x[0] == ItemType.ENTRY]) == 0: self.autofill_action.setDisabled(True) self.sort_fields_action.setDisabled(True) + self.delete_file_action.setDisabled(True) else: self.autofill_action.setDisabled(False) self.sort_fields_action.setDisabled(False) + self.delete_file_action.setDisabled(False) def update_thumbs(self): """Updates search thumbnails.""" @@ -1456,12 +1582,20 @@ class QtDriver(QObject): for i, item_thumb in enumerate(self.item_thumbs, start=0): if i < len(self.nav_frames[self.cur_frame_idx].contents): - filepath = "" + filepath: Path = None # Initialize if self.nav_frames[self.cur_frame_idx].contents[i][0] == ItemType.ENTRY: entry = self.lib.get_entry( self.nav_frames[self.cur_frame_idx].contents[i][1] ) - filepath = self.lib.library_dir / entry.path / entry.filename + filepath: Path = self.lib.library_dir / entry.path / entry.filename + + try: + item_thumb.delete_action.triggered.disconnect() + except RuntimeWarning: + pass + item_thumb.delete_action.triggered.connect( + lambda checked=False, f=filepath: self.delete_files_callback(f) + ) item_thumb.set_item_id(entry.id) item_thumb.assign_archived(entry.has_tag(self.lib, TAG_ARCHIVED)) @@ -1506,7 +1640,9 @@ class QtDriver(QObject): else collation.e_ids_and_pages[0][0] ) cover_e = self.lib.get_entry(cover_id) - filepath = self.lib.library_dir / cover_e.path / cover_e.filename + filepath: Path = ( + self.lib.library_dir / cover_e.path / cover_e.filename + ) item_thumb.set_count(str(len(collation.e_ids_and_pages))) item_thumb.update_clickable( clickable=( diff --git a/tagstudio/src/qt/widgets/fields.py b/tagstudio/src/qt/widgets/fields.py index dffca2a5..9bdc6794 100644 --- a/tagstudio/src/qt/widgets/fields.py +++ b/tagstudio/src/qt/widgets/fields.py @@ -124,6 +124,7 @@ class FieldContainer(QWidget): def set_copy_callback(self, callback: Optional[MethodType]): if self.copy_button.is_connected: self.copy_button.clicked.disconnect() + self.copy_button.is_connected = False self.copy_callback = callback self.copy_button.clicked.connect(callback) @@ -133,6 +134,7 @@ class FieldContainer(QWidget): def set_edit_callback(self, callback: Optional[MethodType]): if self.edit_button.is_connected: self.edit_button.clicked.disconnect() + self.edit_button.is_connected = False self.edit_callback = callback self.edit_button.clicked.connect(callback) @@ -142,10 +144,12 @@ class FieldContainer(QWidget): def set_remove_callback(self, callback: Optional[Callable]): if self.remove_button.is_connected: self.remove_button.clicked.disconnect() + self.remove_button.is_connected = False self.remove_callback = callback self.remove_button.clicked.connect(callback) - self.remove_button.is_connected = True + if callback is not None: + self.remove_button.is_connected = True def set_inner_widget(self, widget: "FieldWidget"): if self.field_layout.itemAt(0): diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index eda6196d..1057e70a 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -9,6 +9,7 @@ import time import typing from pathlib import Path from typing import Optional +import platform from PIL import Image, ImageQt from PySide6.QtCore import Qt, QSize, QEvent, QMimeData, QUrl @@ -207,8 +208,15 @@ class ItemThumb(FlowWidget): open_explorer_action = QAction("Open in Explorer", self) open_explorer_action.triggered.connect(self.opener.open_explorer) + + trash_term: str = "Trash" + if platform.system() == "Windows": + trash_term = "Recycle Bin" + self.delete_action = QAction(f"Send file to {trash_term}", self) + self.thumb_button.addAction(open_file_action) self.thumb_button.addAction(open_explorer_action) + self.thumb_button.addAction(self.delete_action) # Static Badges ======================================================== diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 24cb42ce..9dc68db4 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -12,7 +12,7 @@ import cv2 import rawpy from PIL import Image, UnidentifiedImageError, ImageFont from PIL.Image import DecompressionBombError -from PySide6.QtCore import QModelIndex, Signal, Qt, QSize +from PySide6.QtCore import QModelIndex, Signal, Qt, QSize, QByteArray, QBuffer from PySide6.QtGui import QGuiApplication, QResizeEvent, QAction, QMovie from PySide6.QtWidgets import ( QWidget, @@ -46,6 +46,7 @@ from src.qt.widgets.text_line_edit import EditTextLine from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper from src.qt.widgets.video_player import VideoPlayer from src.qt.helpers.file_tester import is_readable_video +from src.qt.resource_manager import ResourceManager # Only import for type checking/autocompletion, will not be imported at runtime. @@ -98,14 +99,17 @@ class PreviewPanel(QWidget): image_layout.setContentsMargins(0, 0, 0, 0) self.open_file_action = QAction("Open file", self) + self.trash_term: str = "Trash" + if platform.system() == "Windows": + self.trash_term = "Recycle Bin" + self.delete_action = QAction(f"Send file to {self.trash_term}", self) - system = platform.system() self.open_explorer_action = QAction( "Open in explorer", self - ) # Default (mainly going to be for linux) - if system == "Darwin": + ) # Default text (Linux, etc.) + if platform.system() == "Darwin": self.open_explorer_action = QAction("Reveal in Finder", self) - elif system == "Windows": + elif platform.system() == "Windows": self.open_explorer_action = QAction("Open in Explorer", self) self.preview_img = QPushButtonWrapper() @@ -114,6 +118,7 @@ class PreviewPanel(QWidget): self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) self.preview_img.addAction(self.open_file_action) self.preview_img.addAction(self.open_explorer_action) + self.preview_img.addAction(self.delete_action) self.preview_gif = QLabel() self.preview_gif.setMinimumSize(*self.img_button_size) @@ -121,10 +126,13 @@ class PreviewPanel(QWidget): self.preview_gif.setCursor(Qt.CursorShape.ArrowCursor) self.preview_gif.addAction(self.open_file_action) self.preview_gif.addAction(self.open_explorer_action) + self.preview_gif.addAction(self.delete_action) self.preview_gif.hide() + self.gif_buffer: QBuffer = QBuffer() self.preview_vid = VideoPlayer(driver) self.preview_vid.hide() + self.preview_vid.addAction(self.delete_action) self.thumb_renderer = ThumbRenderer() self.thumb_renderer.updated.connect( lambda ts, i, s: (self.preview_img.setIcon(i)) @@ -499,6 +507,14 @@ class PreviewPanel(QWidget): ) if self.preview_img.is_connected: self.preview_img.clicked.disconnect() + self.preview_img.is_connected = False + + try: + self.delete_action.triggered.disconnect() + except RuntimeWarning: + pass + self.delete_action.setEnabled(False) + for i, c in enumerate(self.containers): c.setHidden(True) self.preview_img.show() @@ -538,6 +554,17 @@ class PreviewPanel(QWidget): ) self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor) + try: + self.delete_action.triggered.disconnect() + except RuntimeError: + pass + self.delete_action.setText(f"Send file to {self.trash_term}") + self.delete_action.triggered.connect( + lambda checked=False, + f=filepath: self.driver.delete_files_callback(f) + ) + self.delete_action.setEnabled(True) + self.opener = FileOpenerHelper(filepath) self.open_file_action.triggered.connect(self.opener.open_file) self.open_explorer_action.triggered.connect( @@ -548,16 +575,24 @@ class PreviewPanel(QWidget): ext: str = filepath.suffix.lower() try: if filepath.suffix.lower() in [".gif"]: - movie = QMovie(str(filepath)) + with open(filepath, mode="rb") as f: + if self.preview_gif.movie(): + self.preview_gif.movie().stop() + self.gif_buffer.close() + + ba = f.read() + self.gif_buffer.setData(ba) + movie = QMovie(self.gif_buffer, QByteArray()) + self.preview_gif.setMovie(movie) + movie.start() + image = Image.open(str(filepath)) - self.preview_gif.setMovie(movie) self.resizeEvent( QResizeEvent( QSize(image.width, image.height), QSize(image.width, image.height), ) ) - movie.start() self.preview_img.hide() self.preview_vid.hide() self.preview_gif.show() @@ -669,6 +704,7 @@ class PreviewPanel(QWidget): # TODO: Implement a clickable label to use for the GIF preview. if self.preview_img.is_connected: self.preview_img.clicked.disconnect() + self.preview_img.is_connected = False self.preview_img.clicked.connect( lambda checked=False, filepath=filepath: open_file(filepath) ) @@ -710,6 +746,16 @@ class PreviewPanel(QWidget): ) self.preview_img.setCursor(Qt.CursorShape.ArrowCursor) + try: + self.delete_action.triggered.disconnect() + except RuntimeError: + pass + self.delete_action.setText(f"Send files to {self.trash_term}") + self.delete_action.triggered.connect( + lambda checked=False, f=None: self.driver.delete_files_callback(f) + ) + self.delete_action.setEnabled(True) + ratio: float = self.devicePixelRatio() self.thumb_renderer.render( time.time(), @@ -1149,3 +1195,26 @@ class PreviewPanel(QWidget): # logging.info(result) if result == 3: callback() + + def stop_file_use(self): + """Stops the use of the currently previewed file. Used to release file permissions.""" + logging.info("[PreviewPanel] Stopping file use in video playback...") + # This swaps the video out for a placeholder so the previous video's file + # is no longer in use by this object. + self.preview_vid.play(ResourceManager.get_path("placeholder_mp4"), QSize(8, 8)) + self.preview_vid.hide() + + # NOTE: I'm keeping this here until #357 is merged in the case it still needs to be used. + # logging.info("[PreviewPanel] Stopping file use for animated image playback...") + # logging.info(self.preview_gif.movie()) + # if self.preview_gif.movie(): + # self.preview_gif.movie().stop() + # with open(ResourceManager.get_path("placeholder_gif"), mode="rb") as f: + # ba = f.read() + # self.gif_buffer.setData(ba) + # movie = QMovie(self.gif_buffer, QByteArray()) + # self.preview_gif.setMovie(movie) + # movie.start() + + # self.preview_gif.hide() + # logging.info(self.preview_gif.movie())