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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user