Skip to content

Commit

Permalink
Add copy, paste and delete functionality to fs_explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
Bzero committed Oct 25, 2024
1 parent 80f39e6 commit 4e08702
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 6 deletions.
33 changes: 33 additions & 0 deletions tests/test_fs_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,39 @@ def test_rename(self, tmp_path, qtbot, caplog, monkeypatch):
assert from_path.exists()
assert to_path.exists()

def test_copy_from_to(self, tmp_path, qtbot, caplog, monkeypatch):
"""Make sure copying works."""
fse = fs_explorer.FSExplorer()
qtbot.addWidget(fse)
monkeypatch.setattr(QtWidgets.QMessageBox, "warning", lambda *args: QtWidgets.QMessageBox.Ok)

name = "test_file_name"
from_dir = tmp_path / "from"
to_dir = tmp_path / "to"
from_dir.mkdir()
to_dir.mkdir()
from_path = from_dir / name
to_path = to_dir / name

from_path.touch()

# Make sure the file can be copied
assert from_path.exists()
assert not to_path.exists()
fse.copy_from_to(str(from_path), str(to_dir))
assert from_path.exists()
assert to_path.exists()

# Make sure the file is not overwritten
from_path.touch()
to_path.touch()
assert from_path.exists()
assert to_path.exists()
fse.copy_from_to(str(from_path), str(to_dir))
assert "already exists" in caplog.text
assert from_path.exists()
assert to_path.exists()

def test_delete(self, tmp_path, qtbot, monkeypatch):
"""Make sure a folder or file can be deleted."""
fse = fs_explorer.FSExplorer()
Expand Down
2 changes: 2 additions & 0 deletions typstwriter/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ def __init__(self, parent):
self.copy = QtWidgets.QAction(self)
self.copy.setIcon(QtGui.QIcon.fromTheme(QtGui.QIcon.EditCopy, QtGui.QIcon(util.icon_path("copy.svg"))))
self.copy.setShortcut(QtGui.QKeySequence.Copy)
self.copy.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
self.copy.setText("Copy")

self.paste = QtWidgets.QAction(self)
self.paste.setIcon(QtGui.QIcon.fromTheme(QtGui.QIcon.EditPaste, QtGui.QIcon(util.icon_path("paste.svg"))))
self.paste.setShortcut(QtGui.QKeySequence.Paste)
self.paste.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
self.paste.setText("Paste")

self.search = QtWidgets.QAction(self)
Expand Down
120 changes: 114 additions & 6 deletions typstwriter/fs_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from qtpy import QtWidgets

import os
import shutil

from typstwriter import util

Expand Down Expand Up @@ -37,6 +38,8 @@ def __init__(self, parent):
self.action_delete = QtWidgets.QAction("Delete", triggered=self.handle_delete)
self.setRoot = QtWidgets.QAction("Set as Working Directory", triggered=self.handle_set_root)
self.action_copy_path = QtWidgets.QAction("Copy path", triggered=self.handle_copy_path)
self.action_copy = QtWidgets.QAction("Copy", triggered=self.handle_copy)
self.action_paste = QtWidgets.QAction("Paste", triggered=self.handle_paste)
self.action_main_file = QtWidgets.QAction("Set as main file", triggered=self.handle_set_as_main_file)

def handle_open(self):
Expand Down Expand Up @@ -71,6 +74,14 @@ def handle_copy_path(self):
"""Trigger copying path to the clipboard."""
QtGui.QGuiApplication.clipboard().setText(self.context_path)

def handle_copy(self):
"""Trigger copying file or folder to the clipboard."""
self.parent().copy_to_clipboard(self.context_path)

def handle_paste(self):
"""Trigger pasting from the clipboard."""
self.parent().paste_from_clipboard(self.context_path)

def handle_set_as_main_file(self):
"""Trigger setting the current file as main file."""
state.main_file.Value = self.context_path
Expand All @@ -85,6 +96,7 @@ def __init__(self, parent):

self.addAction(self.action_new_file)
self.addAction(self.action_new_folder)
self.addAction(self.action_paste)


class FileContextMenu(FSContextMenu):
Expand All @@ -98,6 +110,7 @@ def __init__(self, parent):
self.addAction(self.action_open_external)
self.addAction(self.action_rename)
self.addAction(self.action_delete)
self.addAction(self.action_copy)
self.addAction(self.action_copy_path)
self.addAction(self.action_main_file)

Expand All @@ -111,11 +124,25 @@ def __init__(self, parent):

self.addAction(self.action_new_file)
self.addAction(self.action_new_folder)
self.addAction(self.action_copy)
self.addAction(self.action_copy_path)
self.addAction(self.action_paste)
self.addAction(self.action_rename)
self.addAction(self.action_delete)
self.addAction(self.setRoot)


class SelectionContextMenu(FSContextMenu):
"""Context menu when clicking on a selection."""

def __init__(self, parent):
"""Assemble Menu."""
FSContextMenu.__init__(self, parent)

self.addAction(self.action_delete)
self.addAction(self.action_copy)


class FSExplorer(QtWidgets.QWidget):
"""A filesystem explorer widget."""

Expand Down Expand Up @@ -152,6 +179,7 @@ def __init__(self):
self.filesystem_model.setIconProvider(util.FileIconProvider())
self.tree_view.setModel(self.filesystem_model)
self.tree_view.setRootIsDecorated(True)
self.tree_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.tree_view.hideColumn(1)
self.tree_view.hideColumn(2)
self.tree_view.header().setSectionsMovable(False)
Expand All @@ -178,6 +206,7 @@ def __init__(self):
self.NoItemContextMenu = NoItemContextMenu(self)
self.FileContextMenu = FileContextMenu(self)
self.FolderContextMenu = FolderContextMenu(self)
self.SelectionContextMenu = SelectionContextMenu(self)

# Set initial state
root_dir = os.path.expanduser(config.get("General", "working_directory"))
Expand Down Expand Up @@ -243,12 +272,19 @@ def rightclicked(self, event):
index = self.tree_view.indexAt(event)
if not index.isValid():
self.open_no_item_menu()
return
else:
selected_indices = self.tree_view.selectionModel().selectedRows()
if index in selected_indices and len(selected_indices) > 1:
self.open_selection_menu(index)
return
path = self.filesystem_model.filePath(index)
if os.path.isfile(path):
self.open_file_menu(index)
return
if os.path.isdir(path):
self.open_folder_menu(index)
return

def open_no_item_menu(self):
"""Open context menu when clicking on no item."""
Expand All @@ -268,6 +304,31 @@ def open_file_menu(self, index):
self.FileContextMenu.context_path = path
self.FileContextMenu.popup(QtGui.QCursor.pos())

def open_selection_menu(self, index):
"""Open context menu when clicking on a selection."""
paths = self.selected_paths()
self.SelectionContextMenu.context_path = paths
self.SelectionContextMenu.popup(QtGui.QCursor.pos())

def keyPressEvent(self, e): # noqa: N802 This is an overriding function
"""Intercept keyPressEvent."""
if e.key() == QtCore.Qt.Key_Delete and e.modifiers() == QtCore.Qt.NoModifier:
self.delete(self.selected_paths())
if e.key() == QtCore.Qt.Key_C and e.modifiers() == QtCore.Qt.ControlModifier:
self.copy_to_clipboard(self.selected_paths())
if e.key() == QtCore.Qt.Key_V and e.modifiers() == QtCore.Qt.ControlModifier:
self.paste_from_clipboard(self.filesystem_model.rootPath())
super().keyPressEvent(e)

def selected_paths(self):
"""Return the paths of the currently selected items."""
selected_paths = []
for index in self.tree_view.selectionModel().selectedRows():
if index.isValid():
path = self.filesystem_model.filePath(index)
selected_paths.append(path)
return selected_paths

# TODO: Possibly move the functions below to an own class or module as they dont directly relate to the FSExplorer
def new_file_in_dir(self, head_path):
"""Prompts the user to ender a filename and creates that file in head_path."""
Expand Down Expand Up @@ -317,10 +378,57 @@ def rename(self, path_from, path_to, overwrite=False):
logger.warning("{!r} already exists. Will not overwrite.", path_to)
QtWidgets.QMessageBox.warning(self, "Typstwriter", f"'{path_to}' already exists.\nWill not overwrite.")

def delete(self, path):
def copy_to_clipboard(self, paths):
"""Copy a list of files or foler to the clipboard."""
if not isinstance(paths, list):
paths = [paths]

urls = [QtCore.QUrl.fromLocalFile(path) for path in paths if os.path.exists(path)]

if urls:
mime_data = QtCore.QMimeData()
mime_data.setUrls(urls)
QtGui.QGuiApplication.clipboard().setMimeData(mime_data)

def paste_from_clipboard(self, path):
"""Paste a file or folder from cliboard into path."""
mime_data = QtGui.QGuiApplication.clipboard().mimeData()

if mime_data.hasUrls():
for uri in mime_data.data("text/uri-list").data().decode().split():
fs = QtCore.QUrl(uri).toLocalFile()
if os.path.exists(fs):
self.copy_from_to(fs, path)

def copy_from_to(self, path_from, path_to, overwrite=False):
"""Copy a file or folder from path_from into the directory path_to."""
if not os.path.isdir(path_to):
return

if not os.path.exists(path_from):
return

head, tail = os.path.split(path_from)
path_to = os.path.join(path_to, tail)

if not overwrite and os.path.exists(path_to):
logger.warning("{!r} already exists. Will not overwrite.", path_to)
QtWidgets.QMessageBox.warning(self, "Typstwriter", f"'{path_to}' already exists.\nWill not overwrite.")
return

if os.path.isdir(path_from):
shutil.copytree(path_from, path_to, dirs_exist_ok=overwrite)
else:
shutil.copy2(path_from, path_to)

def delete(self, paths):
"""Delete a folder or file."""
msg = QtWidgets.QMessageBox.question(self, "Typstwriter", f"Should '{path}' be deleted?")
if msg == QtWidgets.QMessageBox.StandardButton.Yes:
deletion_succeeded = QtCore.QFile.moveToTrash(path)
if not deletion_succeeded:
logger.warning("Could not move {!r} to trash.", path)
if not isinstance(paths, list):
paths = [paths]

for path in paths:
msg = QtWidgets.QMessageBox.question(self, "Typstwriter", f"Should '{path}' be deleted?")
if msg == QtWidgets.QMessageBox.StandardButton.Yes:
deletion_succeeded = QtCore.QFile.moveToTrash(path)
if not deletion_succeeded:
logger.warning("Could not move {!r} to trash.", path)

0 comments on commit 4e08702

Please sign in to comment.