retro-imager/src/main.qml

1758 lines
60 KiB
QML
Raw Normal View History

2020-03-04 16:55:40 +01:00
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (C) 2020 Raspberry Pi Ltd
2020-03-04 16:55:40 +01:00
*/
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.0
import QtQuick.Controls.Material 2.2
import "qmlcomponents"
2020-03-04 16:55:40 +01:00
ApplicationWindow {
id: window
visible: true
width: imageWriter.isEmbeddedMode() ? -1 : 680
height: imageWriter.isEmbeddedMode() ? -1 : 450
minimumWidth: imageWriter.isEmbeddedMode() ? -1 : 680
minimumHeight: imageWriter.isEmbeddedMode() ? -1 : 420
2020-03-04 16:55:40 +01:00
2020-03-10 17:43:48 +01:00
title: qsTr("Raspberry Pi Imager v%1").arg(imageWriter.constantVersion())
2020-03-04 16:55:40 +01:00
FontLoader {id: roboto; source: "fonts/Roboto-Regular.ttf"}
FontLoader {id: robotoLight; source: "fonts/Roboto-Light.ttf"}
FontLoader {id: robotoBold; source: "fonts/Roboto-Bold.ttf"}
/** hw device list storage
*
* To allow us to filter the OS list, we maintain an application-wide record of the selected device
* tags.
*/
property string hwTags
/** 0: Exclusive, must match explicit device names only, no untagged
1: Exclusive by prefix, must match the device name as a prefix, no untagged
2: Inclusive, match explicit device names and untagged
3: Inclusive by prefix, match explicit device names and untagged
*/
property int hwTagMatchingType
onClosing: {
if (progressBar.visible) {
close.accepted = false
quitpopup.openPopup()
}
}
Shortcut {
sequence: StandardKey.Quit
context: Qt.ApplicationShortcut
onActivated: {
if (!progressBar.visible) {
Qt.quit()
}
}
}
Shortcut {
sequences: ["Shift+Ctrl+X", "Shift+Meta+X"]
context: Qt.ApplicationShortcut
onActivated: {
optionspopup.openPopup()
}
}
2020-03-04 16:55:40 +01:00
ColumnLayout {
id: bg
spacing: 0
Rectangle {
id: logoContainer
implicitHeight: window.height/4
Image {
id: image
source: "icons/logo_sxs_imager.png"
// Specify the maximum size of the image
width: window.width * 0.45
height: window.height / 3
// Within the image's specified size rectangle, resize the
// image to fit within the rectangle while keeping its aspect
// ratio the same. Preserving the aspect ratio implies some
// extra padding between the Image's extend and the actual
// image content: align left so all this padding is on the
// right.
fillMode: Image.PreserveAspectFit
horizontalAlignment: Image.AlignLeft
// Keep the left side of the image 40 pixels from the left
// edge
anchors.left: logoContainer.left
anchors.leftMargin: 40
// Equal padding above and below the image
anchors.top: logoContainer.top
anchors.bottom: logoContainer.bottom
anchors.topMargin: window.height / 25
anchors.bottomMargin: window.height / 25
}
2020-03-04 16:55:40 +01:00
}
Rectangle {
color: "#c31c4a"
implicitWidth: window.width
implicitHeight: window.height * (1 - 1/4)
2020-03-04 16:55:40 +01:00
GridLayout {
id: gridLayout
rowSpacing: 15
2020-03-04 16:55:40 +01:00
anchors.fill: parent
anchors.topMargin: 25
anchors.rightMargin: 50
anchors.leftMargin: 50
rows: 5
columns: 3
columnSpacing: 15
2020-03-04 16:55:40 +01:00
ColumnLayout {
id: columnLayout0
spacing: 0
Layout.row: 0
Layout.column: 0
Layout.fillWidth: true
Text {
id: text0
color: "#ffffff"
text: qsTr("Raspberry Pi Device")
Layout.fillWidth: true
Layout.preferredHeight: 17
Layout.preferredWidth: 100
font.pixelSize: 12
font.family: robotoBold.name
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
ImButton {
id: hwbutton
text: qsTr("CHOOSE DEVICE")
spacing: 0
padding: 0
bottomPadding: 0
topPadding: 0
Layout.minimumHeight: 40
Layout.fillWidth: true
onClicked: {
hwpopup.open()
hwlist.currentItem.forceActiveFocus()
}
Accessible.ignored: ospopup.visible || dstpopup.visible || hwpopup.visible
Accessible.description: qsTr("Select this button to choose your target Raspberry Pi")
}
}
ColumnLayout {
id: columnLayout1
2020-03-04 16:55:40 +01:00
spacing: 0
Layout.row: 0
Layout.column: 1
2020-03-04 16:55:40 +01:00
Layout.fillWidth: true
Text {
id: text1
color: "#ffffff"
text: qsTr("Operating System")
Layout.fillWidth: true
Layout.preferredHeight: 17
font.pixelSize: 12
font.family: robotoBold.name
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
ImButton {
2020-03-04 16:55:40 +01:00
id: osbutton
text: imageWriter.srcFileName() === "" ? qsTr("CHOOSE OS") : imageWriter.srcFileName()
spacing: 0
padding: 0
bottomPadding: 0
topPadding: 0
Layout.minimumHeight: 40
Layout.fillWidth: true
onClicked: {
ospopup.open()
osswipeview.currentItem.forceActiveFocus()
}
Accessible.ignored: ospopup.visible || dstpopup.visible || hwpopup.visible
Accessible.description: qsTr("Select this button to change the operating system")
2020-03-04 16:55:40 +01:00
}
}
ColumnLayout {
id: columnLayout2
spacing: 0
Layout.row: 0
Layout.column: 2
2020-03-04 16:55:40 +01:00
Layout.fillWidth: true
Text {
id: text2
color: "#ffffff"
text: qsTr("Storage")
2020-03-04 16:55:40 +01:00
Layout.fillWidth: true
Layout.preferredHeight: 17
font.pixelSize: 12
font.family: robotoBold.name
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
ImButton {
2020-03-04 16:55:40 +01:00
id: dstbutton
text: qsTr("CHOOSE STORAGE")
spacing: 0
padding: 0
bottomPadding: 0
topPadding: 0
2020-03-04 16:55:40 +01:00
Layout.minimumHeight: 40
Layout.preferredWidth: 200
2020-03-04 16:55:40 +01:00
Layout.fillWidth: true
onClicked: {
imageWriter.startDriveListPolling()
2020-03-04 16:55:40 +01:00
dstpopup.open()
dstlist.forceActiveFocus()
2020-03-04 16:55:40 +01:00
}
Accessible.ignored: ospopup.visible || dstpopup.visible || hwpopup.visible
Accessible.description: qsTr("Select this button to change the destination storage device")
}
2020-03-04 16:55:40 +01:00
}
ColumnLayout {
id: columnLayoutProgress
2020-03-04 16:55:40 +01:00
spacing: 0
Layout.row: 1
Layout.column: 0
Layout.columnSpan: 2
2020-03-04 16:55:40 +01:00
Text {
id: progressText
font.pointSize: 10
color: "white"
font.family: robotoBold.name
font.bold: true
visible: false
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
Layout.bottomMargin: 25
padding: 5
2020-03-04 16:55:40 +01:00
}
ProgressBar {
Layout.bottomMargin: 25
2020-03-04 16:55:40 +01:00
id: progressBar
Layout.fillWidth: true
visible: false
Material.background: "#d15d7d"
}
}
ColumnLayout {
id: columnLayout3
Layout.row: 1
Layout.column: 2
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
spacing: 0
2020-03-04 16:55:40 +01:00
ImButton {
Layout.bottomMargin: 25
Layout.minimumHeight: 40
Layout.preferredWidth: 200
padding: 5
2020-03-04 16:55:40 +01:00
id: cancelwritebutton
text: qsTr("CANCEL WRITE")
onClicked: {
enabled = false
progressText.text = qsTr("Cancelling...")
imageWriter.cancelWrite()
}
Layout.alignment: Qt.AlignRight
visible: false
}
ImButton {
Layout.bottomMargin: 25
Layout.minimumHeight: 40
Layout.preferredWidth: 200
padding: 5
2020-03-04 16:55:40 +01:00
id: cancelverifybutton
text: qsTr("CANCEL VERIFY")
onClicked: {
enabled = false
progressText.text = qsTr("Finalizing...")
imageWriter.setVerifyEnabled(false)
}
Layout.alignment: Qt.AlignRight
visible: false
}
ImButton {
id: writebutton
text: qsTr("Next")
Layout.bottomMargin: 25
Layout.minimumHeight: 40
Layout.preferredWidth: 200
Layout.alignment: Qt.AlignRight
Accessible.ignored: ospopup.visible || dstpopup.visible || hwpopup.visible
Accessible.description: qsTr("Select this button to start writing the image")
enabled: false
onClicked: {
if (!imageWriter.readyToWrite()) {
return
}
if (!optionspopup.visible && imageWriter.imageSupportsCustomization()) {
usesavedsettingspopup.openPopup()
} else {
confirmwritepopup.askForConfirmation()
}
}
}
2020-03-04 16:55:40 +01:00
}
Text {
Layout.columnSpan: 3
color: "#ffffff"
font.pixelSize: 18
font.family: roboto.name
visible: imageWriter.isEmbeddedMode() && imageWriter.customRepo()
text: qsTr("Using custom repository: %1").arg(imageWriter.constantOsListUrl())
}
Text {
Layout.columnSpan: 3
color: "#ffffff"
font.pixelSize: 18
font.family: roboto.name
visible: !imageWriter.hasMouse()
text: qsTr("Keyboard navigation: <tab> navigate to next button <space> press button/select item <arrow up/down> go up/down in lists")
}
Rectangle {
id: langbarRect
Layout.columnSpan: 3
2021-11-27 21:44:32 +01:00
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.bottomMargin: 5
visible: imageWriter.isEmbeddedMode()
implicitWidth: langbar.width
implicitHeight: langbar.height
color: "#ffffe3"
radius: 5
RowLayout {
id: langbar
spacing: 10
Text {
font.pixelSize: 12
font.family: roboto.name
text: qsTr("Language: ")
Layout.leftMargin: 30
Layout.topMargin: 10
Layout.bottomMargin: 10
}
ComboBox {
font.pixelSize: 12
font.family: roboto.name
model: imageWriter.getTranslations()
Layout.preferredWidth: 200
currentIndex: -1
Component.onCompleted: {
currentIndex = find(imageWriter.getCurrentLanguage())
}
onActivated: {
imageWriter.changeLanguage(editText)
}
Layout.topMargin: 10
Layout.bottomMargin: 10
}
Text {
font.pixelSize: 12
font.family: roboto.name
text: qsTr("Keyboard: ")
Layout.topMargin: 10
Layout.bottomMargin: 10
}
ComboBox {
enabled: imageWriter.isEmbeddedMode()
font.pixelSize: 12
font.family: roboto.name
model: imageWriter.getKeymapLayoutList()
currentIndex: -1
Component.onCompleted: {
currentIndex = find(imageWriter.getCurrentKeyboard())
}
onActivated: {
imageWriter.changeKeyboard(editText)
}
Layout.topMargin: 10
Layout.bottomMargin: 10
Layout.rightMargin: 30
}
}
}
/* Language/keyboard bar is normally only visible in embedded mode.
To test translations also show it when shift+ctrl+L is pressed. */
Shortcut {
sequences: ["Shift+Ctrl+L", "Shift+Meta+L"]
context: Qt.ApplicationShortcut
onActivated: {
langbarRect.visible = true
}
}
2020-03-04 16:55:40 +01:00
}
2023-08-13 00:21:31 +01:00
DropArea {
anchors.fill: parent
onEntered: {
if (drag.active && mimeData.hasUrls()) {
drag.acceptProposedAction()
}
}
onDropped: {
if (drop.urls && drop.urls.length > 0) {
onFileSelected(drop.urls[0].toString())
}
}
}
2020-03-04 16:55:40 +01:00
}
}
Popup {
id: hwpopup
2020-03-04 16:55:40 +01:00
x: 50
y: 25
width: parent.width-100
height: parent.height-50
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
property string hwselected: ""
2020-03-04 16:55:40 +01:00
// background of title
Rectangle {
color: "#f5f5f5"
anchors.right: parent.right
anchors.top: parent.top
height: 35
width: parent.width
}
// line under title
Rectangle {
color: "#afafaf"
width: parent.width
y: 35
implicitHeight: 1
}
Text {
text: "X"
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 25
anchors.topMargin: 10
font.family: roboto.name
font.bold: true
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
hwpopup.close()
2020-03-04 16:55:40 +01:00
}
}
}
ColumnLayout {
spacing: 10
Text {
text: qsTr("Raspberry Pi Device")
2020-03-04 16:55:40 +01:00
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.topMargin: 10
font.family: roboto.name
font.bold: true
}
Item {
clip: true
Layout.preferredWidth: hwlist.width
Layout.preferredHeight: hwlist.height
ListView {
id: hwlist
model: ListModel {
id: deviceModel
ListElement {
name: qsTr("[ All ]")
tags: "[]"
icon: ""
description: ""
matching_type: "exclusive"
}
}
currentIndex: -1
delegate: hwdelegate
width: window.width-100
height: window.height-100
boundsBehavior: Flickable.StopAtBounds
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
ScrollBar.vertical: ScrollBar {
width: 10
policy: hwlist.contentHeight > hwlist.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded
}
Keys.onSpacePressed: {
if (currentIndex != -1)
selectHWitem(model.get(currentIndex))
}
Accessible.onPressAction: {
if (currentIndex != -1)
selectHWitem(model.get(currentIndex))
}
Keys.onEnterPressed: Keys.onSpacePressed(event)
Keys.onReturnPressed: Keys.onSpacePressed(event)
}
}
}
}
/*
Popup for OS selection
*/
Popup {
id: ospopup
x: 50
y: 25
width: parent.width-100
height: parent.height-50
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
property string categorySelected : ""
// background of title
Rectangle {
color: "#f5f5f5"
anchors.right: parent.right
anchors.top: parent.top
height: 35
width: parent.width
}
// line under title
Rectangle {
color: "#afafaf"
width: parent.width
y: 35
implicitHeight: 1
}
Text {
text: "X"
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 25
anchors.topMargin: 10
font.family: roboto.name
font.bold: true
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
ospopup.close()
osswipeview.decrementCurrentIndex()
}
}
}
ColumnLayout {
spacing: 10
Text {
text: qsTr("Operating System")
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.topMargin: 10
font.family: roboto.name
font.bold: true
}
2020-03-04 16:55:40 +01:00
Item {
clip: true
Layout.preferredWidth: oslist.width
Layout.preferredHeight: oslist.height
2020-03-04 16:55:40 +01:00
SwipeView {
id: osswipeview
interactive: false
ListView {
id: oslist
model: osmodel
currentIndex: -1
2020-03-04 16:55:40 +01:00
delegate: osdelegate
width: window.width-100
height: window.height-100
2020-03-04 16:55:40 +01:00
boundsBehavior: Flickable.StopAtBounds
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
2020-03-04 16:55:40 +01:00
ScrollBar.vertical: ScrollBar {
width: 10
2020-05-23 13:02:38 +02:00
policy: oslist.contentHeight > oslist.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded
2020-03-04 16:55:40 +01:00
}
Keys.onSpacePressed: {
if (currentIndex != -1)
selectOSitem(model.get(currentIndex), true)
}
Accessible.onPressAction: {
if (currentIndex != -1)
selectOSitem(model.get(currentIndex), true)
}
Keys.onEnterPressed: Keys.onSpacePressed(event)
Keys.onReturnPressed: Keys.onSpacePressed(event)
Keys.onRightPressed: {
// Navigate into sublists but don't select an OS entry
if (currentIndex != -1 && isOSsublist(model.get(currentIndex)))
selectOSitem(model.get(currentIndex), true)
}
2020-03-04 16:55:40 +01:00
}
}
}
}
}
2020-03-04 16:55:40 +01:00
Component {
id: suboslist
ListView {
model: ListModel {
ListElement {
url: ""
icon: "icons/ic_chevron_left_40px.svg"
extract_size: 0
image_download_size: 0
extract_sha256: ""
contains_multiple_files: false
release_date: ""
subitems_url: "internal://back"
subitems_json: ""
name: qsTr("Back")
description: qsTr("Go back to main menu")
tooltip: ""
2021-05-07 13:10:23 +02:00
website: ""
init_format: ""
2020-03-04 16:55:40 +01:00
}
}
currentIndex: -1
delegate: osdelegate
width: window.width-100
height: window.height-100
boundsBehavior: Flickable.StopAtBounds
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
ScrollBar.vertical: ScrollBar {
width: 10
policy: parent.contentHeight > parent.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded
}
Keys.onSpacePressed: {
if (currentIndex != -1)
selectOSitem(model.get(currentIndex))
}
Accessible.onPressAction: {
if (currentIndex != -1)
selectOSitem(model.get(currentIndex))
}
Keys.onEnterPressed: Keys.onSpacePressed(event)
Keys.onReturnPressed: Keys.onSpacePressed(event)
Keys.onRightPressed: {
// Navigate into sublists but don't select an OS entry
if (currentIndex != -1 && isOSsublist(model.get(currentIndex)))
selectOSitem(model.get(currentIndex), true)
}
Keys.onLeftPressed: {
osswipeview.decrementCurrentIndex()
ospopup.categorySelected = ""
}
2020-03-04 16:55:40 +01:00
}
}
ListModel {
id: osmodel
Component.onCompleted: {
if (imageWriter.isOnline()) {
fetchOSlist();
}
2020-03-04 16:55:40 +01:00
}
}
Component {
id: hwdelegate
Item {
width: window.width-100
height: contentLayout.implicitHeight + 24
Accessible.name: name+".\n"+description
MouseArea {
id: hwMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
bgrect.mouseOver = true
}
onExited: {
bgrect.mouseOver = false
}
onClicked: {
selectHWitem(model)
}
}
Rectangle {
id: bgrect
anchors.fill: parent
color: "#f5f5f5"
visible: mouseOver && parent.ListView.view.currentIndex !== index
property bool mouseOver: false
}
Rectangle {
id: borderrect
implicitHeight: 1
implicitWidth: parent.width
color: "#dcdcdc"
y: parent.height
}
RowLayout {
id: contentLayout
anchors {
left: parent.left
top: parent.top
right: parent.right
margins: 12
}
spacing: 12
Image {
source: icon == "icons/ic_build_48px.svg" ? "icons/cat_misc_utility_images.png": icon
Layout.preferredHeight: 64
Layout.preferredWidth: 64
sourceSize.width: 64
sourceSize.height: 64
fillMode: Image.PreserveAspectFit
verticalAlignment: Image.AlignVCenter
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
Text {
text: name
elide: Text.ElideRight
font.family: roboto.name
font.bold: true
}
Text {
Layout.fillWidth: true
font.family: roboto.name
text: description
wrapMode: Text.WordWrap
color: "#1a1a1a"
}
ToolTip {
visible: hwMouseArea.containsMouse && typeof(tooltip) == "string" && tooltip != ""
delay: 1000
text: typeof(tooltip) == "string" ? tooltip : ""
clip: false
}
}
}
}
}
2020-03-04 16:55:40 +01:00
Component {
id: osdelegate
Item {
width: window.width-100
height: contentLayout.implicitHeight + 24
Accessible.name: name+".\n"+description
2020-03-04 16:55:40 +01:00
MouseArea {
id: osMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
bgrect.mouseOver = true
}
onExited: {
bgrect.mouseOver = false
}
onClicked: {
selectOSitem(model)
}
}
2020-03-04 16:55:40 +01:00
Rectangle {
id: bgrect
anchors.fill: parent
color: "#f5f5f5"
visible: mouseOver && parent.ListView.view.currentIndex !== index
property bool mouseOver: false
2020-03-04 16:55:40 +01:00
}
Rectangle {
id: borderrect
implicitHeight: 1
implicitWidth: parent.width
color: "#dcdcdc"
y: parent.height
}
RowLayout {
id: contentLayout
anchors {
left: parent.left
top: parent.top
right: parent.right
margins: 12
}
spacing: 12
Image {
source: icon == "icons/ic_build_48px.svg" ? "icons/cat_misc_utility_images.png": icon
Layout.preferredHeight: 40
Layout.preferredWidth: 40
sourceSize.width: 40
sourceSize.height: 40
fillMode: Image.PreserveAspectFit
verticalAlignment: Image.AlignVCenter
Layout.alignment: Qt.AlignVCenter
2020-03-04 16:55:40 +01:00
}
ColumnLayout {
Layout.fillWidth: true
2020-03-04 16:55:40 +01:00
RowLayout {
spacing: 12
Text {
text: name
elide: Text.ElideRight
font.family: roboto.name
font.bold: true
}
Image {
source: "icons/ic_info_16px.png"
Layout.preferredHeight: 16
Layout.preferredWidth: 16
visible: typeof(website) == "string" && website
MouseArea {
anchors.fill: parent
onClicked: Qt.openUrlExternally(website)
2020-03-04 16:55:40 +01:00
}
}
Item {
Layout.fillWidth: true
}
2020-03-04 16:55:40 +01:00
}
Text {
Layout.fillWidth: true
font.family: roboto.name
text: description
wrapMode: Text.WordWrap
color: "#1a1a1a"
2020-03-04 16:55:40 +01:00
}
Text {
Layout.fillWidth: true
elide: Text.ElideRight
color: "#646464"
font.weight: Font.Light
visible: typeof(release_date) == "string" && release_date
text: qsTr("Released: %1").arg(release_date)
}
Text {
Layout.fillWidth: true
elide: Text.ElideRight
color: "#646464"
font.weight: Font.Light
visible: typeof(url) == "string" && url != "" && url != "internal://format"
text: !url ? "" :
typeof(extract_sha256) != "undefined" && imageWriter.isCached(url,extract_sha256)
? qsTr("Cached on your computer")
: url.startsWith("file://")
? qsTr("Local file")
: qsTr("Online - %1 GB download").arg((image_download_size/1073741824).toFixed(1))
}
2020-03-04 16:55:40 +01:00
ToolTip {
visible: osMouseArea.containsMouse && typeof(tooltip) == "string" && tooltip != ""
delay: 1000
text: typeof(tooltip) == "string" ? tooltip : ""
clip: false
2021-05-07 13:10:23 +02:00
}
2020-03-04 16:55:40 +01:00
}
Image {
source: "icons/ic_chevron_right_40px.svg"
visible: (typeof(subitems_json) == "string" && subitems_json != "") || (typeof(subitems_url) == "string" && subitems_url != "" && subitems_url != "internal://back")
Layout.preferredHeight: 40
Layout.preferredWidth: 40
fillMode: Image.PreserveAspectFit
}
2020-03-04 16:55:40 +01:00
}
}
}
/*
Popup for storage device selection
2020-03-04 16:55:40 +01:00
*/
Popup {
id: dstpopup
x: 50
y: 25
width: parent.width-100
height: parent.height-50
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onClosed: imageWriter.stopDriveListPolling()
2020-03-04 16:55:40 +01:00
// background of title
Rectangle {
color: "#f5f5f5"
anchors.right: parent.right
anchors.top: parent.top
height: 35
width: parent.width
}
// line under title
Rectangle {
color: "#afafaf"
width: parent.width
y: 35
implicitHeight: 1
}
Text {
text: "X"
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 25
anchors.topMargin: 10
font.family: roboto.name
font.bold: true
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
dstpopup.close()
}
}
}
ColumnLayout {
spacing: 10
Text {
text: qsTr("Storage")
2020-03-04 16:55:40 +01:00
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.topMargin: 10
font.family: roboto.name
font.bold: true
}
Item {
clip: true
Layout.preferredWidth: dstlist.width
Layout.preferredHeight: dstlist.height
2020-03-04 16:55:40 +01:00
ListView {
id: dstlist
model: driveListModel
delegate: dstdelegate
width: window.width-100
height: window.height-100
2020-03-04 16:55:40 +01:00
boundsBehavior: Flickable.StopAtBounds
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
2020-03-04 16:55:40 +01:00
ScrollBar.vertical: ScrollBar {
width: 10
2020-05-23 13:02:38 +02:00
policy: dstlist.contentHeight > dstlist.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded
2020-03-04 16:55:40 +01:00
}
Keys.onSpacePressed: {
if (currentIndex == -1)
return
selectDstItem(currentItem)
}
Accessible.onPressAction: {
if (currentIndex == -1)
return
selectDstItem(currentItem)
}
Keys.onEnterPressed: Keys.onSpacePressed(event)
Keys.onReturnPressed: Keys.onSpacePressed(event)
2020-03-04 16:55:40 +01:00
}
}
}
}
Component {
id: dstdelegate
Item {
width: window.width-100
height: 60
Accessible.name: {
var txt = description+" - "+(size/1000000000).toFixed(1)+" gigabytes"
if (mountpoints.length > 0) {
txt += qsTr("Mounted as %1").arg(mountpoints.join(", "))
}
return txt;
}
property string description: model.description
property string device: model.device
property string size: model.size
2020-03-04 16:55:40 +01:00
Rectangle {
id: dstbgrect
anchors.fill: parent
color: "#f5f5f5"
visible: mouseOver && parent.ListView.view.currentIndex !== index
property bool mouseOver: false
2020-03-04 16:55:40 +01:00
}
Rectangle {
id: dstborderrect
implicitHeight: 1
implicitWidth: parent.width
color: "#dcdcdc"
y: parent.height
}
Row {
leftPadding: 25
Column {
width: 64
Image {
source: isUsb ? "icons/ic_usb_40px.svg" : isScsi ? "icons/ic_storage_40px.svg" : "icons/ic_sd_storage_40px.svg"
2020-03-04 16:55:40 +01:00
verticalAlignment: Image.AlignVCenter
height: parent.parent.parent.height
fillMode: Image.Pad
}
}
Column {
width: parent.parent.width-64
Text {
textFormat: Text.StyledText
height: parent.parent.parent.height
verticalAlignment: Text.AlignVCenter
font.family: roboto.name
text: {
var sizeStr = (size/1000000000).toFixed(1)+" GB";
var txt;
if (isReadOnly) {
txt = "<p><font size='4' color='grey'>"+description+" - "+sizeStr+"</font></p>"
txt += "<font color='grey'>"
if (mountpoints.length > 0) {
txt += qsTr("Mounted as %1").arg(mountpoints.join(", "))+" "
}
txt += qsTr("[WRITE PROTECTED]")+"</font>"
} else {
txt = "<p><font size='4'>"+description+" - "+sizeStr+"</font></p>"
if (mountpoints.length > 0) {
txt += "<font color='grey'>"+qsTr("Mounted as %1").arg(mountpoints.join(", "))+"</font>"
}
2020-03-04 16:55:40 +01:00
}
return txt;
}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
dstbgrect.mouseOver = true
2020-03-04 16:55:40 +01:00
}
onExited: {
dstbgrect.mouseOver = false
2020-03-04 16:55:40 +01:00
}
onClicked: {
selectDstItem(model)
2020-03-04 16:55:40 +01:00
}
}
}
}
MsgPopup {
2020-03-04 16:55:40 +01:00
id: msgpopup
}
2020-03-04 16:55:40 +01:00
MsgPopup {
id: quitpopup
continueButton: false
yesButton: true
noButton: true
title: qsTr("Are you sure you want to quit?")
text: qsTr("Raspberry Pi Imager is still busy.<br>Are you sure you want to quit?")
onYes: {
Qt.quit()
2020-03-04 16:55:40 +01:00
}
}
2020-03-04 16:55:40 +01:00
MsgPopup {
id: confirmwritepopup
continueButton: false
yesButton: true
noButton: true
title: qsTr("Warning")
onYes: {
langbarRect.visible = false
writebutton.visible = false
writebutton.enabled = false
cancelwritebutton.enabled = true
cancelwritebutton.visible = true
cancelverifybutton.enabled = true
progressText.text = qsTr("Preparing to write...");
progressText.visible = true
progressBar.visible = true
progressBar.indeterminate = true
progressBar.Material.accent = "#ffffff"
osbutton.enabled = false
dstbutton.enabled = false
hwbutton.enabled = false
imageWriter.setVerifyEnabled(true)
imageWriter.startWrite()
2020-03-04 16:55:40 +01:00
}
function askForConfirmation()
{
text = qsTr("All existing data on '%1' will be erased.<br>Are you sure you want to continue?").arg(dstbutton.text)
openPopup()
2020-03-04 16:55:40 +01:00
}
}
2020-11-23 19:23:20 +01:00
MsgPopup {
id: updatepopup
continueButton: false
yesButton: true
noButton: true
property url url
title: qsTr("Update available")
text: qsTr("There is a newer version of Imager available.<br>Would you like to visit the website to download it?")
onYes: {
Qt.openUrlExternally(url)
}
}
OptionsPopup {
id: optionspopup
onSaveSettingsSignal: {
imageWriter.setSavedCustomizationSettings(settings)
usesavedsettingspopup.hasSavedSettings = true
}
}
UseSavedSettingsPopup {
id: usesavedsettingspopup
onYes: {
optionspopup.initialize()
optionspopup.applySettings()
confirmwritepopup.askForConfirmation()
}
onNo: {
confirmwritepopup.askForConfirmation()
}
onNoClearSettings: {
hasSavedSettings = false
optionspopup.clearCustomizationFields()
imageWriter.clearSavedCustomizationSettings()
confirmwritepopup.askForConfirmation()
}
onEditSettings: {
optionspopup.openPopup()
}
}
2020-03-04 16:55:40 +01:00
/* Utility functions */
function httpRequest(url, callback) {
var xhr = new XMLHttpRequest();
xhr.timeout = 5000
xhr.onreadystatechange = (function(x) {
return function() {
if (x.readyState === x.DONE)
{
if (x.status === 200)
{
callback(x)
}
else
{
onError(qsTr("Error downloading OS list from Internet"))
}
}
}
})(xhr)
xhr.open("GET", url)
xhr.send()
}
/* Slots for signals imagewrite emits */
function onDownloadProgress(now,total) {
var newPos
if (total) {
newPos = now/(total+1)
} else {
newPos = 0
}
if (progressBar.value !== newPos) {
if (progressText.text === qsTr("Cancelling..."))
2020-03-04 16:55:40 +01:00
return
progressText.text = qsTr("Writing... %1%").arg(Math.floor(newPos*100))
progressBar.indeterminate = false
progressBar.value = newPos
}
}
function onVerifyProgress(now,total) {
var newPos
if (total) {
newPos = now/total
} else {
newPos = 0
}
if (progressBar.value !== newPos) {
2020-03-04 16:55:40 +01:00
if (cancelwritebutton.visible) {
cancelwritebutton.visible = false
cancelverifybutton.visible = true
}
if (progressText.text === qsTr("Finalizing..."))
2020-03-04 16:55:40 +01:00
return
progressText.text = qsTr("Verifying... %1%").arg(Math.floor(newPos*100))
progressBar.Material.accent = "#6cc04a"
progressBar.value = newPos
}
}
function onPreparationStatusUpdate(msg) {
progressText.text = qsTr("Preparing to write... (%1)").arg(msg)
}
2020-03-04 16:55:40 +01:00
function resetWriteButton() {
progressText.visible = false
progressBar.visible = false
osbutton.enabled = true
dstbutton.enabled = true
hwbutton.enabled = true
2020-03-04 16:55:40 +01:00
writebutton.visible = true
writebutton.enabled = imageWriter.readyToWrite()
cancelwritebutton.visible = false
cancelverifybutton.visible = false
}
function onError(msg) {
msgpopup.title = qsTr("Error")
msgpopup.text = msg
msgpopup.openPopup()
2020-03-04 16:55:40 +01:00
resetWriteButton()
}
function onSuccess() {
msgpopup.title = qsTr("Write Successful")
if (osbutton.text === qsTr("Erase"))
msgpopup.text = qsTr("<b>%1</b> has been erased<br><br>You can now remove the SD card from the reader").arg(dstbutton.text)
else if (imageWriter.isEmbeddedMode()) {
//msgpopup.text = qsTr("<b>%1</b> has been written to <b>%2</b>").arg(osbutton.text).arg(dstbutton.text)
/* Just reboot to the installed OS */
Qt.quit()
}
else
msgpopup.text = qsTr("<b>%1</b> has been written to <b>%2</b><br><br>You can now remove the SD card from the reader").arg(osbutton.text).arg(dstbutton.text)
if (imageWriter.isEmbeddedMode()) {
msgpopup.continueButton = false
msgpopup.quitButton = true
}
msgpopup.openPopup()
2020-03-04 16:55:40 +01:00
imageWriter.setDst("")
dstbutton.text = qsTr("CHOOSE STORAGE")
2020-03-04 16:55:40 +01:00
resetWriteButton()
}
function onFileSelected(file) {
imageWriter.setSrc(file)
osbutton.text = imageWriter.srcFileName()
ospopup.close()
osswipeview.decrementCurrentIndex()
2020-03-04 16:55:40 +01:00
if (imageWriter.readyToWrite()) {
writebutton.enabled = true
}
}
function onCancelled() {
resetWriteButton()
}
function onFinalizing() {
progressText.text = qsTr("Finalizing...")
}
function shuffle(arr) {
for (var i = 0; i < arr.length - 1; i++) {
var j = i + Math.floor(Math.random() * (arr.length - i));
var t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
function checkForRandom(list) {
for (var i in list) {
var entry = list[i]
if ("subitems" in entry) {
checkForRandom(entry["subitems"])
if ("random" in entry && entry["random"]) {
shuffle(entry["subitems"])
}
}
}
}
function filterItems(list, tags, matchingType)
{
if (!tags || !tags.length)
return
var i = list.length
while (i--) {
var entry = list[i]
if ("devices" in entry && entry["devices"].length) {
var foundTag = false
switch(matchingType) {
case 0: /* exact matching */
case 2: /* exact matching */
for (var j in tags)
{
if (entry["devices"].includes(tags[j]))
{
foundTag = true
break
}
}
/* If there's no match, remove this item from the list. */
if (!foundTag)
{
list.splice(i, 1)
continue
}
break
case 1: /* Exlusive by prefix matching */
case 3: /* Inclusive by prefix matching */
for (var deviceTypePrefix in tags) {
for (var deviceSpec in entry["devices"]) {
if (deviceSpec.startsWith(deviceTypePrefix)) {
foundTag = true
break
}
}
/* Terminate outer loop early if we've already
* decided it's a match
*/
if (foundTag) {
break
}
}
/* If there's no match, remove this item from the list. */
if (!foundTag)
{
list.splice(i, 1)
continue
}
break
}
} else {
/* No device list attached? If we're in an exclusive mode that's bad news indeed. */
switch (matchingType) {
case 0:
case 1:
if (!("subitems" in entry)) {
/* If you're not carrying subitems, you're not going in. */
list.splice(i, 1)
}
break
case 2:
case 3:
/* Inclusive filtering. We're keeping this one. */
break;
}
}
if ("subitems" in entry) {
filterItems(entry["subitems"], tags, hwTagMatchingType)
}
}
}
function oslistFromJson(o) {
var oslist = false
var lang_country = Qt.locale().name
if ("os_list_"+lang_country in o) {
oslist = o["os_list_"+lang_country]
}
else if (lang_country.includes("_")) {
var lang = lang_country.substr(0, lang_country.indexOf("_"))
if ("os_list_"+lang in o) {
oslist = o["os_list_"+lang]
}
}
if (!oslist) {
if (!"os_list" in o) {
onError(qsTr("Error parsing os_list.json"))
return false
}
oslist = o["os_list"]
}
if (hwTags != "") {
filterItems(oslist, JSON.parse(hwTags), hwTagMatchingType)
}
checkForRandom(oslist)
/* Flatten subitems to subitems_json */
for (var i in oslist) {
var entry = oslist[i];
if ("subitems" in entry) {
entry["subitems_json"] = JSON.stringify(entry["subitems"])
delete entry["subitems"]
}
}
return oslist
}
function selectNamedOS(name, collection)
{
for (var i = 0; i < collection.count; i++) {
var os = collection.get(i)
if (typeof(os.subitems_json) == "string" && os.subitems_json != "") {
selectNamedOS(name, os.subitems_json)
}
else if (typeof(os.url) !== "undefined" && name === os.name) {
selectOSitem(os, false)
break
}
}
}
function fetchOSlist() {
httpRequest(imageWriter.constantOsListUrl(), function (x) {
var o = JSON.parse(x.responseText)
var oslist = oslistFromJson(o)
if (oslist === false)
return
osmodel.clear()
for (var i in oslist) {
osmodel.append(oslist[i])
}
2020-11-23 19:23:20 +01:00
if ("imager" in o) {
var imager = o["imager"]
if ("devices" in imager)
{
deviceModel.clear()
var devices = imager["devices"]
for (var j in devices)
{
devices[j]["tags"] = JSON.stringify(devices[j]["tags"])
deviceModel.append(devices[j])
if ("default" in devices[j] && devices[j]["default"])
{
hwlist.currentIndex = deviceModel.count-1
}
}
}
if (imageWriter.getBoolSetting("check_version") && "latest_version" in imager && "url" in imager) {
if (!imageWriter.isEmbeddedMode() && imageWriter.isVersionNewer(imager["latest_version"])) {
2020-11-23 19:23:20 +01:00
updatepopup.url = imager["url"]
updatepopup.openPopup()
}
}
if ("default_os" in imager) {
selectNamedOS(imager["default_os"], osmodel)
}
if (imageWriter.isEmbeddedMode()) {
if ("embedded_default_os" in imager) {
selectNamedOS(imager["embedded_default_os"], osmodel)
}
if ("embedded_default_destination" in imager) {
imageWriter.startDriveListPolling()
setDefaultDest.drive = imager["embedded_default_destination"]
setDefaultDest.start()
}
}
2020-11-23 19:23:20 +01:00
}
/* Add in our 'special' items. */
osmodel.append({
url: "internal://format",
icon: "icons/erase.png",
extract_size: 0,
image_download_size: 0,
extract_sha256: "",
contains_multiple_files: false,
release_date: "",
subitems_url: "",
subitems_json: "",
name: qsTr("Erase"),
description: qsTr("Format card as FAT32"),
tooltip: "",
website: "",
init_format: ""
})
osmodel.append({
url: "",
icon: "icons/use_custom.png",
name: qsTr("Use custom"),
description: qsTr("Select a custom .img from your computer")
})
})
}
Timer {
/* Verify if default drive is in our list after 100 ms */
id: setDefaultDest
property string drive : ""
interval: 100
onTriggered: {
for (var i = 0; i < driveListModel.rowCount(); i++)
{
/* FIXME: there should be a better way to iterate drivelist than
fetch data by numeric role number */
if (driveListModel.data(driveListModel.index(i,0), 0x101) === drive) {
selectDstItem({
device: drive,
description: driveListModel.data(driveListModel.index(i,0), 0x102),
size: driveListModel.data(driveListModel.index(i,0), 0x103),
readonly: false
})
break
}
}
}
}
function newSublist() {
if (osswipeview.currentIndex == (osswipeview.count-1))
{
var newlist = suboslist.createObject(osswipeview)
osswipeview.addItem(newlist)
}
var m = osswipeview.itemAt(osswipeview.currentIndex+1).model
if (m.count>1)
{
m.remove(1, m.count-1)
}
return m
}
function selectHWitem(hwmodel) {
hwTags = hwmodel.tags
if (hwmodel.matching_type) {
switch (hwmodel.matching_type) {
case "exclusive":
hwTagMatchingType = 0
break;
case "exclusive_prefix":
hwTagMatchingType = 1
break;
case "inclusive":
hwTagMatchingType = 2
break;
case "inclusive_prefix":
hwTagMatchingType = 3
break;
}
} else {
/* Default is exclusive exact matching */
hwTagMatchingType = 0
}
/* Reload list */
httpRequest(imageWriter.constantOsListUrl(), function (x) {
var o = JSON.parse(x.responseText)
var oslist = oslistFromJson(o)
if (oslist === false)
return
osmodel.remove(0, osmodel.count-2)
for (var i in oslist) {
osmodel.insert(osmodel.count-2, oslist[i])
}
})
hwbutton.text = hwmodel.name
hwpopup.close()
}
/// Is the item a sub-list or sub-sub-list in the OS selection model?
function isOSsublist(d) {
// Top level category
if (typeof(d.subitems_json) == "string" && d.subitems_json !== "") {
return true
}
// Sub-category
if (typeof(d.subitems_url) == "string" && d.subitems_url !== ""
&& d.subitems_url !== "internal://back")
{
return true
}
return false
}
function selectOSitem(d, selectFirstSubitem)
{
if (typeof(d.subitems_json) == "string" && d.subitems_json !== "") {
var m = newSublist()
var subitems = JSON.parse(d.subitems_json)
for (var i in subitems)
{
var entry = subitems[i];
if ("subitems" in entry) {
/* Flatten sub-subitems entry */
entry["subitems_json"] = JSON.stringify(entry["subitems"])
delete entry["subitems"]
}
m.append(entry)
}
osswipeview.itemAt(osswipeview.currentIndex+1).currentIndex = (selectFirstSubitem === true) ? 0 : -1
osswipeview.incrementCurrentIndex()
ospopup.categorySelected = d.name
} else if (typeof(d.subitems_url) == "string" && d.subitems_url !== "") {
if (d.subitems_url === "internal://back")
{
osswipeview.decrementCurrentIndex()
ospopup.categorySelected = ""
}
else
{
ospopup.categorySelected = d.name
2020-11-23 17:59:18 +01:00
var suburl = d.subitems_url
var m = newSublist()
2020-11-23 17:59:18 +01:00
httpRequest(suburl, function (x) {
var o = JSON.parse(x.responseText)
var oslist = oslistFromJson(o)
if (oslist === false)
return
for (var i in oslist) {
m.append(oslist[i])
}
})
osswipeview.itemAt(osswipeview.currentIndex+1).currentIndex = (selectFirstSubitem === true) ? 0 : -1
osswipeview.incrementCurrentIndex()
}
} else if (d.url === "") {
if (!imageWriter.isEmbeddedMode()) {
imageWriter.openFileDialog()
}
else {
if (imageWriter.mountUsbSourceMedia()) {
var m = newSublist()
var oslist = JSON.parse(imageWriter.getUsbSourceOSlist())
for (var i in oslist) {
m.append(oslist[i])
}
osswipeview.itemAt(osswipeview.currentIndex+1).currentIndex = (selectFirstSubitem === true) ? 0 : -1
osswipeview.incrementCurrentIndex()
}
else
{
onError(qsTr("Connect an USB stick containing images first.<br>The images must be located in the root folder of the USB stick."))
}
}
} else {
imageWriter.setSrc(d.url, d.image_download_size, d.extract_size, typeof(d.extract_sha256) != "undefined" ? d.extract_sha256 : "", typeof(d.contains_multiple_files) != "undefined" ? d.contains_multiple_files : false, ospopup.categorySelected, d.name, typeof(d.init_format) != "undefined" ? d.init_format : "")
osbutton.text = d.name
ospopup.close()
osswipeview.decrementCurrentIndex()
if (imageWriter.readyToWrite()) {
writebutton.enabled = true
}
}
}
function selectDstItem(d) {
if (d.isReadOnly) {
onError(qsTr("SD card is write protected.<br>Push the lock switch on the left side of the card upwards, and try again."))
return
}
dstpopup.close()
imageWriter.setDst(d.device, d.size)
dstbutton.text = d.description
if (imageWriter.readyToWrite()) {
writebutton.enabled = true
}
}
2020-03-04 16:55:40 +01:00
}