apps/explorateur/app.py est l'application la plus complexe et la plus utilisée de GrimOS. C'est l'interface graphique qui permet de naviguer dans l'arborescence des fichiers du système Linux, d'ouvrir des documents avec les bonnes applications, et de se connecter aux réseaux.
ttk.Treeview, un tableau avancé permettant d'afficher des colonnes (Nom, Taille, Date) et de gérer des listes déroulantes de dossiers..png lance visionneuse, .py lance editeur). Lorsqu'on double-clique sur un fichier, le script utilise la référence globale de l'AppManager pour déclencher dynamiquement le lancement de la nouvelle fenêtre.sudo mount -t cifs en arrière-plan pour accrocher le dossier distant dans un sous-dossier de l'utilisateur (généralement ~/Réseau), rendant les fichiers distants accessibles comme s'ils étaient sur le disque dur local.shutil.copy, os.rename) le rendrait pleinement mature..jpg), le script pourrait utiliser PIL (Pillow) en arrière-plan pour générer des miniatures de 32x32 pixels et les afficher dans le Treeview, à la manière d'un explorateur moderne.import os
import shutil
import tkinter as tk
from tkinter import messagebox, simpledialog
from tkinter import ttk
import subprocess
def start(window, app_manager=None, **kwargs):
top_frame = tk.Frame(window, bg="lightgray")
top_frame.pack(side="top", fill="x")
icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "icons")
window.icons = {}
for iname in ['menu_new_folder', 'menu_new_file', 'menu_rename', 'menu_delete', 'icon_folder', 'icon_file_txt', 'icon_file_py', 'icon_file_img', 'icon_file_generic', 'btn_search', 'btn_network', 'btn_disconnect', 'btn_paste', 'btn_print', 'btn_refresh']:
ipath = os.path.join(icon_dir, iname + ".png")
if os.path.exists(ipath):
window.icons[iname] = tk.PhotoImage(file=ipath)
# Initialize Clipboard on Desktop
if app_manager and not hasattr(app_manager.desktop, "file_clipboard"):
app_manager.desktop.file_clipboard = {"action": None, "path": None}
def go_up():
current = path_var.get()
parent = os.path.dirname(current)
if parent != current:
path_var.set(parent)
refresh_list()
path_var = tk.StringVar(value=os.path.expanduser("~"))
path_entry = tk.Entry(top_frame, textvariable=path_var)
path_entry.pack(side="left", fill="x", expand=True, padx=2, pady=2)
def connect_network():
pwd = app_manager.desktop.settings.get("sudo_pwd") if app_manager else None
if not pwd:
messagebox.showerror("Erreur", "Veuillez configurer votre mot de passe système dans Paramètres.")
return
dialog = tk.Toplevel(window)
dialog.title("Connecter Réseau (SMB)")
dialog.geometry("600x480")
dialog.transient(window)
dialog.grab_set()
frame_top = tk.Frame(dialog)
frame_top.pack(fill="both", expand=True, padx=10, pady=5)
tk.Label(frame_top, text="Adresses découvertes sur le réseau :").pack(anchor="w")
tree_frame = tk.Frame(frame_top)
tree_frame.pack(fill="both", expand=True, pady=5)
scroll = tk.Scrollbar(tree_frame)
scroll.pack(side="right", fill="y")
tree_shares = ttk.Treeview(tree_frame, show="tree", yscrollcommand=scroll.set, height=8)
tree_shares.pack(side="left", fill="both", expand=True)
scroll.config(command=tree_shares.yview)
def scan_network():
import socket
import threading
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.255.255.255', 1))
local_ip = s.getsockname()[0]
except Exception:
local_ip = "192.168.1.1"
finally:
s.close()
subnet = ".".join(local_ip.split(".")[:3]) + "."
found_ips = []
def check_ip(ip):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.3)
try:
if sock.connect_ex((ip, 445)) == 0:
found_ips.append(ip)
except Exception: pass
finally: sock.close()
threads = []
for i in range(1, 255):
t = threading.Thread(target=check_ip, args=(subnet + str(i),))
threads.append(t)
t.start()
for t in threads:
t.join()
# Now extract shares and hostnames using smbclient if installed
detailed_shares = {}
for ip in found_ips:
hostname = ""
try:
res_nmb = subprocess.run(['nmblookup', '-A', ip], capture_output=True, text=True, timeout=1)
if res_nmb.returncode == 0:
for line in res_nmb.stdout.split('\n'):
if '<00>' in line and 'GROUP' not in line:
hostname = line.split('<00>')[0].strip()
break
except: pass
display_name = f"{hostname} ({ip})" if hostname else f"[{ip}]"
detailed_shares[display_name] = []
try:
res_smb = subprocess.run(['smbclient', '-L', ip, '-N', '-g'], capture_output=True, text=True, timeout=2)
if res_smb.returncode == 0:
has_shares = False
for line in res_smb.stdout.split('\n'):
if line.startswith('Disk|'):
parts = line.split('|')
if len(parts) >= 2:
share_name = parts[1]
if share_name not in ('print$', 'IPC$'):
detailed_shares[display_name].append(f"//{ip}/{share_name}")
has_shares = True
if not has_shares:
detailed_shares[display_name].append(f"//{ip}/Partage")
else:
detailed_shares[display_name].append(f"//{ip}/Partage")
except:
detailed_shares[display_name].append(f"//{ip}/Partage")
return detailed_shares
def on_scan(auto=False):
btn_scan.config(state="disabled", text="Recherche...")
dialog.update()
shares = scan_network()
btn_scan.config(state="normal", text=" Actualiser", image=window.icons.get('btn_search'), compound="left")
tree_shares.delete(*tree_shares.get_children())
if shares:
icon_folder = window.icons.get('icon_folder')
for machine, share_list in shares.items():
machine_id = tree_shares.insert("", "end", text=f" {machine}", open=False)
for share in share_list:
share_name = share.split('/')[-1]
# Check if already mounted
parts = share.strip("/").split("/")
if len(parts) >= 2:
ip_addr = parts[0]
mount_p = os.path.expanduser(f"~/Réseau/{ip_addr}/{share_name}")
disp_text = f" {share_name} (Connecté ✅)" if os.path.ismount(mount_p) else f" {share_name}"
else:
disp_text = f" {share_name}"
if icon_folder:
tree_shares.insert(machine_id, "end", text=disp_text, image=icon_folder, values=(share,))
else:
tree_shares.insert(machine_id, "end", text=disp_text, values=(share,))
first = tree_shares.get_children()[0]
tree_shares.selection_set(first)
on_tree_select(None)
if not auto:
msg = f"{len(shares)} partage(s) trouvé(s) sur le réseau !"
if not shutil.which("smbclient"):
msg += "\n\n(Note: Installez 'smbclient' via le terminal système pour afficher les noms exacts des dossiers)."
messagebox.showinfo("Scanner", msg)
else:
if not auto:
messagebox.showinfo("Scanner", "Aucune machine trouvée.")
def on_tree_select(e):
selected = tree_shares.selection()
if selected:
item = tree_shares.item(selected[0])
if item.get('values'):
share_path = item['values'][0]
try:
machine_name = tree_shares.item(tree_shares.parent(selected[0]))['text'].strip()
entry_share_var.set(f"{machine_name} -> {share_path}")
except:
entry_share_var.set(share_path)
tree_shares.bind("<<TreeviewSelect>>", on_tree_select)
btn_scan = tk.Button(frame_top, text=" Actualiser", image=window.icons.get('btn_search'), compound="left", command=on_scan, bg="#ffeb3b")
btn_scan.pack(anchor="e", pady=5)
tk.Label(dialog, text="Adresse du partage sélectionné (ex: NAS -> //192.168.1.10/Data):").pack(pady=(10,0))
entry_share_var = tk.StringVar()
entry_share = tk.Entry(dialog, textvariable=entry_share_var, width=50)
entry_share.pack(pady=5)
tk.Label(dialog, text="Nom d'utilisateur (laisser vide si invité) :").pack(pady=5)
entry_user = tk.Entry(dialog, width=30)
entry_user.pack(pady=5)
tk.Label(dialog, text="Mot de passe :").pack(pady=5)
entry_pass = tk.Entry(dialog, width=30, show="*")
entry_pass.pack(pady=5)
def on_connect():
share_input = entry_share_var.get().strip()
user = entry_user.get()
passw = entry_pass.get()
if not share_input: return
if " -> " in share_input:
machine_str, share = share_input.split(" -> ", 1)
else:
machine_str, share = "", share_input
if not share.startswith("//"):
messagebox.showerror("Erreur", "Le format doit être //IP/Partage")
return
share_parts = share.strip("/").split("/")
if len(share_parts) < 2:
messagebox.showerror("Erreur", "Veuillez préciser le nom du dossier à la fin (ex: //192.168.1.10/Documents)")
return
server_ip = share_parts[0]
share_name = share_parts[1]
machine_clean = machine_str.split(" (")[0]
machine_clean = "".join(c for c in machine_clean if c.isalnum() or c in ('-','_','.')).strip()
machine_dir = f"{machine_clean}_{server_ip}" if machine_clean and machine_clean.lower() != "inconnu" else server_ip
mount_point = os.path.expanduser(f"~/Réseau/{machine_dir}/{share_name}")
os.makedirs(mount_point, exist_ok=True)
if os.path.ismount(mount_point):
messagebox.showinfo("Information", f"Ce partage est déjà connecté dans :\n{mount_point}")
path_var.set(mount_point)
refresh_list()
dialog.destroy()
return
opts = f"username={user},password={passw}" if user else "guest"
cmd = f"mount -t cifs '{share}' '{mount_point}' -o {opts},uid=$(id -u),gid=$(id -g)"
res = subprocess.run(['sudo', '-S', 'bash', '-c', cmd], input=pwd+'\n', capture_output=True, text=True)
if res.returncode == 0:
messagebox.showinfo("Succès", f"Partage monté dans :\n{mount_point}")
path_var.set(mount_point)
refresh_list()
dialog.destroy()
else:
messagebox.showerror("Erreur", f"Échec du montage:\n{res.stderr}\n\nAvez-vous installé cifs-utils ?")
btn_frame = tk.Frame(dialog)
btn_frame.pack(pady=15)
tk.Button(btn_frame, text="Annuler", command=dialog.destroy).pack(side="left", padx=5)
tk.Button(btn_frame, text="Connecter", bg="lightgreen", command=on_connect).pack(side="left", padx=5)
dialog.bind("<Escape>", lambda e: dialog.destroy())
dialog.after(200, lambda: on_scan(auto=True))
btn_network = tk.Button(top_frame, text=" Réseau", image=window.icons.get('btn_network'), compound="left", relief="flat", bg="lightblue", command=connect_network)
btn_network.pack(side="right", padx=5, pady=5)
def umount_all():
pwd = app_manager.desktop.settings.get("sudo_pwd") if app_manager else None
if not pwd: return
reseau_dir = os.path.expanduser("~/Réseau")
if not os.path.exists(reseau_dir): return
has_unmounted = False
for root, dirs, files in os.walk(reseau_dir, topdown=False):
for d in dirs:
full_path = os.path.join(root, d)
if os.path.ismount(full_path):
subprocess.run(['sudo', '-S', 'umount', full_path], input=pwd+'\n', text=True)
has_unmounted = True
subprocess.run(['sudo', '-S', 'find', reseau_dir, '-type', 'd', '-empty', '-delete'], input=pwd+'\n', text=True)
if has_unmounted and app_manager:
app_manager.desktop.show_toast("Tous les partages ont été déconnectés.")
def finish_umount():
if path_var.get().startswith(os.path.expanduser("~/Réseau")):
path_var.set(os.path.expanduser("~"))
refresh_list()
window.after(500, finish_umount)
btn_umount = tk.Button(top_frame, text=" Tout démonter", image=window.icons.get('btn_disconnect'), compound="left", relief="flat", bg="#ffcccc", command=umount_all)
btn_umount.pack(side="right", padx=5, pady=5)
btn_go = tk.Button(top_frame, text="Aller", relief="flat", command=lambda: refresh_list())
btn_go.pack(side="right", padx=5, pady=5)
show_hidden = tk.BooleanVar(value=False)
list_frame = tk.Frame(window)
list_frame.pack(fill="both", expand=True)
scrollbar = tk.Scrollbar(list_frame)
scrollbar.pack(side="right", fill="y")
style = ttk.Style()
style.configure("Explorateur.Treeview", rowheight=24, font=("Arial", 10))
tree = ttk.Treeview(list_frame, show="tree", yscrollcommand=scrollbar.set, style="Explorateur.Treeview")
tree.pack(side="left", fill="both", expand=True)
scrollbar.config(command=tree.yview)
def refresh_list(event=None):
current_path = path_var.get()
if not os.path.exists(current_path) or not os.path.isdir(current_path):
return
tree.delete(*tree.get_children())
tree.insert("", "end", iid="..", text=" .. (Dossier parent)", image=window.icons.get('icon_folder'))
try:
items = os.listdir(current_path)
items.sort(key=lambda x: (not os.path.isdir(os.path.join(current_path, x)), x.lower()))
for item in items:
if not show_hidden.get() and item.startswith('.'):
continue
is_dir = os.path.isdir(os.path.join(current_path, item))
if is_dir:
icon = window.icons.get('icon_folder')
else:
ext = os.path.splitext(item)[1].lower()
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
icon = window.icons.get('icon_file_img')
elif ext in ['.txt', '.md', '.log', '.csv']:
icon = window.icons.get('icon_file_txt')
elif ext in ['.py']:
icon = window.icons.get('icon_file_py')
else:
icon = window.icons.get('icon_file_generic')
tree.insert("", "end", iid=item, text=" " + item, image=icon)
except PermissionError:
tree.insert("", "end", iid="ACCESS_DENIED", text=" <Accès refusé>", image=window.icons.get('icon_file_generic'))
def get_selected_item_path(item_id):
if not item_id or item_id == ".." or item_id == "ACCESS_DENIED":
return None
return os.path.join(path_var.get(), item_id)
def on_double_click(event):
selection = tree.selection()
if not selection:
return
item_id = selection[0]
current_path = path_var.get()
if item_id == "..":
new_path = os.path.dirname(current_path)
path_var.set(new_path)
refresh_list()
elif item_id == "ACCESS_DENIED":
return
else:
new_path = os.path.join(current_path, item_id)
if os.path.isdir(new_path):
path_var.set(new_path)
refresh_list()
else:
ext = os.path.splitext(item_id)[1].lower()
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
if app_manager:
app_manager.launch_app({"name": "Visionneuse", "module": "apps.visionneuse.app"}, filepath=new_path)
elif ext in ['.html', '.htm']:
if app_manager:
app_manager.launch_app({"name": "Navigateur Web", "module": "apps.navigateur.app"}, filepath=new_path)
elif ext in ['.py', '.json', '.sh', '.c', '.cpp', '.h', '.js', '.css', '.md']:
if app_manager:
app_manager.launch_app({"name": "Éditeur de Code", "module": "apps.editeur.app"}, filepath=new_path)
else:
if app_manager:
app_manager.launch_app({"name": "Bloc-notes", "module": "apps.blocnotes.app"}, filepath=new_path)
# --- Actions de gestion de fichiers ---
def ask_string_dialog(title, prompt, callback, initialvalue=""):
dialog_frame = tk.Frame(window, bg="white", highlightbackground="black", highlightthickness=2)
dialog_frame.place(relx=0.5, rely=0.5, anchor="center", width=300, height=130)
tk.Label(dialog_frame, text=title, bg="lightgray", font=("Arial", 10, "bold")).pack(fill="x")
tk.Label(dialog_frame, text=prompt, bg="white").pack(pady=5)
entry_var = tk.StringVar(value=initialvalue)
entry = tk.Entry(dialog_frame, textvariable=entry_var)
entry.pack(padx=10, fill="x")
def on_ok(event=None):
val = entry_var.get()
dialog_frame.destroy()
if val:
callback(val)
def on_cancel(event=None):
dialog_frame.destroy()
btn_frame = tk.Frame(dialog_frame, bg="white")
btn_frame.pack(pady=10)
tk.Button(btn_frame, text="OK", command=on_ok, width=8).pack(side="left", padx=5)
tk.Button(btn_frame, text="Annuler", command=on_cancel, width=8).pack(side="left", padx=5)
entry.bind("<Return>", on_ok)
entry.bind("<Escape>", on_cancel)
window.after(100, lambda: entry.focus_set())
def new_file():
def on_name(name):
try:
new_path = os.path.join(path_var.get(), name)
if not os.path.exists(new_path):
open(new_path, 'a').close()
refresh_list()
else:
messagebox.showerror("Erreur", "Un fichier avec ce nom existe déjà.")
except Exception as e:
messagebox.showerror("Erreur", f"Impossible de créer le fichier :\n{e}")
ask_string_dialog("Nouveau Fichier", "Nom du nouveau fichier texte :", on_name)
def new_folder():
def on_name(name):
try:
new_path = os.path.join(path_var.get(), name)
os.makedirs(new_path, exist_ok=False)
refresh_list()
except Exception as e:
messagebox.showerror("Erreur", f"Impossible de créer le dossier :\n{e}")
ask_string_dialog("Nouveau Dossier", "Nom du nouveau dossier :", on_name)
def rename_item(item_id):
old_path = get_selected_item_path(item_id)
if not old_path: return
old_name = os.path.basename(old_path)
def on_name(new_name):
if new_name != old_name:
try:
new_path = os.path.join(os.path.dirname(old_path), new_name)
os.rename(old_path, new_path)
refresh_list()
except Exception as e:
messagebox.showerror("Erreur", f"Impossible de renommer :\n{e}")
ask_string_dialog("Renommer", "Nouveau nom :", on_name, initialvalue=old_name)
def delete_item(item_id):
target_path = get_selected_item_path(item_id)
if not target_path: return
if target_path.startswith(os.path.expanduser("~/Réseau")):
messagebox.showerror("Interdit", "La suppression est interdite sur les partages réseau montés par sécurité.")
return
target_name = os.path.basename(target_path)
is_dir = os.path.isdir(target_path)
msg = f"Voulez-vous vraiment supprimer définitivement le {'dossier' if is_dir else 'fichier'} '{target_name}' ?"
if is_dir:
msg += "\n\nATTENTION: Tout le contenu du dossier sera supprimé !"
if messagebox.askyesno("Confirmer la suppression", msg, icon='warning'):
try:
if is_dir:
shutil.rmtree(target_path)
else:
os.remove(target_path)
refresh_list()
except Exception as e:
messagebox.showerror("Erreur", f"Impossible de supprimer :\n{e}")
def create_shortcut(item_id):
target_path = get_selected_item_path(item_id)
if not target_path: return
target_name = os.path.basename(target_path)
import json
desktop_config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "config", "desktop.json")
try:
icons = []
if os.path.exists(desktop_config_path):
with open(desktop_config_path, "r") as f:
icons = json.load(f)
x = 20 + (len(icons) % 10) * 80
y = 20 + (len(icons) // 10) * 80
icons.append({"name": target_name, "path": target_path, "x": x, "y": y})
with open(desktop_config_path, "w") as f:
json.dump(icons, f, indent=4)
if app_manager and hasattr(app_manager, "desktop"):
app_manager.desktop.load_desktop_icons()
app_manager.desktop.show_toast(f"Raccourci '{target_name}' créé.")
except Exception as e:
messagebox.showerror("Erreur", f"Impossible de créer le raccourci :\n{e}")
def print_item(item_id):
target_path = get_selected_item_path(item_id)
if not target_path or os.path.isdir(target_path): return
try:
res = subprocess.run(['lp', target_path], capture_output=True, text=True)
if res.returncode == 0:
if app_manager and hasattr(app_manager, "desktop"):
app_manager.desktop.show_toast(f"Impression lancée pour '{os.path.basename(target_path)}'")
else:
messagebox.showinfo("Impression", f"Fichier envoyé à l'imprimante :\n{os.path.basename(target_path)}")
else:
messagebox.showerror("Erreur Impression", f"Impossible d'imprimer:\n{res.stderr}\n\nAvez-vous configuré une imprimante par défaut ?")
except FileNotFoundError:
messagebox.showerror("Erreur", "La commande 'lp' est introuvable. Installez 'cups' pour imprimer.")
def copy_item(item_id):
if not app_manager: return
target_path = get_selected_item_path(item_id)
if target_path:
app_manager.desktop.file_clipboard = {"action": "copy", "path": target_path}
app_manager.desktop.show_toast(f"Copié : {os.path.basename(target_path)}")
def cut_item(item_id):
if not app_manager: return
target_path = get_selected_item_path(item_id)
if target_path:
app_manager.desktop.file_clipboard = {"action": "cut", "path": target_path}
app_manager.desktop.show_toast(f"Coupé : {os.path.basename(target_path)}")
def paste_item():
if not app_manager or not hasattr(app_manager.desktop, "file_clipboard"): return
clip = app_manager.desktop.file_clipboard
if not clip.get("path"): return
src = clip["path"]
action = clip["action"]
dst_dir = path_var.get()
dst = os.path.join(dst_dir, os.path.basename(src))
if not os.path.exists(src):
messagebox.showerror("Erreur", "Le fichier source n'existe plus.")
return
if os.path.exists(dst):
messagebox.showerror("Erreur", "Un fichier ou dossier de ce nom existe déjà ici.")
return
try:
if action == "copy":
if os.path.isdir(src):
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
app_manager.desktop.show_toast(f"Collé : {os.path.basename(src)}")
elif action == "cut":
shutil.move(src, dst)
app_manager.desktop.file_clipboard = {"action": None, "path": None}
app_manager.desktop.show_toast(f"Déplacé : {os.path.basename(src)}")
refresh_list()
except Exception as e:
messagebox.showerror("Erreur", f"Erreur :\n{e}")
def disconnect_network(m_path):
pwd = app_manager.desktop.settings.get("sudo_pwd") if app_manager else None
if not pwd: return
has_unmounted = False
if os.path.ismount(m_path):
subprocess.run(['sudo', '-S', 'umount', m_path], input=pwd+'\n', text=True)
has_unmounted = True
else:
for root, dirs, files in os.walk(m_path, topdown=False):
for d in dirs:
full_p = os.path.join(root, d)
if os.path.ismount(full_p):
subprocess.run(['sudo', '-S', 'umount', full_p], input=pwd+'\n', text=True)
has_unmounted = True
if has_unmounted:
subprocess.run(['sudo', '-S', 'find', m_path, '-type', 'd', '-empty', '-delete'], input=pwd+'\n', text=True)
if not os.path.exists(m_path):
# Clean up parent if it became empty
parent = os.path.dirname(m_path)
subprocess.run(['sudo', '-S', 'find', parent, '-type', 'd', '-empty', '-delete'], input=pwd+'\n', text=True)
if app_manager: app_manager.desktop.show_toast(f"Déconnecté : {os.path.basename(m_path)}")
def finish_disconnect():
if not os.path.exists(path_var.get()):
path_var.set(os.path.expanduser("~/Réseau"))
refresh_list()
window.after(500, finish_disconnect)
else:
messagebox.showerror("Erreur", "Aucun point de montage actif trouvé ici.")
# --- Menu Contextuel ---
context_menu = tk.Menu(window, tearoff=0)
def show_context_menu(event):
item_id = tree.identify_row(event.y)
tree.selection_remove(tree.selection())
context_menu.delete(0, tk.END)
if item_id:
tree.selection_set(item_id)
target_path = get_selected_item_path(item_id)
if item_id not in ("..", "ACCESS_DENIED"):
context_menu.add_command(label=" Copier", command=lambda: window.after(50, copy_item, item_id))
if not target_path.startswith(os.path.expanduser("~/Réseau")):
context_menu.add_command(label=" Couper", command=lambda: window.after(50, cut_item, item_id))
context_menu.add_separator()
context_menu.add_command(label=" Renommer", image=window.icons.get('menu_rename'), compound="left", command=lambda: window.after(50, rename_item, item_id))
context_menu.add_command(label=" Supprimer", image=window.icons.get('menu_delete'), compound="left", command=lambda: window.after(50, delete_item, item_id))
else:
context_menu.add_separator()
context_menu.add_command(label=" Raccourci Bureau", command=lambda: window.after(50, create_shortcut, item_id))
if target_path and os.path.isfile(target_path):
ext = os.path.splitext(target_path)[1].lower()
if ext in ['.txt', '.py', '.md', '.json', '.log', '.csv', '.png', '.jpg', '.jpeg']:
context_menu.add_command(label=" Imprimer", image=window.icons.get('btn_print'), compound="left", command=lambda: window.after(50, print_item, item_id))
if target_path and target_path.startswith(os.path.expanduser("~/Réseau")) and target_path != os.path.expanduser("~/Réseau"):
rel_path = os.path.relpath(target_path, os.path.expanduser("~/Réseau"))
if len(rel_path.split(os.sep)) <= 2:
context_menu.add_command(label=" Déconnecter", image=window.icons.get('menu_delete'), compound="left", command=lambda p=target_path: window.after(50, disconnect_network, p))
context_menu.add_separator()
if app_manager and hasattr(app_manager.desktop, "file_clipboard") and app_manager.desktop.file_clipboard.get("path"):
context_menu.add_command(label=" Coller", image=window.icons.get('btn_paste'), compound="left", command=lambda: window.after(50, paste_item))
context_menu.add_separator()
context_menu.add_command(label=" Nouveau dossier", image=window.icons.get('menu_new_folder'), compound="left", command=lambda: window.after(50, new_folder))
context_menu.add_command(label=" Nouveau fichier texte", image=window.icons.get('menu_new_file'), compound="left", command=lambda: window.after(50, new_file))
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
# Bindings
tree.bind("<Double-Button-1>", on_double_click)
tree.bind("<Button-3>", show_context_menu) # Clic droit
# Fermer le menu si on clique ailleurs
window.bind("<Button-1>", lambda e: context_menu.unpost(), add="+")
tree.bind("<Button-1>", lambda e: context_menu.unpost(), add="+")
path_entry.bind("<Return>", refresh_list)
# Boutons d'accès rapide
btn_go = tk.Button(top_frame, text="Aller", command=refresh_list, relief="flat")
btn_go.pack(side="right", padx=2, pady=2)
btn_term = tk.Button(top_frame, text=">_ Terminal", command=lambda: app_manager.launch_app({"name": "Terminal", "module": "apps.terminal.app"}, filepath=path_var.get()) if app_manager else None, relief="flat", bg="#e0e0e0")
btn_term.pack(side="right", padx=5, pady=2)
chk_hidden = tk.Checkbutton(top_frame, text="Fichiers cachés", variable=show_hidden, command=refresh_list, bg="lightgray")
chk_hidden.pack(side="right", padx=5)
def show_help():
msg = (
"Aide de l'Explorateur\n\n"
"Navigation :\n"
"• Double-cliquez sur un dossier pour y entrer.\n"
"• Double-cliquez sur '.. (Dossier parent)' pour remonter.\n"
"• Double-cliquez sur un fichier pour l'ouvrir.\n\n"
"Actions :\n"
"• Clic-droit sur un élément pour le renommer ou le supprimer.\n"
"• Le menu Clic-droit permet aussi de créer des fichiers/dossiers."
)
messagebox.showinfo("Aide Explorateur", msg)
btn_help = tk.Button(top_frame, text=" Aide", image=(app_manager.desktop.icons.get("btn_help") if app_manager else None), compound="left", command=show_help, relief="flat", bg="lightblue")
btn_help.pack(side="right", padx=5, pady=2)
refresh_list()