Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/ii/sidebarLeft/SidebarLeft.qml
Fimeg 630d6912fd sidebar: the keyboard leaves with the panel that summoned it
The drawer clears oskOpen on close and this did not, so the OSK outlived the
surface holding the only text field it was serving — and with nothing focused
there was no obvious way to put it away.

On the state, not on a visibility handler: the panel's `visible` is a binding
to that flag, so reacting to visibility would be reacting to our own effect.
2026-08-07 02:24:24 -04:00

291 lines
9.6 KiB
QML

import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Scope { // Scope
id: root
property bool detach: false
property bool pin: false
property Component contentComponent: SidebarLeftContent {}
property Item sidebarContent
function toggleDetach() {
root.detach = !root.detach;
}
Process { // Dodge cursor away, pin, move cursor back
id: pinWithFunnyHyprlandWorkaroundProc
property var hook: null
property int cursorX;
property int cursorY;
function doIt() {
command = ["hyprctl", "cursorpos"]
hook = (output) => {
cursorX = parseInt(output.split(",")[0]);
cursorY = parseInt(output.split(",")[1]);
doIt2();
}
running = true;
}
function doIt2(output) {
command = ["bash", "-c", "hyprctl dispatch 'hl.dsp.cursor.move({x=9999,y=9999})'"];
hook = () => {
doIt3();
}
running = true;
}
function doIt3(output) {
root.pin = !root.pin;
command = ["bash", "-c", `sleep 0.01; hyprctl dispatch 'hl.dsp.cursor.move({x=${cursorX},y=${cursorY}})'`];
hook = null
running = true;
}
stdout: StdioCollector {
onStreamFinished: {
pinWithFunnyHyprlandWorkaroundProc.hook(text);
}
}
}
function togglePin() {
if (!root.pin) pinWithFunnyHyprlandWorkaroundProc.doIt()
else root.pin = !root.pin;
}
Component.onCompleted: {
root.sidebarContent = contentComponent.createObject(null, {
"scopeRoot": root,
});
sidebarLoader.item.contentParent.children = [root.sidebarContent];
}
onDetachChanged: {
if (root.detach) {
GlobalFocusGrab.removeDismissable(sidebarLoader.item) // Remove sidebar from the focus grab system
sidebarContent.parent = null; // Detach content from sidebar
sidebarLoader.active = false; // Unload sidebar
detachedSidebarLoader.active = true; // Load detached window
detachedSidebarLoader.item.contentParent.children = [sidebarContent];
} else {
sidebarContent.parent = null; // Detach content from window
detachedSidebarLoader.active = false; // Unload detached window
sidebarLoader.active = true; // Load sidebar
sidebarLoader.item.contentParent.children = [sidebarContent];
}
}
Loader {
id: sidebarLoader
active: true
sourceComponent: PanelWindow { // Window
id: panelWindow
visible: GlobalStates.sidebarLeftOpen
property bool extend: false
property real sidebarWidth: panelWindow.extend ? Appearance.sizes.sidebarWidthExtended : Appearance.sizes.sidebarWidth
property var contentParent: sidebarLeftBackground
function hide() {
GlobalStates.sidebarLeftOpen = false
}
exclusionMode: ExclusionMode.Normal
exclusiveZone: root.pin ? sidebarWidth : 0
implicitWidth: Appearance.sizes.sidebarWidthExtended + Appearance.sizes.elevationMargin
WlrLayershell.namespace: "quickshell:sidebarLeft"
// Hyprland 0.49: OnDemand is Exclusive, Exclusive just breaks click-outside-to-close
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
top: true
left: true
bottom: true
}
mask: Region {
item: sidebarLeftBackground
}
onVisibleChanged: {
if (visible && !GlobalStates.oskOpen) {
GlobalFocusGrab.addDismissable(panelWindow);
} else {
GlobalFocusGrab.removeDismissable(panelWindow);
}
}
Connections {
target: GlobalFocusGrab
function onDismissed() {
panelWindow.hide();
}
}
// OSK flee-fix: squeekboard is an external surface not in the
// HyprlandFocusGrab whitelist; typing on it clears the grab and
// hides this panel. While the OSK is open, leave the dismissable
// set so there is nothing to clear. Mirrors Overview.qml.
Connections {
target: GlobalStates
function onOskOpenChanged() {
if (!GlobalStates.sidebarLeftOpen) return;
if (GlobalStates.oskOpen) {
GlobalFocusGrab.removeDismissable(panelWindow);
} else {
GlobalFocusGrab.addDismissable(panelWindow);
}
}
}
// Content
StyledRectangularShadow {
target: sidebarLeftBackground
radius: sidebarLeftBackground.radius
}
Rectangle {
id: sidebarLeftBackground
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: Appearance.sizes.hyprlandGapsOut
anchors.leftMargin: Appearance.sizes.hyprlandGapsOut
width: panelWindow.sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
Behavior on width {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
panelWindow.hide();
}
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_O) {
panelWindow.extend = !panelWindow.extend;
} else if (event.key === Qt.Key_D) {
root.toggleDetach();
} else if (event.key === Qt.Key_P) {
root.togglePin();
}
event.accepted = true;
}
}
}
}
}
Loader {
id: detachedSidebarLoader
active: false
sourceComponent: FloatingWindow {
id: detachedSidebarRoot
property var contentParent: detachedSidebarBackground
color: "transparent"
visible: GlobalStates.sidebarLeftOpen
onVisibleChanged: {
if (!visible) GlobalStates.sidebarLeftOpen = false;
}
Rectangle {
id: detachedSidebarBackground
anchors.fill: parent
color: Appearance.colors.colLayer0
Keys.onPressed: (event) => {
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_D) {
root.toggleDetach();
}
event.accepted = true;
}
}
}
}
}
// The keyboard leaves with the panel that summoned it.
//
// The drawer clears `oskOpen` on close and this did not, so the OSK
// outlived the surface holding the only text field it was serving — and
// with nothing focused there was no obvious way to put it away again.
// Casey, 2026-08-06: the keyboard "can't get it to disappear" on the AI
// side panel.
//
// On the state, not on a visibility handler: the panel's `visible` is a
// binding to this flag, so reacting to visibility would be reacting to
// our own effect (Phosh's rule, and SHELL-ECOSYSTEM's).
Connections {
target: GlobalStates
function onSidebarLeftOpenChanged() {
if (!GlobalStates.sidebarLeftOpen)
GlobalStates.oskOpen = false;
}
}
IpcHandler {
target: "sidebarLeft"
function toggle(): void {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen
}
function close(): void {
GlobalStates.sidebarLeftOpen = false
}
function open(): void {
GlobalStates.sidebarLeftOpen = true
}
}
GlobalShortcut {
name: "sidebarLeftToggle"
description: "Toggles left sidebar on press"
onPressed: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
}
GlobalShortcut {
name: "sidebarLeftOpen"
description: "Opens left sidebar on press"
onPressed: {
GlobalStates.sidebarLeftOpen = true;
}
}
GlobalShortcut {
name: "sidebarLeftClose"
description: "Closes left sidebar on press"
onPressed: {
GlobalStates.sidebarLeftOpen = false;
}
}
GlobalShortcut {
name: "sidebarLeftToggleDetach"
description: "Detach left sidebar into a window/Attach it back"
onPressed: {
root.detach = !root.detach;
}
}
}