├── images ├── main_image.PNG ├── search_image.PNG ├── main_image_light_theme.PNG └── right_click_menu_image.png ├── src ├── view │ ├── __pycache__ │ │ ├── view.cpython-39.pyc │ │ ├── menubar.cpython-39.pyc │ │ ├── todo_list.cpython-39.pyc │ │ ├── link_window.cpython-39.pyc │ │ ├── about_window.cpython-39.pyc │ │ ├── diary_window.cpython-39.pyc │ │ ├── filter_windows.cpython-39.pyc │ │ ├── rename_window.cpython-39.pyc │ │ ├── search_window.cpython-39.pyc │ │ ├── settings_window.cpython-39.pyc │ │ ├── tkexplorer_icons.cpython-39.pyc │ │ ├── new_folders_window.cpython-39.pyc │ │ ├── pdf_tools_windows.cpython-39.pyc │ │ └── treeview_functions.cpython-39.pyc │ ├── about_window.py │ ├── icon_window.py │ ├── new_folders_window.py │ ├── menubar.py │ ├── link_window.py │ ├── rename_window.py │ ├── treeview_functions.py │ ├── filter_windows.py │ ├── todo_list.py │ ├── pdf_tools_windows.py │ ├── diary_window.py │ ├── view.py │ ├── search_window.py │ └── settings_window.py ├── model │ ├── __pycache__ │ │ ├── diary_model.cpython-39.pyc │ │ └── quick_access_tree_model.cpython-39.pyc │ ├── diary_model.py │ ├── config_file_manager.py │ ├── quick_access_tree_model.py │ ├── branch_tab_model.py │ └── model.py ├── custom_widgets │ ├── __pycache__ │ │ ├── root_tab.cpython-39.pyc │ │ ├── autoscrollbar.cpython-39.pyc │ │ └── quick_access_tree.cpython-39.pyc │ ├── autoscrollbar.py │ ├── root_tab.py │ ├── address_bar.py │ ├── quick_access_tree.py │ └── branch_tab.py ├── main.py └── controller │ └── controller.py ├── pyproject.toml ├── README.md └── uv.lock /images/main_image.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/images/main_image.PNG -------------------------------------------------------------------------------- /images/search_image.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/images/search_image.PNG -------------------------------------------------------------------------------- /images/main_image_light_theme.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/images/main_image_light_theme.PNG -------------------------------------------------------------------------------- /images/right_click_menu_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/images/right_click_menu_image.png -------------------------------------------------------------------------------- /src/view/__pycache__/view.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/view.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/menubar.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/menubar.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/todo_list.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/todo_list.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/link_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/link_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/model/__pycache__/diary_model.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/model/__pycache__/diary_model.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/about_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/about_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/diary_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/diary_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/filter_windows.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/filter_windows.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/rename_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/rename_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/search_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/search_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/settings_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/settings_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/tkexplorer_icons.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/tkexplorer_icons.cpython-39.pyc -------------------------------------------------------------------------------- /src/custom_widgets/__pycache__/root_tab.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/custom_widgets/__pycache__/root_tab.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/new_folders_window.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/new_folders_window.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/pdf_tools_windows.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/pdf_tools_windows.cpython-39.pyc -------------------------------------------------------------------------------- /src/view/__pycache__/treeview_functions.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/view/__pycache__/treeview_functions.cpython-39.pyc -------------------------------------------------------------------------------- /src/custom_widgets/__pycache__/autoscrollbar.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/custom_widgets/__pycache__/autoscrollbar.cpython-39.pyc -------------------------------------------------------------------------------- /src/model/__pycache__/quick_access_tree_model.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/model/__pycache__/quick_access_tree_model.cpython-39.pyc -------------------------------------------------------------------------------- /src/custom_widgets/__pycache__/quick_access_tree.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domhnallmorr/Tk-Path-Finder/HEAD/src/custom_widgets/__pycache__/quick_access_tree.cpython-39.pyc -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tk-path-finder" 3 | version = "1.0.0" 4 | description = "Windows File Explorer/Manager with Tabs. Tkinter + ttkbootstrap." 5 | readme = "README.md" 6 | requires-python = "==3.13.9" 7 | dependencies = [ 8 | "natsort", 9 | "openpyxl", 10 | "pyperclip", 11 | "python-docx", 12 | "ttkbootstrap", 13 | "PyPDF2>=3.0", 14 | "pillow>=12.0.0", 15 | "pywin32>=311", 16 | "pandas>=2.3.2", 17 | ] 18 | 19 | [tool.uv] 20 | # keeps the venv in .venv/ at repo root 21 | 22 | -------------------------------------------------------------------------------- /src/view/about_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | 6 | def about(mainapp): 7 | 8 | win = tk.Toplevel() 9 | win.wm_title("About Tk Path Finder") 10 | 11 | l = tk.Label(win, width = 20, text=f"Tk Path Finder Version {mainapp.version}") 12 | l.grid(row=0, column=2, columnspan=1, ipadx=3) 13 | 14 | l2 = tk.Label(win, text=f"Icons From https://icons8.com") 15 | l2.grid(row=1, column=2, columnspan=1) 16 | 17 | l3 = tk.Label(win, text="Tk Path Finder makes no promise of warranty, satisfaction, performance, or anything else. Understand that your use of this tool is completely at your own risk.", wraplength=300, justify="center") 18 | l3.grid(row=2, column=0, columnspan=5, padx=20, sticky="NSEW") 19 | 20 | b = ttk.Button(win, text="OK", command=win.destroy, style="success.TButton") 21 | b.grid(row=3, column=2, pady=10) -------------------------------------------------------------------------------- /src/custom_widgets/autoscrollbar.py: -------------------------------------------------------------------------------- 1 | # Python program to illustrate the usage of 2 | # autohiding scrollbars using tkinter 3 | 4 | # Importing tkinter 5 | from tkinter import * 6 | from tkinter import ttk 7 | 8 | # Creating class AutoScrollbar 9 | class AutoScrollbar(ttk.Scrollbar): 10 | 11 | # Defining set method with all 12 | # its parameter 13 | def set(self, low, high): 14 | 15 | if float(low) <= 0.0 and float(high) >= 1.0: 16 | 17 | # Using grid_remove 18 | self.tk.call("grid", "remove", self) 19 | else: 20 | self.grid() 21 | Scrollbar.set(self, low, high) 22 | 23 | # Defining pack method 24 | def pack(self, **kw): 25 | 26 | # If pack is used it throws an error 27 | raise (TclError,"pack cannot be used with \ 28 | this widget") 29 | 30 | # Defining place method 31 | def place(self, **kw): 32 | 33 | # If place is used it throws an error 34 | raise (TclError, "place cannot be used with \ 35 | this widget") -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ttkbootstrap import Style 3 | from ttkbootstrap.themes import standard 4 | import natsort 5 | import openpyxl 6 | import pyperclip 7 | from docx import Document 8 | from PyPDF2 import PdfReader 9 | 10 | except Exception as e: 11 | import os 12 | print(f"Some requirments not found: {e}") 13 | input("Press Enter to fix issue: ") 14 | os.system("pip install natsort openpyxl pyperclip python-docx ttkbootstrap PyPDF2") 15 | 16 | import datetime 17 | import tkinter as tk 18 | from tkinter import * 19 | from tkinter import ttk 20 | from tkinter.ttk import * 21 | from tkinter import messagebox 22 | 23 | from controller import controller 24 | from model import config_file_manager 25 | 26 | from ttkbootstrap import Style 27 | from ttkbootstrap.themes import standard 28 | 29 | class MainApplication(ttk.Frame): 30 | def __init__(self, parent, *args, **kwargs): 31 | ttk.Frame.__init__(self, parent, *args, **kwargs) 32 | self.root = root 33 | self.parent = parent 34 | 35 | self.controller = controller.Controller(root, parent, self) 36 | 37 | # ----------------- VERSION ----------------------- 38 | self.version = "1.0.0" 39 | 40 | # ----------------- WEEK NUMBER ----------------------- 41 | year, week_num, day_of_week = datetime.date.today().isocalendar() 42 | 43 | self.parent.title(f"Tk Path Finder V{self.version} - Week {week_num}") 44 | 45 | def on_closing(self): 46 | # Ensure Config File is Saved on Closing Main Window 47 | config_file_manager.write_config_file(self.controller.model) 48 | root.destroy() 49 | 50 | if __name__ == "__main__": 51 | root = tk.Tk() 52 | root.resizable(width=tk.TRUE, height=tk.TRUE) 53 | MA = MainApplication(root) 54 | 55 | root.state("zoomed") # mamimise window 56 | root.protocol("WM_DELETE_WINDOW", MA.on_closing) 57 | root.mainloop() -------------------------------------------------------------------------------- /src/view/icon_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | from tkinter import messagebox 6 | 7 | def launch_icon_window(controller): 8 | master = controller.mainapp.root 9 | controller.w = IconWindow(master, controller.view) 10 | master.wait_window(controller.w.top) 11 | 12 | class IconWindow(ttk.Frame): 13 | def __init__(self, master, view): 14 | super(IconWindow, self).__init__() 15 | top=self.top=Toplevel(master) 16 | top.grab_set() 17 | self.view = view 18 | 19 | self.top.title("Change Icon") 20 | 21 | self.button = "cancel" 22 | self.add_icons() 23 | 24 | self.cancel_btn = ttk.Button(self.top, text="Cancel", width=10, style="danger.TButton", command=lambda button="cancel": self.cleanup(button)) 25 | self.cancel_btn.grid(row=2, column=7, padx=self.view.default_padx, pady=self.view.default_pady, sticky="ne") 26 | 27 | def add_icons(self): 28 | 29 | icon_frame = LabelFrame(self.top, text="Select Icon") 30 | icon_frame.grid(row=1, column=0, columnspan=8, padx=self.view.default_padx, pady=self.view.default_pady, sticky="ew") 31 | 32 | col = 0 33 | row = 0 34 | 35 | icons_added = [] 36 | for extension in self.view.known_file_types.keys(): 37 | icon = self.view.known_file_types[extension] 38 | 39 | if icon not in icons_added: # avoid duplicates 40 | icons_added.append(icon) 41 | 42 | if self.view.style_type == "dark": 43 | style = "dark.TButton" 44 | else: 45 | style = "light.TButton" 46 | icon_button = ttk.Button(icon_frame, image=icon, style=style, command=lambda button="ok", extension=extension: self.cleanup(button, extension)) 47 | icon_button.grid(row=row, column=col, padx=self.view.default_padx, pady=self.view.default_pady, sticky="nsew") 48 | 49 | col += 1 50 | 51 | if col == 8: 52 | col = 0 53 | row += 1 54 | 55 | def cleanup(self, button, extension=None): 56 | self.button = button 57 | self.extension = extension 58 | 59 | self.top.destroy() -------------------------------------------------------------------------------- /src/model/diary_model.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | import os 4 | import sqlite3 5 | 6 | def create_database(): 7 | conn = sqlite3.connect('notes.db') 8 | cursor = conn.cursor() 9 | cursor.execute("CREATE TABLE IF NOT EXISTS diary(datestamp TEXT, tags TEXT, text TEXT)") 10 | # cursor.execute("CREATE TABLE IF NOT EXISTS notes(category TEXT, subcategory TEXT, tags TEXT, page TEXT, text TEXT)") 11 | 12 | conn.commit() 13 | cursor.close() 14 | conn.close() 15 | 16 | 17 | def read_database(): 18 | conn = sqlite3.connect('notes.db') 19 | cursor = conn.cursor() 20 | 21 | cursor.execute('SELECT * FROM diary') 22 | data = cursor.fetchall() 23 | 24 | class DiaryModel: 25 | def __init__(self): 26 | if not os.path.isfile("notes.db"): 27 | create_database() 28 | 29 | self.conn = sqlite3.connect('notes.db') 30 | self.cursor = self.conn.cursor() 31 | 32 | def write_diary_text_to_database(self, date, data): 33 | txt = json.dumps(data) 34 | 35 | data = self.cursor.execute(f"SELECT * FROM diary WHERE datestamp = '{date}'").fetchall() 36 | 37 | if len(data) == 0: 38 | self.cursor.execute("INSERT INTO diary VALUES(?, '', ?)", (date, txt)) 39 | else: 40 | self.cursor.execute(f"UPDATE diary SET text = ? WHERE datestamp = ?", (txt, date)) 41 | 42 | self.conn.commit() 43 | 44 | def check_date_in_database(self, date): 45 | self.cursor.execute(f"SELECT * FROM diary WHERE datestamp = '{date}'") 46 | data = self.cursor.fetchall() 47 | if len(data) == 0: 48 | self.cursor.execute(f"INSERT INTO diary VALUES('{date}', '', 'General', 'Some Text')") 49 | self.conn.commit() 50 | 51 | 52 | def read_date_from_database(self, date): 53 | data = self.cursor.execute(f"SELECT * FROM diary WHERE datestamp = '{date}'").fetchall() 54 | if len(data) > 0: 55 | txt = ast.literal_eval(data[0][-1]) 56 | else: 57 | txt = {"General": ""} 58 | 59 | return txt 60 | 61 | if __name__ == "__main__": 62 | 63 | if not os.path.isfile("notes.db"): 64 | create_database() 65 | else: 66 | read_database() -------------------------------------------------------------------------------- /src/view/new_folders_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | 6 | 7 | 8 | class NewFoldersWindow(ttk.Frame): 9 | def __init__(self, master, view, initialvalue): 10 | super(NewFoldersWindow, self).__init__() 11 | top=self.top=Toplevel(master) 12 | top.grab_set() 13 | 14 | self.top.title(f"New Folder(s)") 15 | self.button = "cancel" 16 | 17 | self.folder_text = tk.Text(self.top, width=110, height=10) 18 | self.folder_text.grid(row=1, column=0, columnspan = 8, sticky="NSEW",padx=5, pady=5, ipadx=2, ipady=5) 19 | if initialvalue is not None: 20 | self.folder_text.insert("1.0", initialvalue) 21 | 22 | self.top.grid_rowconfigure(1, weight=1) 23 | self.top.grid_columnconfigure(0, weight=1) 24 | 25 | # Buttons 26 | self.ok_btn = ttk.Button(self.top, text="OK", width=10, style="success.TButton", command=lambda button="ok": self.cleanup(button)) 27 | self.ok_btn.grid(row=2, column=6, padx=5, pady=5, sticky="ne") 28 | self.cancel_btn = ttk.Button(self.top, text="Cancel", width=10, style="danger.TButton", command=lambda button="cancel": self.cleanup(button)) 29 | self.cancel_btn.grid(row=2, column=7, padx=5, pady=5, sticky="nw") 30 | 31 | self.folder_text.bind("", self.duplicate_line) 32 | 33 | def duplicate_line(self, event): 34 | current_line = int(self.folder_text.index(INSERT).split(".")[0])-1 #convert to 0 index system 35 | all_lines = self.folder_text.get("1.0",END).split("\n") 36 | 37 | all_lines.insert(current_line+1, all_lines[current_line]) 38 | #all_lines = "\n".join(all_lines) 39 | lines_to_insert = [] 40 | 41 | for l in all_lines: 42 | if not l.replace("\n", "").strip() == "": #remove any blank lines 43 | lines_to_insert.append(l) 44 | 45 | self.folder_text.delete("1.0", END) 46 | self.folder_text.insert("end", "\n".join(lines_to_insert)) 47 | #self.folder_text.mark_set("insert", "%d.%d" % (current_line+1, 1)) 48 | 49 | def cleanup(self, button): 50 | if button == "ok": 51 | self.folders = list(filter(None, [n.strip() for n in self.folder_text.get("1.0","end").split('\n')])) #avoid empty lines 52 | self.button = "ok" 53 | self.top.destroy() 54 | 55 | else: 56 | self.top.destroy() -------------------------------------------------------------------------------- /src/view/menubar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | 6 | from view import about_window 7 | 8 | def setup_menubar(view): 9 | 10 | menu = tk.Menu(view.controller.mainapp.master) 11 | view.controller.mainapp.master.config(menu=menu) 12 | 13 | # ________ FILE ________ 14 | file_menu = tk.Menu(menu, tearoff=0) 15 | menu.add_cascade(label="File", menu=file_menu) 16 | file_menu.add_command(label="Load Last Session", command=view.controller.load_last_session) 17 | 18 | # ________ SETTINGS ________ 19 | settings_menu = tk.Menu(menu, tearoff=0) 20 | menu.add_cascade(label="Settings", menu=settings_menu) 21 | settings_menu.add_command(label="Edit Settings", command=view.controller.edit_settings) 22 | 23 | style_menu = tk.Menu(menu, tearoff=0) 24 | settings_menu.add_cascade(label="Style", menu=style_menu) 25 | 26 | light_style_menu = tk.Menu(menu, tearoff=0) 27 | dark_style_menu = tk.Menu(menu, tearoff=0) 28 | style_menu.add_cascade(label="Light", menu=light_style_menu) 29 | style_menu.add_cascade(label="Dark", menu=dark_style_menu) 30 | 31 | for s in view.themes["light"]: 32 | light_style_menu.add_command(label=s, command=lambda style=s, style_type="light": view.controller.update_style(style, style_type)) 33 | 34 | for s in view.themes["dark"]: 35 | dark_style_menu.add_command(label=s, command=lambda style=s, style_type="dark": view.controller.update_style(style, style_type)) 36 | 37 | # ________ TOOLS ________ 38 | tools_menu = tk.Menu(menu, tearoff=0) 39 | menu.add_cascade(label="Tools", menu=tools_menu) 40 | 41 | notes_menu = tk.Menu(menu, tearoff=0) 42 | tools_menu.add_command(label="Diary", command=view.controller.launch_diary) 43 | 44 | pdf_menu = tk.Menu(menu, tearoff=0) 45 | tools_menu.add_cascade(label="PDF Tools", menu=pdf_menu) 46 | pdf_menu.add_command(label="Extract Pages", command=view.controller.launch_pdf_extractor) 47 | pdf_menu.add_command(label="Merge PDFs", command=view.controller.launch_pdf_merger) 48 | 49 | tools_menu.add_command(label="To Do List", command=view.controller.launch_to_do_list) 50 | 51 | # ________ ABOUT ________ 52 | about_menu = tk.Menu(menu, tearoff=0) 53 | menu.add_cascade(label="About" ,menu=about_menu) 54 | about_menu.add_command(label="About Tk Path Finder", command=lambda mainapp=view.controller.mainapp: about_window.about(mainapp)) -------------------------------------------------------------------------------- /src/view/link_window.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import * 4 | from tkinter import ttk 5 | from tkinter.ttk import * 6 | from tkinter import simpledialog 7 | from tkinter import messagebox 8 | 9 | 10 | class AddLinkWindow(ttk.Frame): 11 | def __init__(self, master, mode, text="", path="",): 12 | super(AddLinkWindow, self).__init__() 13 | top=self.top=Toplevel(master) 14 | top.grab_set() 15 | 16 | self.mode = mode 17 | if mode == "edit": 18 | self.top.title(f"Edit Link") 19 | else: 20 | self.top.title(f"Add New Link") 21 | self.button = "cancel" 22 | 23 | ttk.Label(self.top, text="Name:").grid(row=0, column=0, padx=5, pady=5, sticky="nsew") 24 | ttk.Label(self.top, text="Path:").grid(row=1, column=0, padx=5, pady=5, sticky="nsew") 25 | 26 | self.name_entry = ttk.Entry(self.top, width=100) 27 | self.name_entry.grid(row=0, column=1, columnspan=4, padx=5, pady=5, sticky="nsew") 28 | 29 | self.path_entry = ttk.Entry(self.top, width=100) 30 | self.path_entry.grid(row=1, column=1, columnspan=4, padx=5, pady=5, sticky="nsew") 31 | 32 | self.ok_btn = ttk.Button(self.top, text="OK", width=10, style="success.TButton", command=lambda button="ok": self.cleanup(button)) 33 | self.ok_btn.grid(row=2, column=3, padx=5, pady=5, sticky="ne") 34 | self.cancel_btn = ttk.Button(self.top, text="Cancel", width=10, style="danger.TButton", command=lambda button="cancel": self.cleanup(button)) 35 | self.cancel_btn.grid(row=2, column=4, padx=5, pady=5, sticky="nw") 36 | 37 | self.top.grid_columnconfigure(1, weight=1) 38 | 39 | if mode == "edit": 40 | self.name_entry.insert(0, text) 41 | self.path_entry.insert(0, path) 42 | self.original_name = text 43 | 44 | def cleanup(self, button): 45 | if button == "ok": 46 | self.name = self.name_entry.get().strip() 47 | self.path = self.path_entry.get() 48 | data_ok = True 49 | 50 | if self.name == "": 51 | messagebox.showerror("Error", message="Enter A Name") 52 | data_ok = False 53 | 54 | elif not os.path.isdir(self.path): 55 | messagebox.showerror("Error", message="The Path Entered Does Not Exist") 56 | data_ok = False 57 | 58 | if data_ok: 59 | # For mapped drive, handle for inclusion of trailing \ e.g C:\ 60 | if len(self.path) == 2 and self.path[-1] == ":": 61 | self.path = self.path + "\\" 62 | self.button = button 63 | self.top.destroy() 64 | else: 65 | self.top.destroy() -------------------------------------------------------------------------------- /src/model/config_file_manager.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import os 4 | import shutil 5 | 6 | def write_config_file(model, startup=False): 7 | save_dict = {} 8 | 9 | # Get the Quick Access Links 10 | save_dict["quick_access_tree"] = model.config_data["quick_access_tree"] 11 | save_dict["text_editor"] = model.config_data["text_editor"] 12 | save_dict["open_with_apps"] = model.config_data["open_with_apps"] 13 | 14 | # ------- TREEVIEW COLUMN WIDTHS ------- 15 | save_dict["default_file_width"] = model.config_data["default_file_width"] 16 | save_dict["default_date_width"] = model.config_data["default_date_width"] 17 | save_dict["default_type_width"] = model.config_data["default_type_width"] 18 | save_dict["default_size_width"] = model.config_data["default_size_width"] 19 | save_dict["default_style"] = model.config_data["default_style"] 20 | 21 | save_dict["to_do_list"] = model.config_data["to_do_list"] 22 | 23 | if startup is False: 24 | save_dict["session_data"] = model.config_data["session_data"] 25 | else: 26 | save_dict["session_data"] = model.last_session 27 | 28 | 29 | with open("tk_path_finder_config.json", "w") as outfile: 30 | json.dump(save_dict, outfile, indent=4) 31 | 32 | def load_config_file(mainapp): 33 | 34 | backup_config_file(mainapp) 35 | 36 | if not os.path.isfile("tk_path_finder_config.json"): 37 | generate_default_config_file() 38 | 39 | with open("tk_path_finder_config.json") as f: 40 | data = json.load(f) 41 | 42 | return data 43 | 44 | def backup_config_file(mainapp): 45 | if os.path.isfile("tk_path_finder_config_backup4.json"): 46 | shutil.copyfile("tk_path_finder_config_backup4.json", "tk_path_finder_config_backup5.json") 47 | 48 | if os.path.isfile("tk_path_finder_config_backup3.json"): 49 | shutil.copyfile("tk_path_finder_config_backup3.json", "tk_path_finder_config_backup4.json") 50 | 51 | if os.path.isfile("tk_path_finder_config_backup2.json"): 52 | shutil.copyfile("tk_path_finder_config_backup2.json", "tk_path_finder_config_backup3.json") 53 | 54 | if os.path.isfile("tk_path_finder_config_backup1.json"): 55 | shutil.copyfile("tk_path_finder_config_backup1.json", "tk_path_finder_config_backup2.json") 56 | 57 | if os.path.isfile("tk_path_finder_config.json"): 58 | shutil.copyfile("tk_path_finder_config.json", "tk_path_finder_config_backup1.json") 59 | 60 | def generate_default_config_file(): 61 | 62 | save_dict = {} 63 | # save_dict['links'] = {"I001": {}} 64 | # save_dict["node_iids"] = {"I001": 'Default'} 65 | # save_dict["nodes"] = {"Default": 'I001'} 66 | # save_dict["open_with_apps"] = {} 67 | save_dict["text_editor"] = "" 68 | save_dict["to_do_list"] = [] 69 | 70 | with open('tk_path_finder_config.json', 'w') as outfile: 71 | json.dump(save_dict, outfile, indent=4) 72 | -------------------------------------------------------------------------------- /src/custom_widgets/root_tab.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | 6 | from custom_widgets import branch_tab 7 | 8 | class RootTab(ttk.Frame): 9 | def __init__(self, view, root_id, text): 10 | super(RootTab, self).__init__(view.main_notebook) 11 | self.view = view 12 | self.root_id = root_id 13 | 14 | self.setup_notebook() 15 | 16 | def setup_notebook(self): 17 | self.notebook = ttk.Notebook(self) 18 | self.notebook.pack(expand=True, fill=BOTH, side=LEFT) 19 | self.notebook.bind('', self.right_click_branch) 20 | self.notebook.bind("", self.reorder) 21 | 22 | def add_branch_tab(self, tab, text): 23 | self.notebook.add(tab, image=self.view.branch_icon2, compound=tk.LEFT, text=f"{text.ljust(20)}") 24 | 25 | def right_click_branch(self, event): 26 | tab_object = event.widget.nametowidget(event.widget.select()) 27 | branch_id = tab_object.branch_id 28 | 29 | popup_menu = tk.Menu(event.widget, tearoff=0) 30 | popup_menu.add_command(label="Add Branch Tab", command=lambda root_id=self.root_id: self.view.controller.add_branch_tab(root_id), image=self.view.branch_icon2, compound="left") 31 | popup_menu.add_command(label="Rename Branch Tab", command=lambda branch_id=branch_id: self.view.controller.rename_branch_tab(branch_id), image=self.view.edit_icon2, compound="left") 32 | popup_menu.add_command(label="Delete Branch Tab", command=lambda root_id=self.root_id, branch_id=branch_id: self.view.controller.delete_branch_tab(root_id, branch_id), image=self.view.delete_icon2, compound="left") 33 | popup_menu.add_command(label="Duplicate Branch Tab", command=lambda root_id=self.root_id, branch_id=branch_id: self.view.controller.duplicate_branch_tab(root_id, branch_id), image=self.view.duplicate_icon2, compound="left") 34 | 35 | popup_menu.add_separator() 36 | popup_menu.add_command(label="Change Icon", command=lambda root_id=self.root_id, branch_id=branch_id: self.view.controller.change_tab_icon(root_id, branch_id)) 37 | 38 | try: 39 | popup_menu.tk_popup(event.x_root, event.y_root, 0) 40 | finally: 41 | popup_menu.grab_release() 42 | 43 | def reorder(self, event): 44 | try: 45 | index = self.notebook.index(f"@{event.x},{event.y}") 46 | self.notebook.insert(index, child=self.notebook.select()) 47 | self.get_branch_tabs_order() 48 | except tk.TclError: 49 | pass 50 | 51 | def get_branch_tabs_order(self): 52 | branch_tabs_order = [] 53 | for t in self.notebook.tabs(): 54 | branch_tabs_order.append(self.notebook.nametowidget(t).branch_id) 55 | 56 | #reordered_dict = {k: self.branch[k] for k in root_tabs_order} 57 | # self.root_tabs = reordered_dict 58 | self.view.controller.update_branch_tabs_order(self.root_id, branch_tabs_order) 59 | -------------------------------------------------------------------------------- /src/view/rename_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | from tkinter import messagebox 6 | 7 | class RenameWindow(ttk.Frame): 8 | def __init__(self, master, view, initialvalue, component_type, mode="Rename"): 9 | super(RenameWindow, self).__init__() 10 | top=self.top=Toplevel(master) 11 | top.grab_set() 12 | self.view = view 13 | 14 | if component_type == "branch_tab": 15 | self.top.title(f"{mode} Branch Tab") 16 | else: 17 | self.top.title(f"{mode} Tab") 18 | 19 | if mode == "new_file": 20 | self.top.title("New File") 21 | if mode == "edit_file": 22 | self.top.title("Rename File") 23 | 24 | if mode == "new_excel": 25 | self.top.title("New Excel File") 26 | 27 | if mode == "new_word": 28 | self.top.title("New Word File") 29 | 30 | if mode == "new_quick_access": 31 | self.top.title("New Quick Access Folder") 32 | 33 | if mode == "edit_quick_access": 34 | self.top.title("Edit Quick Access Folder") 35 | 36 | self.initialvalue = initialvalue 37 | self.component_type = component_type 38 | self.button = "cancel" 39 | 40 | self.name_entry = ttk.Entry(self.top, width=60) 41 | self.name_entry.grid(row=1, column=0, columnspan=5, padx=self.view.default_padx, pady=self.view.default_pady, sticky="ew") 42 | self.name_entry.insert(0, initialvalue) 43 | self.name_entry.bind('', lambda event, button="ok": self.cleanup(button, event)) 44 | self.top.grid_columnconfigure(0, weight=1) 45 | 46 | self.name_entry.icursor(0) 47 | 48 | if component_type == "branch_tab": 49 | self.lock = IntVar(value=1) 50 | ttk.Checkbutton(self.top, text="Lock Name", variable=self.lock).grid(row=2, column=2, sticky='w', padx=self.view.default_padx, pady=self.view.default_pady) 51 | 52 | # Buttons 53 | self.ok_btn = ttk.Button(self.top, text="OK", width=10, style="success.TButton", command=lambda button="ok": self.cleanup(button)) 54 | self.ok_btn.grid(row=2, column=3, padx=self.view.default_padx, pady=self.view.default_pady, sticky='ne') 55 | self.cancel_btn = ttk.Button(self.top, text='Cancel', width=10, style='danger.TButton', command=lambda button='cancel': self.cleanup(button)) 56 | self.cancel_btn.grid(row=2, column=4, padx=self.view.default_padx, pady=self.view.default_pady, sticky='nw') 57 | 58 | self.name_entry.focus() 59 | 60 | def cleanup(self, button, event=None): 61 | if button == "ok": 62 | if self.name_entry.get() == '': 63 | messagebox.showerror("Error", message="Enter a Name") 64 | else: 65 | self.name = self.name_entry.get() 66 | 67 | if self.component_type == "branch_tab": 68 | if self.lock.get() == 1: 69 | self.text_locked = True 70 | else: 71 | self.text_locked = False 72 | 73 | self.button = "ok" 74 | self.top.destroy() 75 | else: 76 | self.top.destroy() 77 | -------------------------------------------------------------------------------- /src/model/quick_access_tree_model.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | class QuickAccessTreeModel: 4 | def __init__(self, model): 5 | self.model = model 6 | self.folders = {} 7 | 8 | def add_new_folder(self, text, folder_id=None): 9 | if folder_id is None: 10 | folder_id = self.model.gen_id() 11 | self.folders[folder_id] = QuickAccessFolder(folder_id, text) 12 | 13 | return folder_id 14 | 15 | def assemble_config_file_data(self): 16 | return {folder_id: self.folders[folder_id].assemble_config_file_data() for folder_id in self.folders.keys()} 17 | 18 | def delete_folder(self, folder_id): 19 | self.folders.pop(folder_id) 20 | 21 | def rename_folder(self, folder_id, text): 22 | self.folders[folder_id].rename(text) 23 | 24 | def add_new_link(self, folder_id, link_id, text, path): 25 | if link_id is None: 26 | link_id = self.model.gen_id() 27 | 28 | self.folders[folder_id].links[link_id] = {"text": text, "path": path} 29 | 30 | return link_id 31 | 32 | def get_all_link_ids(self): 33 | link_ids = [] 34 | 35 | for folder_id in self.folders: 36 | for link in self.folders[folder_id].links.keys(): 37 | link_ids.append(link) 38 | 39 | return link_ids 40 | 41 | def update_order(self, new_order): 42 | msg = None 43 | 44 | new_folders = {} 45 | for folder_id in new_order: 46 | new_folders[folder_id] = self.folders[folder_id] 47 | msg = self.folders[folder_id].update_links_order(new_order[folder_id]) 48 | 49 | if msg is not None: 50 | break 51 | 52 | if msg is None: 53 | self.folders = copy.deepcopy(new_folders) 54 | 55 | return msg 56 | 57 | def get_link_directory(self, folder_id, link_id): 58 | return self.folders[folder_id].links[link_id]["path"] 59 | 60 | def update_link(self, folder_id, link_id, text, path): 61 | self.folders[folder_id].links[link_id]["text"] = text 62 | self.folders[folder_id].links[link_id]["path"] = path 63 | 64 | def delete_link(self, folder_id, link_id): 65 | self.folders[folder_id].links.pop(link_id) 66 | 67 | class QuickAccessFolder: 68 | def __init__(self, folder_id, text): 69 | self.folder_id = folder_id 70 | self.text = text 71 | 72 | self.links = {} 73 | 74 | def assemble_config_file_data(self): 75 | config_file_data = {"text": self.text, "links": self.links} 76 | 77 | return config_file_data 78 | 79 | def rename(self, text): 80 | self.text = text 81 | 82 | def update_links_order(self, links): 83 | msg = None 84 | try: 85 | new_links = {} 86 | for link_id in links: 87 | new_links[link_id] = copy.deepcopy(self.links[link_id]) 88 | 89 | except Expection as e: 90 | msg = str(e) 91 | 92 | if msg is None: 93 | self.links = copy.deepcopy(new_links) 94 | del new_links 95 | 96 | return msg 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tk-Path-Finder ![python](https://img.shields.io/badge/python-3.6+-blue) 2 | 3 | ## Description 4 | A lightweight file explorer based on tabs within tabs written in python (Tkinter). 5 | 6 | It is intended to be a clean, simple interface that facilitates jumping back and forth between different folders on a given project. The tabs within tabs layout is intended to help with working on different projects. The app is focused primarily on text files and MS Office files. Images are not well handled and probably never will be. Finally, the quick access tree is setup so links can be grouped under a given folder, typically I create a folder for each project I am working on. 7 | 8 | The app is largely complete and does what I set out to achieve. I am continuing to add minor improvements. 9 | 10 | Comments, feedback and questions are welcome. 11 | 12 | Icons taken from https://icons8.com/icons/set/gui 13 | 14 | ## Running the App 15 | See below for instructions on setting the app up to run using UV which will manage the required dependencies. 16 | 17 | Note the app creates 2 files, "tk_path_finder_config.json" and "notes.db" (sqlite database). These will be created in the default working directory. The json file stores all links and tabs created by the user. Several backups are automatically generated for this file. The database contains any notes entered into the diary. 18 | 19 | ## Features 20 | - Quick Access Sidebar, links can be grouped into individual folders. 21 | - Tabs within tabs layout. 22 | - Tabs can be reordered. 23 | - Load Last Session. 24 | - Search functionality. 25 | - Search for text with a given file extension. 26 | - Sort by date/file type 27 | - Filter by file extension. 28 | - Filter files containing string. 29 | - Rename files and folders. 30 | - Cut/Copy Files using the default windows dialog. 31 | - Create multiple folders at once (hit cntrl-d in the create folder window to duplicate line). 32 | - "Open with" functionality. 33 | - Open folder in explorer or command prompt. 34 | - Unzip .zip files 35 | - Compatible with MS Teams folders. 36 | - To Do List. 37 | - Diary. 38 | - PDF Tools 39 | - Extract Pages. 40 | - Merge PDFs. 41 | - Dark and Light themes. 42 | 43 | ## Limitations 44 | - Only tested on Windows 10. 45 | - No delete functionality. This is deliberate and not a feature I intend to add. 46 | - Search is very slow on sub-directories with many files. 47 | - Does not automatically refresh if any changes occur (outside of the app) in a directory. I may add this at some point but it is not a priority for me. 48 | 49 | ## Prerequisites 50 | 51 | This project uses [uv](https://docs.astral.sh/uv/) for dependency management and virtual environments. 52 | 53 | git clone https://github.com/domhnallmorr/Tk-Path-Finder.git 54 | 55 | cd Tk-Path-Finder 56 | 57 | uv sync 58 | 59 | uv run python src/main.py 60 | 61 | ## Preview 62 | ![Main](images/main_image.PNG) 63 | 64 | ![Right_Click](images/right_click_menu_image.png) 65 | 66 | ![alt text](https://imgur.com/haNY5f5.png) 67 | 68 | ![alt text](https://i.imgur.com/oJ79w68.png) 69 | 70 | ![alt text](https://i.imgur.com/Ms0HQ7l.png 71 | ) 72 | ![alt text](https://i.imgur.com/C4p6s9J.png) 73 | 74 | ![Light_Theme](images/main_image_light_theme.PNG) 75 | 76 | ![Search](images/search_image.PNG) 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/custom_widgets/address_bar.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import tkinter as tk 4 | from tkinter import * 5 | from tkinter import ttk 6 | from tkinter.ttk import * 7 | from ttkbootstrap.constants import * 8 | 9 | class AddressBarEntry(ttk.Entry): 10 | def __init__(self, branch_tab): 11 | super(AddressBarEntry, self).__init__(branch_tab) 12 | self.branch_tab = branch_tab 13 | self.bind("", self.enter_event) 14 | self.bind("", self.copy_event) 15 | self.raw_path = False 16 | 17 | self.buttons = [] 18 | self.bind("", self.on_focusin) 19 | self.bind("", self.on_focusout) 20 | self.bind("", self.on_escape) 21 | self.bind("", lambda event: self.truncate_breadcrumbs()) 22 | self.text = None 23 | 24 | @property 25 | def controller(self): 26 | return self.branch_tab.view.controller 27 | 28 | @property 29 | def branch_id(self): 30 | return self.branch_tab.branch_id 31 | 32 | def update_bar(self, text): 33 | self.text = text 34 | self.delete(0, END) #deletes the current value 35 | self.update() 36 | 37 | # Add buttons to address bar 38 | for btn in self.buttons: 39 | btn.grid_forget() 40 | 41 | self.raw_path = False 42 | if text.startswith("\\"): 43 | self.raw_path = True 44 | 45 | if self.raw_path is False: 46 | folders = text.split(os.sep) 47 | else: 48 | folders = text[2:].split(os.sep) 49 | folders[0] 50 | 51 | self.buttons = [] 52 | 53 | for idx, folder in enumerate(folders): 54 | if idx == 0: 55 | path = folder + os.sep 56 | else: 57 | path = os.path.join(path, folder) 58 | 59 | btn = Button(self, text=folder, bootstyle="secondary",) 60 | btn.grid(row=0, column=idx) 61 | btn.bind("", lambda event=None, path=path: self.button_clicked(event, path)) 62 | btn.bindtags((btn, btn.winfo_class(), self, self.winfo_class(), "all")) 63 | self.buttons.append(btn) 64 | 65 | self.truncate_breadcrumbs() 66 | self.branch_tab.focus() 67 | 68 | def enter_event(self, event): 69 | # -------------- CALL THE CONTROLLER METHOD TO UPDATE THE BRANCH TAB BASED ON DIRECTORY IN THE ADDRESS BAR ------------- 70 | self.branch_tab.view.controller.update_branch_tab(self.branch_tab.branch_id, self.get().rstrip().lstrip()) 71 | 72 | def copy_event(self, event): 73 | selected_text = self.selection_get() 74 | 75 | self.controller.copy_to_clipboard(selected_text, self.branch_id, "address_bar") 76 | 77 | 78 | def button_clicked(self, event, path): 79 | if self.raw_path == True: 80 | path = r"\\" + path 81 | 82 | self.branch_tab.view.controller.update_branch_tab(self.branch_tab.branch_id, path) 83 | 84 | def on_focusin(self, event): 85 | for btn in self.buttons: 86 | btn.grid_remove() 87 | self.delete(0, END) 88 | self.insert(0, self.text) 89 | 90 | self.select_range(0, "end") 91 | self.icursor("end") 92 | 93 | def on_focusout(self, event): 94 | self.update_bar(self.text) 95 | 96 | def on_escape(self, event): 97 | self.update_bar(self.text) 98 | self.branch_tab.focus() 99 | 100 | def truncate_breadcrumbs(self, event=None): 101 | available_width = self.winfo_width() 102 | 103 | if available_width != 1: 104 | total_width = 0 105 | widths = [] 106 | 107 | for idx, btn in enumerate(self.buttons): 108 | total_width += btn.winfo_reqwidth() 109 | widths.append(btn.winfo_reqwidth()) 110 | 111 | if total_width > available_width: 112 | for idx, btn in enumerate(self.buttons): # remove butons until total_width is less than available_width 113 | btn.grid_forget() 114 | total_width -= widths[idx] 115 | 116 | if total_width < available_width: 117 | break -------------------------------------------------------------------------------- /src/view/treeview_functions.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import os 3 | import tkinter as tk 4 | from tkinter import * 5 | from tkinter import ttk 6 | from tkinter.ttk import * 7 | import ttkbootstrap as ttk 8 | import string 9 | 10 | 11 | def create_treeview(frame, column_names, column_widths, height): 12 | 13 | letters = string.ascii_uppercase 14 | cols = [] 15 | for i in range(len(column_names)-1): 16 | cols.append(letters[i]) 17 | 18 | tree = ttk.Treeview(frame, selectmode="extended",columns=cols, height=height) 19 | cols.insert(0, "#0") 20 | cols = tuple(cols) 21 | 22 | for idx, col in enumerate(cols): 23 | tree.heading(col, text=column_names[idx], anchor=tk.W) 24 | tree.column(col,minwidth=0,width=column_widths[idx], stretch='NO') 25 | 26 | return tree 27 | 28 | def get_columns_values(treeview, column): 29 | values = [] 30 | 31 | children = treeview.get_children() 32 | 33 | for child in children: 34 | if column == 0: 35 | values.append(treeview.item(child, 'text')) 36 | else: 37 | values.append((treeview.item(child, 'values'))[column-1]) 38 | return values 39 | 40 | 41 | def get_all_treeview_items(treeview): 42 | 43 | treeview_data = [] 44 | 45 | children = treeview.get_children() 46 | 47 | for child in children: 48 | treeview_data.append([treeview.item(child, 'text')]) 49 | values = treeview.item(child, 'values') 50 | for x in values: 51 | treeview_data[-1].append(x) 52 | return treeview_data 53 | 54 | def write_data_to_treeview(view, treeview, mode, data, image=None): 55 | 56 | if mode == 'replace': 57 | treeview.delete(*treeview.get_children()) 58 | 59 | for d in data: 60 | filtered = False 61 | image = view.new_icon2 62 | if d[2] == 'Folder': 63 | image = view.folder_icon2 64 | else: 65 | filename, file_extension = os.path.splitext(d[0]) 66 | # if file_extension in branch_tab.filter: 67 | # filtered = True 68 | if not filtered: 69 | if file_extension.lower() in view.known_file_types.keys(): 70 | image = view.known_file_types[file_extension.lower()] 71 | 72 | if not filtered: 73 | if len(d) > 1: 74 | treeview.insert('', 'end', text=d[0], values=tuple(d[1:]), image=image) 75 | else: 76 | treeview.insert('', 'end', text=d[0]) 77 | 78 | def write_data_to_treeview_general(treeview, mode, data): 79 | if mode == 'replace': 80 | treeview.delete(*treeview.get_children()) 81 | 82 | for d in data: 83 | if len(d) > 1: 84 | treeview.insert('', 'end', text=d[0], values=tuple(d[1:])) 85 | else: 86 | treeview.insert('', 'end', text=d[0]) 87 | 88 | def get_treeview_headers(treeview): 89 | 90 | columns = list(treeview['columns']) 91 | columns.insert(0, '#0') 92 | 93 | return [treeview.heading(x)['text'] for x in columns] 94 | 95 | def treeview_to_df(treeview): 96 | treeview_data = {} 97 | 98 | #Columns 99 | headers = get_treeview_headers(treeview) 100 | 101 | for column, h in enumerate(headers): 102 | treeview_data[h] = get_columns_values(treeview, column) 103 | 104 | df = pd.DataFrame.from_dict(treeview_data) 105 | 106 | return df 107 | 108 | def del_selected_items(treeview, msg=False): 109 | selected_item = treeview.selection() 110 | if selected_item: 111 | 112 | if msg: #if we require a messagebox 113 | MsgBox = tk.messagebox.askquestion ('Delete Selected Item??','This Cannont Be Undone!',icon = 'warning') 114 | if MsgBox == 'yes': 115 | treeview.delete(selected_item) 116 | else: 117 | treeview.delete(selected_item) 118 | 119 | #return MsgBox 120 | 121 | def get_current_selection(treeview): 122 | if len(treeview.selection()) > 0: 123 | item = treeview.selection()[0] 124 | index = treeview.index(item) 125 | 126 | data = list(treeview.item(item,"values")) 127 | data.insert(0, treeview.item(item,"text")) 128 | 129 | else: 130 | index = None 131 | data = [] 132 | return index, data -------------------------------------------------------------------------------- /src/view/filter_windows.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | 6 | 7 | class FilterExtensionWindow(ttk.Frame): 8 | def __init__(self, master, file_types, lock_filter): 9 | super(FilterExtensionWindow, self).__init__() 10 | top=self.top=Toplevel(master) 11 | top.grab_set() 12 | self.file_types = file_types 13 | self.lock_filter = lock_filter 14 | 15 | self.button = "cancel" 16 | self.top.title(f"Filter Files by Extension") 17 | 18 | self.all_files = IntVar(value=1) 19 | ttk.Checkbutton(self.top, text=f"File Types in Current Directory:", variable=self.all_files, command=self.select_all_file).grid(row=0, column=1, sticky='w', padx=5, pady=5) 20 | ttk.Separator(self.top, orient="horizontal").grid(row=1, column=1, sticky='ew', padx=5, pady=5) 21 | 22 | row = 2 23 | for file_extension in sorted(list(self.file_types.keys()), key=str.casefold): #ignore case 24 | self.file_types[file_extension]["var"] = IntVar(value=self.file_types[file_extension]["var"]) 25 | var = self.file_types[file_extension]["var"] 26 | description = self.file_types[file_extension]["description"] 27 | ttk.Checkbutton(self.top, text=f"{description} ({file_extension})", variable=var).grid(row=row, column=1, sticky='w', padx=5, pady=5) 28 | row += 1 29 | 30 | ttk.Separator(self.top, orient="horizontal").grid(row=row, column=1, sticky='ew', padx=5, pady=5) 31 | 32 | # Lock filter 33 | initialvalue = 0 34 | if self.lock_filter is True: 35 | initialvalue = 1 36 | self.lock_filter = IntVar(value=initialvalue) 37 | ttk.Checkbutton(self.top, text=f"Lock in this filter", variable=self.lock_filter).grid(row=row+1, column=1, sticky="w", padx=5, pady=10) 38 | 39 | # Buttons 40 | self.ok_btn = ttk.Button(self.top, text="OK", width=10, style="success.TButton", command=lambda button="ok": self.cleanup(button)) 41 | self.ok_btn.grid(row=row+2, column=2, padx=5, pady=10, sticky="ne") 42 | self.cancel_btn = ttk.Button(self.top, text="Cancel", width=10, style="danger.TButton", command=lambda button="cancel": self.cleanup(button)) 43 | self.cancel_btn.grid(row=row+2, column=3, padx=5, pady=10, sticky="nw") 44 | 45 | 46 | def select_all_file(self): 47 | for file_extension in self.file_types.keys(): 48 | self.file_types[file_extension]["var"].set(self.all_files.get()) 49 | 50 | def cleanup(self, button): 51 | if button == "ok": 52 | self.filter = [] 53 | for file_extension in self.file_types.keys(): 54 | if self.file_types[file_extension]["var"].get() == 0: 55 | self.filter.append(file_extension) 56 | 57 | if self.lock_filter.get() == 1: 58 | self.lock_filter = True 59 | else: 60 | self.lock_filter = False 61 | self.button = button 62 | 63 | self.top.destroy() 64 | 65 | 66 | class FilterNameWindow(ttk.Frame): 67 | def __init__(self, master, view, data): 68 | super(FilterNameWindow, self).__init__() 69 | top=self.top=Toplevel(master) 70 | top.grab_set() 71 | 72 | self.view = view 73 | self.files = data["file_data"] 74 | self.lock_filter = data["lock_filter"] 75 | 76 | self.button = "cancel" 77 | self.top.title(f"Filter Files by Name") 78 | 79 | 80 | Label(self.top, text="Enter Text:").grid(row=0, column=0, sticky='ew', padx=5, pady=5) 81 | self.text_entry = Entry(self.top, width=50) 82 | self.text_entry.grid(row=0, column=1, columnspan=3 ,sticky='ew', padx=5, pady=5) 83 | self.text_entry.insert(0, data["filter_text"]) 84 | 85 | # Buttons 86 | self.ok_btn = ttk.Button(self.top, text="OK", width=10, style="success.TButton", command=lambda button="ok": self.cleanup(button)) 87 | self.ok_btn.grid(row=1, column=2, padx=5, pady=10, sticky="ne") 88 | self.cancel_btn = ttk.Button(self.top, text="Cancel", width=10, style="danger.TButton", command=lambda button="cancel": self.cleanup(button)) 89 | self.cancel_btn.grid(row=1, column=3, padx=5, pady=10, sticky="nw") 90 | 91 | self.top.grid_columnconfigure(1, weight=1) 92 | self.text_entry.bind('', lambda event, button="ok": self.cleanup(button, event)) 93 | 94 | def cleanup(self, button, event=None): 95 | if button == "ok": 96 | self.text = self.text_entry.get() 97 | if self.text.strip() == "": 98 | self.text = None 99 | 100 | self.button = button 101 | else: 102 | self.text = None 103 | 104 | self.filter = [self.text] 105 | self.top.destroy() 106 | 107 | -------------------------------------------------------------------------------- /src/view/todo_list.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter.ttk import * 5 | from tkinter import messagebox 6 | from tkinter import simpledialog 7 | from view import treeview_functions 8 | 9 | import time 10 | 11 | from custom_widgets import autoscrollbar 12 | # import config_file_manager 13 | 14 | class ToDoListWindow(ttk.Frame): 15 | def __init__(self, master, view, to_do_list): 16 | super(ToDoListWindow, self).__init__() 17 | top=self.top=Toplevel(master) 18 | top.grab_set() 19 | self.view = view 20 | self.to_do_list = to_do_list 21 | 22 | self.top.title(f"To Do List") 23 | self.button = "cancel" 24 | self.setup_label_frames() 25 | self.setup_input_widgets() 26 | self.setup_buttons() 27 | self.setup_treeview() 28 | 29 | self.top.grid_columnconfigure(7, weight=1) 30 | self.top.grid_rowconfigure(2, weight=1) 31 | self.todo_frame.grid_columnconfigure(14, weight=1) 32 | self.todo_frame.grid_rowconfigure(4, weight=1) 33 | 34 | def setup_label_frames(self): 35 | self.todo_frame = LabelFrame(self.top, text="To Do List:") 36 | self.todo_frame.grid(row=2, column=0, columnspan=8, rowspan=2, sticky="NSEW", padx=self.view.default_padx, pady=5, ipadx=2, ipady=5) 37 | 38 | def setup_treeview(self): 39 | column_names = ["#", "Status", "Task"] 40 | column_widths = [50, 100, 500] 41 | height = 20 42 | 43 | self.treeview = treeview_functions.create_treeview(self.todo_frame, column_names, column_widths, height) 44 | self.treeview.grid(row=4, column=0, columnspan=16, sticky='NSEW', padx=self.view.default_padx, pady=self.view.default_pady) 45 | treeview_functions.write_data_to_treeview_general(self.treeview, 'replace', self.to_do_list) 46 | self.update_tags() 47 | self.treeview.bind("<>", self.on_left_click) 48 | self.treeview.bind("", self.header_click) 49 | 50 | self.treeview.tag_configure("open", foreground="white", background="#6e4905") 51 | self.treeview.tag_configure("closed", foreground="white", background="#24751e") 52 | 53 | vsb = autoscrollbar.AutoScrollbar(self.todo_frame, orient="vertical", command=self.treeview.yview) 54 | vsb.grid(row=4, column=16, sticky='NSEW') 55 | self.treeview.configure(yscrollcommand=vsb.set) 56 | 57 | def setup_input_widgets(self): 58 | 59 | ttk.Label(self.todo_frame, text='Task:').grid(row=0, column=0, columnspan=1, sticky='NSEW', padx=self.view.default_padx, pady=self.view.default_pady) 60 | self.task_entry = ttk.Entry(self.todo_frame) 61 | self.task_entry.grid(row=0, column=1, columnspan=15, sticky='NSEW', padx=self.view.default_padx, pady=self.view.default_pady) 62 | self.task_entry.bind('', self.enter_event) 63 | 64 | ttk.Label(self.todo_frame, text='Status:').grid(row=1, column=0, columnspan=1, sticky='NSEW', padx=self.view.default_padx, pady=self.view.default_pady) 65 | self.status_combo = ttk.Combobox(self.todo_frame, values=["Open", "Closed"], state='readonly') 66 | self.status_combo.set("Open") 67 | self.status_combo.grid(row=1, column=1, columnspan=1, sticky='NSEW', padx=self.view.default_padx, pady=self.view.default_pady) 68 | 69 | def setup_buttons(self): 70 | # submit button 71 | self.submit = ttk.Button(self.todo_frame, text='Add', width=15, style='success.TButton', command=self.add) 72 | self.submit.grid(row=3, column=0, sticky='ew', padx=self.view.default_padx, pady=self.view.default_pady) 73 | 74 | self.edit = ttk.Button(self.todo_frame, text='Edit Selected', width=15, style='info.TButton', command=self.edit_row) 75 | self.edit.grid(row=3, column=1, sticky='ew', padx=self.view.default_padx, pady=self.view.default_pady) 76 | 77 | self.delete_btn = ttk.Button(self.todo_frame, text='Delete Selected', width=15, style='danger.TButton', command=self.delete_row) 78 | self.delete_btn.grid(row=3, column=2, sticky='ew', padx=self.view.default_padx, pady=self.view.default_pady) 79 | 80 | self.delete_all_btn = ttk.Button(self.todo_frame, text='Delete All', width=15, style='danger.TButton', command=self.delete_all) 81 | self.delete_all_btn.grid(row=3, column=15, sticky='ew', padx=self.view.default_padx, pady=self.view.default_pady) 82 | 83 | def add(self): 84 | task = self.task_entry.get() 85 | self.to_do_list.append([len(self.to_do_list)+1, "Open", task]) 86 | 87 | treeview_functions.write_data_to_treeview_general(self.treeview, 'replace', self.to_do_list) 88 | self.update_tags() 89 | 90 | def edit_row(self): 91 | idx, data = treeview_functions.get_current_selection(self.treeview) 92 | selected_iid = self.treeview.selection()[0] 93 | current_idx = self.treeview.index(selected_iid) 94 | 95 | self.treeview.item(selected_iid) 96 | self.treeview.item(selected_iid, text=self.treeview.item(selected_iid, 'text')) 97 | self.treeview.item(selected_iid, values=[self.status_combo.get(), self.task_entry.get()]) 98 | 99 | self.to_do_list[current_idx] = [self.treeview.item(selected_iid, 'text'), self.status_combo.get(), self.task_entry.get()] 100 | self.update_tags() 101 | 102 | def delete_row(self): 103 | selected_iid = self.treeview.selection()[0] 104 | current_idx = self.treeview.index(selected_iid) 105 | 106 | msg = messagebox.askyesno(title='Delete Task', message='Delete This Task? This Cannot be Undone.') 107 | 108 | if msg: 109 | self.treeview.delete(selected_iid) 110 | self.to_do_list.pop(current_idx) 111 | 112 | # renumber all items 113 | for idx, task in enumerate(self.to_do_list): 114 | self.to_do_list[idx][0] = idx + 1 115 | 116 | treeview_functions.write_data_to_treeview_general(self.treeview, 'replace', self.to_do_list) 117 | self.update_tags() 118 | 119 | def delete_all(self): 120 | msg = messagebox.askyesno(title='Delete All Tasks', message='Delete All Tasks? This Cannot be Undone.') 121 | 122 | if msg: 123 | self.to_do_list = [] 124 | treeview_functions.write_data_to_treeview_general(self.treeview, 'replace', self.to_do_list) 125 | 126 | def update_tags(self): 127 | for child in self.treeview.get_children(): 128 | self.treeview.item(child, tags=(self.treeview.item(child)["values"][0].lower())) 129 | 130 | def on_left_click(self, event): 131 | region = self.treeview.identify("region", event.x, event.y) 132 | 133 | if region == 'heading': 134 | for item in self.treeview.selection(): 135 | self.treeview.selection_remove(item) 136 | 137 | else: 138 | if len(self.treeview.item(self.treeview.selection(), "values")) > 0: 139 | self.task_entry.delete(0, "end") 140 | self.task_entry.insert(0, self.treeview.item(self.treeview.selection(), "values")[-1]) 141 | self.status_combo.set(self.treeview.item(self.treeview.selection(), "values")[0]) 142 | 143 | 144 | 145 | def header_click(self, event): 146 | ''' 147 | When we click header, unselect everything in the treeview (so tag colors can be seen clearly) 148 | ''' 149 | region = self.treeview.identify("region", event.x, event.y) 150 | if region == 'heading' or not self.treeview.identify_row(event.y): 151 | for item in self.treeview.selection(): 152 | self.treeview.selection_remove(item) 153 | 154 | def enter_event(self, event): 155 | self.add() -------------------------------------------------------------------------------- /src/custom_widgets/quick_access_tree.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import tkinter as tk 4 | from tkinter import * 5 | from tkinter import ttk 6 | from tkinter.ttk import * 7 | from tkinter import simpledialog 8 | from tkinter import messagebox 9 | 10 | from ttkbootstrap.themes import standard 11 | from ttkbootstrap.tooltip import ToolTip 12 | from ttkbootstrap.constants import * 13 | from view import link_window 14 | 15 | 16 | class QuickAccessTreeview(ttk.Treeview): 17 | def __init__(self, view, nodes=['Default']): 18 | super(QuickAccessTreeview, self).__init__(view.sidebar_frame, selectmode='browse', show="tree") 19 | self.view = view 20 | 21 | # #bind left click event 22 | self.bind('<>',lambda event, : self.single_click(event)) 23 | self.bind("",lambda event,: self.on_right_click(event)) 24 | self.bind("", self.highlight_row) 25 | self.bind("", self.leave_treeview) 26 | 27 | self.close_btn = Button(view.sidebar_frame, text=u"\u2716", 28 | command= lambda action=False: self.open_close_all_nodes(action), bootstyle="danger.TButton") 29 | self.close_btn.grid(row=1, column=0, sticky="w", padx=0, ipady=0) 30 | ToolTip(self.close_btn, text="Close All Folders", bootstyle=(INFO, INVERSE), delay=self.view.tool_top_delay) 31 | 32 | self.up_btn = Button(view.sidebar_frame, text=u"\u2191", command=self.move_up, style="primary.TButton") 33 | self.up_btn.grid(row=1, column=1, sticky="w", padx=0) 34 | ToolTip(self.up_btn, text="Move Selected Folder Up", bootstyle=(INFO, INVERSE), delay=self.view.tool_top_delay) 35 | 36 | self.down_btn = Button(self.view.sidebar_frame, text=u"\u2193", command=self.move_down, style="primary.TButton") 37 | self.down_btn.grid(row=1, column=2, sticky="w", padx=0) 38 | ToolTip(self.down_btn, text="Move Selected Folder Down", bootstyle=(INFO, INVERSE), delay=self.view.tool_top_delay) 39 | 40 | self.update_tags() 41 | 42 | def update_btn_bg(self): 43 | # No Longer using the method from V0.47.3, leaving it here for reference 44 | s = ttk.Style() 45 | bg = s.lookup("TFrame", "background") 46 | 47 | self.close_btn.config(bg=bg) 48 | self.up_btn.config(bg=bg) 49 | self.down_btn.config(bg=bg) 50 | 51 | def update_tags(self): 52 | highlight_color = standard.STANDARD_THEMES[self.view.style_name]["colors"]["active"] 53 | self.tag_configure('highlight', background=highlight_color) 54 | 55 | def highlight_row(self, event): 56 | item = self.identify_row(event.y) 57 | item = f'"{item}"' 58 | self.tk.call(self, "tag", "remove", "highlight") 59 | self.tk.call(self, "tag", "add", "highlight", item) 60 | 61 | def insert_new_folder(self, folder_id, text, idx=0): 62 | self.insert("", idx, iid=folder_id, text=text, image=self.view.folder_icon2) 63 | 64 | def rename_folder(self, folder_id, text): 65 | self.item(folder_id, text=text) 66 | 67 | def single_click(self, event, click='single'): 68 | if click == 'single': 69 | item_iid = event.widget.selection()[0] 70 | parent_iid = event.widget.parent(item_iid) 71 | 72 | if parent_iid: #if it is a link and not a node 73 | current_root_tab = self.view.main_notebook.nametowidget(self.view.main_notebook.select()) 74 | current_branch_tab = current_root_tab.notebook.nametowidget(current_root_tab.notebook.select()).branch_id 75 | self.view.controller.link_clicked(parent_iid, item_iid, current_branch_tab) 76 | 77 | def on_right_click(self, event): 78 | self.unbind("