# Annexe : Noyau - window.py

## Rôle et utilité

Puisque GrimOS démarre directement sur un serveur graphique nu (sans GNOME ni XFCE), il n'existe aucune notion de fenêtre au niveau du système d'exploitation. C'est le fichier `core/window.py` qui invente cette notion. Il crée une fausse fenêtre (un rectangle) contenant une barre de titre, des boutons de contrôle, et la capacité d'être déplacée à la souris.

## Implémentation technique

- Composition Tkinter : La classe `Window` hérite d'un `tk.Frame`. Elle est composée de deux parties : un `title_bar` (le bandeau supérieur coloré contenant le titre et la croix rouge) et un `content_frame` (la zone vide où l'application aura le droit de dessiner).
- Mathématiques du Déplacement (Drag & Drop) : C'est la mécanique la plus complexe du fichier. Le script "écoute" le clic gauche de la souris sur la barre de titre (`<ButtonPress-1>`) pour mémoriser les coordonnées initiales `(x, y)`. Ensuite, lorsqu'il détecte le mouvement (`<B1-Motion>`), il calcule la différence (le delta) et met à jour instantanément la position du composant global sur l'écran (`place(x=..., y=...)`), créant l'illusion parfaite d'une fenêtre qui glisse sous la souris.
- Superposition (Z-Index) : Lorsque l'utilisateur clique sur une fenêtre, le gestionnaire appelle `self.tkraise()`. C'est l'instruction qui ordonne au moteur graphique de placer ce rectangle par-dessus tous les autres, simulant la "mise au premier plan".

## Pistes de modification

- Redimensionnement (Resize) : Actuellement, les fenêtres ont une taille fixe ou calculée. Ajouter des bords réactifs (des minuscules cadres invisibles de 2 pixels sur les bords) écoutant le déplacement de la souris permettrait de coder une fonctionnalité de redimensionnement manuel.
- Animations d'ouverture : Au lieu d'apparaître brutalement, on pourrait utiliser la fonction `after` pour créer une mini-animation de 0.2 secondes, faisant passer la fenêtre d'une taille nulle à sa taille réelle, renforçant l'impression de fluidité du système.

## Code Source

    import tkinter as tk

    class Window(tk.Frame):
        def __init__(self, parent, desktop, app_config, title="Fenêtre", width=400, height=300, x=50, y=50, is_maximized=False, filepath=None):
            super().__init__(parent, highlightbackground="black", highlightthickness=1)
            self.parent = parent
            self.desktop = desktop
            self.app_config = app_config
            self.filepath = filepath

            if filepath:
                import os
                self.title = os.path.basename(filepath)
            else:
                self.title = title

            # Bounding box constraints
            parent_w = parent.winfo_width()
            if parent_w <= 1:
                parent_w = parent.winfo_screenwidth()

            parent_h = parent.winfo_height()
            if parent_h <= 1:
                parent_h = parent.winfo_screenheight()

            if width > parent_w: width = parent_w
            if height > parent_h - 40: height = parent_h - 40
            if x < 0: x = 0
            if y < 0: y = 0
            if x + width > parent_w: x = parent_w - width
            if y + 30 > parent_h: y = parent_h - 60

            self.is_maximized = False
            self.saved_geometry = {"x": x, "y": y, "width": width, "height": height}

            # Récupération du thème
            import sys, os
            sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
            from core.theme import THEMES
            theme_name = self.desktop.settings.get("theme", "GrimOS")
            self.theme_data = THEMES.get(theme_name, THEMES["GrimOS"])

            # Application de la bordure
            self.config(highlightbackground=self.theme_data.get("window_border", "black"))

            self.place(x=x, y=y, width=width, height=height)

            # Title bar
            self.title_bar = tk.Frame(self, bg=self.theme_data.get("title_bg", "darkblue"), relief="raised", bd=1)
            self.title_bar.pack(fill="x")

            self.title_label = tk.Label(self.title_bar, text=self.title, bg=self.theme_data.get("title_bg", "darkblue"), fg=self.theme_data.get("title_fg", "white"), font=("Arial", 10, "bold"))
            self.title_label.pack(side="left", padx=5)

            self.close_btn = tk.Button(self.title_bar, text="X", bg="red", fg="white", command=self.close, bd=0, padx=5)
            self.close_btn.pack(side="right")

            self.max_btn = tk.Button(self.title_bar, text="[ ]", bg="gray", fg="white", command=self.toggle_maximize, bd=0, padx=5)
            self.max_btn.pack(side="right", padx=2)

            self.min_btn = tk.Button(self.title_bar, text="_", bg="gray", fg="white", command=self.minimize, bd=0, padx=5)
            self.min_btn.pack(side="right", padx=2)

            # Content area
            self.content = tk.Frame(self, bg="white")
            self.content.pack(fill="both", expand=True)

            # Resize grip
            self.sizegrip = tk.Label(self, text="◢", fg="gray", bg="lightgray", cursor="bottom_right_corner")
            self.sizegrip.place(relx=1.0, rely=1.0, anchor="se")

            def on_destroy(event):
                if str(event.widget) == str(self):
                    self.desktop.remove_window(self)
            self.bind("<Destroy>", on_destroy, add="+")

            # Bindings
            self.title_bar.bind("<ButtonPress-1>", self.start_drag)
            self.title_label.bind("<ButtonPress-1>", self.start_drag)
            self.title_bar.bind("<B1-Motion>", self.do_drag)
            self.title_label.bind("<B1-Motion>", self.do_drag)

            self.sizegrip.bind("<ButtonPress-1>", self.start_resize)
            self.sizegrip.bind("<B1-Motion>", self.do_resize)

            self._drag_data = {"x": 0, "y": 0}
            self._resize_data = {"width": 0, "height": 0, "x": 0, "y": 0}

            if is_maximized:
                self.toggle_maximize()

            self.desktop.register_window(self)
            self.lift()

        def start_drag(self, event):
            if self.is_maximized:
                return
            self.lift()
            self._drag_data["x"] = event.x
            self._drag_data["y"] = event.y

        def do_drag(self, event):
            if self.is_maximized:
                return
            x = self.winfo_x() - self._drag_data["x"] + event.x
            y = self.winfo_y() - self._drag_data["y"] + event.y
            self.place(x=x, y=y)
            self.update_saved_geometry()

        def start_resize(self, event):
            if self.is_maximized:
                return
            self.lift()
            self._resize_data["x"] = event.x_root
            self._resize_data["y"] = event.y_root
            self._resize_data["width"] = self.winfo_width()
            self._resize_data["height"] = self.winfo_height()

        def do_resize(self, event):
            if self.is_maximized:
                return
            dx = event.x_root - self._resize_data["x"]
            dy = event.y_root - self._resize_data["y"]
            new_width = max(150, self._resize_data["width"] + dx)
            new_height = max(100, self._resize_data["height"] + dy)
            self.place(width=new_width, height=new_height)
            self.update_saved_geometry()

        def toggle_maximize(self):
            if not self.is_maximized:
                self.update_saved_geometry()
                self.config(highlightthickness=0)
                self.place(x=0, y=0, width=self.parent.winfo_width(), height=self.parent.winfo_height())
                self.is_maximized = True
                self.max_btn.config(text="[R]")
            else:
                self.config(highlightthickness=1)
                self.place(x=self.saved_geometry["x"], y=self.saved_geometry["y"],
                           width=self.saved_geometry["width"], height=self.saved_geometry["height"])
                self.is_maximized = False
                self.max_btn.config(text="[ ]")

        def update_saved_geometry(self):
            # Prevent layout artifacts from propagating empty dimensions during creation
            if self.winfo_width() > 10:
                self.saved_geometry = {
                    "x": self.winfo_x(),
                    "y": self.winfo_y(),
                    "width": self.winfo_width(),
                    "height": self.winfo_height()
                }

        def minimize(self):
            self.update_saved_geometry()
            self.place_forget()
            self.desktop.update_taskbar_btn(self)

        def restore(self):
            if self.is_maximized:
                self.config(highlightthickness=0)
                self.place(x=0, y=0, width=self.parent.winfo_width(), height=self.parent.winfo_height())
            else:
                self.config(highlightthickness=1)
                self.place(x=self.saved_geometry["x"], y=self.saved_geometry["y"],
                           width=self.saved_geometry["width"], height=self.saved_geometry["height"])
            self.lift()
            self.desktop.update_taskbar_btn(self)

        def close(self):
            if hasattr(self, 'on_close_callback') and callable(self.on_close_callback):
                if self.on_close_callback() is False:
                    return

            self.update_saved_geometry()
            app_module = self.app_config.get("module")
            if app_module:
                try:
                    import json
                    import os
                    config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', 'applications.json')
                    with open(config_path, 'r', encoding='utf-8') as f:
                        apps = json.load(f)

                    updated = False
                    for app in apps:
                        if app.get("module") == app_module:
                            app["width"] = self.saved_geometry["width"]
                            app["height"] = self.saved_geometry["height"]
                            app["x"] = self.saved_geometry["x"]
                            app["y"] = self.saved_geometry["y"]
                            updated = True
                            break

                    if updated:
                        with open(config_path, 'w', encoding='utf-8') as f:
                            json.dump(apps, f, indent=4)

                        for app in self.desktop.apps:
                            if app.get("module") == app_module:
                                app["width"] = self.saved_geometry["width"]
                                app["height"] = self.saved_geometry["height"]
                                app["x"] = self.saved_geometry["x"]
                                app["y"] = self.saved_geometry["y"]
                                break
                except Exception as e:
                    print(f"Erreur de sauvegarde de géométrie: {e}")

            self.desktop.remove_window(self)
            self.destroy()