import os
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from tkinter import simpledialog
import shutil
# The whole program is implemented by a single class called FileBrowser in this program.
# The FileBrowser class is a custom tkinter frame that functions as a basic file browser. It inherits from tk.Frame,
# which means it can be used as a widget in a larger tkinter application, or as a standalone application. The class
# encapsulates all the necessary functionality for browsing files and directories, including viewing, navigating,
# renaming, copying, pasting, deleting, and creating directories. Followings are overall description of this class.
#
# 1. Initialization: The class constructor (__init__) sets up the base frame, initializes the current path, and a list to
# store copied file paths. It also loads icons to represent folders and files in the interface, calls a method to create
# the interface widgets, and another one to populate the file list based on the current path.
#
# 2. Widgets: The create_widgets method creates and packs all the necessary tkinter widgets to the frame. These
# include an entry box and a button for navigating directories, buttons for file operations (rename, copy, paste,
# delete, create directory), and a treeview with a scrollbar for listing files and directories.
#
# 3. File Operations: The class includes methods for each file operation. Each method generally works by first
# determining which file or directory is currently selected in the treeview, then performs the operation using the
# appropriate function from the os or shutil modules. After the operation, the file list is updated to reflect any
# changes.
#
# 4. File List: The update_file_list method is used to refresh the file list based on the current directory. It first clears
# the treeview, then uses os.listdir to get a list of files and directories in the current directory, and adds each one
# to the treeview.
#
# 5. Navigation: The navigate method is used to change the current directory. It is bound to the double-click event
# of the treeview, so when a user double-clicks on a directory in the file list, this method changes the current
# directory to the selected directory and updates the file list.
#
# 6. Main Loop: As a tkinter application, the file browser runs in a main event loop, which is started by calling the
# mainloop method on the tkinter root object. This is done in the main function, not in the FileBrowser class itself.
# The mainloop continuously waits for events (like button clicks or key presses) and responds to them by calling
# the appropriate methods.
#
class FileBrowser(tk.Frame):
def __init__(self, master=None, path='C:\\'):
super().__init__(master)
self.path = path
self.pack(fill='both', expand=True)
self.paths_to_copy = []
# Load the icons
self.folder_icon = tk.PhotoImage(file="icon_dir.png")
self.file_icon = tk.PhotoImage(file="icon_file.png")
self.create_widgets()
self.update_file_list()
# This function(method) sets up all the necessary graphical user interface (GUI) components, also known as
# "widgets", for the file browser application.
#
# Firstly, it creates a navigation frame that includes an entry box for directory paths and a button to navigate to
# the input path. It also binds the Enter key to the update_file_list function, which updates the file list based on
# the entered path.
#
# Next, it creates a button frame that houses a collection of buttons for file operations. These operations include
# renaming a file, copying a file, pasting a copied file, deleting a file, and creating a new directory. Each button is
# associated with its respective command.
#
# Finally, it constructs a file frame that contains a treeview widget and a scrollbar. The treeview widget is used to
# display the list of files and directories in the current directory. It also binds the double-click event on a directory
# to the navigate function, which changes the current directory to the selected directory. The scrollbar is added to
# provide vertical navigation in the treeview when the list of files and directories is too long to fit in the window.
#
# All the widgets are packed inside their respective frames using the pack method, which arranges them in their
# containers. This function is responsible for organizing the layout of the application and connecting the widgets
# with their corresponding functions.
#
def create_widgets(self):
self.navigation_frame = tk.Frame(self) # Frame to hold the Entry and Button
self.navigation_frame.pack(fill='x')
self.directory = tk.StringVar(value=self.path)
self.entry = ttk.Entry(self.navigation_frame, textvariable=self.directory)
self.button = ttk.Button(self.navigation_frame, text="Navigate", command=self.update_file_list)
self.entry.pack(side='left', fill='x', padx=[5,0], pady=[5,5], expand=True)
self.button.pack(side='right',padx=[5,5])
# Bind the Enter key to the update_file_list function
self.entry.bind('<Return>', lambda event: self.update_file_list())
# Creating button frame
self.button_frame = tk.Frame(self)
self.button_frame.pack(fill='x')
self.rename_button = ttk.Button(self.button_frame, text="Rename", command=self.rename_file)
self.copy_button = ttk.Button(self.button_frame, text="Copy", command=self.copy_file)
self.paste_button = ttk.Button(self.button_frame, text="Paste", command=self.paste_file)
self.delete_button = ttk.Button(self.button_frame, text="Delete", command=self.delete_file)
self.create_dir_button = ttk.Button(self.button_frame, text="Create Dir", command=self.create_dir)
self.rename_button.pack(side='left', padx=[5,0], pady=[0,5])
self.copy_button.pack(side='left', pady=[0,5])
self.paste_button.pack(side='left', pady=[0,5])
self.delete_button.pack(side='left', pady=[0,5])
self.create_dir_button.pack(side='left', padx=[0,5], pady=[0,5])
self.file_frame = tk.Frame(self) # Frame to hold the Treeview and Scrollbar
self.file_frame.pack(fill='both', expand=True)
self.file_list = ttk.Treeview(self.file_frame)
self.file_list.heading('#0', text='File Name')
self.file_list.bind('<Double-1>', self.navigate) # Bind the function navigate to double click event
self.file_list.tag_configure('parent_directory', font=('TkDefaultFont', 14, 'bold'))
self.scrollbar = ttk.Scrollbar(self.file_frame, orient="vertical", command=self.file_list.yview)
self.file_list.configure(yscrollcommand=self.scrollbar.set)
# Use side option to pack Treeview and Scrollbar horizontally
self.file_list.pack(side='left', fill='both', expand=True, padx=[5,0], pady=[0,5])
self.scrollbar.pack(side='right', fill='y')
# This function is designed to rename a selected file or directory in the FileBrowser application.
#
# The first step is to identify which file or directory has been selected in the application's treeview. This is done by
# calling self.file_list.selection()[0] to get the identifier for the selected item and self.file_list.item(selected_item,
# "text") to get the item's name.
#
# Next, an inner function rename is defined to handle the renaming operation. This function fetches a new name
# provided by the user, constructs the old and new paths, and attempts to rename the file or directory at the old
# path to the new path. If the renaming is successful, the file list is updated. If an error occurs, the error message
# is printed out. After the operation, the temporary Entry widget used for input is destroyed.
#
# The position of the selected item in the treeview is determined using self.file_list.bbox(selected_item). A new Entry
# widget is created at this position for the user to type the new name. The current name of the selected item is set
# as the initial value in the Entry widget, and focus is set on it to allow immediate typing.
#
# Finally, the Enter key is bound to the rename function. This means that when the user hits the Enter key while the
# Entry widget is in focus, the rename function will be executed, renaming the selected item.
#
def rename_file(self):
# Get selected file or directory
selected_item = self.file_list.selection()[0]
selected_item_text = self.file_list.item(selected_item, "text")
# Function to handle renaming
def rename(event=None):
new_name = new_name_var.get()
old_path = os.path.join(self.path, selected_item_text)
new_path = os.path.join(self.path, new_name)
try:
os.rename(old_path, new_path)
self.update_file_list()
except Exception as e:
print(f"Error renaming file: {e}")
finally:
# Destroy the Entry widget when done
new_name_entry.destroy()
# Get the position of the selected item in the treeview
x, y, _, _ = self.file_list.bbox(selected_item)
# Create an Entry widget at the position of the selected item
new_name_var = tk.StringVar()
new_name_entry = ttk.Entry(self.file_list, textvariable=new_name_var)
new_name_entry.place(x=x, y=y)
new_name_var.set(selected_item_text)
new_name_entry.focus()
# Bind the Enter key to the rename function
new_name_entry.bind('<Return>', rename)
# This function is designed to copy one or more selected files within the FileBrowser application.
#
# First, the list of files to copy, self.paths_to_copy, is cleared. This ensures that only the files selected during the
# current operation will be copied.
#
# Next, the function loops over the identifiers of all selected items in the treeview, which are obtained by calling
# self.file_list.selection(). For each selected item, its name is obtained and its full path is constructed by joining the
# current path of the FileBrowser and the item's name.
#
# Then, the function checks if the selected item is a file by calling os.path.isfile(full_path). If it is indeed a file, its
# path is added to the self.paths_to_copy list. If it is not a file (meaning it could be a directory), an error message
# is displayed using messagebox.showerror(), informing the user that only files can be copied.
#
# By the end of this function, self.paths_to_copy will contain the paths of all files that the user has selected to copy.
# The actual copying operation is not performed in this function; the paths are merely prepared to be used in another
# function such as paste_file.
#
def copy_file(self):
# Clear the list of files to copy
self.paths_to_copy = []
# Get selected files
for selected_item in self.file_list.selection():
selected_item_text = self.file_list.item(selected_item, "text")
full_path = os.path.join(self.path, selected_item_text)
# If the selected item is a file, add its path to the list
if os.path.isfile(full_path):
self.paths_to_copy.append(full_path)
else:
messagebox.showerror("Error", "Please select only files to copy.")
# This function is designed to paste the files that have been previously selected for copying in the FileBrowser
# application.
#
# The function first checks if there are any files to copy by examining self.paths_to_copy. If this list is empty, it
# means that no files have been selected for copying and hence an error message is displayed using
# messagebox.showerror().
#
# If there are files to copy, the function enters a try block to handle any errors that may occur during the copying
# operation. Within this block, it iterates over each path in self.paths_to_copy.
#
# For each file to be copied, the function obtains the base name of the file (i.e., the file name without any directory
# components) using os.path.basename(). It then computes the full path of the new file by joining the current path
# of the FileBrowser and the base name.
#
# The function then checks whether a file with the same name already exists at the destination. If such a file exists,
# it modifies the base name by appending '-Copy' to it before the file extension, and then recomputes the full path.
#
# The function then uses shutil.copy2() to copy the file from the source path to the destination. This function is
# chosen over shutil.copy() because copy2() also copies the file's metadata (like its creation and modification times),
# not just its content.
#
# After all the files have been copied, the function clears the self.paths_to_copy list and updates the file list by
# calling self.update_file_list().
#
If any error occurs during the copying operation, an error message is displayed with the details of the error.
def paste_file(self):
# If there are files to copy
if self.paths_to_copy:
try:
for path_to_copy in self.paths_to_copy:
# Get the base name of the file to copy
file_name = os.path.basename(path_to_copy)
# Compute the full path of the new file
new_file_path = os.path.join(self.path, file_name)
# If a file with the same name already exists in the destination directory
if os.path.exists(new_file_path):
# Split the file name into name and extension
name, ext = os.path.splitext(file_name)
# Append '-Copy' to the name and reassemble the file name
new_file_name = f"{name}-Copy{ext}"
# Compute the full path of the new file
new_file_path = os.path.join(self.path, new_file_name)
# Copy the file
shutil.copy2(path_to_copy, new_file_path)
# Clear the list of files to copy
self.paths_to_copy = []
self.update_file_list()
except Exception as e:
messagebox.showerror("Error", f"Failed to paste files: {e}")
else:
messagebox.showerror("Error", "No files to paste.")
# This function is responsible for deleting selected files and directories in the FileBrowser application.
#
# Initially, the function fetches the selected items from the file list using self.file_list.selection(). If no items are
# selected, an error message is displayed using messagebox.showerror() and the function returns immediately.
#
# If some items are selected, the function then confirms whether the user wants to delete these items by showing
# a confirmation dialog using messagebox.askokcancel(). If the user confirms, the function starts a loop over the
# selected items.
#
# In each iteration, it fetches the text of the selected item and computes the full path of the item by joining the
# current path of the FileBrowser with the selected item's text.
#
# Then, the function checks if the item at the full path is a file or a directory. If it's a file, the file is deleted using
# os.remove(). If it's a directory, the function checks whether the directory is empty. If the directory is not empty,
# the user is asked for another confirmation before deleting the directory. If the user confirms, or if the directory is
# empty, the directory is deleted using shutil.rmtree(), which removes an entire directory tree, i.e., a directory and
# all its contents.
#
#After deleting each item, the file list is updated by calling self.update_file_list().
#
#If an error occurs during the deletion of any item, an error message is displayed with the details of the error.
#
def delete_file(self):
# Get selected items
selected_items = self.file_list.selection()
if not selected_items:
messagebox.showerror("Error", "No file or directory selected.")
return
response = messagebox.askokcancel("Confirm delete", "Are you sure you want to delete the selected items?")
if response:
for selected_item in selected_items:
selected_item_text = self.file_list.item(selected_item, "text")
full_path = os.path.join(self.path, selected_item_text)
try:
# If the selected item is a file, delete it
if os.path.isfile(full_path):
os.remove(full_path)
# If the selected item is a directory, delete it and all its contents
elif os.path.isdir(full_path):
# Check if directory is not empty
if os.listdir(full_path):
response = messagebox.askokcancel("Directory not empty",
"This directory is not empty. All the files within the directory will be deleted as well. Do you want to delete?")
if response:
shutil.rmtree(full_path)
else:
continue
else:
shutil.rmtree(full_path)
self.update_file_list()
except Exception as e:
messagebox.showerror("Error", f"Failed to delete {selected_item_text}: {e}")
# This function is responsible for creating a new directory.
#
# Firstly, it opens a dialog asking the user to enter the name of the new directory using the simpledialog.askstring()
# function. The title of the dialog is "Create Directory" and the prompt is "Enter new directory name".
#
# If the user doesn't type a name and closes the dialog, the askstring() function returns None and the create_dir
# function just returns without doing anything.
#
# If the user types a name and clicks OK, the askstring() function returns the typed string and the create_dir
# function tries to create a directory with that name.
#
# The function computes the full path of the new directory by joining the current path of the FileBrowser with the
# new directory name. Then it tries to create the directory at this path using the os.mkdir() function.
#
# If the directory is successfully created, the function updates the file list by calling self.update_file_list(), so the
# new directory appears in the list.
#
# If an error occurs during the creation of the directory, the function catches the exception and shows an error
# message using messagebox.showerror(). The error message includes the name of the directory that failed to be
# created and the details of the error.
#
def create_dir(self):
# Show a dialog asking for the name of the new directory
new_dir_name = simpledialog.askstring("Create Directory", "Enter new directory name")
# If the user didn't type a name, return
if not new_dir_name:
return
# Try to create the directory
new_dir_path = os.path.join(self.path, new_dir_name)
try:
os.mkdir(new_dir_path)
self.update_file_list()
except Exception as e:
messagebox.showerror("Error", f"Failed to create directory '{new_dir_name}': {e}")
# This function is a crucial part of the FileBrowser application, handling the task of refreshing the display of files
# and directories.
#
# Firstly, it fetches the current directory from the directory variable and clears the file_list Treeview of any existing
# entries.
#
# If the current path isn't the root directory (C:\\), it inserts a parent directory entry at the top of the list, denoted
# by '..'. This allows navigation back up the directory tree.
#
# The function then attempts to list the contents of the current directory using the os.listdir() function. If the
# directory doesn't exist (which may happen if it was manually deleted or moved outside of the FileBrowser
# application), os.listdir() raises a FileNotFoundError, which is caught and causes the function to return immediately.
#
# For existing directories, the function splits the contents into two separate lists: dirs for directories and files for files.
# This is done by iterating over each item in the directory, forming its full path by joining the item's name to the
# current path, and then checking if it's a directory or a file using os.path.isdir().
#
# Afterwards, it populates the file_list Treeview with the directories and files. The directories and files are inserted
# separately, ensuring that directories always appear before files. Each directory is displayed with a folder_icon and
# each file with a file_icon.
#
def update_file_list(self):
self.path = self.directory.get()
self.file_list.delete(*self.file_list.get_children())
if self.path != 'C:\\':
#self.file_list.insert("", "end", text='..', open=False)
self.file_list.insert("", "end", text='..', open=False, tags=('parent_directory',))
try:
dir_content = os.listdir(self.path)
except FileNotFoundError:
return
dirs = []
files = []
for item in dir_content:
item_full_path = os.path.join(self.path, item)
if os.path.isdir(item_full_path):
dirs.append(item)
else:
files.append(item)
for dir in dirs:
self.file_list.insert("", "end", text=dir, open=False, image=self.folder_icon)
for file in files:
self.file_list.insert("", "end", text=file, image=self.file_icon)
# This function is responsible for changing the current directory based on user's selection in the file browser.
#
# The function is bound to a double-click event on the file list, so it is triggered whenever the user double-clicks
# on a file or directory entry in the file list.
#
# When triggered, it first identifies the item that was double-clicked by calling self.file_list.selection()[0] to get the
# first selected item in the file list. It then retrieves the text of the selected item, which is the name of the file or
# directory.
#
# If the selected item is '..', representing the parent directory, the function uses os.path.dirname(self.path) to get
# the path of the parent directory. Otherwise, it uses os.path.join(self.path, selected_item_text) to form the full
# path of the selected directory.
#
# Next, it checks whether the new path exists using os.path.exists(new_path). This is a safeguard against trying to
# navigate to a directory that has been deleted or moved outside of the FileBrowser application.
#
# If the new path exists, it updates self.directory to the new path and calls self.update_file_list() to refresh the file
# list.
#
def navigate(self, event): # Function to navigate to the selected directory
selected_item = self.file_list.selection()[0]
selected_item_text = self.file_list.item(selected_item, "text")
if selected_item_text == '..':
new_path = os.path.dirname(self.path)
else:
new_path = os.path.join(self.path, selected_item_text)
if os.path.exists(new_path):
self.directory.set(new_path)
self.update_file_list()
def main():
root = tk.Tk()
root.title("Simple File Browser")
root.geometry("600x400")
app = FileBrowser(master=root, path='C:\\')
app.mainloop()
if __name__ == "__main__":
main()
|