Annexe : Application - Bloc-notes

Rôle et utilité

L'application apps/blocnotes/app.py est l'équivalent du célèbre "Notepad" sous Windows. C'est un éditeur de texte pur, minimaliste, permettant à l'utilisateur de prendre des notes rapides, de copier/coller du texte ou de lire de petits fichiers sans la lourdeur d'un traitement de texte complet.

Implémentation technique

Pistes de modification

Code Source

import tkinter as tk
from tkinter import filedialog, messagebox

import os

def start(window, app_manager=None, **kwargs):
    text_area = tk.Text(window, wrap="word", undo=True)
    text_area.pack(fill="both", expand=True)

    menu_frame = tk.Frame(window, bg="lightgray")
    menu_frame.pack(side="top", fill="x", before=text_area)

    current_filepath = kwargs.get("filepath")

    def update_title(modified=False):
        if hasattr(window, 'master') and window.master.__class__.__name__ == 'Window':
            title_text = current_filepath if current_filepath else "Sans titre"
            prefix = "* " if modified else ""
            window.master.title_label.config(text=f"Bloc-notes - {prefix}{title_text}")

    update_title()

    def on_modify(event=None):
        update_title(modified=text_area.edit_modified())

    text_area.bind("<<Modified>>", on_modify)

    def set_clean():
        text_area.edit_modified(False)

    if current_filepath and os.path.exists(current_filepath):
        try:
            with open(current_filepath, "r", encoding="utf-8") as f:
                text_area.insert(tk.END, f.read())
            set_clean()
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible d'ouvrir le fichier :\n{e}")

    def check_unsaved():
        if text_area.edit_modified():
            ans = messagebox.askyesnocancel("Attention", "Voulez-vous enregistrer les modifications ?")
            if ans is True:
                return save_file()
            elif ans is False:
                return True
            else:
                return False
        return True

    def new_file():
        if not check_unsaved(): return
        nonlocal current_filepath
        current_filepath = None
        text_area.delete(1.0, tk.END)
        set_clean()
        update_title()

    def open_file():
        if not check_unsaved(): return
        nonlocal current_filepath
        init_dir = os.path.dirname(current_filepath) if current_filepath else os.path.expanduser("~")
        filepath = filedialog.askopenfilename(initialdir=init_dir, defaultextension=".txt", filetypes=[("All Files", "*.*"), ("Text Files", "*.txt")])
        if not filepath:
            return
        try:
            with open(filepath, "r", encoding="utf-8") as f:
                content = f.read()
            text_area.delete(1.0, tk.END)
            text_area.insert(tk.END, content)
            current_filepath = filepath
            set_clean()
            update_title()
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible d'ouvrir :\n{e}")

    def save_file():
        nonlocal current_filepath
        if not current_filepath:
            return save_file_as()
        try:
            with open(current_filepath, "w", encoding="utf-8") as f:
                f.write(text_area.get(1.0, "end-1c"))
            set_clean()
            update_title()
            return True
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible de sauvegarder :\n{e}")
            return False

    def save_file_as():
        nonlocal current_filepath
        init_dir = os.path.dirname(current_filepath) if current_filepath else os.path.expanduser("~")
        filepath = filedialog.asksaveasfilename(initialdir=init_dir, defaultextension=".txt", filetypes=[("All Files", "*.*"), ("Text Files", "*.txt")])
        if not filepath:
            return False
        current_filepath = filepath
        return save_file()

    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_file', 'menu_open', 'menu_save', 'menu_save_as']:
        ipath = os.path.join(icon_dir, iname + ".png")
        if os.path.exists(ipath):
            window.icons[iname] = tk.PhotoImage(file=ipath)

    btn_new = tk.Button(menu_frame, text=" Nouveau", image=window.icons.get('menu_new_file'), compound="left", command=new_file, relief="flat")
    btn_new.pack(side="left", padx=2, pady=2)

    btn_open = tk.Button(menu_frame, text=" Ouvrir", image=window.icons.get('menu_open'), compound="left", command=open_file, relief="flat")
    btn_open.pack(side="left", padx=2, pady=2)

    btn_save = tk.Button(menu_frame, text=" Enregistrer", image=window.icons.get('menu_save'), compound="left", command=save_file, relief="flat")
    btn_save.pack(side="left", padx=2, pady=2)

    def show_help():
        help_text = (
            "Aide du Bloc-notes\n\n"
            "Raccourcis clavier disponibles :\n"
            "• Ctrl + N : Nouveau fichier\n"
            "• Ctrl + O : Ouvrir un fichier\n"
            "• Ctrl + S : Enregistrer\n"
            "• Ctrl + Shift + S : Enregistrer sous...\n"
            "• Ctrl + C : Copier la sélection\n"
            "• Ctrl + X : Couper la sélection\n"
            "• Ctrl + V : Coller\n"
            "• Ctrl + Z : Annuler la modification\n"
            "• Ctrl + Y : Refaire la modification\n\n"
            "Indicateur visuel :\n"
            "L'étoile (*) dans le titre signifie que le fichier contient des modifications non enregistrées."
        )
        messagebox.showinfo("Aide", help_text)

    btn_save_as = tk.Button(menu_frame, text=" Enregistrer sous...", image=window.icons.get('menu_save_as'), compound="left", command=save_file_as, relief="flat")
    btn_save_as.pack(side="left", padx=2, pady=2)

    btn_help = tk.Button(menu_frame, text=" ? Aide ", command=show_help, relief="flat", bg="lightblue")
    btn_help.pack(side="right", padx=5, pady=2)

    text_area.focus_set()

    text_area.bind("<Control-n>", lambda e: new_file() or "break")
    text_area.bind("<Control-o>", lambda e: open_file() or "break")
    text_area.bind("<Control-s>", lambda e: save_file() or "break")
    text_area.bind("<Control-S>", lambda e: save_file_as() or "break")

    # Lier le gestionnaire de fermeture si possible
    if hasattr(window, 'master') and window.master.__class__.__name__ == 'Window':
        window.master.on_close_callback = check_unsaved