// The applications a focus mode lets through, as chips rather than a second // unbounded list of every application that has ever notified. // // The old editor unfolded the whole application universe under every mode, // which meant scrolling a hundred switches to add one exception. What a person // wants to read is the short list of things that may interrupt; adding to it is // a search, not a scan. // // An id that is allowed but no longer known still gets a chip, spelled as the // raw id, so an exception can always be removed even after the application that // earned it is gone. import QtQuick import qs.config import qs.modules.clipboard Column { id: root // The mode's allow list: application ids. property var allow: [] // The same universe the per-application rules use: [{ id, name }]. property var knownApps: [] property bool divider: true signal toggled(string appId, bool allowed) property bool adding: false function nameOf(appId: string): string { const known = root.knownApps.find(app => String(app.id) === appId); return known ? String(known.name) : appId; } readonly property var allowed: (root.allow ?? []).map(String) // Capped at eight: this is a search box, not a browser, and a list long // enough to scroll defeats the point of having replaced one. readonly property var matches: { const needle = search.text.trim().toLowerCase(); const out = []; for (const app of root.knownApps) { const id = String(app.id); if (root.allowed.indexOf(id) >= 0) continue; const haystack = (String(app.name) + " " + id).toLowerCase(); if (needle !== "" && haystack.indexOf(needle) < 0) continue; out.push(app); if (out.length >= 8) break; } return out; } width: parent ? parent.width : 620 spacing: 0 Item { id: head width: parent.width implicitHeight: Math.max(56, Math.max(copy.implicitHeight, chips.implicitHeight) + 20) Column { id: copy anchors.left: parent.left anchors.right: chips.left anchors.rightMargin: 16 anchors.verticalCenter: parent.verticalCenter spacing: 3 Text { width: parent.width text: "May interrupt" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.Medium elide: Text.ElideRight } Text { width: parent.width text: "Applications that break through while this mode is on" color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap } } Flow { id: chips anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: Math.max(160, parent.width * 0.45) spacing: 7 Repeater { model: root.allowed Rectangle { id: chip required property string modelData height: 26 width: chipLabel.implicitWidth + remove.width + 22 radius: Theme.pillRadius color: Theme.alpha(Theme.accent, 0.1) border.width: 1 border.color: Theme.alpha(Theme.accent, 0.28) Text { id: chipLabel anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: root.nameOf(chip.modelData) color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium } Item { id: remove anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: 20 height: 20 Text { anchors.centerIn: parent text: "×" color: removeHover.hovered ? Theme.danger : Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSize + 2 } HoverHandler { id: removeHover cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: root.toggled(chip.modelData, false) } } } } SettingsChip { id: addChip text: root.adding ? "Cancel" : "+ Add application" onClicked: { root.adding = !root.adding; if (!root.adding) search.text = ""; } } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 1 visible: root.divider && !root.adding color: Theme.alpha(Theme.fg, 0.065) } } Column { width: parent.width visible: root.adding spacing: 0 Item { width: parent.width height: 40 SearchField { id: search anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter placeholder: "Search applications" } } Repeater { model: root.matches SettingRow { id: candidate required property var modelData required property int index width: parent.width label: String(candidate.modelData.name ?? "") detail: String(candidate.modelData.id ?? "") controlWidth: 86 divider: candidate.index < root.matches.length - 1 SettingsButton { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: "Add" onClicked: { root.toggled(String(candidate.modelData.id), true); search.text = ""; } } } } SettingRow { width: parent.width visible: root.matches.length === 0 label: "No applications left to add" detail: "Applications appear here after their first notification" divider: false } } }