mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
292 lines
8.4 KiB
QML
292 lines
8.4 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import "../Helpers/FuzzySort.js" as Fuzzysort
|
|
import qs.Commons
|
|
import qs.Widgets
|
|
|
|
RowLayout {
|
|
id: root
|
|
|
|
property real minimumWidth: 280 * Style.uiScaleRatio
|
|
property real popupHeight: 180 * Style.uiScaleRatio
|
|
|
|
property string label: ""
|
|
property string description: ""
|
|
property ListModel model: {}
|
|
property string currentKey: ""
|
|
property string placeholder: ""
|
|
property string searchPlaceholder: I18n.tr("placeholders.search")
|
|
property Component delegate: null
|
|
|
|
readonly property real preferredHeight: Style.baseWidgetSize * 1.1
|
|
|
|
signal selected(string key)
|
|
|
|
spacing: Style.marginL
|
|
Layout.fillWidth: true
|
|
|
|
// Filtered model for search results
|
|
property ListModel filteredModel: ListModel {}
|
|
property string searchText: ""
|
|
|
|
function findIndexByKey(key) {
|
|
for (var i = 0; i < root.model.count; i++) {
|
|
if (root.model.get(i).key === key) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function findIndexByKeyInFiltered(key) {
|
|
for (var i = 0; i < root.filteredModel.count; i++) {
|
|
if (root.filteredModel.get(i).key === key) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function filterModel() {
|
|
filteredModel.clear();
|
|
|
|
// Check if model exists and has items
|
|
if (!root.model || root.model.count === undefined || root.model.count === 0) {
|
|
return;
|
|
}
|
|
|
|
if (searchText.trim() === "") {
|
|
// If no search text, show all items
|
|
for (var i = 0; i < root.model.count; i++) {
|
|
filteredModel.append(root.model.get(i));
|
|
}
|
|
} else {
|
|
// Convert ListModel to array for fuzzy search
|
|
var items = [];
|
|
for (var i = 0; i < root.model.count; i++) {
|
|
items.push(root.model.get(i));
|
|
}
|
|
|
|
// Use fuzzy search if available, fallback to simple search
|
|
if (typeof Fuzzysort !== 'undefined') {
|
|
var fuzzyResults = Fuzzysort.go(searchText, items, {
|
|
"key": "name",
|
|
"threshold": -1000,
|
|
"limit": 50
|
|
});
|
|
|
|
// Add results in order of relevance
|
|
for (var j = 0; j < fuzzyResults.length; j++) {
|
|
filteredModel.append(fuzzyResults[j].obj);
|
|
}
|
|
} else {
|
|
// Fallback to simple search
|
|
var searchLower = searchText.toLowerCase();
|
|
for (var i = 0; i < items.length; i++) {
|
|
var item = items[i];
|
|
if (item.name.toLowerCase().includes(searchLower)) {
|
|
filteredModel.append(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onSearchTextChanged: filterModel()
|
|
onModelChanged: filterModel()
|
|
|
|
NLabel {
|
|
label: root.label
|
|
description: root.description
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
ComboBox {
|
|
id: combo
|
|
|
|
Layout.minimumWidth: root.minimumWidth
|
|
Layout.preferredHeight: root.preferredHeight
|
|
model: filteredModel
|
|
currentIndex: findIndexByKeyInFiltered(currentKey)
|
|
onActivated: {
|
|
if (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) {
|
|
root.selected(filteredModel.get(combo.currentIndex).key);
|
|
}
|
|
}
|
|
|
|
background: Rectangle {
|
|
implicitWidth: Style.baseWidgetSize * 3.75 * Style.uiScaleRatio
|
|
implicitHeight: preferredHeight
|
|
color: Color.mSurface
|
|
border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline
|
|
border.width: Style.borderS
|
|
radius: Style.radiusM
|
|
|
|
Behavior on border.color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: NText {
|
|
leftPadding: Style.marginL
|
|
rightPadding: combo.indicator.width + Style.marginL
|
|
pointSize: Style.fontSizeM
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
color: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
|
text: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? filteredModel.get(combo.currentIndex).name : root.placeholder
|
|
}
|
|
|
|
indicator: NIcon {
|
|
x: combo.width - width - Style.marginM
|
|
y: combo.topPadding + (combo.availableHeight - height) / 2
|
|
icon: "caret-down"
|
|
pointSize: Style.fontSizeL
|
|
}
|
|
|
|
popup: Popup {
|
|
y: combo.height
|
|
width: combo.width
|
|
height: root.popupHeight + 60
|
|
padding: Style.marginM
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: Style.marginS
|
|
|
|
// Search input
|
|
NTextInput {
|
|
id: searchInput
|
|
inputIconName: "search"
|
|
Layout.fillWidth: true
|
|
placeholderText: root.searchPlaceholder
|
|
text: root.searchText
|
|
onTextChanged: root.searchText = text
|
|
fontSize: Style.fontSizeS
|
|
}
|
|
|
|
NListView {
|
|
id: listView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
model: combo.popup.visible ? filteredModel : null
|
|
horizontalPolicy: ScrollBar.AlwaysOff
|
|
verticalPolicy: ScrollBar.AsNeeded
|
|
|
|
delegate: root.delegate ? root.delegate : defaultDelegate
|
|
|
|
Component {
|
|
id: defaultDelegate
|
|
ItemDelegate {
|
|
id: delegateRoot
|
|
width: listView.width
|
|
hoverEnabled: true
|
|
highlighted: ListView.view.currentIndex === index
|
|
|
|
onHoveredChanged: {
|
|
if (hovered) {
|
|
ListView.view.currentIndex = index;
|
|
}
|
|
}
|
|
|
|
onClicked: {
|
|
root.selected(filteredModel.get(index).key);
|
|
combo.currentIndex = root.findIndexByKeyInFiltered(filteredModel.get(index).key);
|
|
combo.popup.close();
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
width: parent.width
|
|
spacing: Style.marginM
|
|
|
|
NText {
|
|
text: name
|
|
pointSize: Style.fontSizeM
|
|
color: highlighted ? Color.mOnHover : Color.mOnSurface
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.marginS
|
|
Layout.alignment: Qt.AlignRight
|
|
|
|
Repeater {
|
|
model: typeof badgeLocations !== 'undefined' ? badgeLocations : []
|
|
|
|
delegate: Item {
|
|
width: Style.baseWidgetSize * 0.7
|
|
height: Style.baseWidgetSize * 0.7
|
|
|
|
NText {
|
|
anchors.centerIn: parent
|
|
text: modelData
|
|
pointSize: Style.fontSizeXXS
|
|
font.weight: Style.fontWeightBold
|
|
color: highlighted ? Color.mOnHover : Color.mOnSurface
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
background: Rectangle {
|
|
width: listView.width
|
|
color: highlighted ? Color.mHover : Color.transparent
|
|
radius: Style.radiusS
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
background: Rectangle {
|
|
color: Color.mSurfaceVariant
|
|
border.color: Color.mOutline
|
|
border.width: Style.borderS
|
|
radius: Style.radiusM
|
|
}
|
|
}
|
|
|
|
// Update the currentIndex if the currentKey is changed externally
|
|
Connections {
|
|
target: root
|
|
function onCurrentKeyChanged() {
|
|
combo.currentIndex = root.findIndexByKeyInFiltered(currentKey);
|
|
}
|
|
}
|
|
|
|
// Focus search input when popup opens and ensure model is filtered
|
|
Connections {
|
|
target: combo.popup
|
|
function onVisibleChanged() {
|
|
if (combo.popup.visible) {
|
|
// Ensure the model is filtered when popup opens
|
|
filterModel();
|
|
// Small delay to ensure the popup is fully rendered
|
|
Qt.callLater(() => {
|
|
if (searchInput && searchInput.inputItem) {
|
|
searchInput.inputItem.forceActiveFocus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|