Add the four components the Focus commit referenced but forgot to ship

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 15:14:02 -04:00
parent d5b6e62515
commit b8f88a91f3
4 changed files with 1259 additions and 0 deletions
@@ -0,0 +1,257 @@
// 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: "Apps 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)
}
}
}
}
Rectangle {
id: addChip
height: 26
width: addLabel.implicitWidth + 20
radius: Theme.pillRadius
color: Theme.alpha(Theme.fg, addHover.hovered ? 0.1 : 0.05)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.2)
Text {
id: addLabel
anchors.centerIn: parent
text: root.adding ? "Cancel" : "+ Add app"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
HoverHandler {
id: addHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
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 apps"
}
}
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
}
}
}