chore: cleanup comments
This commit is contained in:
@@ -9,8 +9,6 @@ import qs.Config
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Enables a smooth scroll animation only for search jumps, so normal
|
||||
// flicking stays instant.
|
||||
property bool animateScroll: false
|
||||
readonly property int cappedWidth: Math.min(800, width)
|
||||
default property Item contentChild
|
||||
@@ -31,7 +29,7 @@ ColumnLayout {
|
||||
function findAnchor(item: Item, anchor: string): Item {
|
||||
if (!item)
|
||||
return null;
|
||||
if (item.settingAnchor !== undefined && item.settingAnchor === anchor) // qmllint disable missing-property
|
||||
if (item.settingAnchor !== undefined && item.settingAnchor === anchor)
|
||||
return item;
|
||||
const kids = item.children;
|
||||
for (let i = 0; i < kids.length; i++) {
|
||||
@@ -42,14 +40,12 @@ ColumnLayout {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Flash a row without scrolling (used when re-selecting the current setting).
|
||||
function highlightAnchor(anchor: string): void {
|
||||
const row = findAnchor(contentChild, anchor);
|
||||
if (row && row.flashHighlight !== undefined) // qmllint disable missing-property
|
||||
row.flashHighlight(); // qmllint disable missing-property
|
||||
if (row && row.flashHighlight !== undefined)
|
||||
row.flashHighlight();
|
||||
}
|
||||
|
||||
// When the settings search jumps to this page, scroll to the matching row.
|
||||
function scrollToAnchor(anchor: string): bool {
|
||||
if (!anchor || !contentChild)
|
||||
return false;
|
||||
@@ -57,8 +53,6 @@ ColumnLayout {
|
||||
if (!row)
|
||||
return false;
|
||||
const pos = row.mapToItem(flickable.contentItem, 0, 0);
|
||||
// Land the row below the top fade so it isn't dimmed by the edge effect,
|
||||
// clamped to the flickable's real scroll range (which includes margins).
|
||||
const inset = flickable.height * flickable.fadeAmount + Appearance.padding.large;
|
||||
const minY = -flickable.topMargin;
|
||||
const maxY = Math.max(minY, flickable.contentHeight + flickable.bottomMargin - flickable.height);
|
||||
@@ -66,8 +60,8 @@ ColumnLayout {
|
||||
root.animateScroll = true;
|
||||
flickable.contentY = target;
|
||||
Qt.callLater(() => root.animateScroll = false);
|
||||
if (row.flashHighlight !== undefined) // qmllint disable missing-property
|
||||
row.flashHighlight(); // qmllint disable missing-property
|
||||
if (row.flashHighlight !== undefined)
|
||||
row.flashHighlight();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -86,10 +80,6 @@ ColumnLayout {
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
// Pages like the ethernet detail load their content asynchronously
|
||||
// (device info, IP config), so the layout keeps growing for a while.
|
||||
// Wait until contentHeight has held steady for a few frames (or we've
|
||||
// waited long enough) before scrolling, so the target doesn't drift.
|
||||
const h = flickable.contentHeight;
|
||||
if (h === lastHeight && h > flickable.height)
|
||||
stableFrames++;
|
||||
@@ -119,9 +109,8 @@ ColumnLayout {
|
||||
target: root.sState
|
||||
}
|
||||
|
||||
MouseArea { // Prevent clicks from reaching flickable
|
||||
Layout.bottomMargin: -flickable.topMargin // Extra height to block clicks on flickable top margin
|
||||
|
||||
MouseArea {
|
||||
Layout.bottomMargin: -flickable.topMargin
|
||||
implicitHeight: header.implicitHeight - Layout.bottomMargin
|
||||
implicitWidth: header.implicitWidth
|
||||
z: 1
|
||||
|
||||
@@ -10,9 +10,6 @@ import qs.Modules.Settings
|
||||
VerticalFadeFlickable {
|
||||
id: root
|
||||
|
||||
// Results grouped by their top-level page, so the list can show one heading
|
||||
// per page with the matching settings joined underneath it (like the
|
||||
// Android settings search). Each group: { page, entries: [...] }.
|
||||
readonly property var groups: {
|
||||
const out = [];
|
||||
const byPage = ({});
|
||||
@@ -154,12 +151,6 @@ VerticalFadeFlickable {
|
||||
ListView {
|
||||
id: resultList
|
||||
|
||||
// Grouped results: the model is one entry per top-level page, and
|
||||
// each delegate renders that page's heading plus the matching
|
||||
// settings joined into a single rounded card (first/last rounded,
|
||||
// middles square, thin dividers between them), like the Android
|
||||
// settings search. A ScriptModel diffs the groups so only changed
|
||||
// ones animate. Scrolling is delegated to the outer flickable.
|
||||
Layout.fillWidth: true
|
||||
cacheBuffer: 10000
|
||||
implicitHeight: contentHeight
|
||||
@@ -175,7 +166,6 @@ VerticalFadeFlickable {
|
||||
spacing: Appearance.spacing.small
|
||||
width: resultList.width
|
||||
|
||||
// Group heading: the top-level page name, shown once.
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Appearance.padding.small
|
||||
@@ -196,7 +186,6 @@ VerticalFadeFlickable {
|
||||
}
|
||||
}
|
||||
|
||||
// The matching settings, joined into one card.
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
@@ -220,9 +209,6 @@ VerticalFadeFlickable {
|
||||
const h = resultLayout.implicitHeight + resultLayout.anchors.margins * 2;
|
||||
return h % 2 === 0 ? h : h + 1;
|
||||
}
|
||||
// Joined card: round only the outer corners so the
|
||||
// rows read as one block (square where they meet),
|
||||
// matching the page tabs' corner radius.
|
||||
topLeftRadius: isFirst ? Appearance.rounding.large : 0
|
||||
topRightRadius: isFirst ? Appearance.rounding.large : 0
|
||||
|
||||
@@ -242,11 +228,9 @@ VerticalFadeFlickable {
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
// Leave room on the right for the toggle switch.
|
||||
anchors.rightMargin: result.modelData.togglePath ? toggle.width + Appearance.padding.large * 2 : Appearance.padding.large
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
// Location line: deepest icon + "Section > sub", faint.
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
@@ -261,7 +245,6 @@ VerticalFadeFlickable {
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
// The setting itself, most prominent.
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
@@ -271,7 +254,6 @@ VerticalFadeFlickable {
|
||||
textFormat: Text.StyledText
|
||||
}
|
||||
|
||||
// Optional description, faintest and smallest.
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
@@ -312,15 +294,7 @@ VerticalFadeFlickable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The list's implicitHeight tracks contentHeight; while items animate
|
||||
// their position the reported height fluctuates, which left gaps in
|
||||
// the surrounding layout on fast typing. So additions, removals and
|
||||
// reordering are all instant - no transitions - keeping the height
|
||||
// correct at every frame.
|
||||
model: ScriptModel {
|
||||
// Match groups by their page so content updates in place rather
|
||||
// than rebuilding the delegate when ranking shifts the order.
|
||||
objectProp: "pageIdx"
|
||||
values: root.groups
|
||||
}
|
||||
|
||||
@@ -6,37 +6,13 @@ import Quickshell
|
||||
import ZShell
|
||||
import qs.Config
|
||||
|
||||
// Search service over the settings index. The index is generated at build time
|
||||
// from the page QML files by scripts/build-settings-index.py and baked into the
|
||||
// plugin binary (read via CUtils.settingsIndex), so it stays in sync with the UI
|
||||
// without any hand-maintained entries and without a user-editable data file.
|
||||
//
|
||||
// Unlike the launcher's fuzzy searcher, this uses the real inverted index +
|
||||
// ranking baked into the JSON: a query is tokenised, each token is looked up in
|
||||
// the inverted index (exact token or prefix), the matching entry ids are scored
|
||||
// with the precomputed per-token ranking, and the best entries are returned.
|
||||
// SettingEntry QObjects are produced via Variants so the result objects expose
|
||||
// the same properties the result list expects.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// fzf finder over the entries (title + keywords), used as a fuzzy fallback
|
||||
// when the exact/prefix index lookup comes up short. fzf is the same matcher
|
||||
// the launcher uses, so typo and mid-word matching behave consistently.
|
||||
property var fzfFinder: null
|
||||
|
||||
// entries: forward index (one record per setting)
|
||||
// inverted: token -> [entry id...]
|
||||
// ranking: token -> { entry id (string): weight }
|
||||
property var inverted: ({})
|
||||
property var ranking: ({})
|
||||
|
||||
// Wrap the parts of `text` that match the search in the given colour, for use
|
||||
// with a StyledText in Text.StyledText format. Matches each query token as a
|
||||
// prefix at a word boundary (mirroring how lookup matches), so "wall"
|
||||
// highlights the start of "wallpaper". StyledText supports <font color> but
|
||||
// not CSS <span style>. HTML-significant characters are escaped first so the
|
||||
// rich-text parser doesn't choke on names with & < or >.
|
||||
function highlight(text: string, search: string, colour: color): string {
|
||||
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const tokens = tokenize(search);
|
||||
@@ -47,9 +23,6 @@ Singleton {
|
||||
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
|
||||
}
|
||||
|
||||
// Look up a query token in the inverted index: exact match first, then any
|
||||
// indexed token that starts with it (prefix search, so "wif" finds "wifi").
|
||||
// Returns a map of entry id -> best ranking weight for that id.
|
||||
function lookup(token: string): var {
|
||||
const result = ({});
|
||||
const exact = root.inverted[token] !== undefined;
|
||||
@@ -71,31 +44,21 @@ Singleton {
|
||||
if (tokens.length === 0)
|
||||
return [];
|
||||
|
||||
// Accumulate a score per entry id across all query tokens. An entry must
|
||||
// match every query token (AND), and its score is the sum of the ranking
|
||||
// weights of the index tokens it matched, so results stay relevant.
|
||||
const scores = ({});
|
||||
const hitCounts = ({});
|
||||
for (const token of tokens) {
|
||||
const matches = root.lookup(token); // { id: weight }
|
||||
const matches = root.lookup(token);
|
||||
for (const id in matches) {
|
||||
scores[id] = (scores[id] ?? 0) + matches[id];
|
||||
hitCounts[id] = (hitCounts[id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score, breaking ties by id so the order is stable (otherwise
|
||||
// entries with equal scores can be dropped arbitrarily by the limit).
|
||||
const ranked = Object.keys(scores).filter(id => hitCounts[id] === tokens.length).sort((a, b) => scores[b] - scores[a] || (parseInt(a) - parseInt(b))).slice(0, 25);
|
||||
|
||||
const all = entries.instances;
|
||||
const out = ranked.map(id => all[parseInt(id)]).filter(e => e !== undefined);
|
||||
|
||||
// The inverted index only does exact/prefix matches. When it finds little
|
||||
// or nothing - a typo ("trasparency") or a mid-word query ("paper") - fall
|
||||
// back to fzf over the same entries. fzf hits that the index already
|
||||
// returned are skipped, and the rest are appended after the (stronger)
|
||||
// index results, so precise matches always lead.
|
||||
if (out.length < 5 && root.fzfFinder) {
|
||||
const seen = ({});
|
||||
for (const id of ranked)
|
||||
@@ -127,9 +90,6 @@ Singleton {
|
||||
entries.model = data.entries;
|
||||
root.inverted = data.inverted ?? {};
|
||||
root.ranking = data.ranking ?? {};
|
||||
// One searchable string per entry: the title. fzf provides typo and
|
||||
// mid-word matching over titles as a fallback when the exact/prefix
|
||||
// index lookup comes up short.
|
||||
const docs = data.entries.map((e, i) => ({
|
||||
idx: i,
|
||||
text: e.title
|
||||
@@ -164,12 +124,7 @@ Singleton {
|
||||
readonly property var subPath: modelData.subPath
|
||||
readonly property string subtext: modelData.subtext ?? ""
|
||||
readonly property string title: modelData.title
|
||||
|
||||
// A non-empty togglePath means this is a plain on/off setting that can be
|
||||
// flipped straight from the results (e.g. "background.wallpaperEnabled").
|
||||
readonly property string togglePath: modelData.togglePath ?? ""
|
||||
// Live value of the config property, read by walking the path on
|
||||
// GlobalConfig. Re-evaluates when that property changes.
|
||||
readonly property bool toggleValue: {
|
||||
if (!isToggle)
|
||||
return false;
|
||||
@@ -183,7 +138,6 @@ Singleton {
|
||||
return obj ?? false;
|
||||
}
|
||||
|
||||
// Write `value` back to the config property the path points at.
|
||||
function setToggle(value: bool): void {
|
||||
if (!isToggle)
|
||||
return;
|
||||
|
||||
@@ -29,35 +29,24 @@ QtObject {
|
||||
subPageIdxStack.pop();
|
||||
}
|
||||
|
||||
// Jump straight to a setting from search: open the page, then any sub-pages
|
||||
// along subPath, then let the page scroll to the anchor. subPageIdxStack is
|
||||
// filled directly so a freshly loaded StackPage opens the whole chain at
|
||||
// once (see StackPage.Component.onCompleted), which avoids the half-open
|
||||
// state that firing openSubPage signals one by one would cause.
|
||||
function jumpToSetting(pageIdx: int, subPath: var, anchor: string): void {
|
||||
const samePage = currentPageIdx === pageIdx;
|
||||
const sameSub = subPageIdxStack.length === subPath.length && subPath.every((v, i) => subPageIdxStack[i] === v);
|
||||
if (samePage && sameSub && anchor === lastAnchor) {
|
||||
// Re-clicking the exact same setting: flash it again, don't scroll.
|
||||
highlightSetting(anchor);
|
||||
return;
|
||||
}
|
||||
lastAnchor = anchor;
|
||||
if (samePage && sameSub) {
|
||||
// Same page, different setting: just scroll to it.
|
||||
searchAnchor = "";
|
||||
searchAnchor = anchor;
|
||||
return;
|
||||
}
|
||||
// Different page, or same page but different sub-page: point at the
|
||||
// target sub-page chain and load the destination page, which scrolls to
|
||||
// the anchor once it's ready.
|
||||
searchAnchor = anchor;
|
||||
if (!samePage) {
|
||||
pendingSubPath = subPath.slice();
|
||||
currentPageIdx = pageIdx;
|
||||
} else {
|
||||
// Same page: close back to the page root, then open the chain.
|
||||
while (subPageIdxStack.length > 0)
|
||||
closeSubPage();
|
||||
for (let i = 0; i < subPath.length; i++)
|
||||
|
||||
Reference in New Issue
Block a user