Headless-Titelleiste (Tauri decorations:false): Fensterknöpfe links + Drag-Region, Tauri-Fenstersteuerung + Window-Capabilities
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Basis-Permissions + Fenstersteuerung fuer die randlose (decorations:false) Titelleiste: Minimieren/Maximieren/Schliessen + Ziehen der eigenen Titelleiste (data-tauri-drag-region).",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-unmaximize",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-is-maximized",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-start-dragging"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,10 +10,12 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
"label": "main",
|
||||||
"title": "cad",
|
"title": "cad",
|
||||||
"width": 1400,
|
"width": 1400,
|
||||||
"height": 900,
|
"height": 900,
|
||||||
"dragDropEnabled": false
|
"dragDropEnabled": false,
|
||||||
|
"decorations": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
+2
-1
@@ -5317,7 +5317,8 @@ body {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-left: 6px;
|
/* Ganz links in der randlosen Titelleiste (macOS-Konvention). */
|
||||||
|
margin-right: 4px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.win-btn {
|
.win-btn {
|
||||||
|
|||||||
+12
-4
@@ -1299,7 +1299,15 @@ export function TopBar({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="topbar" ref={headerRef}>
|
// `data-tauri-drag-region`: die randlose (decorations:false) Titelleiste ist
|
||||||
|
// selbst der Fenster-Ziehbereich. Kind-Buttons/Dropdowns bleiben klickbar;
|
||||||
|
// nur die leeren Zonen ziehen das Fenster.
|
||||||
|
<header className="topbar" ref={headerRef} data-tauri-drag-region>
|
||||||
|
{/* Fensterknöpfe (Schliessen/Minimieren/Maximieren) GANZ LINKS — die
|
||||||
|
randlose Titelleiste beginnt mit ihnen (macOS-Konvention). Nur unter
|
||||||
|
Electron/Tauri sichtbar (sonst null). */}
|
||||||
|
<WindowControls />
|
||||||
|
|
||||||
{/* Kleine Wortmarke „dossier" (OCS-Stil, klein/kleingeschrieben) + Quick-
|
{/* Kleine Wortmarke „dossier" (OCS-Stil, klein/kleingeschrieben) + Quick-
|
||||||
Access-Leiste: alle Datei-/Export-Aktionen + Ressourcen/Einstellungen
|
Access-Leiste: alle Datei-/Export-Aktionen + Ressourcen/Einstellungen
|
||||||
als kleine Icons direkt in der Zeile (ersetzt das Burger-Menü). */}
|
als kleine Icons direkt in der Zeile (ersetzt das Burger-Menü). */}
|
||||||
@@ -1328,12 +1336,12 @@ export function TopBar({
|
|||||||
Bänder: diese schmale Leiste (Chrome + Tabs) und den Inhalt. */}
|
Bänder: diese schmale Leiste (Chrome + Tabs) und den Inhalt. */}
|
||||||
<RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />
|
<RibbonTabs tab={ribbonTab} onTab={onRibbonTab} />
|
||||||
|
|
||||||
{/* Rechte Gruppe: Layout-Menü · Projektname · Fensterknöpfe. Datei-/Export-
|
{/* Rechte Gruppe: Layout-Menü · Projektname. Die Fensterknöpfe sitzen jetzt
|
||||||
Aktionen liegen jetzt in der Quick-Access-Leiste links (kein Burger). */}
|
ganz links (macOS-Titelleisten-Konvention); Datei-/Export-Aktionen in
|
||||||
|
der Quick-Access-Leiste (kein Burger). */}
|
||||||
<div className="topbar-right">
|
<div className="topbar-right">
|
||||||
{layoutMenu}
|
{layoutMenu}
|
||||||
<span className="project-name">{project.name}</span>
|
<span className="project-name">{project.name}</span>
|
||||||
<WindowControls />
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
+70
-27
@@ -1,33 +1,75 @@
|
|||||||
// Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das randlose
|
// Eigene Fenstersteuerung (Minimieren/Maximieren/Schliessen) für das RANDLOSE
|
||||||
// Electron-Fenster (frame:false, s. scripts/electron-main.cjs) — ohne native
|
// Fenster — sowohl Electron (frame:false, s. scripts/electron-main.cjs) ALS AUCH
|
||||||
// Titelleiste gibt es sonst keine Möglichkeit, das Fenster zu steuern.
|
// Tauri (decorations:false, s. tauri.conf.json). Ohne native Titelleiste gibt es
|
||||||
|
// sonst keine Möglichkeit, das Fenster zu steuern.
|
||||||
//
|
//
|
||||||
// Rendert NUR, wenn `window.dossierWindow` existiert (vom Preload-Skript
|
// Rendert NUR, wenn eine Fenster-API vorhanden ist: unter Electron
|
||||||
// injiziert) — also nur unter Electron. In Tauri (native Fensterdekoration)
|
// `window.dossierWindow` (Preload), unter Tauri `getCurrentWindow()`
|
||||||
// und im Browser-Dev-Modus (?vite) bleibt die Komponente unsichtbar (`null`),
|
// (@tauri-apps/api/window, dynamisch geladen wie in nativeSync.ts). Im reinen
|
||||||
// dort steuert das Betriebssystem bzw. der Browser das Fenster.
|
// Browser-Dev-Modus (?vite) bleibt die Komponente unsichtbar (`null`) — dort
|
||||||
|
// steuert der Browser das Fenster. Die Tauri-Permissions liegen in
|
||||||
|
// src-tauri/capabilities/default.json (core:window:allow-minimize/…/close).
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
/** Von electron-preload.cjs via contextBridge injiziert (nur unter Electron). */
|
/** Gemeinsame, minimale Fenster-API (Electron-Preload ODER Tauri-Adapter). */
|
||||||
interface DossierWindowApi {
|
interface WindowApi {
|
||||||
minimize: () => void;
|
minimize: () => void;
|
||||||
toggleMaximize: () => void;
|
toggleMaximize: () => void;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
isMaximized: () => Promise<boolean>;
|
isMaximized: () => Promise<boolean>;
|
||||||
|
/** Abo auf Maximiert-Wechsel; liefert eine Abmelde-Funktion. */
|
||||||
onMaximizedChange: (callback: (isMaximized: boolean) => void) => () => void;
|
onMaximizedChange: (callback: (isMaximized: boolean) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
dossierWindow?: DossierWindowApi;
|
dossierWindow?: WindowApi;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tauri-Laufzeit erkannt (analog nativeSync.ts). */
|
||||||
|
function isTauri(): boolean {
|
||||||
|
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||||
|
}
|
||||||
|
|
||||||
export function WindowControls() {
|
export function WindowControls() {
|
||||||
const api = window.dossierWindow;
|
const [api, setApi] = useState<WindowApi | null>(null);
|
||||||
const [maximized, setMaximized] = useState(false);
|
const [maximized, setMaximized] = useState(false);
|
||||||
|
|
||||||
|
// Passende Fenster-API einmalig auflösen: Electron synchron, Tauri asynchron
|
||||||
|
// (dynamischer Import, damit Browser-/Build-Pfad ohne @tauri-apps grün bleibt).
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.dossierWindow) {
|
||||||
|
setApi(window.dossierWindow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isTauri()) return;
|
||||||
|
let disposed = false;
|
||||||
|
import(/* @vite-ignore */ "@tauri-apps/api/window")
|
||||||
|
.then(({ getCurrentWindow }) => {
|
||||||
|
if (disposed) return;
|
||||||
|
const w = getCurrentWindow();
|
||||||
|
setApi({
|
||||||
|
minimize: () => void w.minimize(),
|
||||||
|
toggleMaximize: () => void w.toggleMaximize(),
|
||||||
|
close: () => void w.close(),
|
||||||
|
isMaximized: () => w.isMaximized(),
|
||||||
|
onMaximizedChange: (cb) => {
|
||||||
|
// Bei jeder Größenänderung den Maximiert-Zustand neu melden.
|
||||||
|
const unlisten = w.onResized(() => {
|
||||||
|
void w.isMaximized().then(cb);
|
||||||
|
});
|
||||||
|
return () => void unlisten.then((fn) => fn());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -43,8 +85,25 @@ export function WindowControls() {
|
|||||||
|
|
||||||
if (!api) return null;
|
if (!api) return null;
|
||||||
|
|
||||||
|
// Reihenfolge (macOS-Titelleiste links): Schliessen · Minimieren · Maximieren.
|
||||||
return (
|
return (
|
||||||
<div className="win-controls" role="group" aria-label="Fenster">
|
<div className="win-controls" role="group" aria-label="Fenster">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="win-btn win-btn-close"
|
||||||
|
onClick={() => api.close()}
|
||||||
|
title="Schliessen"
|
||||||
|
aria-label="Schliessen"
|
||||||
|
>
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M1 1 L9 9 M9 1 L1 9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="win-btn"
|
className="win-btn"
|
||||||
@@ -86,22 +145,6 @@ export function WindowControls() {
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="win-btn win-btn-close"
|
|
||||||
onClick={() => api.close()}
|
|
||||||
title="Schliessen"
|
|
||||||
aria-label="Schliessen"
|
|
||||||
>
|
|
||||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M1 1 L9 9 M9 1 L1 9"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user