258 lines
7.9 KiB
QML
258 lines
7.9 KiB
QML
// 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
|
||
}
|
||
}
|
||
}
|