diff options
Diffstat (limited to 'crates/flasher-qt/src/ui.cpp')
| -rw-r--r-- | crates/flasher-qt/src/ui.cpp | 409 |
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(); +} |