From 1da6636ef354b77857a57059e47c3801304361cf Mon Sep 17 00:00:00 2001 From: Andrew Guschin Date: Mon, 3 Aug 2026 17:52:58 +0400 Subject: remake ui with kirigami --- crates/flasher-qt/Cargo.toml | 10 +- crates/flasher-qt/build.rs | 27 +-- crates/flasher-qt/qml/Main.qml | 437 +++++++++++++++++++++++++++++++++++++++++ crates/flasher-qt/src/main.rs | 59 +++--- crates/flasher-qt/src/ui.cpp | 404 ------------------------------------- 5 files changed, 469 insertions(+), 468 deletions(-) create mode 100644 crates/flasher-qt/qml/Main.qml delete mode 100644 crates/flasher-qt/src/ui.cpp (limited to 'crates') diff --git a/crates/flasher-qt/Cargo.toml b/crates/flasher-qt/Cargo.toml index d964edc..a4f0642 100644 --- a/crates/flasher-qt/Cargo.toml +++ b/crates/flasher-qt/Cargo.toml @@ -2,10 +2,12 @@ name = "flasher-qt" version = "0.1.0" edition = "2024" -description = "A native Qt Widgets interface for preparing ALT Linux SBC images" +description = "A Qt 6/KDE Kirigami interface for preparing ALT Linux SBC images" license = "GPL-2.0-or-later" -build = "build.rs" + +[dependencies] +cxx-qt-lib = { version = "0.9.1", features = ["qt_gui", "qt_qml", "link_qt_object_files"] } +serde_json = "1" [build-dependencies] -cc = "1.4" -pkg-config = "0.3" +cxx-qt-build = { version = "0.9.1", features = ["link_qt_object_files"] } diff --git a/crates/flasher-qt/build.rs b/crates/flasher-qt/build.rs index e4bdfc2..d57bff5 100644 --- a/crates/flasher-qt/build.rs +++ b/crates/flasher-qt/build.rs @@ -1,25 +1,6 @@ fn main() { - println!("cargo:rerun-if-changed=src/ui.cpp"); - - let qt = pkg_config::Config::new() - .atleast_version("5.12") - .cargo_metadata(false) - .probe("Qt5Widgets") - .expect("Qt 5 Widgets development files were not found (install qt5-base-devel)"); - - let mut build = cc::Build::new(); - build.cpp(true).file("src/ui.cpp").flag_if_supported("-std=c++17"); - for path in &qt.include_paths { - build.include(path); - } - build.compile("flasher_qt_ui"); - - // Keep shared Qt libraries after our static C++ archive on the linker - // command line; GNU ld resolves static archives from left to right. - for path in &qt.link_paths { - println!("cargo:rustc-link-search=native={}", path.display()); - } - println!("cargo:rustc-link-lib=Qt5Widgets"); - println!("cargo:rustc-link-lib=Qt5Gui"); - println!("cargo:rustc-link-lib=Qt5Core"); + cxx_qt_build::CxxQtBuilder::new() + .qt_module("Qml") + .qt_module("Quick") + .build(); } diff --git a/crates/flasher-qt/qml/Main.qml b/crates/flasher-qt/qml/Main.qml new file mode 100644 index 0000000..805c6ed --- /dev/null +++ b/crates/flasher-qt/qml/Main.qml @@ -0,0 +1,437 @@ +import QtQuick +import QtQuick.Controls as Controls +import QtQuick.Layouts +import QtQuick.Dialogs +import org.kde.kirigami as Kirigami + +Kirigami.ApplicationWindow { + id: root + visible: true + width: 760 + height: 820 + minimumWidth: 600 + minimumHeight: 580 + title: qsTr("ALT Image Writer") + + readonly property string supportedBoardsText: @SUPPORTED_BOARDS@ + readonly property string availableDrivesText: @AVAILABLE_DRIVES@ + property bool sourceIsImage: false + property bool outputIsFile: false + property string sourcePath: "" + property string outputPath: "" + property string selectedBoard: "" + property string selectedPlatform: "" + property var driveNames: availableDrivesText.length ? availableDrivesText.split("\n") : [] + + function localPath(url) { + var value = url.toString() + return value.startsWith("file://") ? decodeURIComponent(value.substring(7)) : value + } + + function fileName(path) { + var parts = path.split("/") + return parts[parts.length - 1] + } + + function ready() { + return sourcePath.length > 0 && selectedBoard.length > 0 + && (outputIsFile ? outputPath.length > 0 : driveBox.currentIndex >= 0 && driveNames.length > 0) + } + + globalDrawer: Kirigami.GlobalDrawer { + isMenu: true + actions: [ + Kirigami.Action { + text: qsTr("About ALT Image Writer") + icon.name: "help-about" + onTriggered: aboutDialog.open() + } + ] + } + + pageStack.initialPage: Kirigami.ScrollablePage { + title: qsTr("Create Installation Media") + actions: [ + Kirigami.Action { + text: qsTr("About") + icon.name: "help-about" + displayHint: Kirigami.DisplayHint.IconOnly + onTriggered: aboutDialog.open() + } + ] + + ColumnLayout { + width: Math.min(parent.width, Kirigami.Units.gridUnit * 42) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Kirigami.Units.largeSpacing + + Controls.Label { + text: qsTr("Prepare an ALT Linux root filesystem or disk image for your board.") + color: Kirigami.Theme.disabledTextColor + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.Heading { text: qsTr("Installation Source"); level: 3 } + Controls.Label { + text: qsTr("Choose a root filesystem archive or a ready-made disk image.") + color: Kirigami.Theme.disabledTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + Kirigami.FormLayout { + Layout.fillWidth: true + RowLayout { + Kirigami.FormData.label: qsTr("Source type:") + Controls.RadioButton { + text: qsTr("Root filesystem") + checked: true + onCheckedChanged: if (checked) sourceIsImage = false + } + Controls.RadioButton { + text: qsTr("Disk image") + onCheckedChanged: if (checked) { + sourceIsImage = true + driveOutput.checked = true + } + } + } + RowLayout { + Kirigami.FormData.label: qsTr("Source:") + Controls.Label { + text: sourcePath.length ? fileName(sourcePath) : qsTr("No source selected") + elide: Text.ElideMiddle + Layout.fillWidth: true + } + Controls.Button { + text: qsTr("Choose…") + icon.name: "document-open" + onClicked: sourceDialog.open() + } + } + } + Controls.Label { + text: sourcePath.length ? sourcePath + : sourceIsImage ? qsTr("Supported: .img, .img.xz") + : qsTr("Supported: .tar, .tar.gz, .tar.xz") + color: Kirigami.Theme.disabledTextColor + elide: Text.ElideMiddle + Layout.fillWidth: true + } + } + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + Kirigami.Heading { text: qsTr("Target Hardware"); level: 3 } + Controls.Label { + text: qsTr("Boards are grouped by platform, SoC family, and architecture.") + color: Kirigami.Theme.disabledTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + RowLayout { + Layout.fillWidth: true + Kirigami.Icon { + source: "computer-symbolic" + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: implicitWidth + } + ColumnLayout { + spacing: 0 + Layout.fillWidth: true + Controls.Label { + text: selectedBoard.length ? selectedBoard : qsTr("No board selected") + font.bold: selectedBoard.length > 0 + } + Controls.Label { + text: selectedBoard.length ? selectedPlatform : qsTr("Choose a supported target board") + color: Kirigami.Theme.disabledTextColor + } + } + Controls.Button { + text: qsTr("Select Board…") + onClicked: boardDialog.open() + } + } + } + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + Kirigami.Heading { text: qsTr("Destination"); level: 3 } + Controls.Label { + text: qsTr("Write to removable media or create an image file.") + color: Kirigami.Theme.disabledTextColor + } + Kirigami.FormLayout { + Layout.fillWidth: true + RowLayout { + Kirigami.FormData.label: qsTr("Output type:") + Controls.RadioButton { + id: driveOutput + text: qsTr("Drive") + checked: true + onCheckedChanged: if (checked) outputIsFile = false + } + Controls.RadioButton { + text: qsTr("Image file") + enabled: !sourceIsImage + onCheckedChanged: if (checked) outputIsFile = true + } + } + Controls.ComboBox { + id: driveBox + visible: !outputIsFile + Kirigami.FormData.label: qsTr("Storage device:") + Layout.fillWidth: true + model: driveNames + displayText: currentIndex < 0 || driveNames.length === 0 + ? qsTr("No removable drives detected") : currentText + enabled: driveNames.length > 0 + } + RowLayout { + visible: outputIsFile + Kirigami.FormData.label: qsTr("Image file:") + Controls.Label { + text: outputPath.length ? outputPath : qsTr("Choose where to save the image") + elide: Text.ElideMiddle + Layout.fillWidth: true + } + Controls.Button { + text: qsTr("Choose…") + icon.name: "document-save" + onClicked: outputDialog.open() + } + } + } + } + } + + Kirigami.AbstractCard { + Layout.fillWidth: true + contentItem: ColumnLayout { + Kirigami.Heading { text: qsTr("Options"); level: 3 } + Kirigami.FormLayout { + Layout.fillWidth: true + Controls.ComboBox { + Kirigami.FormData.label: qsTr("Root filesystem:") + model: ["ext4", "f2fs"] + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Resize root partition:") + text: qsTr("Grow to fill the destination") + checked: true + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Boot partition:") + text: qsTr("Create a separate 512 MiB partition") + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Encryption:") + text: qsTr("Encrypt root partition with LUKS") + } + Controls.CheckBox { + Kirigami.FormData.label: qsTr("Serial console:") + text: qsTr("Add to boot arguments") + } + Controls.ComboBox { + Kirigami.FormData.label: qsTr("EFI system partition:") + model: [qsTr("None"), qsTr("EFI (GPT)"), qsTr("EFI (MBR)")] + } + Controls.ComboBox { + Kirigami.FormData.label: qsTr("First-boot VNC:") + model: [qsTr("Use image default"), qsTr("Enabled"), qsTr("Disabled")] + } + } + } + } + + Kirigami.InlineMessage { + id: previewMessage + Layout.fillWidth: true + type: Kirigami.MessageType.Information + text: qsTr("Flashing is not implemented in this interface preview.") + showCloseButton: true + } + } + } + + footer: Controls.ToolBar { + contentItem: RowLayout { + Controls.Label { + text: qsTr("Nothing will be written until you confirm.") + color: Kirigami.Theme.disabledTextColor + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + } + Controls.Button { + text: qsTr("Review and Write") + icon.name: "media-flash-sd-mmc" + highlighted: true + enabled: root.ready() + Layout.rightMargin: Kirigami.Units.largeSpacing + onClicked: reviewDialog.open() + } + } + } + + Controls.Dialog { + id: boardDialog + parent: Controls.Overlay.overlay + anchors.centerIn: parent + width: Math.min(root.width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 34) + height: Math.min(root.height - Kirigami.Units.gridUnit * 4, Kirigami.Units.gridUnit * 34) + modal: true + title: qsTr("Select Target Board") + standardButtons: Controls.Dialog.Cancel + + ListModel { id: allBoards } + ListModel { id: filteredBoards } + + function populate() { + if (allBoards.count > 0) return + var platform = "Other" + var lines = supportedBoardsText.split("\n") + for (var i = 0; i < lines.length; ++i) { + var line = lines[i].trim() + if (!line.length) continue + if (line.endsWith(":")) { + platform = line.substring(0, line.length - 1) + } else { + var names = line.split(/\s+/) + for (var j = 0; j < names.length; ++j) + allBoards.append({ boardName: names[j], platformName: platform }) + } + } + filter("") + } + + function filter(query) { + filteredBoards.clear() + var needle = query.toLowerCase() + for (var i = 0; i < allBoards.count; ++i) { + var item = allBoards.get(i) + if (!needle.length || item.boardName.toLowerCase().includes(needle) + || item.platformName.toLowerCase().includes(needle)) + filteredBoards.append({ boardName: item.boardName, platformName: item.platformName }) + } + } + + onOpened: { + populate() + searchField.forceActiveFocus() + } + + contentItem: ColumnLayout { + Controls.TextField { + id: searchField + Layout.fillWidth: true + placeholderText: qsTr("Search boards or platforms…") + onTextChanged: boardDialog.filter(text) + } + Controls.Label { + text: qsTr("%1 matching boards").arg(filteredBoards.count) + color: Kirigami.Theme.disabledTextColor + } + ListView { + id: boardList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: filteredBoards + section.property: "platformName" + section.criteria: ViewSection.FullString + section.delegate: Kirigami.ListSectionHeader { text: section } + delegate: Controls.ItemDelegate { + required property string boardName + required property string platformName + width: boardList.width + text: boardName + icon.name: "computer-symbolic" + onClicked: { + selectedBoard = boardName + selectedPlatform = platformName + boardDialog.close() + } + } + Controls.ScrollBar.vertical: Controls.ScrollBar {} + } + } + } + + Controls.Dialog { + id: reviewDialog + parent: Controls.Overlay.overlay + anchors.centerIn: parent + modal: true + title: qsTr("Ready to write?") + standardButtons: Controls.Dialog.Cancel | Controls.Dialog.Ok + onAccepted: previewMessage.visible = true + contentItem: Kirigami.FormLayout { + Controls.Label { Kirigami.FormData.label: qsTr("Board:"); text: selectedBoard } + Controls.Label { Kirigami.FormData.label: qsTr("Platform:"); text: selectedPlatform } + Controls.Label { + Kirigami.FormData.label: qsTr("Destination:") + text: outputIsFile ? outputPath : driveBox.currentText + elide: Text.ElideMiddle + } + Controls.Label { + text: qsTr("This is an interface preview. No data will be written.") + color: Kirigami.Theme.disabledTextColor + wrapMode: Text.WordWrap + } + } + } + + Controls.Dialog { + id: aboutDialog + parent: Controls.Overlay.overlay + anchors.centerIn: parent + modal: true + title: qsTr("About ALT Image Writer") + standardButtons: Controls.Dialog.Ok + contentItem: ColumnLayout { + Kirigami.Icon { + source: "media-flash-sd-mmc" + implicitWidth: Kirigami.Units.iconSizes.huge + implicitHeight: implicitWidth + Layout.alignment: Qt.AlignHCenter + } + Kirigami.Heading { text: qsTr("ALT Image Writer"); level: 2; Layout.alignment: Qt.AlignHCenter } + Controls.Label { text: qsTr("Version 0.1.0"); Layout.alignment: Qt.AlignHCenter } + Controls.Label { + text: qsTr("Prepare ALT Linux installation media for supported single-board computers.") + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: Kirigami.Units.gridUnit * 22 + } + } + } + + FileDialog { + id: sourceDialog + title: qsTr("Choose Installation Source") + fileMode: FileDialog.OpenFile + nameFilters: sourceIsImage + ? [qsTr("Disk images (*.img *.img.xz)"), qsTr("All files (*)")] + : [qsTr("Root filesystem archives (*.tar *.tar.gz *.tar.xz)"), qsTr("All files (*)")] + onAccepted: sourcePath = localPath(selectedFile) + } + + FileDialog { + id: outputDialog + title: qsTr("Create Disk Image") + fileMode: FileDialog.SaveFile + defaultSuffix: "img" + nameFilters: [qsTr("Raw disk images (*.img)"), qsTr("All files (*)")] + onAccepted: outputPath = localPath(selectedFile) + } +} diff --git a/crates/flasher-qt/src/main.rs b/crates/flasher-qt/src/main.rs index e304418..243406b 100644 --- a/crates/flasher-qt/src/main.rs +++ b/crates/flasher-qt/src/main.rs @@ -1,43 +1,30 @@ -use std::{ - env, - ffi::CString, - fs, - os::raw::{c_char, c_int}, -}; +use cxx_qt_lib::{QByteArray, QGuiApplication, QQmlApplicationEngine, QString, QUrl}; +use std::fs; const BOARDS: &str = include_str!("../../../alt-rootfs-installer/SUPPORTED-BOARDS"); - -unsafe extern "C" { - fn run_qt_app( - argc: c_int, - argv: *mut *mut c_char, - boards: *const c_char, - drives: *const c_char, - ) -> c_int; -} +const QML: &str = include_str!("../qml/Main.qml"); fn main() { - let arguments: Vec = env::args() - .map(|arg| CString::new(arg).expect("application argument contains a NUL byte")) - .collect(); - let mut argument_pointers: Vec<*mut c_char> = arguments - .iter() - .map(|arg| arg.as_ptr().cast_mut()) - .collect(); - let boards = CString::new(BOARDS).expect("board list contains a NUL byte"); - let drives = CString::new(available_drives().join("\n")) - .expect("drive description contains a NUL byte"); + let mut app = QGuiApplication::new(); + app.pin_mut() + .set_application_name(&QString::from("ALT Image Writer")); + app.pin_mut() + .set_organization_name(&QString::from("ALT Linux")); + app.pin_mut() + .set_application_version(&QString::from(env!("CARGO_PKG_VERSION"))); - // QApplication only borrows argv and the two strings for the duration of - // this call; all backing CStrings remain alive until the event loop exits. - unsafe { - run_qt_app( - argument_pointers.len() as c_int, - argument_pointers.as_mut_ptr(), - boards.as_ptr(), - drives.as_ptr(), - ); - } + let boards = serde_json::to_string(BOARDS).expect("serialize supported boards"); + let drives = serde_json::to_string(&available_drives().join("\n")) + .expect("serialize available drives"); + let qml = QML + .replace("@SUPPORTED_BOARDS@", &boards) + .replace("@AVAILABLE_DRIVES@", &drives); + + let mut engine = QQmlApplicationEngine::new(); + engine + .pin_mut() + .load_data(&QByteArray::from(qml.as_str()), &QUrl::default()); + app.pin_mut().exec(); } fn available_drives() -> Vec { @@ -45,7 +32,6 @@ fn available_drives() -> Vec { let Ok(entries) = fs::read_dir("/sys/block") else { return drives; }; - for entry in entries.flatten() { let name = entry.file_name().to_string_lossy().into_owned(); if name.starts_with("loop") || name.starts_with("ram") || name.starts_with("zram") { @@ -57,7 +43,6 @@ fn available_drives() -> Vec { if !removable && !name.starts_with("mmcblk") { continue; } - let sectors = fs::read_to_string(entry.path().join("size")) .ok() .and_then(|value| value.trim().parse::().ok()) diff --git a/crates/flasher-qt/src/ui.cpp b/crates/flasher-qt/src/ui.cpp deleted file mode 100644 index 0af1987..0000000 --- a/crates/flasher-qt/src/ui.cpp +++ /dev/null @@ -1,404 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -struct Board { - QString name; - QString platform; -}; - -QVector parseBoards(const QString &text) { - QVector boards; - QString platform = QStringLiteral("Other"); - for (const QString &rawLine : text.split('\n')) { - const QString line = rawLine.trimmed(); - if (line.isEmpty()) - continue; - if (line.endsWith(':')) { - platform = line.left(line.size() - 1); - continue; - } - for (const QString &name : line.split(QRegExp(QStringLiteral("\\s+")), Qt::SkipEmptyParts)) - boards.push_back({name, platform}); - } - return boards; -} - -QLabel *description(const QString &text) { - auto *label = new QLabel(text); - label->setWordWrap(true); - label->setForegroundRole(QPalette::Mid); - return label; -} - -QGroupBox *group(const QString &title, QLayout *layout) { - auto *box = new QGroupBox(title); - box->setLayout(layout); - return box; -} - -class BoardDialog final : public QDialog { -public: - explicit BoardDialog(const QVector &boards, QWidget *parent = nullptr) - : QDialog(parent), boards_(boards) { - setWindowTitle(tr("Select Target Board")); - resize(620, 620); - setModal(true); - - auto *layout = new QVBoxLayout(this); - search_ = new QLineEdit; - search_->setPlaceholderText(tr("Search boards, platforms, or architectures…")); - search_->setClearButtonEnabled(true); - tree_ = new QTreeWidget; - tree_->setHeaderHidden(true); - tree_->setRootIsDecorated(true); - tree_->setAlternatingRowColors(true); - tree_->setUniformRowHeights(true); - layout->addWidget(search_); - layout->addWidget(tree_, 1); - - auto *buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); - layout->addWidget(buttons); - connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - connect(search_, &QLineEdit::textChanged, this, [this](const QString &text) { populate(text); }); - connect(tree_, &QTreeWidget::itemActivated, this, [this](QTreeWidgetItem *item) { - if (item && item->parent()) { - selectedName_ = item->data(0, Qt::UserRole).toString(); - selectedPlatform_ = item->parent()->text(0); - accept(); - } - }); - populate({}); - } - - QString selectedName() const { return selectedName_; } - QString selectedPlatform() const { return selectedPlatform_; } - -private: - void populate(const QString &query) { - tree_->clear(); - QMap headings; - const QString needle = query.trimmed(); - for (const Board &board : boards_) { - if (!needle.isEmpty() && !board.name.contains(needle, Qt::CaseInsensitive) - && !board.platform.contains(needle, Qt::CaseInsensitive)) - continue; - auto *&heading = headings[board.platform]; - if (!heading) { - heading = new QTreeWidgetItem(tree_, {board.platform}); - QFont font = heading->font(0); - font.setBold(true); - heading->setFont(0, font); - heading->setFlags(heading->flags() & ~Qt::ItemIsSelectable); - } - auto *item = new QTreeWidgetItem(heading, {board.name}); - item->setData(0, Qt::UserRole, board.name); - } - tree_->expandAll(); - if (!query.isEmpty() && tree_->topLevelItemCount() > 0) { - auto *heading = tree_->topLevelItem(0); - if (heading->childCount() > 0) - tree_->setCurrentItem(heading->child(0)); - } - } - - QVector boards_; - QLineEdit *search_ = nullptr; - QTreeWidget *tree_ = nullptr; - QString selectedName_; - QString selectedPlatform_; -}; - -class MainWindow final : public QMainWindow { -public: - MainWindow(const QString &boardsText, const QString &drivesText) - : boards_(parseBoards(boardsText)) { - setWindowTitle(tr("ALT Image Writer")); - resize(780, 820); - setMinimumSize(620, 600); - - auto *about = menuBar()->addMenu(tr("&Help"))->addAction(tr("&About")); - connect(about, &QAction::triggered, this, [this] { - QMessageBox::about(this, tr("About ALT Image Writer"), - tr("

ALT Image Writer

Version 0.1.0

" - "

Prepare ALT Linux installation media for supported " - "single-board computers.

")); - }); - - auto *scroll = new QScrollArea; - scroll->setWidgetResizable(true); - scroll->setFrameShape(QFrame::NoFrame); - setCentralWidget(scroll); - - auto *viewport = new QWidget; - auto *outer = new QHBoxLayout(viewport); - outer->setContentsMargins(20, 20, 20, 20); - auto *content = new QWidget; - content->setMaximumWidth(700); - auto *layout = new QVBoxLayout(content); - layout->setSpacing(16); - outer->addStretch(); - outer->addWidget(content, 1); - outer->addStretch(); - scroll->setWidget(viewport); - - auto *title = new QLabel(tr("Create Installation Media")); - QFont titleFont = title->font(); - titleFont.setPointSize(titleFont.pointSize() + 7); - titleFont.setBold(true); - title->setFont(titleFont); - title->setAlignment(Qt::AlignCenter); - layout->addWidget(title); - auto *intro = description(tr("Prepare an ALT Linux root filesystem or disk image for your board.")); - intro->setAlignment(Qt::AlignCenter); - layout->addWidget(intro); - - createSource(layout); - createHardware(layout); - createDestination(layout, drivesText); - createOptions(layout); - layout->addStretch(); - - auto *bar = new QWidget; - auto *barLayout = new QHBoxLayout(bar); - barLayout->setContentsMargins(12, 8, 12, 8); - barLayout->addWidget(description(tr("Nothing will be written until you confirm.")), 1); - writeButton_ = new QPushButton(tr("Review and Write")); - writeButton_->setDefault(true); - writeButton_->setEnabled(false); - barLayout->addWidget(writeButton_); - statusBar()->addPermanentWidget(bar, 1); - connect(writeButton_, &QPushButton::clicked, this, [this] { review(); }); - } - -private: - void createSource(QVBoxLayout *page) { - auto *layout = new QVBoxLayout; - layout->addWidget(description(tr("Choose a root filesystem archive or a ready-made disk image."))); - auto *types = new QHBoxLayout; - types->addWidget(new QLabel(tr("Source type"))); - types->addStretch(); - rootfsButton_ = new QRadioButton(tr("Root filesystem")); - imageButton_ = new QRadioButton(tr("Disk image")); - rootfsButton_->setChecked(true); - types->addWidget(rootfsButton_); - types->addWidget(imageButton_); - layout->addLayout(types); - - auto *picker = new QHBoxLayout; - sourceLabel_ = new QLabel(tr("No source selected")); - sourceLabel_->setTextInteractionFlags(Qt::TextSelectableByMouse); - auto *choose = new QPushButton(tr("Choose…")); - choose->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); - picker->addWidget(sourceLabel_, 1); - picker->addWidget(choose); - layout->addLayout(picker); - sourceHint_ = description(tr("Supported: .tar, .tar.gz, .tar.xz")); - layout->addWidget(sourceHint_); - page->addWidget(group(tr("Installation Source"), layout)); - - connect(rootfsButton_, &QRadioButton::toggled, this, [this](bool checked) { - if (checked) { - sourceHint_->setText(tr("Supported: .tar, .tar.gz, .tar.xz")); - fileOutputButton_->setEnabled(true); - } - }); - connect(imageButton_, &QRadioButton::toggled, this, [this](bool checked) { - if (checked) { - sourceHint_->setText(tr("Supported: .img, .img.xz")); - driveOutputButton_->setChecked(true); - fileOutputButton_->setEnabled(false); - } - }); - connect(choose, &QPushButton::clicked, this, [this] { - const QString filter = imageButton_->isChecked() - ? tr("Disk images (*.img *.img.xz);;All files (*)") - : tr("Root filesystem archives (*.tar *.tar.gz *.tar.xz);;All files (*)"); - const QString path = QFileDialog::getOpenFileName(this, tr("Choose Installation Source"), {}, filter); - if (!path.isEmpty()) { - sourcePath_ = path; - sourceLabel_->setText(QFileInfo(path).fileName()); - sourceLabel_->setToolTip(path); - sourceHint_->setText(QFileInfo(path).absolutePath()); - updateReady(); - } - }); - } - - void createHardware(QVBoxLayout *page) { - auto *layout = new QVBoxLayout; - layout->addWidget(description(tr("Boards are grouped by platform, SoC family, and architecture."))); - auto *row = new QHBoxLayout; - auto *labels = new QVBoxLayout; - boardLabel_ = new QLabel(tr("No board selected")); - boardHint_ = description(tr("Choose from %1 supported boards").arg(boards_.size())); - labels->addWidget(boardLabel_); - labels->addWidget(boardHint_); - auto *select = new QPushButton(tr("Select Board…")); - row->addLayout(labels, 1); - row->addWidget(select); - layout->addLayout(row); - page->addWidget(group(tr("Target Hardware"), layout)); - - connect(select, &QPushButton::clicked, this, [this] { - BoardDialog dialog(boards_, this); - if (dialog.exec() == QDialog::Accepted) { - selectedBoard_ = dialog.selectedName(); - selectedPlatform_ = dialog.selectedPlatform(); - boardLabel_->setText(selectedBoard_); - QFont font = boardLabel_->font(); - font.setBold(true); - boardLabel_->setFont(font); - boardHint_->setText(selectedPlatform_); - updateReady(); - } - }); - } - - void createDestination(QVBoxLayout *page, const QString &drivesText) { - auto *layout = new QVBoxLayout; - layout->addWidget(description(tr("Write to removable media or create an image file."))); - auto *types = new QHBoxLayout; - types->addWidget(new QLabel(tr("Output type"))); - types->addStretch(); - driveOutputButton_ = new QRadioButton(tr("Drive")); - fileOutputButton_ = new QRadioButton(tr("Image file")); - driveOutputButton_->setChecked(true); - types->addWidget(driveOutputButton_); - types->addWidget(fileOutputButton_); - layout->addLayout(types); - - driveRow_ = new QWidget; - auto *driveLayout = new QFormLayout(driveRow_); - driveBox_ = new QComboBox; - const QStringList drives = drivesText.split('\n', Qt::SkipEmptyParts); - driveBox_->addItems(drives); - if (drives.isEmpty()) { - driveBox_->addItem(tr("No removable drives detected")); - driveBox_->setEnabled(false); - } - driveLayout->addRow(tr("Storage device"), driveBox_); - layout->addWidget(driveRow_); - - fileRow_ = new QWidget; - auto *fileLayout = new QHBoxLayout(fileRow_); - fileLayout->setContentsMargins(0, 0, 0, 0); - outputLabel_ = new QLabel(tr("Choose where to save the image")); - auto *choose = new QPushButton(tr("Choose…")); - choose->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton)); - fileLayout->addWidget(outputLabel_, 1); - fileLayout->addWidget(choose); - fileRow_->hide(); - layout->addWidget(fileRow_); - page->addWidget(group(tr("Destination"), layout)); - - connect(driveOutputButton_, &QRadioButton::toggled, this, [this](bool checked) { - driveRow_->setVisible(checked); - fileRow_->setVisible(!checked); - updateReady(); - }); - connect(choose, &QPushButton::clicked, this, [this] { - const QString path = QFileDialog::getSaveFileName(this, tr("Create Disk Image"), - QStringLiteral("alt-linux.img"), tr("Raw disk images (*.img);;All files (*)")); - if (!path.isEmpty()) { - outputPath_ = path; - outputLabel_->setText(QFileInfo(path).fileName()); - outputLabel_->setToolTip(path); - updateReady(); - } - }); - } - - void createOptions(QVBoxLayout *page) { - auto *layout = new QFormLayout; - auto *filesystem = new QComboBox; - filesystem->addItems({QStringLiteral("ext4"), QStringLiteral("f2fs")}); - layout->addRow(tr("Root filesystem"), filesystem); - auto *resize = new QCheckBox(tr("Grow to fill the destination")); - resize->setChecked(true); - layout->addRow(tr("Resize root partition"), resize); - layout->addRow(tr("Boot partition"), new QCheckBox(tr("Create a separate 512 MiB partition"))); - layout->addRow(tr("Encryption"), new QCheckBox(tr("Encrypt root partition with LUKS"))); - layout->addRow(tr("Serial console"), new QCheckBox(tr("Add to boot arguments"))); - auto *efi = new QComboBox; - efi->addItems({tr("None"), tr("EFI (GPT)"), tr("EFI (MBR)")}); - layout->addRow(tr("EFI system partition"), efi); - auto *vnc = new QComboBox; - vnc->addItems({tr("Use image default"), tr("Enabled"), tr("Disabled")}); - layout->addRow(tr("First-boot VNC"), vnc); - page->addWidget(group(tr("Options"), layout)); - } - - void updateReady() { - const bool destination = driveOutputButton_->isChecked() - ? driveBox_->isEnabled() && driveBox_->currentIndex() >= 0 - : !outputPath_.isEmpty(); - writeButton_->setEnabled(!sourcePath_.isEmpty() && !selectedBoard_.isEmpty() && destination); - } - - void review() { - const QString destination = driveOutputButton_->isChecked() ? driveBox_->currentText() : outputPath_; - QMessageBox dialog(QMessageBox::Warning, tr("Ready to write?"), - tr("Board: %1\nPlatform: %2\nDestination: %3\n\n" - "This is an interface preview. No data will be written.") - .arg(selectedBoard_, selectedPlatform_, destination), - QMessageBox::Cancel | QMessageBox::Ok, this); - dialog.setDefaultButton(QMessageBox::Cancel); - dialog.button(QMessageBox::Ok)->setText(tr("Write")); - if (dialog.exec() == QMessageBox::Ok) - statusBar()->showMessage(tr("Flashing is not implemented in this preview"), 4000); - } - - QVector boards_; - QString sourcePath_; - QString outputPath_; - QString selectedBoard_; - QString selectedPlatform_; - QRadioButton *rootfsButton_ = nullptr; - QRadioButton *imageButton_ = nullptr; - QRadioButton *driveOutputButton_ = nullptr; - QRadioButton *fileOutputButton_ = nullptr; - QLabel *sourceLabel_ = nullptr; - QLabel *sourceHint_ = nullptr; - QLabel *boardLabel_ = nullptr; - QLabel *boardHint_ = nullptr; - QLabel *outputLabel_ = nullptr; - QWidget *driveRow_ = nullptr; - QWidget *fileRow_ = nullptr; - QComboBox *driveBox_ = nullptr; - QPushButton *writeButton_ = nullptr; -}; - -} // namespace - -extern "C" int run_qt_app(int argc, char **argv, const char *boards, const char *drives) { - QApplication app(argc, argv); - QCoreApplication::setApplicationName(QStringLiteral("ALT Image Writer")); - QCoreApplication::setOrganizationName(QStringLiteral("ALT Linux")); - QApplication::setWindowIcon(QApplication::style()->standardIcon(QStyle::SP_DriveHDIcon)); - MainWindow window(QString::fromUtf8(boards), QString::fromUtf8(drives)); - window.show(); - return app.exec(); -} -- cgit v1.2.3