diff --git a/pdm.lock b/pdm.lock index b6b9ce9..79640cf 100755 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "docs", "ruff"] strategy = [] lock_version = "4.5.0" -content_hash = "sha256:9b7a74d0a595fc2a3847dafbfe9f5111d07deb81fdbe435e0df159078dc1b9e0" +content_hash = "sha256:8f47e9fcd02f8c071091fc176058e14c9a7c7ad5f749fd2781c8940d4993ca9e" [[metadata.targets]] requires_python = ">=3.9,<3.11" @@ -869,6 +869,16 @@ files = [ {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] +[[package]] +name = "send2trash" +version = "1.8.3" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "Send file to trash natively under Mac OS X, Windows and Linux" +files = [ + {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, + {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, +] + [[package]] name = "setuptools" version = "73.0.1" diff --git a/pyproject.toml b/pyproject.toml index 9229a56..53a6bb6 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "distro>=1.8.0", "semver>=3.0.2", "zstandard>=0.23.0", + "send2trash>=1.8.3", ] requires-python = ">=3.9, <3.11" readme = "README.md" diff --git a/source/threads/remover.py b/source/threads/remover.py index 21829cf..a081efc 100644 --- a/source/threads/remover.py +++ b/source/threads/remover.py @@ -4,19 +4,24 @@ from modules.task import Task from PyQt5.QtCore import pyqtSignal +from send2trash import send2trash @dataclass class RemovalTask(Task): path: Path + trash: bool = True finished = pyqtSignal(bool) def run(self): try: - if self.path.is_dir(): - rmtree(self.path.as_posix()) + if self.trash: + send2trash(str(self.path)) else: - self.path.unlink() + if self.path.is_dir(): + rmtree(self.path.as_posix()) + else: + self.path.unlink() self.finished.emit(0) except OSError: diff --git a/source/widgets/base_menu_widget.py b/source/widgets/base_menu_widget.py index 4ae5e35..65feac0 100644 --- a/source/widgets/base_menu_widget.py +++ b/source/widgets/base_menu_widget.py @@ -1,14 +1,15 @@ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QCursor +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QCursor, QKeyEvent from PyQt5.QtWidgets import QDesktopWidget, QMenu class BaseMenuWidget(QMenu): action_height = 30 + holding_shift = pyqtSignal(bool) def __init__(self, title="", parent=None): super().__init__(title=title, parent=parent) - self.setWindowFlags(self.windowFlags() | Qt.NoDropShadowWindowHint) + self.setWindowFlags(self.windowFlags() | Qt.WindowType.NoDropShadowWindowHint) self.action_height = BaseMenuWidget.action_height self.screen_size = QDesktopWidget().screenGeometry() self.setToolTipsVisible(True) @@ -47,3 +48,14 @@ def trigger(self): i = i + 1 self.exec_(cursor) + + def enable_shifting(self): + """ This is an optional feature because it can be very expensive to do this all the time. """ + self.installEventFilter(self) + + + def eventFilter(self, obj, event): + if isinstance(event, QKeyEvent): + self.holding_shift.emit(event.modifiers() in (Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier.ControlModifier)) + + return super().eventFilter(obj, event) diff --git a/source/widgets/library_widget.py b/source/widgets/library_widget.py index 93cd08a..2bc0392 100644 --- a/source/widgets/library_widget.py +++ b/source/widgets/library_widget.py @@ -182,6 +182,12 @@ def draw(self, build_info: BuildInfo): self.menu_extended = BaseMenuWidget(parent=self) self.menu_extended.setFont(self.parent.font_10) + # For checking if shift is held on menus + self.menu.enable_shifting() + self.menu_extended.enable_shifting() + self.menu.holding_shift.connect(self.update_delete_action) + self.menu_extended.holding_shift.connect(self.update_delete_action) + self.deleteAction = QAction("Delete From Drive", self) self.deleteAction.setIcon(self.parent.icons.delete) self.deleteAction.triggered.connect(self.ask_remove_from_drive) @@ -314,6 +320,8 @@ def context_menu(self): if self.is_damaged: return + self.update_delete_action(self.hovering_and_shifting) + if len(self.list_widget.selectedItems()) > 1: self.menu_extended.trigger() return @@ -327,6 +335,13 @@ def context_menu(self): self.menu.trigger() + @pyqtSlot(bool) + def update_delete_action(self, shifting: bool): + if shifting: + self.deleteAction.setText("Delete from Drive") + else: + self.deleteAction.setText("Send to Trash") + def mouseDoubleClickEvent(self, _event): if self.build_info is not None and self.hovering_and_shifting: self.launch(launch_mode=LaunchOpenLast()) @@ -512,6 +527,13 @@ def build_info_writer_finished(self): @QtCore.pyqtSlot() def ask_remove_from_drive(self): + + # if not shift clicked, ask to send to trash instead of deleting + mod = QApplication.keyboardModifiers() + if mod not in (Qt.KeyboardModifier.ShiftModifier, Qt.KeyboardModifier.ControlModifier): + self.ask_send_to_trash() + return + self.item.setSelected(True) self.dlg = DialogWindow( parent=self.parent, @@ -533,17 +555,43 @@ def remove_from_drive_extended(self): self.list_widget.itemWidget(item).remove_from_drive() @QtCore.pyqtSlot() - def remove_from_drive(self): + def remove_from_drive(self, trash=False): if self.parent_widget is not None: self.parent_widget.remove_from_drive() return path = Path(get_library_folder()) / self.link - a = RemovalTask(path) + a = RemovalTask(path, trash=trash) a.finished.connect(self.remover_completed) self.parent.task_queue.append(a) self.remover_started() + @QtCore.pyqtSlot() + def ask_send_to_trash(self): + self.item.setSelected(True) + self.dlg = DialogWindow( + parent=self.parent, + title="Warning", + text="Are you sure you want to
\ + send selected builds to trash?", + accept_text="Yes", + cancel_text="No", + ) + + if len(self.list_widget.selectedItems()) > 1: + self.dlg.accepted.connect(self.send_to_trash_extended) + else: + self.dlg.accepted.connect(self.send_to_trash) + + @QtCore.pyqtSlot() + def send_to_trash_extended(self): + for item in self.list_widget.selectedItems(): + self.list_widget.itemWidget(item).remove_from_drive(trash=True) + + @QtCore.pyqtSlot() + def send_to_trash(self): + self.remove_from_drive(trash=True) + # TODO Clear icon if build in quick launch def remover_started(self): self.launchButton.set_text("Deleting")