pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls 2.15 import QtQuick.Layouts 2.15 // Выбор сборки Java отдельным окном: слева типы, справа сами версии с поиском. // Устроено так же, как выбор версии Minecraft, но выбранное здесь ещё и // качается — в отличие от версий игры, где загрузку начинает сама сборка. Dialog { id: picker required property var backend // Сборка, с которой окно открылось: по «Отмене» выбор к ней и возвращается. property string selectedId: "" property string category: "java" property string filterText: "" property bool installedOnly: false // Java, ниже которой выбранной версии игры не запуститься. 0 — версия игры // не выбрана, предупреждать не о чем. property int requiredMajor: 0 signal runtimeChosen(string runtimeId) modal: true padding: 0 width: 720 height: 480 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: Rectangle { color: "#1e1e1e" radius: 10 border.color: "#91B315" border.width: 1 } readonly property var categories: [ { key: "java", title: Loc.t.common.java.kind.java, hint: Loc.t.java.filter.mojang }, { key: "jdk", title: Loc.t.common.java.kind.jdk, hint: Loc.t.java.filter.temurinJdk }, { key: "jre", title: Loc.t.common.java.kind.jre, hint: Loc.t.java.filter.temurinJre } ] // Каталог приходит из C++ уже отсортированным (новые сверху, скачанные // первыми), поэтому здесь только отбор — порядок не трогаем. readonly property var visibleEntries: { const query = picker.filterText.trim().toLowerCase() const source = picker.backend.javaCatalog const out = [] for (var i = 0; i < source.length; ++i) { const entry = source[i] if (entry.kind !== picker.category) continue if (picker.installedOnly && !entry.installed) continue if (query !== "" && entry.search.indexOf(query) === -1) continue out.push(entry) } return out } readonly property var selectedEntry: { const list = picker.backend.javaCatalog for (var i = 0; i < list.length; ++i) { if (list[i].id === picker.selectedId) return list[i] } return null } function openFor(runtimeId, required) { picker.selectedId = runtimeId picker.requiredMajor = required || 0 picker.filterText = "" searchField.text = "" picker.category = picker.categoryOf(runtimeId) picker.backend.refreshJavaCatalog() picker.open() picker.revealSelected() } function categoryOf(runtimeId) { const source = picker.backend.javaCatalog for (var i = 0; i < source.length; ++i) { if (source[i].id === runtimeId) return source[i].kind } return "java" } function indexOfSelected() { const list = picker.visibleEntries for (var i = 0; i < list.length; ++i) { if (list[i].id === picker.selectedId) return i } return -1 } function revealSelected() { const index = picker.indexOfSelected() runtimeList.currentIndex = index if (index >= 0) runtimeList.positionViewAtIndex(index, ListView.Center) } // «Выбрать» и скачивает: сборка, которой нет на диске, запуску не поможет. function acceptSelection() { const entry = picker.selectedEntry if (!entry) return picker.runtimeChosen(entry.id) if (!entry.installed && entry.downloadable) picker.backend.installJavaRuntime(entry.id) picker.close() } header: Item { implicitHeight: 52 Text { anchors.centerIn: parent text: Loc.t.java.label.version color: "#ffffff" font.pixelSize: 17 font.bold: true } Rectangle { anchors.bottom: parent.bottom width: parent.width height: 1 color: "#333333" } } contentItem: RowLayout { spacing: 0 // ── Типы сборок ───────────────────────────────────────────────── Rectangle { Layout.preferredWidth: 170 Layout.fillHeight: true color: "transparent" Column { anchors.fill: parent anchors.margins: 8 spacing: 2 Repeater { model: picker.categories delegate: Rectangle { id: categoryRow required property var modelData width: parent.width height: 44 radius: 5 color: picker.category === categoryRow.modelData.key ? "#91B315" : categoryArea.containsMouse ? "#2d2d2d" : "transparent" Column { anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter spacing: 1 Text { text: categoryRow.modelData.title color: "#ffffff" font.pixelSize: 13 } Text { text: categoryRow.modelData.hint color: picker.category === categoryRow.modelData.key ? "#1e1e1e" : "#777777" font.pixelSize: 10 } } MouseArea { id: categoryArea anchors.fill: parent hoverEnabled: true onClicked: { picker.category = categoryRow.modelData.key picker.revealSelected() } } } } Item { width: 1; height: 8 } // Требование выбранной сборки — чтобы не пришлось держать в // голове, какая Java нужна какой версии игры. Text { width: parent.width - 4 visible: picker.requiredMajor > 0 wrapMode: Text.Wrap color: "#888888" font.pixelSize: 10 text: Loc.t.java.hint.required .arg(picker.requiredMajor) } } } Rectangle { Layout.preferredWidth: 1 Layout.fillHeight: true color: "#333333" } // ── Версии ────────────────────────────────────────────────────── Item { Layout.fillWidth: true Layout.fillHeight: true TextField { id: searchField anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 8 height: 32 placeholderText: Loc.t.common.placeholder.searchVersion color: "#ffffff" placeholderTextColor: "#666666" background: Rectangle { color: "#232323" radius: 5 border.color: searchField.activeFocus ? "#91B315" : "#444444" border.width: 1 } onTextChanged: { picker.filterText = text runtimeList.currentIndex = picker.indexOfSelected() } Keys.onEscapePressed: picker.close() Keys.onReturnPressed: picker.acceptSelection() Keys.onEnterPressed: picker.acceptSelection() Keys.onDownPressed: runtimeList.step(1) Keys.onUpPressed: runtimeList.step(-1) } CheckBox { id: installedOnlyBox anchors.left: parent.left anchors.leftMargin: 8 anchors.bottom: parent.bottom anchors.bottomMargin: 4 height: 28 checked: picker.installedOnly onToggled: { picker.installedOnly = checked picker.revealSelected() } indicator: Rectangle { implicitWidth: 16 implicitHeight: 16 x: 0 y: (installedOnlyBox.height - height) / 2 radius: 4 color: installedOnlyBox.checked ? "#91B315" : "#2a2a2a" border.color: installedOnlyBox.checked ? "#91B315" : "#444444" border.width: 1 Text { anchors.centerIn: parent visible: installedOnlyBox.checked text: "✓" color: "#1e1e1e" font.pixelSize: 12 font.bold: true } } contentItem: Text { leftPadding: installedOnlyBox.indicator.width + 8 text: Loc.t.java.filter.downloadedOnly color: "#aaaaaa" font.pixelSize: 11 verticalAlignment: Text.AlignVCenter } } ListView { id: runtimeList anchors.top: searchField.bottom anchors.topMargin: 6 anchors.left: parent.left anchors.right: parent.right anchors.bottom: installedOnlyBox.top anchors.leftMargin: 8 anchors.rightMargin: 8 clip: true spacing: 1 model: picker.visibleEntries ScrollIndicator.vertical: ScrollIndicator {} function step(delta) { const next = runtimeList.currentIndex + delta if (next < 0 || next >= runtimeList.count) return runtimeList.currentIndex = next runtimeList.positionViewAtIndex(next, ListView.Contain) picker.selectedId = picker.visibleEntries[next].id } delegate: Rectangle { id: runtimeRow required property var modelData required property int index readonly property bool chosen: picker.selectedId === runtimeRow.modelData.id // Сборка ниже требования игры выбирается, но помечается: она // может пригодиться для другой сборки. readonly property bool tooOld: picker.requiredMajor > 0 && runtimeRow.modelData.major < picker.requiredMajor width: runtimeList.width height: 44 radius: 4 color: runtimeRow.chosen ? "#91B315" : runtimeArea.containsMouse ? "#2d2d2d" : "transparent" Column { anchors.left: parent.left anchors.leftMargin: 10 anchors.right: rightMarks.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter spacing: 1 Row { spacing: 6 Text { text: runtimeRow.modelData.label color: "#ffffff" font.pixelSize: 13 } Text { visible: runtimeRow.modelData.lts text: "LTS" color: runtimeRow.chosen ? "#1e1e1e" : "#91B315" font.pixelSize: 10 anchors.verticalCenter: parent.verticalCenter } } Text { // Колонка растянута якорями, поэтому её ширина // задана, а не выведена из детей — цикла тут нет. width: parent.width elide: Text.ElideRight color: runtimeRow.chosen ? "#1e1e1e" : runtimeRow.tooOld ? "#aa7777" : "#888888" font.pixelSize: 10 text: { const parts = [runtimeRow.modelData.detail, runtimeRow.modelData.coverage] if (runtimeRow.modelData.sizeMb > 0) parts.push(Loc.t.common.status.megabytes.arg(runtimeRow.modelData.sizeMb)) if (runtimeRow.tooOld) parts.push(Loc.t.java.warning.tooOldForVersion) return parts.join(" · ") } } } Row { id: rightMarks anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter spacing: 8 Text { visible: runtimeRow.modelData.installed text: "✓" color: runtimeRow.chosen ? "#1e1e1e" : "#91B315" font.pixelSize: 13 font.bold: true anchors.verticalCenter: parent.verticalCenter } // Сборки весят по двести мегабайт — удалять их нужно // прямо здесь, иначе папка лаунчера растёт молча. Image { visible: runtimeRow.modelData.installed source: "images/Trash.svg" width: 14 height: 14 fillMode: Image.PreserveAspectFit anchors.verticalCenter: parent.verticalCenter opacity: trashArea.containsMouse ? 1.0 : 0.55 MouseArea { id: trashArea anchors.fill: parent anchors.margins: -5 hoverEnabled: true onClicked: { picker.backend.removeJavaRuntime(runtimeRow.modelData.id) if (picker.selectedId === runtimeRow.modelData.id) picker.selectedId = "" } } } } MouseArea { id: runtimeArea anchors.fill: parent anchors.rightMargin: 40 hoverEnabled: true onClicked: { picker.selectedId = runtimeRow.modelData.id runtimeList.currentIndex = runtimeRow.index } onDoubleClicked: { picker.selectedId = runtimeRow.modelData.id picker.acceptSelection() } } } } // Пустая категория, ничего не нашлось или каталог ещё не пришёл. Text { anchors.centerIn: runtimeList width: runtimeList.width - 20 visible: picker.visibleEntries.length === 0 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap color: "#888888" font.pixelSize: 12 font.italic: true text: picker.backend.javaCatalogLoading ? Loc.t.java.progress.loadingCatalog : picker.backend.javaCatalog.length === 0 ? Loc.t.java.error.catalogUnavailable : Loc.t.common.status.nothingFound } } } footer: Item { implicitHeight: 60 Rectangle { anchors.top: parent.top width: parent.width height: 1 color: "#333333" } Text { anchors.left: parent.left anchors.leftMargin: 20 anchors.verticalCenter: parent.verticalCenter width: parent.width - 320 elide: Text.ElideRight color: picker.selectedEntry === null ? "#666666" : "#91B315" font.pixelSize: 13 text: picker.selectedEntry === null ? Loc.t.common.status.noBuildSelected : picker.selectedEntry.installed ? picker.selectedEntry.label : Loc.t.java.status.willDownload.arg(picker.selectedEntry.label) } Row { anchors.right: parent.right anchors.rightMargin: 20 anchors.verticalCenter: parent.verticalCenter spacing: 12 Button { text: Loc.t.common.button.cancel width: 110; height: 36 contentItem: Text { text: parent.text color: "#ffffff" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { color: parent.pressed ? "#444444" : "#333333" radius: 6 } onClicked: picker.close() } Button { text: picker.selectedEntry !== null && !picker.selectedEntry.installed ? Loc.t.common.button.download : Loc.t.common.button.select width: 110; height: 36 enabled: picker.selectedEntry !== null opacity: enabled ? 1.0 : 0.45 contentItem: Text { text: parent.text color: "#ffffff" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { color: parent.pressed ? "#6a8510" : "#91B315" radius: 6 } onClicked: picker.acceptSelection() } } } onOpened: searchField.forceActiveFocus() }