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

Pistes de modification

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()