521 lines
20 KiB
QML
521 lines
20 KiB
QML
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: qsTr("Java"), hint: qsTr("сборки Mojang") },
|
||
{ key: "jdk", title: qsTr("JDK"), hint: qsTr("Temurin, с инструментами") },
|
||
{ key: "jre", title: qsTr("JRE"), hint: qsTr("Temurin, только запуск") }
|
||
]
|
||
|
||
// Каталог приходит из 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: qsTr("Версия Java")
|
||
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: qsTr("Выбранной версии игры нужна Java %1 или новее")
|
||
.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: qsTr("Поиск версии…")
|
||
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: qsTr("Только скачанные")
|
||
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(qsTr("%1 МБ").arg(runtimeRow.modelData.sizeMb))
|
||
if (runtimeRow.tooOld)
|
||
parts.push(qsTr("не хватит для выбранной версии игры"))
|
||
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
|
||
? qsTr("Загрузка списка сборок Java…")
|
||
: picker.backend.javaCatalog.length === 0
|
||
? qsTr("Список сборок Java недоступен — проверьте соединение")
|
||
: qsTr("Ничего не найдено")
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
? qsTr("Сборка не выбрана")
|
||
: picker.selectedEntry.installed
|
||
? picker.selectedEntry.label
|
||
: qsTr("%1 — будет скачана").arg(picker.selectedEntry.label)
|
||
}
|
||
|
||
Row {
|
||
anchors.right: parent.right
|
||
anchors.rightMargin: 20
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: 12
|
||
|
||
Button {
|
||
text: qsTr("Отмена")
|
||
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
|
||
? qsTr("Скачать") : qsTr("Выбрать")
|
||
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()
|
||
}
|