Files
minecraft-launcher/JavaPickerDialog.qml
T

521 lines
20 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}