import QtQuick import QtQuick.Controls 2.15 // Выпадающий список версий: сверху установленные, ниже через разделитель // весь каталог Mojang, сверху поле поиска. // // Это Item с Popup, а не ComboBox: editable-ComboBox привязывает поле ввода // к currentText и запускает своё автодополнение, а при активном фильтре // currentIndex перестаёт указывать в исходную модель. Item { id: vcombo property var catalog: [] property string selectedId: "" property string placeholder: qsTr("Выберите версию") property string filterText: "" signal aboutToOpen() implicitHeight: 36 height: implicitHeight // Фильтр по номеру версии. ~900 строк за нажатие — доли миллисекунды, // а делегаты ListView создаёт только для видимых строк. function filterEntries(needle) { const query = needle.trim().toLowerCase() if (query === "") return vcombo.catalog const out = [] for (var i = 0; i < vcombo.catalog.length; ++i) { const entry = vcombo.catalog[i] if (entry.selectable === false) continue if (entry.search.indexOf(query) !== -1) out.push(entry) } return out } readonly property var visibleEntries: vcombo.filterEntries(vcombo.filterText) function openPopup() { vcombo.aboutToOpen() vcombo.filterText = "" vcomboFilter.text = "" vcomboPopup.open() vcomboFilter.forceActiveFocus() } Rectangle { id: vcomboField anchors.fill: parent color: "#2a2a2a" radius: 6 border.color: vcomboPopup.opened ? "#91B315" : "#444444" border.width: 1 Text { anchors.fill: parent leftPadding: 10 rightPadding: 32 verticalAlignment: Text.AlignVCenter elide: Text.ElideRight color: vcombo.selectedId === "" ? "#666666" : "#ffffff" text: vcombo.selectedId === "" ? vcombo.placeholder : vcombo.selectedId } // Та же стрелка, что у остальных списков окна. Image { width: 10; height: 10 x: vcomboField.width - width - 12 y: (vcomboField.height - height) / 2 source: vcomboPopup.opened ? "images/Profile_Box/Asset_23.svg" : "images/Profile_Box/Asset_24.svg" rotation: 180 sourceSize.width: 10; sourceSize.height: 10 fillMode: Image.PreserveAspectFit } MouseArea { anchors.fill: parent onClicked: vcombo.openPopup() } } Popup { id: vcomboPopup y: vcombo.height + 2 width: vcombo.width padding: 1 height: 44 + Math.min(vcomboList.contentHeight, 240) background: Rectangle { color: "#2a2a2a" radius: 6 border.color: "#444444" border.width: 1 } contentItem: Item { TextField { id: vcomboFilter anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 4 height: 30 placeholderText: qsTr("Поиск версии…") color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#232323" radius: 5 border.color: vcomboFilter.activeFocus ? "#91B315" : "#444444" border.width: 1 } onTextChanged: { vcombo.filterText = text vcomboList.currentIndex = vcomboList.firstSelectable(0, 1) } Keys.onEscapePressed: vcomboPopup.close() Keys.onDownPressed: vcomboList.step(1) Keys.onUpPressed: vcomboList.step(-1) Keys.onReturnPressed: vcomboList.acceptCurrent() Keys.onEnterPressed: vcomboList.acceptCurrent() } ListView { id: vcomboList anchors.top: vcomboFilter.bottom anchors.topMargin: 4 anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom clip: true model: vcombo.visibleEntries ScrollIndicator.vertical: ScrollIndicator {} // Разделитель и служебные строки клавишами пропускаем. function firstSelectable(from, delta) { for (var i = from; i >= 0 && i < count; i += delta) { if (model[i].selectable !== false) return i } return -1 } function step(delta) { const next = firstSelectable(currentIndex + delta, delta) if (next >= 0) { currentIndex = next positionViewAtIndex(next, ListView.Contain) } } function acceptCurrent() { if (currentIndex < 0 || currentIndex >= count) return const entry = model[currentIndex] if (entry.selectable === false) return vcombo.selectedId = entry.id vcomboPopup.close() } delegate: Item { id: vcomboRow required property var modelData required property int index width: vcomboList.width height: modelData.kind === "separator" ? 26 : modelData.kind === "status" ? 30 : 34 // Разделитель между установленными и каталогом. Item { anchors.fill: parent visible: vcomboRow.modelData.kind === "separator" Rectangle { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: parent.right anchors.margins: 8 height: 1 color: "#3a3a3a" } Rectangle { anchors.centerIn: parent color: "#2a2a2a" width: separatorLabel.implicitWidth + 12 height: 18 Text { id: separatorLabel anchors.centerIn: parent text: vcomboRow.modelData.label color: "#888888" font.pixelSize: 10 } } } // «Загрузка списка…» или сообщение о недоступности. Text { anchors.centerIn: parent visible: vcomboRow.modelData.kind === "status" text: vcomboRow.modelData.label color: "#888888" font.pixelSize: 11 font.italic: true } Rectangle { anchors.fill: parent visible: vcomboRow.modelData.selectable !== false color: (vcomboRowArea.containsMouse || vcomboList.currentIndex === vcomboRow.index) ? "#3a3a3a" : "#2a2a2a" Text { anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter width: parent.width - 100 elide: Text.ElideRight text: vcomboRow.modelData.label color: "#ffffff" font.pixelSize: 12 } Text { anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter text: vcomboRow.modelData.installed ? qsTr("установлена") : vcomboRow.modelData.type color: vcomboRow.modelData.installed ? "#91B315" : "#666666" font.pixelSize: 10 } MouseArea { id: vcomboRowArea anchors.fill: parent hoverEnabled: true onClicked: { vcombo.selectedId = vcomboRow.modelData.id vcomboPopup.close() } } } } } } } }