GNU/Linux Vs Windows11 OS İçin Python File Manager v6.0

Programlama ve Script dilleri konusunda bilgi paylaşım alanıdır.
Cevapla
Kullanıcı avatarı
TRWE_2012
Zettabyte1
Zettabyte1
Mesajlar: 15164
Kayıt: 25 Eyl 2013, 13:38
cinsiyet: Erkek
Teşekkür etti: 2510 kez
Teşekkür edildi: 5314 kez

GNU/Linux Vs Windows11 OS İçin Python File Manager v6.0

Mesaj gönderen TRWE_2012 »

Resim
KOD İÇERİĞİ : ( file_explorer_v6.py )

Kod: Tümünü seç


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# =============================================================================
# Project      : VB6 -> Python Conversion
# Module       : File Explorer  (Section 10)
# Platform     : GNU/Linux Mint 22.2 MATE x64  |  Windows 11 24H2 x64
# Python       : 3.8+  (stdlib only)
# Fix v1.2     : - Context menu auto-dismisses on left-click / Refresh
#                - Root (/) and C:\ visible; system paths read-only
#                - Other mount points (D:\ /media /mnt) fully unrestricted
#                - Virtual FS (/proc /sys /dev /run) shown but not traversed
# =============================================================================

import os
import sys

# ---------------------------------------------------------------------------
# VENV BOOTSTRAPPER  (v1.1 - robust)
# ---------------------------------------------------------------------------
VENV_DIR       = "/home/linuxmaster/python-toolchain"
_SENTINEL_FLAG = "_VENV_BOOTSTRAP_DONE"
_candidates    = [
    os.path.join(VENV_DIR, "bin", "python3"),
    os.path.join(VENV_DIR, "bin", "python"),
    os.path.join(VENV_DIR, "Scripts", "python.exe"),
]

def _venv_is_healthy():
    lib = os.path.join(VENV_DIR, "lib")
    if os.path.isdir(lib):
        for e in os.listdir(lib):
            if os.path.isdir(os.path.join(lib, e, "site-packages")):
                return True
    return os.path.isdir(os.path.join(VENV_DIR, "Lib", "site-packages"))

def _bootstrap_venv():
    if os.environ.get(_SENTINEL_FLAG) == "1":
        return
    inside = os.path.normcase(os.path.realpath(sys.executable)).startswith(
        os.path.normcase(os.path.realpath(VENV_DIR)))
    if inside or not _venv_is_healthy():
        return
    for c in _candidates:
        if os.path.isfile(c):
            os.environ[_SENTINEL_FLAG] = "1"
            os.execv(c, [c] + sys.argv)

_bootstrap_venv()

# ---------------------------------------------------------------------------
import shutil, fnmatch, string, datetime
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog

IS_WINDOWS = sys.platform.startswith("win")

# ---------------------------------------------------------------------------
# VIRTUAL FILESYSTEMS  (Linux) — visible in tree but NOT traversed/listed
# ---------------------------------------------------------------------------
_LINUX_VFS = {"/proc", "/sys", "/dev", "/run"}

def _is_virtual_fs(path: str) -> bool:
    if IS_WINDOWS:
        return False
    real = os.path.realpath(path)
    for vfs in _LINUX_VFS:
        if real == vfs or real.startswith(vfs + "/"):
            return True
    return False

# ---------------------------------------------------------------------------
# PROTECTION RULES
# ---------------------------------------------------------------------------
# Linux: / and its direct system subdirs are protected.
#        /home/<current_user>/  and everything under it = FREE
#        /media/* and /mnt/* = FREE
# Windows: C:\ system dirs protected, D:\ E:\ ... = fully FREE

_LINUX_PROTECTED_DIRS = {
    "/bin", "/sbin", "/usr", "/lib", "/lib32", "/lib64", "/libx32",
    "/boot", "/etc", "/opt", "/root", "/snap", "/srv", "/tmp", "/var",
    "/proc", "/sys", "/dev", "/run",
}

_WINDOWS_PROTECTED_DIRS = {
    "windows", "system32", "syswow64", "program files",
    "program files (x86)", "programdata",
}

HOME = os.path.realpath(os.path.expanduser("~"))

def _is_protected(path: str) -> bool:
    real = os.path.realpath(path)

    if IS_WINDOWS:
        # Only C:\ drive is subject to protection rules
        drive = os.path.splitdrive(real)[0].upper()
        if drive != "C:":
            return False                          # D:\ E:\ etc. — fully free
        # Anything directly inside C:\Windows, C:\Program Files, etc.
        parts_lower = real.lower().replace("\\", "/").split("/")
        # parts_lower[0] = "c:", parts_lower[1] = top-level dir
        if len(parts_lower) >= 2:
            top = parts_lower[1] if len(parts_lower) > 1 else ""
            if top in _WINDOWS_PROTECTED_DIRS:
                return True
            # Also protect C:\ itself from direct file operations
            if real.lower() in ("c:\\", "c:/", "c:"):
                return True
        return False

    else:
        # Always free: home dir and everything inside it
        if real == HOME or real.startswith(HOME + "/"):
            return False
        # Always free: /media and /mnt (removable / external)
        if real.startswith("/media/") or real.startswith("/mnt/"):
            return False
        # Root itself and all system subdirs are protected
        if real == "/":
            return True
        for pdir in _LINUX_PROTECTED_DIRS:
            if real == pdir or real.startswith(pdir + "/"):
                return True
        return False

# ---------------------------------------------------------------------------
# MISC HELPERS
# ---------------------------------------------------------------------------
WIN_HIDDEN = 0x02

def is_hidden(name: str, full: str) -> bool:
    if IS_WINDOWS:
        try:
            import ctypes
            a = ctypes.windll.kernel32.GetFileAttributesW(full)
            return bool(a != -1 and a & WIN_HIDDEN)
        except Exception:
            return False
    return name.startswith(".")

def get_roots() -> list:
    if IS_WINDOWS:
        return [f"{l}:\\" for l in string.ascii_uppercase if os.path.exists(f"{l}:\\")]
    else:
        roots = ["/", HOME]
        try:
            with open("/proc/mounts") as f:
                for line in f:
                    p = line.split()
                    if len(p) >= 2 and (p[1].startswith("/media") or p[1].startswith("/mnt")):
                        if os.path.isdir(p[1]):
                            roots.append(p[1])
        except OSError:
            pass
        return roots

FILTER_OPTIONS = ["*.*","*.txt","*.py","*.sh","*.jpg","*.png",
                  "*.pdf","*.mp3","*.mp4","*.zip","*.tar.gz"]
TOOLBAR_BG = "#f0f0f0"
STATUS_BG  = "#e8e8e8"

# ===========================================================================
# MAIN APP
# ===========================================================================

class FileExplorerApp:

    def __init__(self, master: tk.Tk):
        self.master      = master
        self._roots      = get_roots()
        self._cur_dir    = HOME
        self._tree_nodes = {}
        self._ctx_active = False      # tracks if context menu is posted

        master.title("File Explorer")
        master.minsize(700, 500)

        self._build_toolbar()
        self._build_filter_bar()
        self._build_panes()
        self._build_status_bar()
        self._build_ctx_menu()

        self._populate_tree(self._roots[0])
        self._list_files(self._cur_dir)
        self._center_window()
        master.protocol("WM_DELETE_WINDOW", self._on_quit)

    # ------------------------------------------------------------------
    # TOOLBAR
    # ------------------------------------------------------------------
    def _build_toolbar(self):
        tb = tk.Frame(self.master, bg=TOOLBAR_BG, relief=tk.RAISED, bd=1)
        tb.pack(fill=tk.X)
        for lbl, cmd in [
            ("New File",   self._op_new_file),
            ("New Folder", self._op_new_folder),
            ("Delete",     self._op_delete),
            ("Copy",       self._op_copy),
            ("Rename",     self._op_rename),
            ("Refresh",    self._op_refresh),
            ("Help",       self._op_help),
        ]:
            tk.Button(tb, text=lbl, width=9, relief=tk.GROOVE,
                      bg=TOOLBAR_BG, activebackground="#d0d0d0",
                      command=cmd).pack(side=tk.LEFT, padx=3, pady=3)

    # ------------------------------------------------------------------
    # FILTER BAR
    # ------------------------------------------------------------------
    def _build_filter_bar(self):
        bar = tk.Frame(self.master, bg=TOOLBAR_BG)
        bar.pack(fill=tk.X, padx=4, pady=2)

        tk.Label(bar, text="Drive / Root:", bg=TOOLBAR_BG).pack(side=tk.LEFT)
        self.combo_drive = ttk.Combobox(bar, values=self._roots,
                                        width=22, state="readonly")
        self.combo_drive.current(0)
        self.combo_drive.pack(side=tk.LEFT, padx=(2, 16))
        self.combo_drive.bind("<<ComboboxSelected>>", self._on_drive_change)

        tk.Label(bar, text="Filter:", bg=TOOLBAR_BG).pack(side=tk.LEFT)
        self.combo_filter = ttk.Combobox(bar, values=FILTER_OPTIONS, width=12)
        self.combo_filter.current(0)
        self.combo_filter.pack(side=tk.LEFT, padx=2)
        self.combo_filter.bind("<<ComboboxSelected>>", self._on_filter_change)
        self.combo_filter.bind("<Return>", self._on_filter_change)

    # ------------------------------------------------------------------
    # PANES
    # ------------------------------------------------------------------
    def _build_panes(self):
        paned = tk.PanedWindow(self.master, orient=tk.HORIZONTAL,
                               sashwidth=5, sashrelief=tk.RAISED)
        paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=2)

        # LEFT
        lf = tk.Frame(paned, relief=tk.SUNKEN, bd=1)
        tk.Label(lf, text="Folders", anchor="w", bg="#d4d0c8",
                 font=("Helvetica", 9, "bold")).pack(fill=tk.X)
        sy = tk.Scrollbar(lf); sy.pack(side=tk.RIGHT, fill=tk.Y)
        sx = tk.Scrollbar(lf, orient=tk.HORIZONTAL)
        sx.pack(side=tk.BOTTOM, fill=tk.X)
        self.dir_tree = ttk.Treeview(lf, yscrollcommand=sy.set,
                                     xscrollcommand=sx.set,
                                     show="tree", selectmode="browse")
        self.dir_tree.pack(fill=tk.BOTH, expand=True)
        sy.config(command=self.dir_tree.yview)
        sx.config(command=self.dir_tree.xview)
        self.dir_tree.bind("<<TreeviewSelect>>", self._on_tree_select)
        self.dir_tree.bind("<<TreeviewOpen>>",   self._on_tree_expand)
        self.dir_tree.bind("<Button-1>",         self._dismiss_ctx)
        paned.add(lf, minsize=180)

        # RIGHT
        rf = tk.Frame(paned, relief=tk.SUNKEN, bd=1)
        tk.Label(rf, text="Files", anchor="w", bg="#d4d0c8",
                 font=("Helvetica", 9, "bold")).pack(fill=tk.X)
        sfy = tk.Scrollbar(rf); sfy.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_list = tk.Listbox(rf, yscrollcommand=sfy.set,
                                    selectbackground="#0078d7",
                                    selectforeground="#ffffff",
                                    font=("Courier", 10), activestyle="none")
        self.file_list.pack(fill=tk.BOTH, expand=True)
        sfy.config(command=self.file_list.yview)
        self.file_list.bind("<Double-Button-1>", self._on_dbl_click)
        self.file_list.bind("<Button-3>",        self._show_ctx)
        # Left-click on empty area dismisses context menu
        self.file_list.bind("<Button-1>",        self._on_list_left_click)
        paned.add(rf, minsize=200)

    # ------------------------------------------------------------------
    # STATUS BAR
    # ------------------------------------------------------------------
    def _build_status_bar(self):
        bot = tk.Frame(self.master, relief=tk.SUNKEN, bd=1, bg=STATUS_BG)
        bot.pack(fill=tk.X, side=tk.BOTTOM)
        tk.Button(bot, text="Quit", width=8, fg="#cc0000",
                  activeforeground="#ffffff", activebackground="#cc0000",
                  command=self._on_quit).pack(side=tk.RIGHT, padx=4, pady=2)
        self.lbl_status = tk.Label(bot, text="  Ready.", anchor="w",
                                   bg=STATUS_BG, font=("Courier", 9))
        self.lbl_status.pack(side=tk.LEFT, fill=tk.X, expand=True)

    # ------------------------------------------------------------------
    # CONTEXT MENU
    # ------------------------------------------------------------------
    def _build_ctx_menu(self):
        self.ctx = tk.Menu(self.master, tearoff=0)
        self.ctx.add_command(label="Open",       command=self._ctx_open)
        self.ctx.add_separator()
        self.ctx.add_command(label="Copy",       command=self._op_copy)
        self.ctx.add_command(label="Rename",     command=self._op_rename)
        self.ctx.add_command(label="Delete",     command=self._op_delete)
        self.ctx.add_separator()
        self.ctx.add_command(label="Properties", command=self._ctx_props)

    def _show_ctx(self, event: tk.Event):
        """Show context menu only when pointer is over an actual item."""
        idx = self.file_list.nearest(event.y)
        if idx < 0 or idx >= self.file_list.size():
            return
        # Verify the click is really on the item (not below last item)
        bbox = self.file_list.bbox(idx)
        if not bbox:
            return
        self.file_list.selection_clear(0, tk.END)
        self.file_list.selection_set(idx)
        self._ctx_active = True
        try:
            self.ctx.tk_popup(event.x_root, event.y_root)
        finally:
            self.ctx.grab_release()

    def _dismiss_ctx(self, _event=None):
        """Dismiss context menu and clear file list selection."""
        if self._ctx_active:
            try:
                self.ctx.unpost()
            except Exception:
                pass
            self._ctx_active = False

    def _on_list_left_click(self, event: tk.Event):
        """Left-click: dismiss menu; deselect if click is on empty area."""
        self._dismiss_ctx()
        idx = self.file_list.nearest(event.y)
        if idx >= 0:
            bbox = self.file_list.bbox(idx)
            if bbox and event.y > bbox[1] + bbox[3]:
                self.file_list.selection_clear(0, tk.END)

    # ------------------------------------------------------------------
    # DIRECTORY TREE
    # ------------------------------------------------------------------
    def _populate_tree(self, root_path: str):
        self.dir_tree.delete(*self.dir_tree.get_children())
        self._tree_nodes.clear()
        rid = self.dir_tree.insert("", "end", text=root_path, open=True)
        self._tree_nodes[rid] = root_path
        self._insert_subdirs(rid, root_path)

    def _insert_subdirs(self, parent_id: str, path: str):
        try:
            entries = sorted(os.listdir(path))
        except (PermissionError, OSError):
            return
        for name in entries:
            full = os.path.join(path, name)
            if not os.path.isdir(full):
                continue
            if is_hidden(name, full):
                continue
            child = self.dir_tree.insert(parent_id, "end", text=name)
            self._tree_nodes[child] = full
            # Virtual FS: show but never expand (prevents hang)
            if _is_virtual_fs(full):
                continue
            if self._has_subdirs(full):
                self.dir_tree.insert(child, "end", text="...", tags=("dummy",))

    def _has_subdirs(self, path: str) -> bool:
        if _is_virtual_fs(path):
            return False
        try:
            for name in os.listdir(path):
                full = os.path.join(path, name)
                if os.path.isdir(full) and not is_hidden(name, full):
                    return True
        except (PermissionError, OSError):
            pass
        return False

    def _on_tree_expand(self, _event):
        node     = self.dir_tree.focus()
        children = self.dir_tree.get_children(node)
        if len(children) == 1 and self.dir_tree.item(children[0], "tags") == ("dummy",):
            self.dir_tree.delete(children[0])
            path = self._tree_nodes.get(node, "")
            if path:
                self._insert_subdirs(node, path)

    def _on_tree_select(self, _event):
        node = self.dir_tree.focus()
        path = self._tree_nodes.get(node)
        if not path or not os.path.isdir(path):
            return
        if _is_virtual_fs(path):
            self._set_status(f"  [Virtual FS] {path} — cannot browse safely.")
            return
        self._cur_dir = path
        self._list_files(path)

    # ------------------------------------------------------------------
    # FILE LIST
    # ------------------------------------------------------------------
    def _list_files(self, path: str):
        self.file_list.delete(0, tk.END)
        if _is_virtual_fs(path):
            self._set_status(f"  [Virtual FS] {path} — skipped.")
            return
        pattern = self.combo_filter.get().strip() or "*.*"
        dir_count = file_count = hidden = 0
        try:
            entries = sorted(os.listdir(path))
        except (PermissionError, OSError):
            self._set_status("  Permission denied.")
            return

        # ---- Directories first ----
        for name in entries:
            full = os.path.join(path, name)
            if not os.path.isdir(full):
                continue
            if _is_virtual_fs(full):
                continue
            if is_hidden(name, full):
                hidden += 1
                continue
            self.file_list.insert(tk.END, f"[DIR]  {name}")
            self.file_list.itemconfig(tk.END, fg="#0055aa")
            dir_count += 1

        # ---- Files (filtered by pattern) ----
        for name in entries:
            full = os.path.join(path, name)
            if not os.path.isfile(full):
                continue
            if is_hidden(name, full):
                hidden += 1
                continue
            if not fnmatch.fnmatch(name.lower(), pattern.lower()):
                hidden += 1
                continue
            self.file_list.insert(tk.END, name)
            file_count += 1

        lock = " [READ-ONLY]" if _is_protected(path) else ""
        self._set_status(
            f"  {path}{lock}   |   "
            f"{dir_count} folder(s)  {file_count} file(s)  "
            f"[{hidden} hidden/filtered]"
        )

    # ------------------------------------------------------------------
    # EVENTS
    # ------------------------------------------------------------------
    def _on_drive_change(self, _event):
        root = self.combo_drive.get()
        self._cur_dir = root
        self._populate_tree(root)
        self._list_files(root)

    def _on_filter_change(self, _event):
        self._list_files(self._cur_dir)

    def _on_dbl_click(self, _event):
        path = self._sel_path()
        if not path:
            return
        if os.path.isdir(path):
            if _is_virtual_fs(path):
                self._set_status(f"  [Virtual FS] {path} — cannot browse.")
                return
            self._cur_dir = path
            self._list_files(path)
        else:
            self._open_path(path)

    def _ctx_open(self):
        path = self._sel_path()
        if path:
            self._open_path(path)

    @staticmethod
    def _open_path(path: str):
        try:
            if IS_WINDOWS:
                os.startfile(path)
            else:
                os.system(f'xdg-open "{path}" &')
        except Exception as exc:
            messagebox.showerror("Open Error", str(exc))

    # ------------------------------------------------------------------
    # PROTECTION GUARD
    # ------------------------------------------------------------------
    def _guard(self, path: str, action: str) -> bool:
        if _is_protected(path):
            messagebox.showerror(
                "Access Denied",
                f"Cannot {action}:\n\n{path}\n\n"
                "This path is protected (system / critical directory).\n"
                "You can VIEW files here, but not modify them.\n\n"
                "Tip: Only your home folder and removable drives are writable.",
                parent=self.master,
            )
            return True
        return False

    # ------------------------------------------------------------------
    # HELPERS
    # ------------------------------------------------------------------
    def _sel_path(self) -> str | None:
        sel = self.file_list.curselection()
        if not sel:
            return None
        raw  = self.file_list.get(sel[0])
        name = raw[7:] if raw.startswith("[DIR]  ") else raw   # strip prefix
        return os.path.join(self._cur_dir, name)

    def _sel_is_dir(self) -> bool:
        sel = self.file_list.curselection()
        if not sel:
            return False
        return self.file_list.get(sel[0]).startswith("[DIR]  ")

    # ------------------------------------------------------------------
    # TOOLBAR OPERATIONS
    # ------------------------------------------------------------------
    def _op_new_file(self):
        if self._guard(self._cur_dir, "create files in"):
            return
        name = simpledialog.askstring("New File", "Enter new file name:",
                                      parent=self.master)
        if not name:
            return
        full = os.path.join(self._cur_dir, name.strip())
        if os.path.exists(full):
            messagebox.showwarning("New File", f'"{name}" already exists.',
                                   parent=self.master)
            return
        try:
            open(full, "w").close()
            self._list_files(self._cur_dir)
            self._set_status(f"  Created: {full}")
        except OSError as exc:
            messagebox.showerror("New File Error", str(exc), parent=self.master)

    def _op_new_folder(self):
        if self._guard(self._cur_dir, "create folders in"):
            return
        name = simpledialog.askstring(
            "New Folder", "Enter new folder name:", parent=self.master
        )
        if not name:
            return
        full = os.path.join(self._cur_dir, name.strip())
        if os.path.exists(full):
            messagebox.showwarning("New Folder", f'"{name}" already exists.',
                                   parent=self.master)
            return
        try:
            os.makedirs(full)
            self._op_refresh()
            self._set_status(f"  Folder created: {full}")
        except OSError as exc:
            messagebox.showerror("New Folder Error", str(exc), parent=self.master)

    def _op_delete(self):
        path = self._sel_path()
        if not path:
            messagebox.showinfo("Delete", "No file selected.", parent=self.master)
            return
        if self._guard(path, "delete"):
            return
        name = os.path.basename(path)
        if not messagebox.askyesno(
            "Delete",
            f'Permanently delete:\n"{name}"?\n\nThis cannot be undone.',
            icon=messagebox.WARNING, parent=self.master):
            return
        try:
            shutil.rmtree(path) if os.path.isdir(path) else os.remove(path)
            self._list_files(self._cur_dir)
            self._set_status(f"  Deleted: {name}")
        except OSError as exc:
            messagebox.showerror("Delete Error", str(exc), parent=self.master)

    def _op_copy(self):
        path = self._sel_path()
        if not path:
            messagebox.showinfo("Copy", "No file selected.", parent=self.master)
            return
        dest_dir = filedialog.askdirectory(title="Copy to — select destination",
                                           parent=self.master)
        if not dest_dir:
            return
        if self._guard(dest_dir, "copy into"):
            return
        dest = os.path.join(dest_dir, os.path.basename(path))
        try:
            shutil.copy2(path, dest) if os.path.isfile(path) \
                else shutil.copytree(path, dest)
            self._set_status(f"  Copied to: {dest_dir}")
        except OSError as exc:
            messagebox.showerror("Copy Error", str(exc), parent=self.master)

    def _op_rename(self):
        path = self._sel_path()
        if not path:
            messagebox.showinfo("Rename", "No file selected.", parent=self.master)
            return
        if self._guard(path, "rename"):
            return
        old = os.path.basename(path)
        new = simpledialog.askstring("Rename", f'Rename "{old}" to:',
                                     initialvalue=old, parent=self.master)
        if not new or new.strip() == old:
            return
        dest = os.path.join(self._cur_dir, new.strip())
        if os.path.exists(dest):
            messagebox.showwarning("Rename", f'"{new}" already exists.',
                                   parent=self.master)
            return
        try:
            os.rename(path, dest)
            self._list_files(self._cur_dir)
            self._set_status(f'  Renamed: "{old}" -> "{new.strip()}"')
        except OSError as exc:
            messagebox.showerror("Rename Error", str(exc), parent=self.master)

    def _op_refresh(self):
        self._dismiss_ctx()
        self.file_list.selection_clear(0, tk.END)
        # Rebuild tree only from the current drive root (preserves _cur_dir)
        self._populate_tree(self.combo_drive.get())
        # Re-list the currently open directory
        self._list_files(self._cur_dir)
        self._set_status(f"  Refreshed: {self._cur_dir}")

    def _op_help(self):
        messagebox.showinfo(
            "Help",
            "File Explorer — Usage Guide\n\n"
            "New File : Create an empty file in the current directory.\n"
            "Delete   : Permanently delete the selected file.\n"
            "Copy     : Copy selected file to a chosen directory.\n"
            "Rename   : Rename the selected file.\n"
            "Refresh  : Reload tree and file list; dismisses any open menu.\n\n"
            "Left pane  : Click a folder to browse its contents.\n"
            "Right pane : Double-click a file to open it.\n"
            "             Right-click a file for more options.\n"
            "             Left-click on empty area dismisses the menu.\n\n"
            "PROTECTION RULES\n"
            "Linux : /  and all system directories are READ-ONLY.\n"
            "        Your home folder and /media /mnt are fully writable.\n"
            "Windows: C:\\Windows, C:\\Program Files etc. are READ-ONLY.\n"
            "         D:\\ E:\\ and other drives are fully writable.\n\n"
            "Virtual FS: /proc /sys /dev /run are shown but not listed\n"
            "            (prevents system hang).",
            parent=self.master,
        )

    def _ctx_props(self):
        path = self._sel_path()
        if not path:
            return
        try:
            st        = os.stat(path)
            mod       = datetime.datetime.fromtimestamp(st.st_mtime).strftime(
                "%Y-%m-%d %H:%M:%S")
            protected = "Yes (read-only)" if _is_protected(path) else "No"
            name      = os.path.basename(path)

            # Build rows as (label, value) pairs
            rows = [
                ("Name",      name),
                ("Path",      path),
                ("Size",      f"{st.st_size:,} bytes"),
                ("Modified",  mod),
                ("Protected", protected),
            ]

            # ---- Custom auto-sizing Toplevel ----
            dlg = tk.Toplevel(self.master)
            dlg.title("Properties")
            dlg.resizable(False, False)
            dlg.grab_set()                    # modal

            frm = tk.Frame(dlg, padx=18, pady=14)
            frm.pack(fill=tk.BOTH, expand=True)
            # column 1 stretches so the window grows to fit the longest value
            frm.columnconfigure(1, weight=1)

            FONT_BOLD  = ("Courier", 10, "bold")
            FONT_PLAIN = ("Courier", 10)

            for r, (lbl, val) in enumerate(rows):
                tk.Label(frm, text=f"{lbl} :", anchor="w",
                         font=FONT_BOLD).grid(
                    row=r, column=0, sticky="w", padx=(0, 10), pady=2)
                # wraplength=0  → no text wrapping; window auto-widens instead
                tk.Label(frm, text=val, anchor="w",
                         font=FONT_PLAIN, wraplength=0).grid(
                    row=r, column=1, sticky="w", pady=2)

            tk.Button(dlg, text="OK", width=10,
                      command=dlg.destroy,
                      default=tk.ACTIVE).pack(pady=(0, 10))
            dlg.bind("<Return>", lambda _e: dlg.destroy())
            dlg.bind("<Escape>", lambda _e: dlg.destroy())

            # Let Tk compute natural size FIRST, then center over parent
            dlg.update_idletasks()
            dw = dlg.winfo_reqwidth()
            dh = dlg.winfo_reqheight()
            mx = self.master.winfo_rootx() + (self.master.winfo_width()  - dw) // 2
            my = self.master.winfo_rooty() + (self.master.winfo_height() - dh) // 2
            dlg.geometry(f"{dw}x{dh}+{mx}+{my}")
            dlg.wait_window()

        except OSError as exc:
            messagebox.showerror("Properties Error", str(exc), parent=self.master)

    # ------------------------------------------------------------------
    # MISC
    # ------------------------------------------------------------------
    def _set_status(self, text: str):
        self.lbl_status.config(text=text)

    def _on_quit(self):
        if messagebox.askyesno("Quit", "Are you sure you want to quit?",
                               icon=messagebox.QUESTION, parent=self.master):
            self.master.destroy()

    def _center_window(self):
        sw = self.master.winfo_screenwidth()
        sh = self.master.winfo_screenheight()
        self.master.geometry(f"900x600+{(sw-900)//2}+{(sh-600)//2}")


# ---------------------------------------------------------------------------
def main():
    root = tk.Tk()
    FileExplorerApp(root)
    root.mainloop()
    input("\nPress ENTER to exit.")

if __name__ == "__main__":
    main()

Güle güle kullanın ve kendiniz daha iyisine geliştirin...Fikiriyat benden, geliştirmesi sizden.....(ben bu kadar yapabiliyorum, çünkü acemi ve amatörüm)
Cevapla

“Programlama ve Script dilleri” sayfasına dön