summaryrefslogtreecommitdiff
path: root/crates/flasher-qt/src/ui.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'crates/flasher-qt/src/ui.cpp')
-rw-r--r--crates/flasher-qt/src/ui.cpp409
1 files changed, 409 insertions, 0 deletions
diff --git a/crates/flasher-qt/src/ui.cpp b/crates/flasher-qt/src/ui.cpp
new file mode 100644
index 0000000..eb34457
--- /dev/null
+++ b/crates/flasher-qt/src/ui.cpp
@@ -0,0 +1,409 @@
+#include <QAction>
+#include <QApplication>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QFileDialog>
+#include <QFileInfo>
+#include <QFont>
+#include <QFormLayout>
+#include <QFrame>
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMainWindow>
+#include <QMap>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QPalette>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QRegExp>
+#include <QScrollArea>
+#include <QStatusBar>
+#include <QStyle>
+#include <QTreeWidget>
+#include <QVBoxLayout>
+
+namespace {
+
+struct Board {
+ QString name;
+ QString platform;
+};
+
+QVector<Board> parseBoards(const QString &text) {
+ QVector<Board> 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<Board> &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<QString, QTreeWidgetItem *> 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<Board> 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("<h3>ALT Image Writer</h3><p>Version 0.1.0</p>"
+ "<p>Prepare ALT Linux installation media for supported "
+ "single-board computers.</p>"));
+ });
+
+ 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<Board> 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();
+}