mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
settings: added search functionality
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings.Tabs
|
||||
import qs.Modules.Panels.Settings.Tabs.About
|
||||
@@ -38,11 +39,182 @@ Item {
|
||||
property int currentTabIndex: 0
|
||||
property var tabsModel: []
|
||||
property var activeScrollView: null
|
||||
property var activeTabContent: null
|
||||
property bool sidebarExpanded: true
|
||||
|
||||
// Search state
|
||||
property string searchText: ""
|
||||
property var searchIndex: []
|
||||
property var searchResults: []
|
||||
property string highlightLabelKey: ""
|
||||
|
||||
// Signal when close button is clicked
|
||||
signal closeRequested
|
||||
|
||||
// Load search index
|
||||
FileView {
|
||||
id: searchIndexFile
|
||||
path: Quickshell.shellDir + "/Assets/settings-search-index.json"
|
||||
watchChanges: false
|
||||
printErrors: false
|
||||
|
||||
onLoaded: {
|
||||
try {
|
||||
root.searchIndex = JSON.parse(text());
|
||||
} catch (e) {
|
||||
root.searchIndex = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search function
|
||||
onSearchTextChanged: {
|
||||
if (searchText.trim() === "") {
|
||||
searchResults = [];
|
||||
return;
|
||||
}
|
||||
if (searchIndex.length === 0)
|
||||
return;
|
||||
|
||||
// Build searchable items with resolved translations
|
||||
let items = [];
|
||||
for (let j = 0; j < searchIndex.length; j++) {
|
||||
const entry = searchIndex[j];
|
||||
items.push({
|
||||
"labelKey": entry.labelKey,
|
||||
"descriptionKey": entry.descriptionKey,
|
||||
"widget": entry.widget,
|
||||
"tab": entry.tab,
|
||||
"tabLabel": entry.tabLabel,
|
||||
"subTab": entry.subTab,
|
||||
"subTabLabel": entry.subTabLabel || null,
|
||||
"label": I18n.tr(entry.labelKey),
|
||||
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : ""
|
||||
});
|
||||
}
|
||||
|
||||
const results = FuzzySort.go(searchText.trim(), items, {
|
||||
"keys": ["label", "description"],
|
||||
"threshold": -1000,
|
||||
"limit": 20
|
||||
});
|
||||
|
||||
let extracted = [];
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
extracted.push(results[i].obj);
|
||||
}
|
||||
searchResults = extracted;
|
||||
}
|
||||
|
||||
// Navigate to a search result
|
||||
property int _pendingSubTab: -1
|
||||
|
||||
function navigateToResult(entry) {
|
||||
if (entry.tab < 0 || entry.tab >= tabsModel.length)
|
||||
return;
|
||||
|
||||
highlightLabelKey = entry.labelKey;
|
||||
_pendingSubTab = (entry.subTab !== null && entry.subTab !== undefined) ? entry.subTab : -1;
|
||||
|
||||
// Check if we're already on this tab
|
||||
const alreadyOnTab = (currentTabIndex === entry.tab);
|
||||
|
||||
currentTabIndex = entry.tab;
|
||||
|
||||
if (alreadyOnTab && activeTabContent) {
|
||||
// Tab is already loaded, apply subtab + highlight directly
|
||||
if (_pendingSubTab >= 0) {
|
||||
setSubTabIndex(_pendingSubTab);
|
||||
_pendingSubTab = -1;
|
||||
}
|
||||
highlightScrollTimer.targetKey = highlightLabelKey;
|
||||
highlightScrollTimer.restart();
|
||||
}
|
||||
|
||||
// Clear highlight after a delay
|
||||
highlightClearTimer.restart();
|
||||
}
|
||||
|
||||
// Set sub-tab on the currently loaded tab content
|
||||
function setSubTabIndex(subTabIndex) {
|
||||
if (activeTabContent) {
|
||||
setSubTabRecursive(activeTabContent, subTabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function setSubTabRecursive(item, subTabIndex) {
|
||||
if (!item)
|
||||
return false;
|
||||
|
||||
if (item.objectName === "NTabBar") {
|
||||
item.currentIndex = subTabIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
const childCount = item.children ? item.children.length : 0;
|
||||
for (let i = 0; i < childCount; i++) {
|
||||
if (setSubTabRecursive(item.children[i], subTabIndex))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find and highlight a widget by its label key
|
||||
function findAndHighlightWidget(item, labelKey) {
|
||||
if (!item)
|
||||
return null;
|
||||
|
||||
// Check if this item has a matching label
|
||||
if (item.hasOwnProperty("label") && item.label === I18n.tr(labelKey)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Recursively search children
|
||||
if (item.children) {
|
||||
for (let i = 0; i < item.children.length; i++) {
|
||||
const found = findAndHighlightWidget(item.children[i], labelKey);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: highlightClearTimer
|
||||
interval: 3000
|
||||
onTriggered: root.highlightLabelKey = ""
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: highlightScrollTimer
|
||||
interval: 200
|
||||
property string targetKey: ""
|
||||
onTriggered: {
|
||||
if (root.activeTabContent && targetKey) {
|
||||
const widget = root.findAndHighlightWidget(root.activeTabContent, targetKey);
|
||||
if (widget && root.activeScrollView) {
|
||||
// Scroll widget into view
|
||||
const mapped = widget.mapToItem(root.activeScrollView.contentItem, 0, 0);
|
||||
const scrollBar = root.activeScrollView.ScrollBar.vertical;
|
||||
if (scrollBar) {
|
||||
const targetPos = (mapped.y - root.activeScrollView.height / 3) / root.activeScrollView.contentHeight;
|
||||
scrollBar.position = Math.max(0, Math.min(targetPos, 1.0 - scrollBar.size));
|
||||
}
|
||||
|
||||
// Position highlight overlay
|
||||
const overlayPos = widget.mapToItem(tabContentArea, 0, 0);
|
||||
highlightOverlay.x = overlayPos.x - Style.marginM;
|
||||
highlightOverlay.y = overlayPos.y - Style.marginM;
|
||||
highlightOverlay.width = widget.width + Style.marginM * 2;
|
||||
highlightOverlay.height = widget.height + Style.marginM * 2;
|
||||
highlightAnimation.restart();
|
||||
}
|
||||
}
|
||||
targetKey = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Save sidebar state when it changes
|
||||
onSidebarExpandedChanged: {
|
||||
ShellState.setSettingsSidebarExpanded(sidebarExpanded);
|
||||
@@ -428,13 +600,108 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Search input
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("common.search")
|
||||
inputIconName: "search"
|
||||
visible: root.sidebarExpanded
|
||||
opacity: root.sidebarExpanded ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: root.searchText = text
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: Style.marginXL
|
||||
|
||||
// Search results list
|
||||
NListView {
|
||||
id: searchResultsList
|
||||
anchors.fill: parent
|
||||
model: root.searchResults
|
||||
spacing: Style.marginXS
|
||||
visible: root.searchText.trim() !== ""
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
delegate: Rectangle {
|
||||
id: resultItem
|
||||
width: searchResultsList.width - (searchResultsList.verticalScrollBarActive ? Style.marginM : 0)
|
||||
height: resultColumn.implicitHeight + Style.marginS * 2
|
||||
radius: Style.iRadiusS
|
||||
color: resultItem.hovering ? Color.mHover : "transparent"
|
||||
property bool hovering: false
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: resultColumn
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginS
|
||||
anchors.rightMargin: Style.marginS
|
||||
anchors.topMargin: Style.marginXS
|
||||
anchors.bottomMargin: Style.marginXS
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NText {
|
||||
text: I18n.tr(modelData.labelKey)
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: resultItem.hovering ? Color.mOnHover : Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
let t = I18n.tr(modelData.tabLabel);
|
||||
if (modelData.subTabLabel)
|
||||
t += " › " + I18n.tr(modelData.subTabLabel);
|
||||
return t;
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: resultItem.hovering ? Color.mOnHover : Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: resultItem.hovering = true
|
||||
onExited: resultItem.hovering = false
|
||||
onCanceled: resultItem.hovering = false
|
||||
onClicked: {
|
||||
root.navigateToResult(modelData);
|
||||
searchInput.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab list
|
||||
NListView {
|
||||
id: sidebarList
|
||||
visible: root.searchText.trim() === ""
|
||||
anchors.fill: parent
|
||||
model: root.tabsModel
|
||||
spacing: Style.marginXS
|
||||
@@ -633,6 +900,7 @@ Item {
|
||||
|
||||
// Tab content area
|
||||
Rectangle {
|
||||
id: tabContentArea
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: -Style.marginM
|
||||
@@ -640,6 +908,7 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: root.tabsModel
|
||||
delegate: Loader {
|
||||
anchors.fill: parent
|
||||
@@ -677,6 +946,16 @@ Item {
|
||||
if (item && item.hasOwnProperty("screen")) {
|
||||
item.screen = root.screen;
|
||||
}
|
||||
root.activeTabContent = item;
|
||||
// Handle pending subtab + highlight from search navigation
|
||||
if (root.highlightLabelKey) {
|
||||
if (root._pendingSubTab >= 0) {
|
||||
root.setSubTabIndex(root._pendingSubTab);
|
||||
root._pendingSubTab = -1;
|
||||
}
|
||||
highlightScrollTimer.targetKey = root.highlightLabelKey;
|
||||
highlightScrollTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +997,42 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight overlay for search results
|
||||
Rectangle {
|
||||
id: highlightOverlay
|
||||
visible: opacity > 0
|
||||
opacity: 0
|
||||
color: Qt.alpha(Color.mSecondary, 0.12)
|
||||
border.color: Qt.alpha(Color.mSecondary, 0.4)
|
||||
border.width: Style.borderM
|
||||
radius: Style.radiusS
|
||||
z: 100
|
||||
|
||||
SequentialAnimation {
|
||||
id: highlightAnimation
|
||||
|
||||
NumberAnimation {
|
||||
target: highlightOverlay
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 2000
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: highlightOverlay
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+345
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build settings search index from QML source files.
|
||||
|
||||
Parses settings tab QML files to extract searchable metadata
|
||||
(i18n keys, widget types, tab/sub-tab locations).
|
||||
|
||||
Output: Assets/settings-search-index.json
|
||||
|
||||
Usage:
|
||||
python Scripts/dev/build-settings-search-index.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
SETTINGS_DIR = ROOT / "Modules" / "Panels" / "Settings"
|
||||
TABS_DIR = SETTINGS_DIR / "Tabs"
|
||||
OUTPUT = ROOT / "Assets" / "settings-search-index.json"
|
||||
|
||||
# Widget types that have searchable label/description
|
||||
WIDGET_TYPES = (
|
||||
"NToggle",
|
||||
"NComboBox",
|
||||
"NValueSlider",
|
||||
"NSpinBox",
|
||||
"NSearchableComboBox",
|
||||
"NTextInputButton",
|
||||
"NTextInput",
|
||||
"NCheckbox",
|
||||
"NLabel",
|
||||
)
|
||||
|
||||
# Regex patterns
|
||||
RE_WIDGET_OPEN = re.compile(
|
||||
r"^\s*(" + "|".join(WIDGET_TYPES) + r")\s*\{", re.MULTILINE
|
||||
)
|
||||
RE_LABEL = re.compile(r'label:\s*I18n\.tr\("([^"]+)"')
|
||||
RE_DESCRIPTION = re.compile(r'description:\s*I18n\.tr\("([^"]+)"')
|
||||
|
||||
|
||||
def parse_component_declarations(content: str) -> dict[str, str]:
|
||||
"""
|
||||
Parse Component declarations from SettingsContent.qml.
|
||||
|
||||
Returns: component_id -> QML type name (e.g. "generalTab" -> "GeneralTab")
|
||||
"""
|
||||
components = {}
|
||||
# Match patterns like:
|
||||
# Component {
|
||||
# id: generalTab
|
||||
# GeneralTab {}
|
||||
# }
|
||||
pattern = re.compile(
|
||||
r"Component\s*\{\s*\n\s*id:\s*(\w+)\s*\n\s*(\w+)\s*\{",
|
||||
re.MULTILINE,
|
||||
)
|
||||
for m in pattern.finditer(content):
|
||||
comp_id = m.group(1)
|
||||
type_name = m.group(2)
|
||||
components[comp_id] = type_name
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def parse_tabs_model_order(content: str) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Parse updateTabsModel() to get the ordered list of (source_id, label_key) pairs.
|
||||
|
||||
Returns: list of (component_id, i18n_label_key) in display order.
|
||||
"""
|
||||
# Find the updateTabsModel function body
|
||||
match = re.search(r"function updateTabsModel\(\)\s*\{", content)
|
||||
if not match:
|
||||
return []
|
||||
|
||||
func_body = content[match.end():]
|
||||
|
||||
# Extract tab entries: each has "label" and "source" fields
|
||||
entries = []
|
||||
for m in re.finditer(
|
||||
r'"label":\s*"([^"]+)"[^}]*?"source":\s*(\w+)',
|
||||
func_body,
|
||||
re.DOTALL,
|
||||
):
|
||||
entries.append((m.group(2), m.group(1)))
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def build_tab_mappings(content: str) -> tuple[dict[str, int], dict[str, str]]:
|
||||
"""
|
||||
Build mappings from QML type name to tabsModel index and label key.
|
||||
|
||||
Parses Component declarations and updateTabsModel() order.
|
||||
Returns: (type_to_index, type_to_label)
|
||||
- type_to_index: e.g. {"GeneralTab": 0, ...}
|
||||
- type_to_label: e.g. {"GeneralTab": "common.general", ...}
|
||||
"""
|
||||
components = parse_component_declarations(content)
|
||||
entries = parse_tabs_model_order(content)
|
||||
|
||||
type_to_index = {}
|
||||
type_to_label = {}
|
||||
for idx, (source_id, label_key) in enumerate(entries):
|
||||
type_name = components.get(source_id)
|
||||
if type_name:
|
||||
type_to_index[type_name] = idx
|
||||
type_to_label[type_name] = label_key
|
||||
|
||||
return type_to_index, type_to_label
|
||||
|
||||
|
||||
def get_subtab_info(parent_tab_file: Path) -> tuple[list[str], list[str | None]]:
|
||||
"""
|
||||
Parse a parent tab file to get subtab order and labels.
|
||||
|
||||
Returns: (subtab_type_names, subtab_label_keys)
|
||||
- subtab_type_names: list of component names like ["VolumesSubTab", ...]
|
||||
- subtab_label_keys: list of i18n keys like ["common.volumes", ...] (same order)
|
||||
"""
|
||||
content = parent_tab_file.read_text()
|
||||
|
||||
# Extract NTabButton labels in order from NTabBar
|
||||
labels = []
|
||||
in_tabbar = False
|
||||
tabbar_depth = 0
|
||||
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
if not in_tabbar:
|
||||
if re.match(r"NTabBar\s*\{", stripped):
|
||||
in_tabbar = True
|
||||
tabbar_depth = 1
|
||||
continue
|
||||
continue
|
||||
|
||||
tabbar_depth += stripped.count("{") - stripped.count("}")
|
||||
|
||||
if tabbar_depth <= 0:
|
||||
break
|
||||
|
||||
# Match text: I18n.tr("...") inside NTabButton
|
||||
m = re.search(r'text:\s*I18n\.tr\("([^"]+)"', stripped)
|
||||
if m:
|
||||
labels.append(m.group(1))
|
||||
|
||||
# Extract subtab component names from NTabView
|
||||
subtabs = []
|
||||
in_tabview = False
|
||||
tabview_depth = 0
|
||||
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
if not in_tabview:
|
||||
if re.match(r"NTabView\s*\{", stripped):
|
||||
in_tabview = True
|
||||
tabview_depth = 1
|
||||
continue
|
||||
continue
|
||||
|
||||
tabview_depth += stripped.count("{") - stripped.count("}")
|
||||
|
||||
if tabview_depth <= 0:
|
||||
break
|
||||
|
||||
# Match component instantiations like "VolumesSubTab {}" or "VolumesSubTab {"
|
||||
m = re.match(r"(\w+SubTab)\s*\{", stripped)
|
||||
if m:
|
||||
subtabs.append(m.group(1))
|
||||
|
||||
# Pad labels list if shorter than subtabs (shouldn't happen, but safety)
|
||||
while len(labels) < len(subtabs):
|
||||
labels.append(None)
|
||||
|
||||
return subtabs, labels[:len(subtabs)]
|
||||
|
||||
|
||||
def resolve_tab_info(
|
||||
qml_file: Path,
|
||||
type_to_index: dict[str, int],
|
||||
type_to_label: dict[str, str],
|
||||
) -> tuple[int | None, str | None, int | None, str | None]:
|
||||
"""
|
||||
Determine the tab index, tab label, sub-tab index, and sub-tab label for a QML file.
|
||||
|
||||
Returns (tab_index, tab_label_key, sub_tab_index, sub_tab_label_key)
|
||||
"""
|
||||
parent = qml_file.parent
|
||||
stem = qml_file.stem
|
||||
|
||||
# Top-level tab files (directly in Tabs/)
|
||||
if parent == TABS_DIR:
|
||||
tab_index = type_to_index.get(stem)
|
||||
tab_label = type_to_label.get(stem)
|
||||
return tab_index, tab_label, None, None
|
||||
|
||||
# Sub-directory files
|
||||
dir_name = parent.name # e.g. "Audio", "Bar"
|
||||
parent_type = f"{dir_name}Tab" # e.g. "AudioTab"
|
||||
tab_index = type_to_index.get(parent_type)
|
||||
tab_label = type_to_label.get(parent_type)
|
||||
|
||||
if tab_index is None:
|
||||
return None, None, None, None
|
||||
|
||||
# Skip the parent tab file itself (e.g. AudioTab.qml) — still scan for widgets
|
||||
if stem.endswith("Tab") and not stem.endswith("SubTab"):
|
||||
return tab_index, tab_label, None, None
|
||||
|
||||
# Determine sub-tab index and label from parent tab's NTabBar/NTabView
|
||||
parent_tab_file = parent / f"{dir_name}Tab.qml"
|
||||
if not parent_tab_file.exists():
|
||||
return tab_index, tab_label, None, None
|
||||
|
||||
subtab_names, subtab_labels = get_subtab_info(parent_tab_file)
|
||||
try:
|
||||
idx = subtab_names.index(stem)
|
||||
sub_label = subtab_labels[idx] if idx < len(subtab_labels) else None
|
||||
return tab_index, tab_label, idx, sub_label
|
||||
except ValueError:
|
||||
return tab_index, tab_label, None, None
|
||||
|
||||
|
||||
def extract_widget_blocks(content: str) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Extract (widget_type, block_text) pairs from QML content.
|
||||
|
||||
Uses brace-depth tracking to capture the full widget block.
|
||||
"""
|
||||
results = []
|
||||
lines = content.splitlines()
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
m = RE_WIDGET_OPEN.match(lines[i])
|
||||
if m:
|
||||
widget_type = m.group(1)
|
||||
depth = 0
|
||||
block_lines = []
|
||||
j = i
|
||||
|
||||
while j < len(lines):
|
||||
line = lines[j]
|
||||
block_lines.append(line)
|
||||
depth += line.count("{") - line.count("}")
|
||||
if depth <= 0:
|
||||
break
|
||||
j += 1
|
||||
|
||||
block_text = "\n".join(block_lines)
|
||||
results.append((widget_type, block_text))
|
||||
i = j + 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def extract_entries(
|
||||
qml_file: Path,
|
||||
type_to_index: dict[str, int],
|
||||
type_to_label: dict[str, str],
|
||||
) -> list[dict]:
|
||||
"""Extract all searchable settings entries from a QML file."""
|
||||
tab_index, tab_label, sub_tab, sub_tab_label = resolve_tab_info(
|
||||
qml_file, type_to_index, type_to_label
|
||||
)
|
||||
if tab_index is None:
|
||||
return []
|
||||
|
||||
content = qml_file.read_text()
|
||||
entries = []
|
||||
|
||||
for widget_type, block in extract_widget_blocks(content):
|
||||
label_match = RE_LABEL.search(block)
|
||||
if not label_match:
|
||||
continue
|
||||
|
||||
label_key = label_match.group(1)
|
||||
desc_match = RE_DESCRIPTION.search(block)
|
||||
desc_key = desc_match.group(1) if desc_match else None
|
||||
|
||||
entry = {
|
||||
"labelKey": label_key,
|
||||
"descriptionKey": desc_key,
|
||||
"widget": widget_type,
|
||||
"tab": tab_index,
|
||||
"tabLabel": tab_label,
|
||||
"subTab": sub_tab,
|
||||
}
|
||||
if sub_tab_label is not None:
|
||||
entry["subTabLabel"] = sub_tab_label
|
||||
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def main():
|
||||
if not TABS_DIR.exists():
|
||||
print(f"Error: Tabs directory not found: {TABS_DIR}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
settings_content = SETTINGS_DIR / "SettingsContent.qml"
|
||||
if not settings_content.exists():
|
||||
print(f"Error: SettingsContent.qml not found: {settings_content}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Build type -> tabsModel index/label mappings from SettingsContent.qml
|
||||
content = settings_content.read_text()
|
||||
type_to_index, type_to_label = build_tab_mappings(content)
|
||||
|
||||
if not type_to_index:
|
||||
print("Error: Could not parse tab model from SettingsContent.qml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Parsed {len(type_to_index)} tab types from SettingsContent.qml")
|
||||
|
||||
all_entries = []
|
||||
seen_labels = set()
|
||||
|
||||
# Scan all QML files in Tabs/ (recursive)
|
||||
for qml_file in sorted(TABS_DIR.rglob("*.qml")):
|
||||
entries = extract_entries(qml_file, type_to_index, type_to_label)
|
||||
for entry in entries:
|
||||
if entry["labelKey"] not in seen_labels:
|
||||
seen_labels.add(entry["labelKey"])
|
||||
all_entries.append(entry)
|
||||
|
||||
# Write output
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT, "w") as f:
|
||||
json.dump(all_entries, f, indent=2)
|
||||
|
||||
print(f"Generated {len(all_entries)} entries -> {OUTPUT.relative_to(ROOT)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,6 +6,7 @@ import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
objectName: "NTabBar"
|
||||
|
||||
// Public properties
|
||||
property int currentIndex: 0
|
||||
|
||||
Reference in New Issue
Block a user