mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
NSearchableComboBox: created, uses fuzzy find
GeneralTab: replace NComboBox with NSearchableComboBox
This commit is contained in:
@@ -200,12 +200,13 @@ ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NComboBox {
|
||||
NSearchableComboBox {
|
||||
label: "Default Font"
|
||||
description: "Main font used throughout the interface."
|
||||
model: FontService.availableFonts
|
||||
currentKey: Settings.data.ui.fontDefault
|
||||
placeholder: "Select default font..."
|
||||
searchPlaceholder: "Search fonts..."
|
||||
popupHeight: 420 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
@@ -213,12 +214,13 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
NSearchableComboBox {
|
||||
label: "Fixed Width Font"
|
||||
description: "Monospace font used for terminal and code display."
|
||||
model: FontService.monospaceFonts
|
||||
currentKey: Settings.data.ui.fontFixed
|
||||
placeholder: "Select monospace font..."
|
||||
searchPlaceholder: "Search monospace fonts..."
|
||||
popupHeight: 320 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
@@ -226,12 +228,13 @@ ColumnLayout {
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
NSearchableComboBox {
|
||||
label: "Billboard Font"
|
||||
description: "Large font used for clocks and prominent displays."
|
||||
model: FontService.displayFonts
|
||||
currentKey: Settings.data.ui.fontBillboard
|
||||
placeholder: "Select display font..."
|
||||
searchPlaceholder: "Search display fonts..."
|
||||
popupHeight: 320 * scaling
|
||||
minimumWidth: 300 * scaling
|
||||
onSelected: function (key) {
|
||||
|
||||
254
Widgets/NSearchableComboBox.qml
Normal file
254
Widgets/NSearchableComboBox.qml
Normal file
@@ -0,0 +1,254 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property real minimumWidth: 280 * scaling
|
||||
property real popupHeight: 180 * scaling
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property ListModel model: {
|
||||
|
||||
}
|
||||
property string currentKey: ""
|
||||
property string placeholder: ""
|
||||
property string searchPlaceholder: "Search..."
|
||||
|
||||
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
|
||||
|
||||
signal selected(string key)
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update filtered model when search text or original model changes
|
||||
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 * scaling
|
||||
implicitHeight: preferredHeight
|
||||
color: Color.mSurface
|
||||
border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: NText {
|
||||
leftPadding: Style.marginL * scaling
|
||||
rightPadding: combo.indicator.width + Style.marginL * scaling
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
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 * scaling
|
||||
y: combo.topPadding + (combo.availableHeight - height) / 2
|
||||
icon: "caret-down"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
y: combo.height
|
||||
width: combo.width
|
||||
height: root.popupHeight + 60 * scaling
|
||||
padding: Style.marginM * scaling
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Search input
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: root.searchPlaceholder
|
||||
text: root.searchText
|
||||
onTextChanged: root.searchText = text
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
}
|
||||
|
||||
// Font list
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: combo.popup.visible ? filteredModel : null
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
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: NText {
|
||||
text: name
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: highlighted ? Color.mSurface : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
width: listView.width - Style.marginM * scaling * 2
|
||||
color: highlighted ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusS * scaling
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
Connections {
|
||||
target: combo.popup
|
||||
function onVisibleChanged() {
|
||||
if (combo.popup.visible) {
|
||||
// Small delay to ensure the popup is fully rendered
|
||||
Qt.callLater(function () {
|
||||
if (searchInput && searchInput.inputItem) {
|
||||
searchInput.inputItem.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user