Device-first OS list filtering

Rather than a drop down dialog, which could present users with images
that may not run on their hardware, allow selection of Raspberry Pi as a
first stage. If users adopt this feature, they are presented with a
subset of images that we know will actually run on their hardware.

This is achieved by leveraging @maxnet's excellent OS filtering scheme.

Future work will attach image and description support to this OS list.
This commit is contained in:
Tom Dewey tom.dewey@raspberrypi.com 2023-10-03 22:24:49 +01:00
parent 591452aba9
commit 3f665e01b4
3 changed files with 315 additions and 90 deletions

View file

@ -194,7 +194,8 @@
"extract_sha256": "ceb7d7489847ed811e7746fa779837f78fc06d43663148a696280e6a1cfe00e3",
"image_download_size": 1306588543,
"release_date": "2022-01-28",
"init_format": "systemd"
"init_format": "systemd",
"devices": ["pi1a"]
}
],
"required": [
@ -205,7 +206,8 @@
"extract_size",
"extract_sha256",
"image_download_size",
"release_date"
"release_date",
"devices"
],
"properties": {
"name": {
@ -298,6 +300,18 @@
"systemd"
]
},
"devices": {
"$id": "#/properties/os_list/items/anyOf/0/properties/compat_with",
"type": "array",
"title": "The compat_with schema",
"description": "Provides a JSON-format list of strings representing Raspberry Pi devices that are supported with this image",
"default": "",
"examples": [
"[\"1a\", \"1b\"]",
"[\"4\", \"5\"]",
"[\"cm3\", \"cm4\"]"
]
},
"website": {
"$id": "#/properties/os_list/items/anyOf/1/properties/website",
"type": "string",

View file

@ -467,18 +467,28 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../main.qml" line="97"/>
<location filename="../main.qml" line="109"/>
<source>CHOOSE DEVICE</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../main.qml" line="120"/>
<source>Select this button to choose your target Raspberry Pi</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../main.qml" line="132"/>
<location filename="../main.qml" line="413"/>
<source>Operating System</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../main.qml" line="109"/>
<location filename="../main.qml" line="144"/>
<source>CHOOSE OS</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../main.qml" line="121"/>
<location filename="../main.qml" line="156"/>
<source>Select this button to change the operating system</source>
<translation type="unfinished"></translation>
</message>

View file

@ -14,9 +14,9 @@ ApplicationWindow {
id: window
visible: true
width: imageWriter.isEmbeddedMode() ? -1 : 680
width: imageWriter.isEmbeddedMode() ? -1 : 800
height: imageWriter.isEmbeddedMode() ? -1 : 450
minimumWidth: imageWriter.isEmbeddedMode() ? -1 : 680
minimumWidth: imageWriter.isEmbeddedMode() ? -1 : 800
minimumHeight: imageWriter.isEmbeddedMode() ? -1 : 420
title: qsTr("Raspberry Pi Imager v%1").arg(imageWriter.constantVersion())
@ -25,6 +25,8 @@ ApplicationWindow {
FontLoader {id: robotoLight; source: "fonts/Roboto-Light.ttf"}
FontLoader {id: robotoBold; source: "fonts/Roboto-Bold.ttf"}
property string hwTags
onClosing: {
if (progressBar.visible) {
close.accepted = false
@ -83,11 +85,47 @@ ApplicationWindow {
anchors.leftMargin: 50
rows: 6
columns: 3
columns: 4
columnSpacing: 25
ColumnLayout {
id: columnLayout
id: columnLayout0
spacing: 0
Layout.fillWidth: true
Text {
id: text0
color: "#ffffff"
text: qsTr("Select your 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()
hwswipeview.currentItem.forceActiveFocus()
}
Accessible.ignored: ospopup.visible || dstpopup.visible
Accessible.description: qsTr("Select this button to choose your target Raspberry Pi")
}
}
ColumnLayout {
id: columnLayout1
spacing: 0
Layout.fillWidth: true
@ -359,6 +397,107 @@ ApplicationWindow {
}
}
Popup {
id: hwpopup
x: 50
y: 25
width: parent.width-100
height: parent.height-50
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
property string hwselected: ""
// 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()
}
}
}
ColumnLayout {
spacing: 10
Text {
text: qsTr("Raspberry Pi Device")
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
SwipeView {
id: hwswipeview
interactive: false
ListView {
id: hwlist
model: ListModel {
id: deviceModel
ListElement {
name: qsTr("[ All ]")
tags: "[]"
}
}
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
*/
@ -419,59 +558,6 @@ ApplicationWindow {
font.bold: true
}
Rectangle {
id: modelRowRect
color: "#ffffe3"
Layout.fillWidth: true
implicitHeight: modelRow.implicitHeight
visible: osswipeview.currentIndex == 0
Layout.bottomMargin: -10
Row {
id: modelRow
spacing: 15
leftPadding: 15
Text {
id: modelText
text: qsTr("Pi model:")
font.family: roboto.name
verticalAlignment: Qt.AlignVCenter
height: parent.height
}
ComboBox {
id: deviceModelCombo
model: ListModel {
id: deviceModel
ListElement {
name: qsTr("[ All ]")
tags: "[]"
}
}
width: 300
textRole: "name"
font.family: roboto.name
font.pixelSize: 12
currentIndex: 0
onCurrentIndexChanged: {
/* 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])
}
})
}
}
}
}
Item {
clip: true
Layout.preferredWidth: oslist.width
@ -487,7 +573,7 @@ ApplicationWindow {
currentIndex: -1
delegate: osdelegate
width: window.width-100
height: modelRowRect.visible ? window.height-100-modelRowRect.height : window.height-100
height: window.height-100
boundsBehavior: Flickable.StopAtBounds
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
ScrollBar.vertical: ScrollBar {
@ -559,30 +645,6 @@ ApplicationWindow {
ListModel {
id: osmodel
ListElement {
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: ""
}
ListElement {
url: ""
icon: "icons/use_custom.png"
name: qsTr("Use custom")
description: qsTr("Select a custom .img from your computer")
}
Component.onCompleted: {
if (imageWriter.isOnline()) {
fetchOSlist();
@ -590,6 +652,97 @@ ApplicationWindow {
}
}
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: 40
Layout.preferredWidth: 40
sourceSize.width: 40
sourceSize.height: 40
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
}
}
}
}
}
Component {
id: osdelegate
@ -1206,7 +1359,9 @@ ApplicationWindow {
oslist = o["os_list"]
}
filterItems(oslist, JSON.parse(deviceModel.get(deviceModelCombo.currentIndex).tags))
if (hwTags != "") {
filterItems(oslist, JSON.parse(hwTags))
}
checkForRandom(oslist)
/* Flatten subitems to subitems_json */
@ -1242,8 +1397,9 @@ ApplicationWindow {
var oslist = oslistFromJson(o)
if (oslist === false)
return
osmodel.clear()
for (var i in oslist) {
osmodel.insert(osmodel.count-2, oslist[i])
osmodel.append(oslist[i])
}
if ("imager" in o) {
@ -1251,6 +1407,7 @@ ApplicationWindow {
if ("devices" in imager)
{
deviceModel.clear()
var devices = imager["devices"]
for (var j in devices)
{
@ -1258,7 +1415,7 @@ ApplicationWindow {
deviceModel.append(devices[j])
if ("default" in devices[j] && devices[j]["default"])
{
deviceModelCombo.currentIndex = deviceModel.count-1
hwlist.currentIndex = deviceModel.count-1
}
}
}
@ -1283,6 +1440,31 @@ ApplicationWindow {
}
}
}
/* 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")
})
})
}
@ -1326,6 +1508,25 @@ ApplicationWindow {
return m
}
function selectHWitem(hwmodel) {
hwTags = hwmodel.tags
/* 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()
}
function selectOSitem(d, selectFirstSubitem)
{
if (typeof(d.subitems_json) == "string" && d.subitems_json !== "") {