mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
661dc27d26
Reopening the autostart "Browse" picker crashed in
QQmlIncubatorPrivate::incubate. Drop the ListView model on hide()
and rebind it on show() so each open starts with a fresh
QQmlDelegateModel instead of reusing a stale incubation queue.
(cherry picked from commit 3182c70857)
359 lines
13 KiB
QML
359 lines
13 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
FloatingWindow {
|
|
id: root
|
|
|
|
property bool disablePopupTransparency: true
|
|
property string searchQuery: ""
|
|
property var filteredApps: []
|
|
property int selectedIndex: -1
|
|
property bool keyboardNavigationActive: false
|
|
property var appsModel: []
|
|
property var parentModal: null
|
|
parentWindow: parentModal
|
|
|
|
signal appSelected(string appId)
|
|
|
|
objectName: "appBrowserPopup"
|
|
title: I18n.tr("Select Application")
|
|
minimumSize: Qt.size(400, 350)
|
|
implicitWidth: 500
|
|
implicitHeight: 550
|
|
color: "transparent"
|
|
visible: false
|
|
|
|
onClosed: hide()
|
|
|
|
WindowBlur {
|
|
targetWindow: root
|
|
blurX: 0
|
|
blurY: 0
|
|
blurWidth: root.visible ? root.width : 0
|
|
blurHeight: root.visible ? root.height : 0
|
|
blurRadius: Theme.cornerRadius
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: Theme.cornerRadius
|
|
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
|
border.color: Theme.outlineMedium
|
|
border.width: Theme.layerOutlineWidth
|
|
}
|
|
|
|
FocusScope {
|
|
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.selectApp();
|
|
} else if (root.filteredApps.length > 0) {
|
|
root.selectAppByIndex(0);
|
|
}
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: 48
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onPressed: windowControls.tryStartMove()
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.5)
|
|
}
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingL
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "add_circle"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Select Application")
|
|
font.pixelSize: Theme.fontSizeXLarge
|
|
color: Theme.surfaceText
|
|
font.weight: Font.Medium
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingM
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
|
|
DankActionButton {
|
|
iconName: "close"
|
|
iconSize: Theme.iconSize - 4
|
|
iconColor: Theme.surfaceText
|
|
onClicked: root.hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - 48
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
DankTextField {
|
|
id: searchField
|
|
width: parent.width
|
|
height: 48
|
|
cornerRadius: Theme.cornerRadius
|
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, 0.8)
|
|
normalBorderColor: Theme.outlineMedium
|
|
focusedBorderColor: Theme.primary
|
|
leftIconName: "search"
|
|
leftIconSize: Theme.iconSize
|
|
leftIconColor: Theme.surfaceVariantText
|
|
leftIconFocusedColor: Theme.primary
|
|
showClearButton: true
|
|
textColor: Theme.surfaceText
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
placeholderText: I18n.tr("Search applications...")
|
|
text: root.searchQuery
|
|
onTextEdited: {
|
|
root.searchQuery = text;
|
|
root.updateFilteredApps();
|
|
}
|
|
}
|
|
|
|
DankListView {
|
|
id: appList
|
|
width: parent.width
|
|
height: parent.height - searchField.height - Theme.spacingM
|
|
spacing: Theme.spacingS
|
|
// Start with no model. It is (re)bound in show() so that each open
|
|
// gets a fresh QQmlDelegateModel, avoiding a crash where a stale
|
|
// incubation queue from a previous open races with a background
|
|
// refreshApplications() replacing the underlying DesktopEntries
|
|
// QObjectModel (SIGSEGV in QQmlIncubatorPrivate::incubate via
|
|
// libQt6QmlModels). See hide()/onVisibleChanged() for the teardown.
|
|
model: null
|
|
clip: true
|
|
|
|
delegate: Rectangle {
|
|
width: appList.width
|
|
height: 60
|
|
radius: Theme.cornerRadius
|
|
required property int index
|
|
required property var modelData
|
|
|
|
readonly property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
|
|
|
|
color: isSelected ? Theme.withAlpha(Theme.primary, 0.16) : appArea.containsMouse ? Theme.withAlpha(Theme.primary, 0.08) : Theme.withAlpha(Theme.surfaceVariant, 0.3)
|
|
border.color: isSelected ? Theme.primary : Theme.outlineMedium
|
|
border.width: isSelected ? 2 : Theme.layerOutlineWidth
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM
|
|
spacing: Theme.spacingM
|
|
|
|
Image {
|
|
width: 28
|
|
height: 28
|
|
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
|
|
sourceSize.width: 28
|
|
sourceSize.height: 28
|
|
fillMode: Image.PreserveAspectFit
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
onStatusChanged: {
|
|
if (status === Image.Error)
|
|
source = "image://icon/application-x-executable";
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXXS
|
|
width: parent.width - 28 - Theme.spacingM * 3 - 24
|
|
|
|
StyledText {
|
|
text: modelData.name || modelData.id || ""
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
|
|
StyledText {
|
|
text: modelData.comment || modelData.genericName || ""
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.outline
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
DankIcon {
|
|
name: "add"
|
|
size: Theme.iconSize - 4
|
|
color: Theme.primary
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: appArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
const appId = modelData.id || modelData.execString || "";
|
|
root.appSelected(appId);
|
|
root.hide();
|
|
}
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FloatingWindowControls {
|
|
id: windowControls
|
|
targetWindow: root
|
|
}
|
|
}
|
|
|
|
function updateFilteredApps() {
|
|
const allApps = root.appsModel || [];
|
|
var filtered = [];
|
|
if (!searchQuery || searchQuery.length === 0) {
|
|
filtered = allApps.slice();
|
|
} else {
|
|
var query = searchQuery.toLowerCase();
|
|
for (var i = 0; i < allApps.length; i++) {
|
|
var app = allApps[i];
|
|
var name = (app.name || "").toLowerCase();
|
|
var id = (app.id || "").toLowerCase();
|
|
var comment = (app.comment || app.genericName || "").toLowerCase();
|
|
if (name.indexOf(query) !== -1 || id.indexOf(query) !== -1 || comment.indexOf(query) !== -1)
|
|
filtered.push(app);
|
|
}
|
|
}
|
|
filteredApps = filtered;
|
|
selectedIndex = -1;
|
|
keyboardNavigationActive = false;
|
|
}
|
|
|
|
function selectNext() {
|
|
if (filteredApps.length === 0)
|
|
return;
|
|
keyboardNavigationActive = true;
|
|
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
|
|
}
|
|
|
|
function selectPrevious() {
|
|
if (filteredApps.length === 0)
|
|
return;
|
|
keyboardNavigationActive = true;
|
|
selectedIndex = Math.max(selectedIndex - 1, -1);
|
|
if (selectedIndex === -1)
|
|
keyboardNavigationActive = false;
|
|
}
|
|
|
|
function selectApp() {
|
|
if (selectedIndex < 0 || selectedIndex >= filteredApps.length)
|
|
return;
|
|
selectAppByIndex(selectedIndex);
|
|
}
|
|
|
|
function selectAppByIndex(idx) {
|
|
const app = filteredApps[idx];
|
|
if (!app)
|
|
return;
|
|
root.appSelected(app.id || app.execString || "");
|
|
hide();
|
|
}
|
|
|
|
function show() {
|
|
updateFilteredApps();
|
|
// Rebind the model reactively (search relies on filteredApps changes flowing
|
|
// through), and do it after populating so the ListView starts incubating from
|
|
// a fully-formed array rather than [].
|
|
appList.model = Qt.binding(() => root.filteredApps);
|
|
visible = true;
|
|
Qt.callLater(() => searchField.forceActiveFocus());
|
|
}
|
|
|
|
function hide() {
|
|
// Drop the model before clearing visible, so the ListView releases its
|
|
// QQmlDelegateModel (and any in-flight incubators) synchronously on this
|
|
// frame. This is the crux of the fix: a later show() will allocate a fresh
|
|
// DelegateModel instead of reusing one whose incubation queue may hold
|
|
// references invalidated by a concurrent refreshApplications().
|
|
appList.model = null;
|
|
visible = false;
|
|
searchQuery = "";
|
|
filteredApps = [];
|
|
selectedIndex = -1;
|
|
keyboardNavigationActive = false;
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (!visible) {
|
|
// Guard against visibility being cleared without going through hide()
|
|
// (e.g. window manager close, FloatingWindow.onClosed -> hide()).
|
|
appList.model = null;
|
|
searchQuery = "";
|
|
filteredApps = [];
|
|
selectedIndex = -1;
|
|
keyboardNavigationActive = false;
|
|
}
|
|
}
|
|
}
|