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