""" FileListWidget — file list based on ttk.Treeview with dark theme styling. """ import tkinter as tk from tkinter import ttk import customtkinter as ctk _THEME_APPLIED = False def _apply_dark_theme(): global _THEME_APPLIED if _THEME_APPLIED: return _THEME_APPLIED = True style = ttk.Style() style.theme_use("clam") style.configure( "Dark.Treeview", background="#1e1e1e", foreground="#dcdcdc", fieldbackground="#1e1e1e", borderwidth=0, font=("Consolas", 11), rowheight=24, ) style.configure( "Dark.Treeview.Heading", background="#2b2b2b", foreground="#9ca3af", borderwidth=0, font=("Segoe UI", 10, "bold"), relief="flat", ) style.map( "Dark.Treeview", background=[("selected", "#3b82f6")], foreground=[("selected", "#ffffff")], ) style.map( "Dark.Treeview.Heading", background=[("active", "#333333")], ) style.layout("Dark.Treeview", [ ("Dark.Treeview.treearea", {"sticky": "nswe"}), ]) class FileListWidget(ctk.CTkFrame): """File list with columns, sorting, multi-selection, drag-and-drop.""" def __init__(self, master, columns: list[tuple[str, int]], on_navigate=None, on_select=None, on_drop=None): super().__init__(master, fg_color="#1e1e1e", corner_radius=6) self._on_navigate = on_navigate self._on_select = on_select self._on_drop = on_drop self._items: list[dict] = [] self._sort_col = None self._sort_reverse = False self._drag_partner: "FileListWidget | None" = None self._drag_start_x = 0 self._drag_start_y = 0 self._dragging = False self._orig_fg_color = "#1e1e1e" _apply_dark_theme() col_ids = [c[0] for c in columns] self._tree = ttk.Treeview( self, columns=col_ids, show="headings", selectmode="extended", style="Dark.Treeview", ) for col_name, col_width in columns: self._tree.heading( col_name, text=col_name, anchor="w", command=lambda c=col_name: self._sort_by_column(c), ) self._tree.column(col_name, width=col_width, minwidth=40, anchor="w") scrollbar = ctk.CTkScrollbar(self, command=self._tree.yview) self._tree.configure(yscrollcommand=scrollbar.set) self._tree.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") self._tree.bind("", self._on_double_click) self._tree.bind("<>", self._on_tree_select) self._tree.bind("", self._on_enter) # Drag-and-drop bindings self._tree.bind("", self._on_drag_start, add="+") self._tree.bind("", self._on_drag_motion) self._tree.bind("", self._on_drag_release, add="+") def populate(self, items: list[dict]): """Fill list with items. Each item: {name, size, date, perm, is_dir}.""" self._items = items self._tree.delete(*self._tree.get_children()) dirs = [i for i in items if i.get("is_dir")] files = [i for i in items if not i.get("is_dir")] dirs.sort(key=lambda x: x["name"].lower()) files.sort(key=lambda x: x["name"].lower()) for item in dirs + files: prefix = "\U0001F4C1 " if item.get("is_dir") else "\U0001F4C4 " cols = self._tree.cget("columns") values = [] for c in cols: if c == cols[0]: # Name column values.append(prefix + item.get("name", "")) elif c.lower() in ("size", "sizes"): values.append(item.get("size", "")) elif c.lower() in ("date", "dates"): values.append(item.get("date", "")) elif c.lower() in ("perm", "perms"): values.append(item.get("perm", "")) else: values.append(item.get(c.lower(), "")) iid = self._tree.insert("", "end", values=values) self._tree.item(iid, tags=("dir",) if item.get("is_dir") else ("file",)) def get_selected(self) -> list[dict]: """Return selected items as dicts.""" result = [] for iid in self._tree.selection(): idx = self._tree.index(iid) dirs = [i for i in self._items if i.get("is_dir")] files = [i for i in self._items if not i.get("is_dir")] dirs.sort(key=lambda x: x["name"].lower()) files.sort(key=lambda x: x["name"].lower()) ordered = dirs + files if 0 <= idx < len(ordered): result.append(ordered[idx]) return result def _on_double_click(self, event): sel = self.get_selected() if sel and sel[0].get("is_dir") and self._on_navigate: self._on_navigate(sel[0]["name"]) def _on_enter(self, event): sel = self.get_selected() if sel and sel[0].get("is_dir") and self._on_navigate: self._on_navigate(sel[0]["name"]) def _on_tree_select(self, event): if self._on_select: self._on_select(self.get_selected()) def _sort_by_column(self, col): if self._sort_col == col: self._sort_reverse = not self._sort_reverse else: self._sort_col = col self._sort_reverse = False cols = self._tree.cget("columns") col_map = {cols[0]: "name"} for c in cols[1:]: col_map[c] = c.lower() field = col_map.get(col, "name") dirs = [i for i in self._items if i.get("is_dir")] files = [i for i in self._items if not i.get("is_dir")] dirs.sort(key=lambda x: str(x.get(field, "")).lower(), reverse=self._sort_reverse) files.sort(key=lambda x: str(x.get(field, "")).lower(), reverse=self._sort_reverse) self._items = dirs + files self.populate(self._items) # ── Drag-and-drop ── def set_drag_partner(self, partner: "FileListWidget"): """Link two panels for drag-and-drop.""" self._drag_partner = partner def _on_drag_start(self, event): self._drag_start_x = event.x_root self._drag_start_y = event.y_root self._dragging = False def _on_drag_motion(self, event): if not self._drag_partner: return dx = abs(event.x_root - self._drag_start_x) dy = abs(event.y_root - self._drag_start_y) if not self._dragging and (dx > 10 or dy > 10): sel = self.get_selected() if sel and any(s["name"] != ".." for s in sel): self._dragging = True self._tree.configure(cursor="hand2") self._drag_partner.configure(fg_color="#1e3a5f") def _on_drag_release(self, event): if not self._dragging or not self._drag_partner: self._dragging = False return self._dragging = False self._tree.configure(cursor="") self._drag_partner.configure(fg_color=self._orig_fg_color) # Check if mouse is over partner widget px = self._drag_partner.winfo_rootx() py = self._drag_partner.winfo_rooty() pw = self._drag_partner.winfo_width() ph = self._drag_partner.winfo_height() mx, my = event.x_root, event.y_root if px <= mx <= px + pw and py <= my <= py + ph: items = [s for s in self.get_selected() if s["name"] != ".."] if items and self._drag_partner._on_drop: self._drag_partner._on_drop(items, self)