Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to copy, cut folder from one folder to another using ctrl+c and ctrl+v

Tags:

python

tkinter

My title can look a little ambiguous, so here is an explanation.

Professional IDE like Pycharm or Visual Studio Code allow copying the folder, navigating to a specific directory and pasting it there. I would also like to implement that.

But in my case, shutil.copytree needs 2 arguments - source folder and destination folder.

So is there any way that one can copy a folder, navigate through the explorer, click paste or press ctrl+v and the folder will be copied or pasted there, unlike shutil.copytree where the user already need to provide the path?

Currently, I have a code that will copy the folder name to the clipboard.

import os
import tkinter as tk
import tkinter.ttk as ttk
import clipboard
class App(tk.Frame):
    def __init__(self, master, path):
        tk.Frame.__init__(self, master)
        self.tree = ttk.Treeview(self)
        ysb = ttk.Scrollbar(self, orient='vertical', command=self.tree.yview)
        xsb = ttk.Scrollbar(self, orient='horizontal', command=self.tree.xview)
        self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
        self.tree.heading('#0', text=path, anchor='w')

        abspath = os.path.abspath(path)
        root_node = self.tree.insert('', 'end', text=abspath, open=True)
        self.process_directory(root_node, abspath)

        self.tree.bind("<Control-c>",self.copy_to_clipboard)
        self.tree.grid(row=0, column=0)
        ysb.grid(row=0, column=1, sticky='ns')
        xsb.grid(row=1, column=0, sticky='ew')
        self.grid()
    def copy_to_clipboard(self,event,*args):
        item = self.tree.identify_row(event.y)
        clipboard.copy(self.tree.item(item,"text"))
    def process_directory(self, parent, path):
        try:
            for p in os.listdir(path):
                abspath = os.path.join(path, p)
                isdir = os.path.isdir(abspath)
                oid = self.tree.insert(parent, 'end', text=p, open=False)
                if isdir:
                    self.process_directory(oid, abspath)
        except PermissionError:
            pass

root = tk.Tk()
path_to_my_project = 'C:\\Users\\91996\\Documents'
app = App(root, path=path_to_my_project)
app.mainloop()

2 Answers

Note: This answer does not answer the OP's question as it makes it possible to copy from an external filebrowser into the folder chosen in tkinter application, but not the opposite, as wanted by the OP.

Firstly, to make retrieving the absolute paths of the items easier, I use the absolute path as item identifier in the tree.

Then, to implement the pasting part, I added a .paste() method, called with Ctrl+V. In this method, I obtain the destination folder by getting the currently selected item. If this item is a file, then I use the parent folder as the destination. I get the path of the file/folder to copy from the clipboard. If it is a file, I use shutil.copy2(src, dest). As it will copy the file even if it already exists in dest, you will probably want to add some code before to check that and show a messagebox. If the source is a folder, I use shutil.copytree(src, os.path.join(dest, src_dirname)) where src_dirname is the name of the copied folder.

As suggested in the comments, I used tkinter's methods .clipboard_clear(), .clipboard_append() and .clipboard_get() instead of using the clipboard module.

In .copy_to_clipboard(), I suggest that you use self.tree.focus() instead of self.tree.identify_row(y), so as to get the selected item, not the one below the mouse cursor (I have just added a comment next to the relevant line in the code but not implemented this suggestion).

Here is the code:

import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.messagebox import showerror
import shutil
import traceback


class App(tk.Frame):
    def __init__(self, master, path):
        tk.Frame.__init__(self, master)
        self.tree = ttk.Treeview(self)
        ysb = ttk.Scrollbar(self, orient='vertical', command=self.tree.yview)
        xsb = ttk.Scrollbar(self, orient='horizontal', command=self.tree.xview)
        self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
        self.tree.heading('#0', text=path, anchor='w')

        abspath = os.path.abspath(path)
        self.tree.insert('', 'end', abspath, text=abspath, open=True)
        self.process_directory(abspath)

        self.tree.bind("<Control-c>", self.copy_to_clipboard)
        self.tree.bind("<Control-v>", self.paste)
        self.tree.grid(row=0, column=0)
        ysb.grid(row=0, column=1, sticky='ns')
        xsb.grid(row=1, column=0, sticky='ew')
        self.grid()

    def copy_to_clipboard(self, event, *args):
        item = self.tree.identify_row(event.y) # you may want to use self.tree.focus() instead so that
                                               # the selected item is copied, not the one below the mouse cursor
        self.clipboard_clear()
        self.clipboard_append(item)

    def paste(self, event):
        src = self.clipboard_get()

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

        dest = self.tree.focus()
        if not dest:
            dest = self.tree.get_children("")[0] # get root folder path
        elif not os.path.isdir(dest):  # selected item is a file, use parent folder
            dest = self.tree.parent(dest)

        if os.path.isdir(src):
            try:
                dirname = os.path.split(src)[1]
                newpath = shutil.copytree(src, os.path.join(dest, dirname))
                self.tree.insert(dest, "end", newpath, text=dirname)
                self.process_directory(newpath)
                self.tree.item(dest, open=True)
            except Exception:
                showerror("Error", traceback.format_exc())
        else:
            try:
                # you might want to check if the file already exists in dest and ask what to do
                # otherwise shutil.copy2() will replace it
                newpath = shutil.copy2(src, dest)
                self.tree.insert(dest, "end", newpath, text=os.path.split(src)[1])
            except tk.TclError:  # the item already exists
                pass
            except Exception:
                showerror("Error", traceback.format_exc())

    def process_directory(self, path):
        try:
            for p in os.listdir(path):
                abspath = os.path.join(path, p)
                isdir = os.path.isdir(abspath)
                # use abspath as item IID
                self.tree.insert(path, 'end', abspath, text=p, open=False)
                if isdir:
                    self.process_directory(abspath)
        except PermissionError:
            pass

root = tk.Tk()
path_to_my_project = '/tmp/truc'
app = App(root, path=path_to_my_project)
app.mainloop()
    

Partial implementation of copying from the tkinter application into an external filebrowser: The issue with copying in this direction is that it is platform specific as the clipboard is handled differently by different platforms. The following solution works for me in Linux, in the XFCE desktop environment and using Thunar filebrowser.

I used the klembord library to access the system's clipboard with richer content than just plain text. It is possible to paste a file/folder in Thunar if it has been copied to the clipboard with

klembord.set({'x-special/gnome-copied-files': f'copy\nfile://{abspath}'.encode()})

where abspath is the HTML-escaped absolute path of the file/folder.

To implement this into App, import klembord and urllib.parse and replace

self.clipboard_clear()
self.clipboard_append(item)

in .copy_to_clipboard() by

klembord.set({'x-special/gnome-copied-files': 
              f'copy\nfile://{urllib.parse.quote(item)}'.encode()})
like image 141
j_4321 Avatar answered Oct 31 '25 06:10

j_4321


You should keep the file or directory "copied" value as internal variable, and only echo it to the clipboard. This way you will enjoy the same bahavior as the mentioned IDEs.

Please see functions copy_ and paste_.

"""A directory browser using Tk Treeview.

Based on the demo found in Tk 8.5 library/demos/browse
"""
import os
import glob
import tkinter
import tkinter.ttk as ttk
import shutil


clipboard_val = ''
 
def populate_tree(tree, node):
    if tree.set(node, "type") != 'directory':
        return

    path = tree.set(node, "fullpath")
    tree.delete(*tree.get_children(node))

    parent = tree.parent(node)
    special_dirs = [] if parent else glob.glob('.') + glob.glob('..')

    for p in special_dirs + os.listdir(path):
        ptype = None
        p = os.path.join(path, p).replace('\\', '/')
        if os.path.isdir(p): ptype = "directory"
        elif os.path.isfile(p): ptype = "file"

        fname = os.path.split(p)[1]
        id = tree.insert(node, "end", text=fname, values=[p, ptype])

        if ptype == 'directory':
            if fname not in ('.', '..'):
                tree.insert(id, 0, text="dummy")
                tree.item(id, text=fname)
        elif ptype == 'file':
            size = os.stat(p).st_size
            tree.set(id, "size", "%d bytes" % size)


def populate_roots(tree):
    dir = os.path.abspath('.').replace('\\', '/')
    node = tree.insert('', 'end', text=dir, values=[dir, "directory"])
    populate_tree(tree, node)

def update_tree(event):
    tree = event.widget
    populate_tree(tree, tree.focus())

def autoscroll(sbar, first, last):
    """Hide and show scrollbar as needed."""
    first, last = float(first), float(last)
    if first <= 0 and last >= 1:
        sbar.grid_remove()
    else:
        sbar.grid()
    sbar.set(first, last)

def copy_(event):
    global clipboard_val
    tree = event.widget
    node = tree.focus()
    if tree.parent(node):
        path = os.path.abspath(tree.set(node, "fullpath"))
        clipboard_val = path
        root.clipboard_clear()
        root.clipboard_append(clipboard_val)
        
def paste_(event):
    global clipboard_val
    tree = event.widget
    node = tree.focus()
    if tree.parent(node):
        path = os.path.abspath(tree.set(node, "fullpath"))
        
        # make sure path is a directory, even if a file selected
        if os.path.isfile(path):
            path = os.path.split(path)[0]

        if os.path.exists(clipboard_val):
            # copy regular file
            if os.path.isfile(clipboard_val):
                shutil.copy(clipboard_val, path)
            # recursively copy directory
            elif os.path.isdir(clipboard_val):
                shutil.copytree(clipboard_val, os.path.join(path, os.path.split(clipboard_val)[1]))
            # update the view
            populate_tree(tree, node)


root = tkinter.Tk()

vsb = ttk.Scrollbar(orient="vertical")
hsb = ttk.Scrollbar(orient="horizontal")

tree = ttk.Treeview(columns=("fullpath", "type", "size"),
    displaycolumns="size", yscrollcommand=lambda f, l: autoscroll(vsb, f, l),
    xscrollcommand=lambda f, l:autoscroll(hsb, f, l))

vsb['command'] = tree.yview
hsb['command'] = tree.xview

tree.heading("#0", text="Directory Structure", anchor='w')
tree.heading("size", text="File Size", anchor='w')
tree.column("size", stretch=0, width=100)

populate_roots(tree)
tree.bind('<<TreeviewOpen>>', update_tree)
tree.bind('<Control-c>', copy_)
tree.bind('<Control-v>', paste_)

# Arrange the tree and its scrollbars in the toplevel
tree.grid(column=0, row=0, sticky='nswe')
vsb.grid(column=1, row=0, sticky='ns')
hsb.grid(column=0, row=1, sticky='ew')
root.grid_columnconfigure(0, weight=1)
root.grid_rowconfigure(0, weight=1)

root.mainloop()
like image 40
igrinis Avatar answered Oct 31 '25 08:10

igrinis



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!