492 lines
19 KiB
QML
492 lines
19 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.I3
|
|
import Quickshell.Wayland
|
|
import "modules"
|
|
import "controlcenter"
|
|
import "windows"
|
|
import "datecenter"
|
|
import "notifications"
|
|
import "status"
|
|
import "theme"
|
|
import "clipboard"
|
|
import "overview"
|
|
import "applications"
|
|
|
|
PanelWindow {
|
|
id: bar
|
|
// Identité layer-shell stable pour cibler seulement cette barre dans SwayFX.
|
|
WlrLayershell.namespace: "labfy-sway-bar"
|
|
|
|
required property var modelData
|
|
required property var notificationService
|
|
required property bool startupHost
|
|
required property bool resizeMode
|
|
required property var applicationCoordinator
|
|
required property var keepAwakeController
|
|
screen: modelData
|
|
|
|
// INVARIANT: une seule surface de barre porte l'inhibiteur, même avec
|
|
// plusieurs sorties. La tasse et les demandes applicatives sont deux
|
|
// propriétaires indépendants ; la libération de l'un préserve l'autre.
|
|
IdleInhibitor {
|
|
window: bar
|
|
enabled: bar.startupHost && (bar.keepAwakeController.keepAwake
|
|
|| bar.keepAwakeController.applicationRequestCount > 0)
|
|
}
|
|
|
|
anchors {
|
|
left: true
|
|
right: true
|
|
top: true
|
|
}
|
|
|
|
margins {
|
|
top: 6
|
|
left: 8
|
|
right: 8
|
|
}
|
|
|
|
implicitHeight: 34
|
|
// La couche basse laisse les fenêtres plein écran conserver leur priorité.
|
|
aboveWindows: false
|
|
// CONTRACT: le PopupWindow reçoit les frappes seulement si son parent
|
|
// layer-shell est focusable dès son ouverture. OnDemand n'accapare pas
|
|
// le clavier hors interaction avec la barre ou son popup.
|
|
focusable: true
|
|
color: Theme.panelBackground
|
|
|
|
property string applicationsError: ""
|
|
property string queuedApplicationId: ""
|
|
readonly property string applicationsBackend: (Quickshell.env("XDG_CONFIG_HOME") ||
|
|
(Quickshell.env("HOME") + "/.config")) + "/quickshell/labfy-sway/applications/backend.py"
|
|
function closeApplications() { applicationCoordinator.applicationsOutput = ""; }
|
|
function toggleApplications() {
|
|
if (applicationCoordinator.applicationsOutput !== screen.name) applicationsError = "";
|
|
applicationCoordinator.toggleApplications(screen.name);
|
|
return true;
|
|
}
|
|
function launchApplication(id) {
|
|
if (applicationLaunch.running || launchDelay.running) return;
|
|
queuedApplicationId = id;
|
|
closeApplications();
|
|
// WHY: release exclusive layer focus before the new client requests it.
|
|
launchDelay.start();
|
|
}
|
|
Timer {
|
|
id: launchDelay
|
|
interval: 140; repeat: false
|
|
onTriggered: {
|
|
applicationLaunch.command = ["python3", "-B", bar.applicationsBackend,
|
|
"launch", bar.queuedApplicationId];
|
|
applicationLaunch.running = true;
|
|
}
|
|
}
|
|
Process {
|
|
id: applicationLaunch
|
|
stdout: StdioCollector { id: launchOutput; waitForEnd: true }
|
|
onExited: (code, status) => {
|
|
let result = {};
|
|
try { result = JSON.parse(launchOutput.text); } catch (_) {}
|
|
if (code !== 0 || result.error) {
|
|
bar.applicationsError = result.error || "Lancement impossible";
|
|
if (!bar.applicationCoordinator.applicationsOutput)
|
|
bar.applicationCoordinator.toggleApplications(bar.screen.name);
|
|
}
|
|
bar.queuedApplicationId = "";
|
|
}
|
|
}
|
|
Connections {
|
|
target: bar.applicationCoordinator
|
|
function onApplicationsOpening(output) {
|
|
bar.closeOverview();
|
|
bar.closeClipboard();
|
|
controlCenter.visible = false;
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.closeRemovableMedia();
|
|
}
|
|
}
|
|
IpcHandler {
|
|
target: "applicationsUi-" + bar.screen.name
|
|
function toggle(): bool { return bar.toggleApplications(); }
|
|
function state(): string {
|
|
return JSON.stringify({ loaded: applicationsLoader.active,
|
|
visible: !!applicationsLoader.item && applicationsLoader.item.visible,
|
|
searchFocused: !!applicationsLoader.item && applicationsLoader.item.searchFocused,
|
|
navigationFocused: !!applicationsLoader.item && applicationsLoader.item.navigationFocused,
|
|
gridFocused: !!applicationsLoader.item && applicationsLoader.item.gridFocused,
|
|
category: applicationsLoader.item ? applicationsLoader.item.selectedCategory : "",
|
|
resultCount: applicationsLoader.item ? applicationsLoader.item.visibleApps.length : 0,
|
|
firstIds: applicationsLoader.item ? applicationsLoader.item.visibleApps.slice(0, 4).map(item => item.id) : [],
|
|
bounds: applicationsLoader.item ? {
|
|
x: applicationsLoader.item.cardX, y: applicationsLoader.item.cardY,
|
|
width: applicationsLoader.item.cardWidth,
|
|
height: applicationsLoader.item.cardHeight
|
|
} : null });
|
|
}
|
|
}
|
|
LazyLoader {
|
|
id: applicationsLoader
|
|
active: bar.applicationCoordinator.applicationsOutput === bar.screen.name
|
|
ApplicationsMenu {
|
|
barWindow: bar
|
|
errorMessage: bar.applicationsError
|
|
onMenuDismissed: bar.closeApplications()
|
|
onLaunchRequested: id => bar.launchApplication(id)
|
|
}
|
|
}
|
|
|
|
function closeClipboard() {
|
|
if (clipboardLoader.item && !clipboardLoader.item.closing)
|
|
clipboardLoader.item.dismiss(true);
|
|
}
|
|
// CONTRACT: clic et IPC suivent la même exclusion que les panneaux de barre.
|
|
function toggleClipboard() {
|
|
if (clipboardLoader.active) {
|
|
closeClipboard();
|
|
} else {
|
|
closeApplications();
|
|
closeOverview();
|
|
controlCenter.visible = false;
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.closeRemovableMedia();
|
|
clipboardLoader.active = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// CONTRACT: un seul écran reçoit l'Overview ; une capture sans overlay
|
|
// précède l'ouverture afin de montrer le workspace actif tel qu'affiché.
|
|
function closeOverview() {
|
|
overviewRequested = false;
|
|
if (overviewLoader.item) overviewLoader.item.dismiss();
|
|
}
|
|
function toggleOverview() {
|
|
if (overviewRequested || overviewLoader.active) { closeOverview(); return true; }
|
|
closeApplications();
|
|
closeClipboard();
|
|
controlCenter.visible = false;
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.closeRemovableMedia();
|
|
overviewRequested = true;
|
|
if (!overviewCapture.running) overviewCapture.running = true;
|
|
return true;
|
|
}
|
|
property bool overviewRequested: false
|
|
readonly property string overviewBackend: (Quickshell.env("XDG_CONFIG_HOME") ||
|
|
(Quickshell.env("HOME") + "/.config")) + "/quickshell/labfy-sway/overview/backend.py"
|
|
IpcHandler {
|
|
target: "overviewUi-" + bar.screen.name
|
|
function toggle(): bool { return bar.toggleOverview(); }
|
|
function state(): string {
|
|
return JSON.stringify({ loaded: overviewLoader.active,
|
|
visible: !!overviewLoader.item && overviewLoader.item.visible,
|
|
requested: bar.overviewRequested,
|
|
cards: overviewLoader.item ? overviewLoader.item.cards.map(item => ({
|
|
number: item.number, windows: item.windows.length,
|
|
snapshot: item.snapshot !== "", output: item.output
|
|
})) : [],
|
|
bounds: overviewLoader.item ? {
|
|
x: overviewLoader.item.cardX, y: overviewLoader.item.cardY,
|
|
width: overviewLoader.item.cardWidth, height: overviewLoader.item.cardHeight
|
|
} : null });
|
|
}
|
|
}
|
|
Process {
|
|
id: overviewCapture
|
|
command: ["python3", "-B", bar.overviewBackend, "capture", "--output", bar.screen.name]
|
|
onExited: (code, status) => {
|
|
if (bar.overviewRequested) overviewLoader.active = true;
|
|
else if (code !== 0) console.warn("Capture du workspace impossible");
|
|
}
|
|
}
|
|
// CONTRACT: un workspace nouvellement visible reçoit une capture après
|
|
// que Sway a présenté sa première frame, sans attendre le cycle de 15 s.
|
|
Connections {
|
|
target: I3
|
|
function onRawEvent(event) {
|
|
if (event.type === "workspace") overviewCaptureRefresh.restart();
|
|
}
|
|
}
|
|
Timer {
|
|
id: overviewCaptureRefresh
|
|
interval: 350; repeat: false
|
|
onTriggered: {
|
|
if (!overviewCapture.running && !overviewLoader.active && !clipboardLoader.active
|
|
&& !applicationsLoader.active
|
|
&& !controlCenter.visible && !dateCenter.visible)
|
|
overviewCapture.running = true;
|
|
}
|
|
}
|
|
// Un seul snapshot toutes les 15 secondes pour le workspace actuellement
|
|
// visible ; aucune boucle de capture pendant l'Overview ou un autre panneau.
|
|
Timer {
|
|
interval: 15000; repeat: true; running: true; triggeredOnStart: true
|
|
onTriggered: {
|
|
if (!overviewCapture.running && !overviewLoader.active && !clipboardLoader.active
|
|
&& !applicationsLoader.active
|
|
&& !controlCenter.visible && !dateCenter.visible)
|
|
overviewCapture.running = true;
|
|
}
|
|
}
|
|
LazyLoader {
|
|
id: overviewLoader
|
|
active: false
|
|
WorkspaceOverview {
|
|
barWindow: bar
|
|
onOverviewDismissed: { bar.overviewRequested = false; overviewLoader.active = false; }
|
|
}
|
|
}
|
|
|
|
// CONTRACT: contrôle événementiel par écran pour ouvrir les pages depuis
|
|
// les raccourcis et les vérifications visuelles ; aucun état n'est dupliqué.
|
|
IpcHandler {
|
|
target: "appearanceUi-" + bar.screen.name
|
|
function openPage(page: string): bool {
|
|
if (controlCenter.pages[page] === undefined) return false;
|
|
bar.closeApplications();
|
|
bar.closeClipboard();
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
controlCenter.openPage(page);
|
|
controlCenter.visible = true;
|
|
return true;
|
|
}
|
|
function close(): bool { controlCenter.visible = false; return true; }
|
|
function openDateCenter(): bool { bar.toggleDateCenter(); return true; }
|
|
function openWindowMenu(): bool {
|
|
bar.closeApplications();
|
|
bar.closeClipboard();
|
|
controlCenter.visible = false;
|
|
dateCenter.requestClose();
|
|
return windowStrip.openActiveMenu();
|
|
}
|
|
}
|
|
|
|
// CONTRACT: un seul écran reçoit l'appel IPC ; le composant et ses données
|
|
// n'existent que pendant l'ouverture ou le nettoyage de fermeture.
|
|
IpcHandler {
|
|
target: "clipboardUi-" + bar.screen.name
|
|
function toggle(): bool { return bar.toggleClipboard(); }
|
|
function state(): string {
|
|
// Diagnostic sans nombre, identifiant ni aperçu de presse-papiers.
|
|
return JSON.stringify({ loaded: clipboardLoader.active,
|
|
visible: !!clipboardLoader.item && clipboardLoader.item.visible,
|
|
searchFocused: !!clipboardLoader.item && clipboardLoader.item.searchFocused,
|
|
listLoaded: !!clipboardLoader.item && clipboardLoader.item.listLoaded,
|
|
bounds: clipboardLoader.item ? {
|
|
x: clipboardLoader.item.cardX, y: clipboardLoader.item.cardY,
|
|
width: clipboardLoader.item.cardWidth, height: clipboardLoader.item.cardHeight
|
|
} : null });
|
|
}
|
|
}
|
|
LazyLoader {
|
|
id: clipboardLoader
|
|
active: false
|
|
ClipboardPanel {
|
|
barWindow: bar
|
|
onCleanupFinished: clipboardLoader.active = false
|
|
}
|
|
}
|
|
|
|
// CONTRACT: toute la capsule centrale partage le même popup et la même exclusion XOR.
|
|
function toggleDateCenter() {
|
|
const opening = !dateCenter.visible && !dateCenter.opening;
|
|
closeApplications();
|
|
closeOverview();
|
|
closeClipboard();
|
|
controlCenter.visible = false;
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.closeRemovableMedia();
|
|
if (opening) dateCenter.requestOpen();
|
|
else dateCenter.requestClose();
|
|
}
|
|
|
|
Item {
|
|
id: leftArea
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
// Garder les titres longs hors de la capsule centrale.
|
|
width: Math.max(0, centerClock.x - 8)
|
|
height: bar.height
|
|
clip: true
|
|
|
|
Rectangle {
|
|
id: applicationsButton
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 30; height: 28; radius: 4
|
|
color: bar.applicationCoordinator.applicationsOutput === bar.screen.name
|
|
? Theme.buttonPressed : applicationsPointer.containsMouse
|
|
? Theme.buttonHover : "transparent"
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: ""; color: Theme.lavender
|
|
font.family: "JetBrainsMono Nerd Font"; font.pixelSize: 25
|
|
}
|
|
MouseArea {
|
|
id: applicationsPointer
|
|
anchors.fill: parent; hoverEnabled: true
|
|
onClicked: bar.toggleApplications()
|
|
}
|
|
}
|
|
|
|
// Même séparation neutre que les groupes de droite, sans zone de clic.
|
|
Rectangle {
|
|
id: applicationsSeparator
|
|
anchors.left: applicationsButton.right
|
|
anchors.leftMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 1; height: 14
|
|
color: Theme.separator
|
|
}
|
|
|
|
Rectangle {
|
|
id: resizeIndicator
|
|
visible: bar.resizeMode
|
|
anchors.left: applicationsSeparator.right
|
|
anchors.leftMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: visible ? resizeLabel.implicitWidth + 12 : 0
|
|
height: 24
|
|
radius: 4
|
|
color: Theme.buttonBackground
|
|
Text {
|
|
id: resizeLabel
|
|
anchors.centerIn: parent
|
|
text: "RESIZE"
|
|
font.pixelSize: 11
|
|
font.bold: true
|
|
color: Theme.peach
|
|
}
|
|
}
|
|
|
|
Workspaces {
|
|
id: workspaces
|
|
screen: bar.screen
|
|
anchors.left: resizeIndicator.right
|
|
anchors.leftMargin: resizeIndicator.visible ? 6 : 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Rectangle {
|
|
id: workspacesSeparator
|
|
anchors.left: workspaces.right
|
|
anchors.leftMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 1; height: 14
|
|
color: Theme.separator
|
|
}
|
|
|
|
WindowStrip {
|
|
id: windowStrip
|
|
screen: bar.screen
|
|
anchors.left: workspacesSeparator.right
|
|
anchors.leftMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
maxWidth: Math.max(0, leftArea.width - x - 8)
|
|
onMenuOpened: {
|
|
bar.closeApplications();
|
|
bar.closeOverview();
|
|
bar.closeClipboard();
|
|
controlCenter.visible = false;
|
|
dateCenter.requestClose();
|
|
rightStatusArea.closeRemovableMedia();
|
|
}
|
|
}
|
|
}
|
|
|
|
Clock {
|
|
id: centerClock
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
open: dateCenter.visible || dateCenter.opening
|
|
unreadCount: bar.notificationService.unreadCount
|
|
criticalUnreadCount: bar.notificationService.criticalUnreadCount
|
|
onToggled: bar.toggleDateCenter()
|
|
}
|
|
|
|
Item {
|
|
id: rightArea
|
|
anchors.left: centerClock.right
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: bar.height
|
|
clip: true
|
|
|
|
// INVARIANT: la zone droite s'étend depuis le bord sans modifier l'ancrage central.
|
|
RightStatusArea {
|
|
id: rightStatusArea
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
barWindow: bar
|
|
wifiDevice: controlCenter.wifiDevice
|
|
adapter: controlCenter.adapter
|
|
controlCenterOpen: controlCenter.visible
|
|
audioBrightnessController: controlCenter
|
|
keepAwakeController: bar.keepAwakeController
|
|
clipboardOpen: clipboardLoader.active && !!clipboardLoader.item && !clipboardLoader.item.closing
|
|
onOpenPageRequested: page => {
|
|
bar.closeApplications();
|
|
bar.closeOverview();
|
|
bar.closeClipboard();
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.closeRemovableMedia();
|
|
controlCenter.currentPage = page;
|
|
controlCenter.visible = true;
|
|
}
|
|
onToggleControlCenterRequested: {
|
|
bar.closeApplications();
|
|
bar.closeOverview();
|
|
const opening = !controlCenter.visible;
|
|
bar.closeClipboard();
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.closeRemovableMedia();
|
|
controlCenter.currentPage = 0;
|
|
controlCenter.visible = opening;
|
|
}
|
|
onToggleRemovableMediaRequested: {
|
|
bar.closeApplications();
|
|
bar.closeOverview();
|
|
const opening = !rightStatusArea.removableMediaOpen;
|
|
bar.closeClipboard();
|
|
controlCenter.visible = false;
|
|
dateCenter.requestClose();
|
|
windowStrip.closeMenu();
|
|
rightStatusArea.setRemovableMediaOpen(opening);
|
|
}
|
|
onToggleClipboardRequested: bar.toggleClipboard()
|
|
}
|
|
}
|
|
|
|
ControlCenter {
|
|
id: controlCenter
|
|
barWindow: bar
|
|
keepAwakeController: bar.keepAwakeController
|
|
onThresholdApplied: rightStatusArea.refreshBatteryThreshold()
|
|
}
|
|
|
|
DateCenter {
|
|
id: dateCenter
|
|
barWindow: bar
|
|
clockItem: centerClock
|
|
notificationService: bar.notificationService
|
|
}
|
|
NotificationToastArea {
|
|
barWindow: bar
|
|
service: bar.notificationService
|
|
}
|
|
SessionStartupPromptV2 {
|
|
barWindow: bar
|
|
startupHost: bar.startupHost
|
|
}
|
|
}
|