Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/modules/ii/overview/Overview.qml

503 lines
22 KiB
QML

// Pixel3Arch patch to ii's stock Overview.qml. History:
//
// 2026-07-07 — opened the search/overview with the on-screen keyboard
// (GlobalStates.oskOpen) and closed it with the keyboard too.
// 2026-08-05 — finesse pass (TASK-14): the keyboard no longer auto-raises
// with the drawer; it belongs to the search pane and is summoned by tapping
// it (see oskSummoner). The drawer gained a translucent theme sheet
// (panelBg), the dock is suppressed while it is open, and both bodies fill
// 0.78 of the panel height instead of 0.7.
//
// While the OSK is open, this panel does NOT register with
// GlobalFocusGrab.addDismissable — see the onOskOpenChanged handler below.
// Reason: GlobalFocusGrab's HyprlandFocusGrab (hyprland_focus_grab_v1, a
// real Wayland protocol) clears whenever a tap lands outside its
// whitelisted surfaces, and wvkbd (the real OSK — see OnScreenKeyboard.qml)
// is a separate process that CANNOT be added to that whitelist — Quickshell
// only exposes whitelisting its own in-process windows, and there's no
// hyprctl or other lever either. Two attempts at a "shield window" to fake
// wvkbd's inclusion both failed on real device testing (see
// OnScreenKeyboard.qml's history for what was tried and why). So instead of
// fighting the grab, this panel just stops being dismissable-by-outside-tap
// while the keyboard is up — it still closes normally via Escape, the
// explicit toggle, or picking a search result. Everything else in this file
// is unchanged stock ii — diff against upstream before re-applying this
// patch if ii updates.
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions as CF
import Qt.labs.synchronizer
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.modules.souveraine.navigation
Scope {
id: overviewScope
property bool dontAutoCancelSearch: false
// Whether panelWindow is currently registered with GlobalFocusGrab so an
// outside tap dismisses the drawer. Gated on the OSK (see onOskOpenChanged)
// and deliberately tracked rather than add/remove blind: adding the same
// window twice is the double-add race the registration comment below warns
// about, and re-opening the drawer while already registered would re-add.
property bool panelDismissableRegistered: false
PanelWindow {
id: panelWindow
property string searchingText: ""
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
// Two ways in, one surface. The pill's second-stage swipe has set
// `missionControlOpen` since 2026-07, and TASK-14 records that NO
// SURFACE CONSUMES IT — the Auxo-like card surface it was meant to
// raise was never built. WindowOverview is that surface, so the state
// finally reaches something instead of being set and dropped.
//
// Mission control is the cards alone: no search, because it is a
// "switch to what is running" gesture, not a "find something" one.
visible: GlobalStates.overviewOpen || GlobalStates.missionControlOpen
WlrLayershell.namespace: "quickshell:overview"
// Overlay, not Top: the second-stage edge swipe must bring the
// overview up over a fullscreen app (fullscreen renders above Top).
WlrLayershell.layer: WlrLayer.Overlay
// Only the search half wants the keyboard. Mission control taking it
// would summon the OSK over the cards for a surface with no text field.
WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
mask: Region {
// The window only exists where the drawer does: panelBg (which
// wraps the column with a breathing margin) is the drawn surface
// AND the input region. This replaced `columnLayout` when the
// panel sheet was added — the mask must cover what panelBg
// covers, or the sheet renders clipped and its margin is dead
// input space.
item: panelBg
}
anchors {
top: true
bottom: true
left: true
right: true
}
Connections {
target: GlobalStates
function onOverviewOpenChanged() {
if (!GlobalStates.overviewOpen) {
searchWidget.disableExpandAnimation();
overviewScope.dontAutoCancelSearch = false;
GlobalFocusGrab.dismiss();
GlobalStates.oskOpen = false;
// Undo the drawer-time suppression (see below) so the dock
// returns to its pre-drawer posture: its own swipe-down
// state, or the empty-desktop reveal it had coming.
GlobalStates.dockSuppressed = false;
} else {
if (!overviewScope.dontAutoCancelSearch) {
searchWidget.cancelSearch();
}
// Keyboard-less on open — 2026-08-05: the OSK no longer
// auto-raises with the drawer. The keyboard belongs to the
// search pane: tapping it summons the OSK (see oskSummoner
// below), tapping anything else doesn't. The field still
// auto-focuses so the first tap has somewhere to go.
GlobalStates.oskOpen = false;
// No OSK: the drawer is dismissable by outside tap, and it
// must register right here — onOskOpenChanged only fires on
// a keyboard toggle, which no longer happens on open.
if (!overviewScope.panelDismissableRegistered) {
GlobalFocusGrab.addDismissable(panelWindow);
overviewScope.panelDismissableRegistered = true;
}
// The drawer takes the space, so the dock goes down while
// it is open — and suppression, not dockRevealed=false:
// the dock is pinned on the phone, and a pinned dock
// ignores dockRevealed entirely (Dock.qml computeDockState:
// only dockSuppressed beats effectivePinned). The overview
// button lives on the dock, which is why the drawer needs
// other doors in (pill swipe home / IPC). The close branch
// above restores the flag.
GlobalStates.dockSuppressed = true;
}
}
}
// Registering as dismissable is gated on the OSK's state, not
// just overviewOpen — see the file-header comment for why. The
// drawer registers itself in onOverviewOpenChanged (the OSK no
// longer flips on open); this handler only re-tracks when the OSK
// is toggled independently while the drawer stays open.
// One surface at a time. Home and the running-work view are opened by
// different gestures, but they are not modes to be stacked: a swipe to
// mission control while the drawer is up should replace it, not float
// the cards over the grid. Neither flag's writers enforce this, so the
// body's owner does.
Connections {
target: GlobalStates
function onOverviewOpenChanged() {
if (GlobalStates.overviewOpen)
GlobalStates.missionControlOpen = false;
}
function onMissionControlOpenChanged() {
if (GlobalStates.missionControlOpen)
GlobalStates.overviewOpen = false;
}
}
Connections {
target: GlobalStates
function onOskOpenChanged() {
if (!GlobalStates.overviewOpen) return;
if (GlobalStates.oskOpen) {
GlobalFocusGrab.removeDismissable(panelWindow);
overviewScope.panelDismissableRegistered = false;
} else if (!overviewScope.panelDismissableRegistered) {
GlobalFocusGrab.addDismissable(panelWindow);
overviewScope.panelDismissableRegistered = true;
}
}
}
Connections {
target: GlobalFocusGrab
function onDismissed() {
GlobalStates.overviewOpen = false;
}
}
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
function setSearchingText(text) {
searchWidget.setSearchingText(text);
searchWidget.focusFirstItem();
}
// Tap-to-dismiss for the dead space INSIDE the overview page: the
// window's input mask only covers the panel sheet (panelBg), and the
// grid's gaps (between tiles, around the grid) consume nothing —
// taps there used to be silently ignored, which read as the page
// being stuck. Declared before (= stacked below) the column, sized to
// the overview grid only, so the search bar's padding stays inert and
// every real control (workspaces, windows, search) wins the tap.
// While the OSK is up this is the ONLY outside-tap dismiss — the
// focus-grab route is deliberately disabled then (see header).
MouseArea {
enabled: (GlobalStates.overviewOpen || GlobalStates.missionControlOpen)
&& overviewLoader.active
x: columnLayout.x + overviewLoader.x
y: columnLayout.y + overviewLoader.y
width: overviewLoader.width
height: overviewLoader.height
// Closes whichever one is up. Mission control has no focus grab to
// fall back on — it takes no keyboard focus — so this is its only
// outside-tap dismissal, and leaving it out stranded the surface
// with no way back but the rail.
onClicked: {
GlobalStates.overviewOpen = false;
GlobalStates.missionControlOpen = false;
}
}
// The drawer's sheet. A solid, slightly translucent layer of the
// theme surface behind the search bar and grid — the "blur/look"
// finesse item. Deliberately NOT a real GaussianBlur: that needs
// Qt5Compat and dies on the phone's GLES-ish backend (2026-08-05),
// so this is the phone-safe substitute — an opaque-enough sheet that
// the wallpaper reads as a soft base behind it. Declared before the
// column (stacked below it) so nothing here can eat taps; it is
// input-transparent anyway.
Rectangle {
id: panelBg
visible: columnLayout.visible
anchors.fill: columnLayout
// The sheet breathes past the column so the drawer reads as a
// panel, not as widgets floating on the wallpaper. The mask
// follows this same box (see `mask` above).
anchors.leftMargin: -18
anchors.rightMargin: -18
anchors.topMargin: -6
anchors.bottomMargin: -14
radius: Appearance?.rounding?.large ?? 20
// colLayer1 at ~88% — opaque enough that wallpaper detail behind
// the grid is noise, translucent enough to still be "surface".
color: CF.ColorUtils.transparentize(Appearance?.colors?.colLayer1 ?? "#1a1a1a", 0.12)
border.width: 1
border.color: CF.ColorUtils.transparentize(Appearance?.colors?.colOnLayer1 ?? "#ffffff", 0.88)
}
Column {
id: columnLayout
// Both ways in. The panel, the mask and the loader were all moved
// to `overviewOpen || missionControlOpen` when mission control
// landed and this was left behind, so the second-stage pill swipe
// raised a panel whose entire contents were invisible — the cards
// were built, instantiated, and never shown.
visible: GlobalStates.overviewOpen || GlobalStates.missionControlOpen
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
}
spacing: -8
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
GlobalStates.overviewOpen = false;
}
}
SearchWidget {
id: searchWidget
// Search belongs to the overview, not to mission control:
// "switch to what is running" has nothing to type into, and
// the panel deliberately takes no keyboard focus in that mode
// (see `keyboardFocus` above), so a field here would be one
// you could see and not use.
visible: GlobalStates.overviewOpen
anchors.horizontalCenter: parent.horizontalCenter
Synchronizer on searchingText {
property alias source: panelWindow.searchingText
}
}
Loader {
id: overviewLoader
anchors.horizontalCenter: parent.horizontalCenter
active: (GlobalStates.overviewOpen || GlobalStates.missionControlOpen)
&& (Config?.options.overview.enable ?? true)
// Two ways in, two bodies — TASK-14's split. The overview
// (Home) is the app drawer: search above (this file's
// SearchWidget) and AppGrid below. Mission control, raised by
// the pill's second-stage swipe, is the running-work view:
// WindowOverview's cards alone, deliberately no search and no
// keyboard (see `keyboardFocus` above). Previously this one
// body rendered the running cards for BOTH flags, which left
// Home showing what is running instead of what can run.
sourceComponent: GlobalStates.missionControlOpen
? windowOverviewComponent : appGridComponent
}
Component {
id: windowOverviewComponent
// WindowOverview, not OverviewWidget. The latter draws a grid
// of workspaces from HyprlandData; viewtop has neither, so it
// rendered an empty frame and read as the overview being
// broken. See WindowOverview.qml's header.
WindowOverview {
width: overviewLoader.parent.width
height: panelWindow.height * 0.78
visible: (panelWindow.searchingText == "")
onActivated: toplevel => {
toplevel?.activate();
GlobalStates.overviewOpen = false;
GlobalStates.missionControlOpen = false;
}
onClosed: toplevel => toplevel?.close()
}
}
Component {
id: appGridComponent
// The phone app drawer. TASK-14: a paged grid from the
// desktop-entry list, alphabetical; search stays exactly
// as-is on top. See AppGrid.qml's header.
AppGrid {
width: overviewLoader.parent.width
height: panelWindow.height * 0.78
visible: (panelWindow.searchingText == "")
}
}
}
// The search pane is now the keyboard's door (2026-08-05): the OSK
// no longer auto-raises with the drawer, it raises when this pane is
// TAPPED. This overlay sits above the search bar but outside its
// widget tree, so it can add behavior without forking SearchWidget:
// it re-focuses the field first (a tap on the grid may have left it)
// and only then opens the keyboard — the OSK's input redirection
// returns keys to whatever had focus before it opened, so focus must
// land BEFORE the keyboard does. The press propagates on afterwards,
// so the field still gets its normal tap.
MouseArea {
id: oskSummoner
enabled: GlobalStates.overviewOpen && !GlobalStates.oskOpen
visible: columnLayout.visible
// searchWidget is inside the column, not a sibling, so anchors
// cannot bind it — same idiom as the tap-to-dismiss MouseArea:
// geometry in columnLayout-local terms, offset by the column's
// own position within the panel.
x: columnLayout.x + searchWidget.x
y: columnLayout.y + searchWidget.y
width: searchWidget.width
height: searchWidget.height
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
onPressed: (mouse) => {
if (!GlobalStates.oskOpen) {
searchWidget.focusSearchInput();
GlobalStates.oskOpen = true;
}
mouse.accepted = false;
}
}
}
function toggleClipboard() {
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
GlobalStates.overviewOpen = false;
return;
}
overviewScope.dontAutoCancelSearch = true;
panelWindow.setSearchingText(Config.options.search.prefix.clipboard);
GlobalStates.overviewOpen = true;
}
function toggleEmojis() {
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
GlobalStates.overviewOpen = false;
return;
}
overviewScope.dontAutoCancelSearch = true;
panelWindow.setSearchingText(Config.options.search.prefix.emojis);
GlobalStates.overviewOpen = true;
}
// sessiond calls `ipc call overview toggle` for `Action::Overview` — the
// three-finger-tap binding (`device_state.rs`) and anything else that
// reaches for the overview by name. Nothing answered to `overview`: the
// only target here was `search`, so the executor shelled out to a handler
// that did not exist and the tap did nothing, silently. Two things were
// hiding that — the shipped sessiond has no `gesture` verb at all and
// refuses the op before it gets this far, so the second half of the break
// could not be seen from the device.
//
// This makes the existing binding reach the surface it already names. It
// does not decide what the tap *should* raise; that is TASK-55's Q1, and
// the answer there may retarget this without changing it.
IpcHandler {
target: "overview"
function toggle(): void {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
function open(): void {
GlobalStates.overviewOpen = true;
}
function close(): void {
GlobalStates.overviewOpen = false;
}
}
IpcHandler {
target: "search"
function toggle() {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
function workspacesToggle() {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
function close() {
GlobalStates.overviewOpen = false;
}
function open() {
GlobalStates.overviewOpen = true;
}
// Device-side acceptance/debug surface: lets deploy checks exercise
// the exact query path the text field uses without synthesizing keys.
function setQuery(query: string): void {
overviewScope.dontAutoCancelSearch = true;
GlobalStates.overviewOpen = true;
panelWindow.setSearchingText(query);
}
function status(): string {
return JSON.stringify({
open: GlobalStates.overviewOpen,
query: LauncherSearch.query,
results: LauncherSearch.results.length,
desktopEntries: DesktopEntries.applications.values.length,
appSearchEntries: AppSearch.list.length,
widget: searchWidget.debugState()
});
}
function toggleReleaseInterrupt() {
GlobalStates.superReleaseMightTrigger = false;
}
function clipboardToggle() {
overviewScope.toggleClipboard();
}
}
GlobalShortcut {
name: "searchToggle"
description: "Toggles search on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewWorkspacesClose"
description: "Closes overview on press"
onPressed: {
GlobalStates.overviewOpen = false;
}
}
GlobalShortcut {
name: "overviewWorkspacesToggle"
description: "Toggles overview on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "searchToggleRelease"
description: "Toggles search on release"
onPressed: {
GlobalStates.superReleaseMightTrigger = true;
}
onReleased: {
if (!GlobalStates.superReleaseMightTrigger) {
GlobalStates.superReleaseMightTrigger = true;
return;
}
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "searchToggleReleaseInterrupt"
description: "Interrupts possibility of search being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything."
onPressed: {
GlobalStates.superReleaseMightTrigger = false;
}
}
GlobalShortcut {
name: "overviewClipboardToggle"
description: "Toggle clipboard query on overview widget"
onPressed: {
overviewScope.toggleClipboard();
}
}
GlobalShortcut {
name: "overviewEmojiToggle"
description: "Toggle emoji query on overview widget"
onPressed: {
overviewScope.toggleEmojis();
}
}
}