4

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()
  • Do you want to just copy the contents of the folder or the folder included? – Art Jun 28 '21 at 16:07
  • If I press ```ctrl+c``` on a file, then copy the file, and if I press ```ctrl+c``` on a folder, then copy the whole folder with its contents. @Art –  Jun 28 '21 at 16:15
  • @Sujay instead of using `clipboard` you can use tkinter's built in methods like `.clipboard_get()`, `.clipboard_append(...)`, and `.clipboard_clear()` – TheLizzard Jun 28 '21 at 16:42

3 Answers3

3

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()})
j_4321
  • 15,431
  • 3
  • 34
  • 61
  • That is a good answer but I am limited only to the folder in the ```ttk.Treeview```. I cannot copy paste it in any other folder in the Windows Explorer –  Jul 01 '21 at 09:56
  • @Sujay Ok, I've changed the code to allow pasting path copied from outside the App – j_4321 Jul 01 '21 at 11:36
  • Can you please explain what did you change? I still cannot paste a folder into other directory by copying from the ```Treeview```, navigate to the destination and paste –  Jul 01 '21 at 11:40
  • @Sujay I removed the part where I checked that the clipboard content is the same as the path that was copied from the app (`self._copied_path`). On my computer, I can copy from my system file browser into the App. But I am using Linux so maybe there is some difference in Windows. – j_4321 Jul 01 '21 at 11:57
  • @Sujay If you click on the text "edited ... ago" below my answer, you will see the diff of the edit. – j_4321 Jul 01 '21 at 11:59
  • Ohh! My bad. I almost forgot –  Jul 01 '21 at 12:00
  • @Sujay I have just realized that I misunderstood your question, just like Art, my answer allow to copy from an external program into the App, not the other way around. – j_4321 Jul 01 '21 at 13:55
  • No need to remove the answer, I can use it for my future reference. You can just edit the answer –  Jul 01 '21 at 14:34
2

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()
igrinis
  • 12,398
  • 20
  • 45
1

For windows, you can use the Powershell command Set-Clipboard. To you can run the command use the subprocess module. Now that the file/folder is copied you can now paste it in file explorer using ctrl+v or using the right-click context menu.

To handle the paste simply use clipboard_get() provided by tkinter which will provide you with the path to the file/folder. You can then make use of shutil.copy/shutil.copytree to copy the contents from src in your application.

You can then reload the tree view to make the changes visible.

Example:

import os
import subprocess
import shutil
import tkinter as tk
import tkinter.ttk as ttk


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')

        self.abspath = os.path.abspath(path)
        
        self.tree.bind("<Control-c>",self.copy_to_clipboard)
        self.tree.bind("<Control-v>",self.create_directory_from_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()

        self.store_path = []
        self.reload()

    def copy_to_clipboard(self,event,*args):
        item = self.tree.focus()
        self.store_path.append(self.tree.item(item,"text"))
        
        absolute_path = self.find_absolutePath(item)
        #cmd = r"ls '{}' | Set-Clipboard".format(absolute_path) # if you only want the contents of folder to be copied

        cmd = r"gi '{}' | Set-Clipboard".format(absolute_path) # copies both folder and its contents
        subprocess.run(["powershell", "-command", cmd], shell=True)  # windows specific

        print("copied")

    def find_absolutePath(self, item):

        parent_id = self.tree.parent(item)
        if parent_id:
            parent = self.tree.item(parent_id, 'text')
            self.store_path.append(parent)
            return self.find_absolutePath(parent_id)

        else:
            absolute_path = os.path.join(*self.store_path[::-1])
            self.store_path = []
            return absolute_path
                
    def create_directory_from_clipboard(self, event):

        cur_item = self.tree.focus()
        self.store_path.append(self.tree.item(cur_item, "text"))

        dest = self.find_absolutePath(cur_item)
        
        src_path = self.clipboard_get()

        try:
            if os.path.isdir(src_path):
                src = os.path.basename(os.path.normpath(src_path))
                #os.mkdir(os.path.join(src_path, src))
                shutil.copytree(src_path, os.path.join(dest, src))

            else:
                shutil.copy(src_path, dest)

            self.reload()
            print("pasted")

        except (FileExistsError, FileNotFoundError, shutil.SameFileError) as e:
            print(f"Error: {e}")

        
    def reload(self):
        self.tree.delete(*self.tree.get_children())
        root = self.tree.insert('', 'end', text=self.abspath, open=True)
        self.process_directory(root, self.abspath)
    
    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 = r'mypath\ '
app = App(root, path=path_to_my_project)
app.mainloop()

If you want this to work with other OS you will have to find the respective commands eg

alternatively, you can also make use of win32clipboard, 1 or you can make use of PyQt/pyslide's QClipboard or PyGTK clipboard which provides convenient methods to do these kinds of operations

Art
  • 2,836
  • 4
  • 17
  • 34