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()
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()})
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With