From 33291657255a3c6d57e1cdc269e545a10a6c78c3 Mon Sep 17 00:00:00 2001 From: jwansek Date: Sun, 21 Dec 2025 16:51:31 +0000 Subject: Made into a module, updated --- __init__.py | 1 + fileTester.py | 25 --- fileTreeBrowser.py | 549 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tkFileBrowser.py | 547 ---------------------------------------------------- winIcon.py | 52 ----- windowsIcons.py | 52 +++++ 6 files changed, 602 insertions(+), 624 deletions(-) create mode 100644 __init__.py delete mode 100644 fileTester.py create mode 100644 fileTreeBrowser.py delete mode 100644 tkFileBrowser.py delete mode 100644 winIcon.py create mode 100644 windowsIcons.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a331161 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .fileTreeBrowser import TkFileBrowser \ No newline at end of file diff --git a/fileTester.py b/fileTester.py deleted file mode 100644 index 65a2f2d..0000000 --- a/fileTester.py +++ /dev/null @@ -1,25 +0,0 @@ -import tkinter as tk -from tkinter import ttk -import os - -def on_close(): - if os.path.exists(os.path.join("E:", "temp")): - os.rmdir(os.path.join("E:", "temp")) - if os.path.exists(os.path.join(os.path.expanduser("~"), "temp")): - os.rmdir(os.path.join(os.path.expanduser("~"), "temp")) - - root.destroy() - -root = tk.Tk() -root.title("File tester program") -root.resizable(False, False) -root.protocol("WM_DELETE_WINDOW", on_close) - -tk.Label(root, text = "File tester program", font = ("Verdana", 16, "bold")).grid(row = 0, column = 0, columnspan = 2, padx = 3, pady = 3) - -ttk.Button(root, text = "Add in user", command = lambda: os.mkdir(os.path.join(os.path.expanduser("~"), "temp"))).grid(row = 1, column = 0, padx = 3, pady = 3, ipady = 3, ipadx = 3) -ttk.Button(root, text = "Delete in user", command = lambda: os.rmdir(os.path.join(os.path.expanduser("~"), "temp"))).grid(row = 1, column = 1, padx = 3, pady = 3, ipady = 3, ipadx = 3) -ttk.Button(root, text = "Add in E:\\", command = lambda: os.mkdir(os.path.join("E:", "temp"))).grid(row = 2, column = 0, padx = 3, pady = 3, ipady = 3, ipadx = 3) -ttk.Button(root, text = "Delete in E:\\", command = lambda: os.rmdir(os.path.join("E:", "temp"))).grid(row = 2, column = 1, padx = 3, pady = 3, ipady = 3, ipadx = 3) - -root.mainloop() \ No newline at end of file diff --git a/fileTreeBrowser.py b/fileTreeBrowser.py new file mode 100644 index 0000000..162c981 --- /dev/null +++ b/fileTreeBrowser.py @@ -0,0 +1,549 @@ +from glob import glob +from operator import itemgetter +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox +from PIL import Image, ImageTk +import win32api +import os + +from . import windowsIcons + +MENU_DELETE = "menu_delete" +MENU_OPEN = "open" +MENU_CLIPBOARD = "clipboard" +ALL = [MENU_DELETE, MENU_OPEN, MENU_CLIPBOARD] + +class TkFileBrowser(tk.Frame): + + _open = [] + + def __init__(self, parent, command, rightclick_options = ALL, refresh = 20, types = [], showhidden = False): + """Widget for browsing a windows filesystem. + + Arguments: + parent {tk.Frame} -- parent frame + command {function} -- Called when the user clicks on a file + + Keyword Arguments: + rightclick_options {list} -- the options to show up in the menu when the user right + clicks. Choose from [MENU_DELETE, MENU_OPEN, MENU_CLIPBOARD] or just use ALL (default: ALL) + refresh {int} -- how often to refresh browser (ms) (default: {20}) + types {list} -- file types that show up. Leave blank for all files: e + e.g. [".png", ".jpg"] (default: {[]}) + showhiddem {bool} -- should the tree show hidden files or not (default: {False}) + """ + + tk.Frame.__init__(self, parent) + self._parent = parent + self._command = command + self._rightclick_options = rightclick_options + self._refresh = refresh + self._types = types + self._showhidden = showhidden + + self._img_clipboard = tk.PhotoImage(file = os.path.join(os.path.dirname(__file__), "Assets", "clipboard.png")) + self._img_cross = tk.PhotoImage(file = os.path.join(os.path.dirname(__file__), "Assets", "cross.png")) + + self._book = DriveBook(self) + self._book.pack(fill = tk.BOTH, expand = True) + self.after(self._refresh, self.refresh) + + #TODO: fix a bug here + def see(self, path): + """Open the tree and book to this folder + + Arguments: + path {str} -- path to open to + """ + if not os.path.exists(path): + print("The system couldn't find the path: '%s'" % path) + return + + #open the correct book tab + split = path.replace("\\", "/").split("/") + + #if requested is file, go to the folder in which it's in + if os.path.isfile(path): + del split[-1] + + drive = split[0] + "\\" + self._book.select(self._book._get_drives().index(drive)) + + try: + self._book._tabs[drive]._tree.see("\\".join(split)) + except tk.TclError: + #the node probably isn't loaded yet so we have to make it load before + #using .see() + for i in range(len(split) - 1): + self._book._tabs[drive]._populate_path("\\".join(split[:i+2])) + self._book._tabs[drive]._tree.item("\\".join(split[:i+2]), open = True) + + #add to list of open nodes + self._open.append([split[0]+"\\", "\\".join(split[:i+2])]) + + self._book._tabs[drive]._tree.see("\\".join(split)) + + def refresh(self): + def get_drive_and_path(search): + out = [] + for i in self._open: + if i[1] == search: + out.append(i) + return out + + self._book._refresh() + + print(self._open) + + #work out which open nodes need to be updated + nodes_to_refresh = [] + for i in self._open: + dir = i[1] + if not os.path.exists(dir): + continue + #work out all the dirs that the node is showing, ignoring the dummy by checking for paths + childirs = [p.split("\\")[-1] for p in self._book._tabs[i[0]]._tree.get_children(dir) if "\\" in p] + + #work out all the dirs that are in the file system. Use our own method so that settings + #are still here, show hidden files, etc. + files, folders = self._book._tabs[i[0]]._get_dirs_in_path(dir) + actualdirs = files + folders + + if actualdirs != childirs: + nodes_to_refresh.append(dir) + + + #add drives to the list of stuff to check for updates + for drive in self._book._get_drives(): + childirs = [p.split("\\")[-1] for p in self._book._tabs[drive]._tree.get_children("")] + files, folders = self._book._tabs[drive]._get_dirs_in_path(drive) + actualdirs = files + folders + if actualdirs != childirs: + nodes_to_refresh.append(drive) + + #print(nodes_to_refresh) + + drive_and_node = [] + for node in nodes_to_refresh: + for drive, node1 in get_drive_and_path(node): + + orignode = node1 + if node1 == drive: + node1 = "" + + #print("node: ", node1, "\ndrive: ", drive) + tree = self._book._tabs[drive]._tree + children = tree.get_children(node1) + + #get the full path of removals and additons + glob_pattern = os.path.join(orignode, "*") + deletions = list(set(list(children)) - set(sorted(glob(glob_pattern), key=os.path.getctime))) + additions = list(set(sorted(glob(glob_pattern), key=os.path.getctime)) - set(list(children))) + + #print("deletions: ", deletions, "\nadditions: ", additions) + + #delete from tree + for deletion in deletions: + tree.delete(deletion) + #if the node is open, and it's just been deleteted, remove it from the list + if [drive.replace("\\", "\\"), deletion.replace("\\", "\\")] in self._open: + self._open.remove([drive.replace("\\", "\\"), deletion.replace("\\", "\\")]) + + #add new entries in the correct place, highlighting them and getting appropitate icons + for addition in additions: + + if os.path.isdir(addition): + #make an icon if it doesn't already exist + if addition not in self._book._foldericons: + self._book._foldericons[addition] = ImageTk.PhotoImage( + self._get_icon(addition)) + + #get the name + folder = addition.split("\\")[-1] + #work out all the other folders so we know an approprate place to put the new folder + glob_pattern = os.path.join(orignode, "*") + folders = [i for i in sorted(glob(glob_pattern), key=os.path.getctime) if os.path.isdir(i)] + + tree.insert( + parent = node, + index = folders.index(addition), #insert in an appropriate place + iid = addition, + tags = (addition, ), + text = folder, + image = self._book._foldericons[addition], + values = [folder, "", ""]) + #setup a dummy so the '+' appears before it's loaded. Child stuff will be loaded when the user + #clicks on the plus, and the dummy will be removed. (or not if there are no files) + tree.insert(parent = addition, index = tk.END, tag = "dummy", text = "No avaliable files") + + elif os.path.isfile(addition): + _, type_ = os.path.splitext(addition) + + if type_ == ".lnk" or type_ == ".exe": + if addition not in self._book._fileicons: + self._book._fileicons[addition] = ImageTk.PhotoImage( + self._get_icon(addition) + ) + icon = self._book._fileicons[addition] + else: + if type_ not in self._book._fileicons: + self._book._fileicons[type_] = ImageTk.PhotoImage( + self._get_icon(type_) + ) + icon = self._book._fileicons[type_] + + file = addition.split("\\")[-1] + tree.insert( + parent = node, + index = tk.END, + iid = addition, + tags = (addition, ), + text = file, + image = icon, + values = [file, "", self._book._tabs[drive]._get_size(addition)]) + + #highlight + tree.tag_configure(addition, background = "orange") + + self.after(self._refresh, self.refresh) + + def _get_icon(self, PATH): + """Gets the icon association for any folder or file in the system + + Arguments: + PATH {str} -- path to file or folder + + Raises: + TypeError -- Thrown if invalid arguments are given + + Returns: + PIL.Image -- PIL Image of the icon at the correct size + """ + + #https://stackoverflow.com/questions/21070423/python-sAaving-accessing-file-extension-icons-and-using-them-in-a-tkinter-progra/52957794#52957794 + #https://aecomputervision.blogspot.com/2018/10/getting-icon-association-for-any-file.html + return windowsIcons.get_icon(PATH, windowsIcons.SMALL) + +class DriveBook(ttk.Notebook): + + _foldericons = {} + _fileicons = {} + _tabs = {} + + def __init__(self, parent): + ttk.Notebook.__init__(self) + self._parent = parent + + self._draw_tabs() + + def _draw_tabs(self): + self._drive_icons = self._get_icons() #store to attribute so it doesn't get removed by garbage deletion + for drive in self._drive_icons: + self._tabs[drive[0]] = FileTree(self, drive[0]) + if os.path.split(drive[0])[1] != "": + self.add(self._tabs[drive[0]], text = "Home", image = drive[1], compound = tk.LEFT) + else: + self.add(self._tabs[drive[0]], text = drive[0], image = drive[1], compound = tk.LEFT) + + def _refresh(self): + """Checks if a drive as been added or removed. If it has, tabs are refreshed. + """ + + #check if we need to refresh before refreshing + if list(map(itemgetter(0), self._drive_icons)) != self._get_drives(): + display = [i[0] for i in self._drive_icons] + new = self._get_drives() + + removals = list(set(display).difference(new)) + additions = list(set(new).difference(display)) + + print("removed: ", removals, "added: ", additions) + + for removal in removals: + #delete open nodes that have just been removed + self._parent._open = [i for i in self._parent._open if i[0] != removal] + + #deelte the tab + try: + self.forget(self._tabs[removal]) + except tk.TclError: + #TODO: work out why this throws an error when it still works + pass + + #remove from tab dictionary + del self._tabs[removal] + + #reset drive icons + self._drive_icons = [i for i in self._drive_icons if i[0] != removal] + + for addition in additions: + self._drive_icons.append([addition, ImageTk.PhotoImage(self._parent._get_icon(addition))]) + self._tabs[addition] = FileTree(self, addition) + self.add(self._tabs[addition], text = addition, image = self._drive_icons[-1][1], compound = tk.LEFT) + + def _get_drives(self): + return [os.path.expanduser("~")] + win32api.GetLogicalDriveStrings().split('\x00')[:-1] + + def _get_icons(self): + drives = self._get_drives() + return [[drive, ImageTk.PhotoImage(self._parent._get_icon(drive))] for drive in drives] + + def _get_tab_name(self): + """Returns the name of the tab which is currently open + + Returns: + int -- the name of the tab that's currently open e.g. C:\\, C:\\Users\\Fred + """ + + return self._get_drives()[self.index(self.select())] + +class FileTree(tk.Frame): + def __init__(self, parent, drive): + tk.Frame.__init__(self, parent) + self._parent = parent + self._command = self._parent._parent._command + self._types = self._parent._parent._types + self._showhidden = self._parent._parent._showhidden + + self._tree = ttk.Treeview(self, height = 20, columns = ('path', 'filetype', 'size'), displaycolumns = 'size') + self._tree.heading('#0', text = 'Directory', anchor = tk.W) + self._tree.heading('size', text = 'Size', anchor = tk.W) + self._tree.column('path', width = 180) + self._tree.column('size', stretch = 1, width = 48) + self._tree.grid(row = 0, column = 0, sticky = 'nsew') + + sbr_y = ttk.Scrollbar(self, orient = tk.VERTICAL, command = self._tree.yview) + sbr_x = ttk.Scrollbar(self, orient = tk.HORIZONTAL, command = self._tree.xview) + self._tree['yscroll'] = sbr_y.set + self._tree['xscroll'] = sbr_x.set + sbr_y.grid(row = 0, column = 1, sticky = 'ns') + sbr_x.grid(row = 1, column = 0, sticky = 'ew') + + self.rowconfigure(0, weight = 1) + self.columnconfigure(0, weight = 1) + + self._populate_path(drive) + self._tree.bind('<>', self._on_click) + self._tree.bind('<>', self._on_close) + if self._parent._parent._rightclick_options != []: + self._tree.bind('', self._draw_menu) + + def _draw_menu(self, event): + menu = RightClickMenu(self, self._parent._parent._rightclick_options, self._tree.identify_row(event.y)) + try: + menu.tk_popup(event.x_root, event.y_root) + finally: + menu.grab_release() + + def _on_click(self, event): + id = os.path.normpath(self._tree.focus()) + + #add to the list of open nodes + if os.path.isdir(id): + #add both the tab name and the path, since the same path could be open in multiple places + self._parent._parent._open.append([self._parent._get_tab_name(), id]) + #print(self._parent._parent._open) + + if os.path.isfile(id): + self._command(id) + else: + try: + self._populate_path(id) + except tk.TclError: + #this node has already been opened, so delete children and load again + for child in self._tree.get_children(id): + self._tree.delete(child) + self._populate_path(id) + + #print("\n clicked: ", self._parent._parent._open) + + def _on_close(self, event): + """Method called when the user closes a node. This must delete the node, + and it's children, from the list of open nodes. To do this, a recursive + algoritm is used. We have to use this, as opposed to something like os.walk() + for preformance. A large folder could have thousands of children. This doesn't + 'go deeper' unless the node in the tree is open. + + Arguments: + event {tkinter event} -- tkinter event is used to get the id, + """ + + id = os.path.normpath(self._tree.focus()) + drive = self._parent._get_tab_name() + + def recurse(path): + glob_pattern = os.path.join(path, '*') + for dir in sorted(glob(glob_pattern), key=os.path.getctime): + if os.path.isdir(dir) and self._tree.item(dir, "open"): + self._parent._parent._open.remove([drive, dir]) + recurse(dir) + + try: + self._parent._parent._open.remove([drive, id]) + except ValueError: + #already removed by parent + pass + + recurse(id) + + def _get_size(self, path): + """Returns the size of a file. From: + https://pyinmyeye.blogspot.co.uk/2012/07/tkinter-multi-column-list-demo.html + + Arguments: + path {str} -- Path to file + + Returns: + str -- file size in bytes/KB/MB/GB + """ + + size = os.path.getsize(path) + KB = 1024.0 + MB = KB * KB + GB = MB * KB + if size >= GB: + return ('{:,.1f} GB').format(size / GB) + elif size >= MB: + return ('{:,.1f} MB').format(size / MB) + elif size >= KB: + return ('{:,.1f} KB').format(size / KB) + else: + return ('{} bytes').format(size) + + def _populate_path(self, path): + #if the tree is blank, write to the root + if self._tree.get_children() == (): + node = "" + else: + node = path + + folders, files = self._get_dirs_in_path(path) + + for folder in folders: + fullpath = os.path.join(path, folder) + self._tree.insert( + parent = node, + index = tk.END, + iid = fullpath, + tags = (fullpath, ), + text = folder, + image = self._parent._foldericons[fullpath], + values = [folder, "", ""]) + #setup a dummy so the '+' appears before it's loaded. Child stuff will be loaded when the user + #clicks on the plus, and the dummy will be removed. (or not if there are no files) + self._tree.insert(parent = fullpath, index = tk.END, tag = "dummy", text = "No avaliable files") + + for file in files: + fullpath = os.path.join(path, file) + name, type_ = os.path.splitext(file) + #if the type is a shortcut, get the whole path as a key + if type_ == ".lnk" or type_ == ".exe": + icon = self._parent._fileicons[fullpath] + else: + icon = self._parent._fileicons[type_] + self._tree.insert( + parent = node, + index = tk.END, + iid = fullpath, + tag = fullpath, + text = file, + image = icon, + values = [file, "", self._get_size(fullpath)]) + + #if there is more than one child, delete every child that isn't a path + #and hence delete the dummy + children = self._tree.get_children(node) + if len(children) > 1: + for child in children: + if not os.path.exists(child): + self._tree.delete(child) + + def _get_dirs_in_path(self, path): + """Returns two lists, the first is a list of all folders in a directory, + the second is a list of files. Also loads the icons for these files and folders. + For folders, it puts a tk.PhotoImage as the value of the _foldericons dictionary + with the key being the full folder path. For files, it uses the _fileicons dictionary, + with they key being the filetype, e.g. "mp4". If the file is a shortcut (".lnk") or + executable (".exe"), set the whole path as the key, since the icon is different for + every file of that type. + + Arguments: + path {str} -- full path to the folder + + Returns: + tuple -- two lists of folders and files. + """ + + def add(p, type_): + files.append(p) + + #if the file is a shortcut, set the key as the whole path + if type_ == ".lnk" or type_ == ".exe": + if os.path.join(path, p) not in self._parent._fileicons: + self._parent._fileicons[os.path.join(path, p)] = ImageTk.PhotoImage( + self._parent._parent._get_icon(os.path.join(path, p))) + else: + if type_ not in self._parent._fileicons: + self._parent._fileicons[type_] = ImageTk.PhotoImage( + self._parent._parent._get_icon(os.path.join(path, p))) + + #print(self._parent._fileicons, "\n") + + files = [] + folders = [] + try: + for p in os.listdir(path): + if p.startswith(".") and not self._showhidden: + continue + + if os.path.isfile(os.path.join(path, p)): + name, type_ = os.path.splitext(p) + if self._types == []: + add(p, type_) + else: + if type_ in self._types: + add(p, type_) + + else: + folders.append(p) + if os.path.join(path, p) not in self._parent._foldericons: + self._parent._foldericons[os.path.join(path, p)] = ImageTk.PhotoImage( + self._parent._parent._get_icon(os.path.join(path, p))) + except OSError: + pass + + + #print("\n\nfolders: ", folders, "\nfiles: ", files) + return folders, files + +#for efficiency, it would be better to instatiate this only once, since now it's instatiated +#every time the user right clicks +class RightClickMenu(tk.Menu): + def __init__(self, parent, options, item): + tk.Menu.__init__(self, parent, tearoff = False) + self.parent = parent + + print(item) + #only show menu options if the dev selected them + #if the dev selected no options, binding is never set + #it must be a file for the open option to appear + if MENU_OPEN in options and os.path.isfile(item): + self.add_command(label = "Open with default program", command = lambda: print("Open")) + if MENU_DELETE in options: + self.add_command(label = "Delete", image = self.parent._parent._parent._img_cross, compound = tk.LEFT, command = lambda: os.remove(item)) + if MENU_CLIPBOARD in options: + self.add_command(label = "Copy path to clipboard", image = self.parent._parent._parent._img_clipboard, compound = tk.LEFT, command = lambda: print("clipboard")) + +def on_click(path): + print("click: ", path) + +# to debug, `python -m tkFileBrowser.fileTreeBrowser` from parent directory +if __name__ == "__main__": + root = tk.Tk() + browser = TkFileBrowser(root, command = on_click) + browser.pack(side = tk.LEFT) + + ttk.Button(root, text = "Goto", command = lambda: browser.see(r"C:\Users\eden\Pictures")).pack(side = tk.LEFT) + + root.mainloop() \ No newline at end of file diff --git a/tkFileBrowser.py b/tkFileBrowser.py deleted file mode 100644 index 0a32cb4..0000000 --- a/tkFileBrowser.py +++ /dev/null @@ -1,547 +0,0 @@ -from glob import glob -from operator import itemgetter -import tkinter as tk -from tkinter import ttk -from tkinter import messagebox -from PIL import Image, ImageTk -import win32api -import winIcon -import os - -MENU_DELETE = "menu_delete" -MENU_OPEN = "open" -MENU_CLIPBOARD = "clipboard" -ALL = [MENU_DELETE, MENU_OPEN, MENU_CLIPBOARD] - -class TkFileBrowser(tk.Frame): - - _open = [] - - def __init__(self, parent, command, rightclick_options = ALL, refresh = 20, types = [], showhidden = False): - """Widget for browsing a windows filesystem. - - Arguments: - parent {tk.Frame} -- parent frame - command {function} -- Called when the user clicks on a file - - Keyword Arguments: - rightclick_options {list} -- the options to show up in the menu when the user right - clicks. Choose from [MENU_DELETE, MENU_OPEN, MENU_CLIPBOARD] or just use ALL (default: ALL) - refresh {int} -- how often to refresh browser (ms) (default: {20}) - types {list} -- file types that show up. Leave blank for all files: e - e.g. [".png", ".jpg"] (default: {[]}) - showhiddem {bool} -- should the tree show hidden files or not (default: {False}) - """ - - tk.Frame.__init__(self, parent) - self._parent = parent - self._command = command - self._rightclick_options = rightclick_options - self._refresh = refresh - self._types = types - self._showhidden = showhidden - - self._img_clipboard = tk.PhotoImage(file = os.path.join("Assets", "clipboard.png")) - self._img_cross = tk.PhotoImage(file = os.path.join("Assets", "cross.png")) - - self._book = DriveBook(self) - self._book.pack(fill = tk.BOTH, expand = True) - self.after(self._refresh, self.refresh) - - #TODO: fix a bug here - def see(self, path): - """Open the tree and book to this folder - - Arguments: - path {str} -- path to open to - """ - if not os.path.exists(path): - print("The system couldn't find the path: '%s'" % path) - return - - #open the correct book tab - split = path.replace("\\", "/").split("/") - - #if requested is file, go to the folder in which it's in - if os.path.isfile(path): - del split[-1] - - drive = split[0] + "\\" - self._book.select(self._book._get_drives().index(drive)) - - try: - self._book._tabs[drive]._tree.see("\\".join(split)) - except tk.TclError: - #the node probably isn't loaded yet so we have to make it load before - #using .see() - for i in range(len(split) - 1): - self._book._tabs[drive]._populate_path("\\".join(split[:i+2])) - self._book._tabs[drive]._tree.item("\\".join(split[:i+2]), open = True) - - #add to list of open nodes - self._open.append([split[0]+"\\", "\\".join(split[:i+2])]) - - self._book._tabs[drive]._tree.see("\\".join(split)) - - def refresh(self): - def get_drive_and_path(search): - out = [] - for i in self._open: - if i[1] == search: - out.append(i) - return out - - self._book._refresh() - - print(self._open) - - #work out which open nodes need to be updated - nodes_to_refresh = [] - for i in self._open: - dir = i[1] - if not os.path.exists(dir): - continue - #work out all the dirs that the node is showing, ignoring the dummy by checking for paths - childirs = [p.split("\\")[-1] for p in self._book._tabs[i[0]]._tree.get_children(dir) if "\\" in p] - - #work out all the dirs that are in the file system. Use our own method so that settings - #are still here, show hidden files, etc. - files, folders = self._book._tabs[i[0]]._get_dirs_in_path(dir) - actualdirs = files + folders - - if actualdirs != childirs: - nodes_to_refresh.append(dir) - - - #add drives to the list of stuff to check for updates - for drive in self._book._get_drives(): - childirs = [p.split("\\")[-1] for p in self._book._tabs[drive]._tree.get_children("")] - files, folders = self._book._tabs[drive]._get_dirs_in_path(drive) - actualdirs = files + folders - if actualdirs != childirs: - nodes_to_refresh.append(drive) - - #print(nodes_to_refresh) - - drive_and_node = [] - for node in nodes_to_refresh: - for drive, node1 in get_drive_and_path(node): - - orignode = node1 - if node1 == drive: - node1 = "" - - #print("node: ", node1, "\ndrive: ", drive) - tree = self._book._tabs[drive]._tree - children = tree.get_children(node1) - - #get the full path of removals and additons - glob_pattern = os.path.join(orignode, "*") - deletions = list(set(list(children)) - set(sorted(glob(glob_pattern), key=os.path.getctime))) - additions = list(set(sorted(glob(glob_pattern), key=os.path.getctime)) - set(list(children))) - - #print("deletions: ", deletions, "\nadditions: ", additions) - - #delete from tree - for deletion in deletions: - tree.delete(deletion) - #if the node is open, and it's just been deleteted, remove it from the list - if [drive.replace("\\", "\\"), deletion.replace("\\", "\\")] in self._open: - self._open.remove([drive.replace("\\", "\\"), deletion.replace("\\", "\\")]) - - #add new entries in the correct place, highlighting them and getting appropitate icons - for addition in additions: - - if os.path.isdir(addition): - #make an icon if it doesn't already exist - if addition not in self._book._foldericons: - self._book._foldericons[addition] = ImageTk.PhotoImage( - self._get_icon(addition)) - - #get the name - folder = addition.split("\\")[-1] - #work out all the other folders so we know an approprate place to put the new folder - glob_pattern = os.path.join(orignode, "*") - folders = [i for i in sorted(glob(glob_pattern), key=os.path.getctime) if os.path.isdir(i)] - - tree.insert( - parent = node, - index = folders.index(addition), #insert in an appropriate place - iid = addition, - tags = (addition, ), - text = folder, - image = self._book._foldericons[addition], - values = [folder, "", ""]) - #setup a dummy so the '+' appears before it's loaded. Child stuff will be loaded when the user - #clicks on the plus, and the dummy will be removed. (or not if there are no files) - tree.insert(parent = addition, index = tk.END, tag = "dummy", text = "No avaliable files") - - elif os.path.isfile(addition): - _, type_ = os.path.splitext(addition) - - if type_ == ".lnk" or type_ == ".exe": - if addition not in self._book._fileicons: - self._book._fileicons[addition] = ImageTk.PhotoImage( - self._get_icon(addition) - ) - icon = self._book._fileicons[addition] - else: - if type_ not in self._book._fileicons: - self._book._fileicons[type_] = ImageTk.PhotoImage( - self._get_icon(type_) - ) - icon = self._book._fileicons[type_] - - file = addition.split("\\")[-1] - tree.insert( - parent = node, - index = tk.END, - iid = addition, - tags = (addition, ), - text = file, - image = icon, - values = [file, "", self._book._tabs[drive]._get_size(addition)]) - - #highlight - tree.tag_configure(addition, background = "orange") - - self.after(self._refresh, self.refresh) - - def _get_icon(self, PATH): - """Gets the icon association for any folder or file in the system - - Arguments: - PATH {str} -- path to file or folder - - Raises: - TypeError -- Thrown if invalid arguments are given - - Returns: - PIL.Image -- PIL Image of the icon at the correct size - """ - - #https://stackoverflow.com/questions/21070423/python-sAaving-accessing-file-extension-icons-and-using-them-in-a-tkinter-progra/52957794#52957794 - #https://aecomputervision.blogspot.com/2018/10/getting-icon-association-for-any-file.html - return winIcon.get_icon(PATH, winIcon.SMALL) - -class DriveBook(ttk.Notebook): - - _foldericons = {} - _fileicons = {} - _tabs = {} - - def __init__(self, parent): - ttk.Notebook.__init__(self) - self._parent = parent - - self._draw_tabs() - - def _draw_tabs(self): - self._drive_icons = self._get_icons() #store to attribute so it doesn't get removed by garbage deletion - for drive in self._drive_icons: - self._tabs[drive[0]] = FileTree(self, drive[0]) - if os.path.split(drive[0])[1] != "": - self.add(self._tabs[drive[0]], text = "Home", image = drive[1], compound = tk.LEFT) - else: - self.add(self._tabs[drive[0]], text = drive[0], image = drive[1], compound = tk.LEFT) - - def _refresh(self): - """Checks if a drive as been added or removed. If it has, tabs are refreshed. - """ - - #check if we need to refresh before refreshing - if list(map(itemgetter(0), self._drive_icons)) != self._get_drives(): - display = [i[0] for i in self._drive_icons] - new = self._get_drives() - - removals = list(set(display).difference(new)) - additions = list(set(new).difference(display)) - - print("removed: ", removals, "added: ", additions) - - for removal in removals: - #delete open nodes that have just been removed - self._parent._open = [i for i in self._parent._open if i[0] != removal] - - #deelte the tab - try: - self.forget(self._tabs[removal]) - except tk.TclError: - #TODO: work out why this throws an error when it still works - pass - - #remove from tab dictionary - del self._tabs[removal] - - #reset drive icons - self._drive_icons = [i for i in self._drive_icons if i[0] != removal] - - for addition in additions: - self._drive_icons.append([addition, ImageTk.PhotoImage(self._parent._get_icon(addition))]) - self._tabs[addition] = FileTree(self, addition) - self.add(self._tabs[addition], text = addition, image = self._drive_icons[-1][1], compound = tk.LEFT) - - def _get_drives(self): - return [os.path.expanduser("~")] + win32api.GetLogicalDriveStrings().split('\x00')[:-1] - - def _get_icons(self): - drives = self._get_drives() - return [[drive, ImageTk.PhotoImage(self._parent._get_icon(drive))] for drive in drives] - - def _get_tab_name(self): - """Returns the name of the tab which is currently open - - Returns: - int -- the name of the tab that's currently open e.g. C:\\, C:\\Users\\Fred - """ - - return self._get_drives()[self.index(self.select())] - -class FileTree(tk.Frame): - def __init__(self, parent, drive): - tk.Frame.__init__(self, parent) - self._parent = parent - self._command = self._parent._parent._command - self._types = self._parent._parent._types - self._showhidden = self._parent._parent._showhidden - - self._tree = ttk.Treeview(self, height = 20, columns = ('path', 'filetype', 'size'), displaycolumns = 'size') - self._tree.heading('#0', text = 'Directory', anchor = tk.W) - self._tree.heading('size', text = 'Size', anchor = tk.W) - self._tree.column('path', width = 180) - self._tree.column('size', stretch = 1, width = 48) - self._tree.grid(row = 0, column = 0, sticky = 'nsew') - - sbr_y = ttk.Scrollbar(self, orient = tk.VERTICAL, command = self._tree.yview) - sbr_x = ttk.Scrollbar(self, orient = tk.HORIZONTAL, command = self._tree.xview) - self._tree['yscroll'] = sbr_y.set - self._tree['xscroll'] = sbr_x.set - sbr_y.grid(row = 0, column = 1, sticky = 'ns') - sbr_x.grid(row = 1, column = 0, sticky = 'ew') - - self.rowconfigure(0, weight = 1) - self.columnconfigure(0, weight = 1) - - self._populate_path(drive) - self._tree.bind('<>', self._on_click) - self._tree.bind('<>', self._on_close) - if self._parent._parent._rightclick_options != []: - self._tree.bind('', self._draw_menu) - - def _draw_menu(self, event): - menu = RightClickMenu(self, self._parent._parent._rightclick_options, self._tree.identify_row(event.y)) - try: - menu.tk_popup(event.x_root, event.y_root) - finally: - menu.grab_release() - - def _on_click(self, event): - id = os.path.normpath(self._tree.focus()) - - #add to the list of open nodes - if os.path.isdir(id): - #add both the tab name and the path, since the same path could be open in multiple places - self._parent._parent._open.append([self._parent._get_tab_name(), id]) - #print(self._parent._parent._open) - - if os.path.isfile(id): - self._command(id) - else: - try: - self._populate_path(id) - except tk.TclError: - #this node has already been opened, so delete children and load again - for child in self._tree.get_children(id): - self._tree.delete(child) - self._populate_path(id) - - #print("\n clicked: ", self._parent._parent._open) - - def _on_close(self, event): - """Method called when the user closes a node. This must delete the node, - and it's children, from the list of open nodes. To do this, a recursive - algoritm is used. We have to use this, as opposed to something like os.walk() - for preformance. A large folder could have thousands of children. This doesn't - 'go deeper' unless the node in the tree is open. - - Arguments: - event {tkinter event} -- tkinter event is used to get the id, - """ - - id = os.path.normpath(self._tree.focus()) - drive = self._parent._get_tab_name() - - def recurse(path): - glob_pattern = os.path.join(path, '*') - for dir in sorted(glob(glob_pattern), key=os.path.getctime): - if os.path.isdir(dir) and self._tree.item(dir, "open"): - self._parent._parent._open.remove([drive, dir]) - recurse(dir) - - try: - self._parent._parent._open.remove([drive, id]) - except ValueError: - #already removed by parent - pass - - recurse(id) - - def _get_size(self, path): - """Returns the size of a file. From: - https://pyinmyeye.blogspot.co.uk/2012/07/tkinter-multi-column-list-demo.html - - Arguments: - path {str} -- Path to file - - Returns: - str -- file size in bytes/KB/MB/GB - """ - - size = os.path.getsize(path) - KB = 1024.0 - MB = KB * KB - GB = MB * KB - if size >= GB: - return ('{:,.1f} GB').format(size / GB) - elif size >= MB: - return ('{:,.1f} MB').format(size / MB) - elif size >= KB: - return ('{:,.1f} KB').format(size / KB) - else: - return ('{} bytes').format(size) - - def _populate_path(self, path): - #if the tree is blank, write to the root - if self._tree.get_children() == (): - node = "" - else: - node = path - - folders, files = self._get_dirs_in_path(path) - - for folder in folders: - fullpath = os.path.join(path, folder) - self._tree.insert( - parent = node, - index = tk.END, - iid = fullpath, - tags = (fullpath, ), - text = folder, - image = self._parent._foldericons[fullpath], - values = [folder, "", ""]) - #setup a dummy so the '+' appears before it's loaded. Child stuff will be loaded when the user - #clicks on the plus, and the dummy will be removed. (or not if there are no files) - self._tree.insert(parent = fullpath, index = tk.END, tag = "dummy", text = "No avaliable files") - - for file in files: - fullpath = os.path.join(path, file) - name, type_ = os.path.splitext(file) - #if the type is a shortcut, get the whole path as a key - if type_ == ".lnk" or type_ == ".exe": - icon = self._parent._fileicons[fullpath] - else: - icon = self._parent._fileicons[type_] - self._tree.insert( - parent = node, - index = tk.END, - iid = fullpath, - tag = fullpath, - text = file, - image = icon, - values = [file, "", self._get_size(fullpath)]) - - #if there is more than one child, delete every child that isn't a path - #and hence delete the dummy - children = self._tree.get_children(node) - if len(children) > 1: - for child in children: - if not os.path.exists(child): - self._tree.delete(child) - - def _get_dirs_in_path(self, path): - """Returns two lists, the first is a list of all folders in a directory, - the second is a list of files. Also loads the icons for these files and folders. - For folders, it puts a tk.PhotoImage as the value of the _foldericons dictionary - with the key being the full folder path. For files, it uses the _fileicons dictionary, - with they key being the filetype, e.g. "mp4". If the file is a shortcut (".lnk") or - executable (".exe"), set the whole path as the key, since the icon is different for - every file of that type. - - Arguments: - path {str} -- full path to the folder - - Returns: - tuple -- two lists of folders and files. - """ - - def add(p, type_): - files.append(p) - - #if the file is a shortcut, set the key as the whole path - if type_ == ".lnk" or type_ == ".exe": - if os.path.join(path, p) not in self._parent._fileicons: - self._parent._fileicons[os.path.join(path, p)] = ImageTk.PhotoImage( - self._parent._parent._get_icon(os.path.join(path, p))) - else: - if type_ not in self._parent._fileicons: - self._parent._fileicons[type_] = ImageTk.PhotoImage( - self._parent._parent._get_icon(os.path.join(path, p))) - - #print(self._parent._fileicons, "\n") - - files = [] - folders = [] - try: - for p in os.listdir(path): - if p.startswith(".") and not self._showhidden: - continue - - if os.path.isfile(os.path.join(path, p)): - name, type_ = os.path.splitext(p) - if self._types == []: - add(p, type_) - else: - if type_ in self._types: - add(p, type_) - - else: - folders.append(p) - if os.path.join(path, p) not in self._parent._foldericons: - self._parent._foldericons[os.path.join(path, p)] = ImageTk.PhotoImage( - self._parent._parent._get_icon(os.path.join(path, p))) - except OSError: - pass - - - #print("\n\nfolders: ", folders, "\nfiles: ", files) - return folders, files - -#for efficiency, it would be better to instatiate this only once, since now it's instatiated -#every time the user right clicks -class RightClickMenu(tk.Menu): - def __init__(self, parent, options, item): - tk.Menu.__init__(self, parent, tearoff = False) - self.parent = parent - - print(item) - #only show menu options if the dev selected them - #if the dev selected no options, binding is never set - #it must be a file for the open option to appear - if MENU_OPEN in options and os.path.isfile(item): - self.add_command(label = "Open with default program", command = lambda: print("Open")) - if MENU_DELETE in options: - self.add_command(label = "Delete", image = self.parent._parent._parent._img_cross, compound = tk.LEFT, command = lambda: os.remove(item)) - if MENU_CLIPBOARD in options: - self.add_command(label = "Copy path to clipboard", image = self.parent._parent._parent._img_clipboard, compound = tk.LEFT, command = lambda: print("clipboard")) - -def on_click(path): - print("click: ", path) - -if __name__ == "__main__": - root = tk.Tk() - browser = TkFileBrowser(root, command = on_click) - browser.pack(side = tk.LEFT) - - ttk.Button(root, text = "Goto", command = lambda: browser.see(r"F:\Python Projects\tkFileBrowser")).pack(side = tk.LEFT) - - root.mainloop() \ No newline at end of file diff --git a/winIcon.py b/winIcon.py deleted file mode 100644 index 2a40a05..0000000 --- a/winIcon.py +++ /dev/null @@ -1,52 +0,0 @@ -from win32com.shell import shell, shellcon -from PIL import Image, ImageTk -import win32api -import win32con -import win32ui -import win32gui -import os - -LARGE = "large" -SMALL = "small" - -def get_icon(PATH, size): - #print("\nPath: ", PATH, "Type: ", type(PATH), "Count: ", count, "\n") - - SHGFI_ICON = 0x000000100 - SHGFI_ICONLOCATION = 0x000001000 - if size == "small": - SHIL_SIZE = 0x00001 - elif size == "large": - SHIL_SIZE = 0x00002 - else: - raise TypeError("Invalid argument for 'size'. Must be equal to 'small' or 'large'") - - try: - ret, info = shell.SHGetFileInfo(PATH, 0, SHGFI_ICONLOCATION | SHGFI_ICON | SHIL_SIZE) - hIcon, iIcon, dwAttr, name, typeName = info - ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON) - hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0)) - hbmp = win32ui.CreateBitmap() - hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_x) - hdc = hdc.CreateCompatibleDC() - hdc.SelectObject(hbmp) - hdc.DrawIcon((0, 0), hIcon) - win32gui.DestroyIcon(hIcon) - - bmpinfo = hbmp.GetInfo() - bmpstr = hbmp.GetBitmapBits(True) - img = Image.frombuffer( - "RGBA", - (bmpinfo["bmWidth"], bmpinfo["bmHeight"]), - bmpstr, "raw", "BGRA", 0, 1 - ) - except win32ui.error: - raise WinIconError("Couldn't get icon for '%s'" % PATH) - return - - if size == "small": - img = img.resize((16, 16), Image.ANTIALIAS) - return img - -class WinIconError(Exception): - pass \ No newline at end of file diff --git a/windowsIcons.py b/windowsIcons.py new file mode 100644 index 0000000..6c614c5 --- /dev/null +++ b/windowsIcons.py @@ -0,0 +1,52 @@ +from win32com.shell import shell, shellcon +from PIL import Image, ImageTk +import win32api +import win32con +import win32ui +import win32gui +import os + +LARGE = "large" +SMALL = "small" + +def get_icon(PATH, size): + #print("\nPath: ", PATH, "Type: ", type(PATH), "Count: ", count, "\n") + + SHGFI_ICON = 0x000000100 + SHGFI_ICONLOCATION = 0x000001000 + if size == "small": + SHIL_SIZE = 0x00001 + elif size == "large": + SHIL_SIZE = 0x00002 + else: + raise TypeError("Invalid argument for 'size'. Must be equal to 'small' or 'large'") + + try: + ret, info = shell.SHGetFileInfo(PATH, 0, SHGFI_ICONLOCATION | SHGFI_ICON | SHIL_SIZE) + hIcon, iIcon, dwAttr, name, typeName = info + ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON) + hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0)) + hbmp = win32ui.CreateBitmap() + hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_x) + hdc = hdc.CreateCompatibleDC() + hdc.SelectObject(hbmp) + hdc.DrawIcon((0, 0), hIcon) + win32gui.DestroyIcon(hIcon) + + bmpinfo = hbmp.GetInfo() + bmpstr = hbmp.GetBitmapBits(True) + img = Image.frombuffer( + "RGBA", + (bmpinfo["bmWidth"], bmpinfo["bmHeight"]), + bmpstr, "raw", "BGRA", 0, 1 + ) + except win32ui.error: + raise WinIconError("Couldn't get icon for '%s'" % PATH) + return + + if size == "small": + img = img.resize((16, 16)) + return img + +class WinIconError(Exception): + pass \ No newline at end of file -- cgit v1.2.3