Files
DankMaterialShell/quickshell/Modules/Settings/WidgetPickerWindow.qml
T
purian23 5623476126 feat(island): host the island as a widget in any bar configuration
- The island is now a bar widget so any bar can host one alongside the island layout and dot
- Routing got simpler: pick which shortcuts open here, the rest follow the shared rules
- IPC close/hide take down every surface showing the activity
- Qt 6.11 pointer prune workaround so the expanded island stays clickable
2026-09-27 02:02:30 -04:00

260 lines
8.1 KiB
QML

import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Settings.Widgets
DankFloatingWindow {
id: root
property var widgets: []
property string searchQuery: ""
property var filteredWidgets: []
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property var parentModal: null
property string headerTitle: title
property string intro: ""
property bool featuredFirst: false
property bool showEmptyState: false
property Component widgetDelegate: null
parentWindow: parentModal
signal widgetChosen(var widget)
function widgetMatches(widget, query) {
const label = (widget.name || widget.text || "").toLowerCase();
const description = (widget.description || "").toLowerCase();
const id = (widget.id || "").toLowerCase();
return label.indexOf(query) !== -1 || description.indexOf(query) !== -1 || id.indexOf(query) !== -1;
}
function updateFilteredWidgets() {
const source = widgets || [];
const query = (searchQuery || "").toLowerCase();
const filtered = query ? source.filter(widget => widgetMatches(widget, query)) : source.slice();
if (featuredFirst)
filtered.sort((a, b) => a.featured === b.featured ? 0 : (a.featured ? -1 : 1));
filteredWidgets = filtered;
selectedIndex = -1;
keyboardNavigationActive = false;
}
onWidgetsChanged: updateFilteredWidgets()
function selectableIndex(from, step) {
for (let i = from; i >= 0 && i < filteredWidgets.length; i += step) {
if (!filteredWidgets[i].disabled)
return i;
}
return -1;
}
function selectNext() {
const next = selectableIndex(selectedIndex + 1, 1);
if (next < 0)
return;
keyboardNavigationActive = true;
selectedIndex = next;
}
function selectPrevious() {
if (filteredWidgets.length === 0)
return;
keyboardNavigationActive = true;
selectedIndex = selectableIndex(selectedIndex - 1, -1);
if (selectedIndex === -1)
keyboardNavigationActive = false;
}
function selectWidget() {
if (selectedIndex < 0 || selectedIndex >= filteredWidgets.length)
return;
widgetChosen(filteredWidgets[selectedIndex]);
}
function restoreParentFocus() {
if (!parentModal)
return;
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
Qt.callLater(() => {
if (parentModal && parentModal.modalFocusScope)
parentModal.modalFocusScope.forceActiveFocus();
});
}
function show() {
updateFilteredWidgets();
if (parentModal)
parentModal.shouldHaveFocus = false;
visible = true;
Qt.callLater(() => searchField.forceActiveFocus());
}
function hide() {
visible = false;
restoreParentFocus();
}
minimumSize: Qt.size(400, 350)
implicitWidth: 500
implicitHeight: 550
visible: false
onClosed: hide()
onVisibleChanged: {
if (visible) {
updateFilteredWidgets();
Qt.callLater(() => searchField.forceActiveFocus());
return;
}
searchQuery = "";
filteredWidgets = [];
selectedIndex = -1;
keyboardNavigationActive = false;
restoreParentFocus();
}
FocusScope {
id: widgetKeyHandler
anchors.fill: parent
focus: true
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
root.hide();
event.accepted = true;
return;
case Qt.Key_Down:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
root.selectPrevious();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (root.keyboardNavigationActive)
root.selectWidget();
else if (root.selectableIndex(0, 1) >= 0)
root.widgetChosen(root.filteredWidgets[root.selectableIndex(0, 1)]);
event.accepted = true;
return;
}
if (!(event.modifiers & Qt.ControlModifier))
return;
switch (event.key) {
case Qt.Key_N:
case Qt.Key_J:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_P:
case Qt.Key_K:
root.selectPrevious();
event.accepted = true;
return;
}
}
Column {
anchors.fill: parent
spacing: 0
DankWindowHeader {
id: titleBar
width: parent.width
controls: windowControls
title: root.headerTitle
onCloseRequested: root.hide()
}
Item {
width: parent.width
height: parent.height - titleBar.height
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
visible: root.intro !== ""
text: root.intro
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
width: parent.width
wrapMode: Text.WordWrap
}
DankSearchField {
id: searchField
width: parent.width
height: Theme.fieldHeightLarge
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search widgets...")
text: root.searchQuery
focus: true
ignoreLeftRightKeys: true
keyForwardTargets: [widgetKeyHandler]
onTextEdited: {
root.searchQuery = text;
root.updateFilteredWidgets();
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.hide();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0))
event.accepted = false;
}
}
DankListView {
id: widgetList
width: parent.width
height: parent.height - y
spacing: Theme.spacingS
model: root.filteredWidgets
clip: true
delegate: root.widgetDelegate
footer: root.showEmptyState ? emptyState : null
}
}
}
}
}
Component {
id: emptyState
Item {
width: widgetList.width
height: emptyText.visible ? 60 : 0
StyledText {
id: emptyText
visible: root.filteredWidgets.length === 0
text: root.searchQuery.length > 0 ? I18n.tr("No widgets match your search") : I18n.tr("No widgets available")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
}
}
FloatingWindowControls {
id: windowControls
targetWindow: root
}
}