From 5530920b2523e66090c6607aee07fb11fca5c009 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Mon, 16 Mar 2026 02:25:26 -0700 Subject: [PATCH] Add data directory selection dialog on first run Shows an intro dialog on first launch letting users choose where to store blockchain data. Saves the choice in QSettings so it only appears once. Styled to match the existing Triangles dark theme. Co-Authored-By: Claude Opus 4.6 --- src/qt/introdialog.cpp | 205 +++++++++++++++++++++++++++++++++++++++++ src/qt/introdialog.h | 42 +++++++++ src/qt/triangles.cpp | 5 + triangles-qt.pro | 4 + 4 files changed, 256 insertions(+) create mode 100644 src/qt/introdialog.cpp create mode 100644 src/qt/introdialog.h diff --git a/src/qt/introdialog.cpp b/src/qt/introdialog.cpp new file mode 100644 index 0000000..9146365 --- /dev/null +++ b/src/qt/introdialog.cpp @@ -0,0 +1,205 @@ +#include "introdialog.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +IntroDialog::IntroDialog(QWidget *parent) : + QDialog(parent) +{ + setWindowTitle("Triangles"); + setMinimumWidth(520); + + // Match existing Triangles dark theme + setStyleSheet( + "QDialog { background-color: #000; color: #f26522; }" + "QLabel { color: #f26522; }" + "QRadioButton { color: #f26522; }" + "QRadioButton::indicator { border: 1px solid #f26522; background-color: #000; width: 12px; height: 12px; border-radius: 7px; }" + "QRadioButton::indicator:checked { background-color: #f26522; }" + "QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 4px; }" + "QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 4px 16px; min-height: 20px; }" + "QPushButton:hover { background-color: #61280E; }" + ); + + defaultDataDir = QString::fromStdString(GetDefaultDataDir().string()); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(20, 20, 20, 20); + mainLayout->setSpacing(12); + + // Welcome header + QLabel *welcomeLabel = new QLabel(tr("Welcome to Triangles!")); + welcomeLabel->setStyleSheet("font-size: 16px; font-weight: bold; color: #f26522;"); + mainLayout->addWidget(welcomeLabel); + + // Description + QLabel *descLabel = new QLabel(tr( + "Triangles will store its blockchain data, wallet, and configuration in a data directory. " + "You can use the default directory or choose a custom location. " + "The data directory requires several hundred MB of free space." + )); + descLabel->setWordWrap(true); + mainLayout->addWidget(descLabel); + + mainLayout->addSpacing(8); + + // Default directory radio + defaultRadio = new QRadioButton(tr("Use the default data directory")); + defaultRadio->setChecked(true); + mainLayout->addWidget(defaultRadio); + + // Show default path + QLabel *defaultPathLabel = new QLabel(defaultDataDir); + defaultPathLabel->setStyleSheet("color: #999; margin-left: 24px; font-size: 11px;"); + mainLayout->addWidget(defaultPathLabel); + + mainLayout->addSpacing(4); + + // Custom directory radio + customRadio = new QRadioButton(tr("Use a custom data directory:")); + mainLayout->addWidget(customRadio); + + // Path input + browse button + QHBoxLayout *pathLayout = new QHBoxLayout(); + pathLayout->setContentsMargins(24, 0, 0, 0); + + pathEdit = new QLineEdit(defaultDataDir); + pathEdit->setEnabled(false); + pathLayout->addWidget(pathEdit); + + browseButton = new QPushButton(tr("Browse...")); + browseButton->setEnabled(false); + pathLayout->addWidget(browseButton); + + mainLayout->addLayout(pathLayout); + + // Free space label + freeSpaceLabel = new QLabel(); + freeSpaceLabel->setStyleSheet("color: #999; margin-left: 24px; font-size: 11px;"); + mainLayout->addWidget(freeSpaceLabel); + + mainLayout->addStretch(1); + + // OK / Cancel buttons + QHBoxLayout *buttonLayout = new QHBoxLayout(); + buttonLayout->addStretch(1); + + QPushButton *okButton = new QPushButton(tr("OK")); + QPushButton *cancelButton = new QPushButton(tr("Cancel")); + + buttonLayout->addWidget(okButton); + buttonLayout->addWidget(cancelButton); + mainLayout->addLayout(buttonLayout); + + // Connections + connect(defaultRadio, SIGNAL(toggled(bool)), this, SLOT(on_defaultRadio_toggled(bool))); + connect(browseButton, SIGNAL(clicked()), this, SLOT(on_browseButton_clicked())); + connect(pathEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFreeSpace())); + connect(okButton, SIGNAL(clicked()), this, SLOT(accept())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); + + updateFreeSpace(); +} + +QString IntroDialog::getDataDirectory() const +{ + if (defaultRadio->isChecked()) + return defaultDataDir; + return pathEdit->text(); +} + +void IntroDialog::setDataDirectory(const QString &dir) +{ + pathEdit->setText(dir); + if (dir == defaultDataDir) { + defaultRadio->setChecked(true); + } else { + customRadio->setChecked(true); + } +} + +void IntroDialog::on_browseButton_clicked() +{ + QString dir = QFileDialog::getExistingDirectory(this, tr("Choose data directory"), pathEdit->text()); + if (!dir.isEmpty()) + pathEdit->setText(dir); +} + +void IntroDialog::on_defaultRadio_toggled(bool checked) +{ + pathEdit->setEnabled(!checked); + browseButton->setEnabled(!checked); + if (checked) + pathEdit->setText(defaultDataDir); + updateFreeSpace(); +} + +void IntroDialog::updateFreeSpace() +{ + QString path = getDataDirectory(); + boost::filesystem::path fsPath(path.toStdString()); + + // Walk up to find an existing parent + try { + while (!fsPath.empty() && !boost::filesystem::exists(fsPath)) + fsPath = fsPath.parent_path(); + + if (!fsPath.empty()) { + boost::filesystem::space_info si = boost::filesystem::space(fsPath); + double freeGB = (double)si.available / (1024.0 * 1024.0 * 1024.0); + freeSpaceLabel->setText(tr("Free space: %1 GB").arg(QString::number(freeGB, 'f', 2))); + } else { + freeSpaceLabel->setText(tr("Cannot determine free space")); + } + } catch (const boost::filesystem::filesystem_error &) { + freeSpaceLabel->setText(tr("Cannot determine free space")); + } +} + +bool IntroDialog::pickDataDirectory() +{ + namespace fs = boost::filesystem; + + QSettings settings; + // If -datadir was passed on the command line, skip the dialog entirely + if (mapArgs.count("-datadir")) + return true; + + QString dataDir = settings.value("strDataDir", "").toString(); + + if (dataDir.isEmpty()) { + // First run - show the dialog + IntroDialog dlg; + if (dlg.exec() != QDialog::Accepted) + return false; + + dataDir = dlg.getDataDirectory(); + settings.setValue("strDataDir", dataDir); + } + + // If the saved path is the default, don't set -datadir (let normal defaults work) + QString defaultDir = QString::fromStdString(GetDefaultDataDir().string()); + if (dataDir != defaultDir) { + mapArgs["-datadir"] = dataDir.toStdString(); + } + + // Ensure the directory exists + try { + fs::create_directories(fs::path(dataDir.toStdString())); + } catch (const fs::filesystem_error &) { + QMessageBox::critical(0, "Triangles", + QString("Error: Could not create data directory \"%1\".").arg(dataDir)); + return false; + } + + return true; +} diff --git a/src/qt/introdialog.h b/src/qt/introdialog.h new file mode 100644 index 0000000..672ddcd --- /dev/null +++ b/src/qt/introdialog.h @@ -0,0 +1,42 @@ +#ifndef INTRODIALOG_H +#define INTRODIALOG_H + +#include +#include +#include +#include +#include + +/** Data directory selection dialog shown on first run. */ +class IntroDialog : public QDialog +{ + Q_OBJECT + +public: + explicit IntroDialog(QWidget *parent = 0); + + QString getDataDirectory() const; + void setDataDirectory(const QString &dir); + + /** + * Check settings or show the dialog to choose data directory. + * Returns true if a directory was selected, false if the user cancelled. + * Sets mapArgs["-datadir"] if a non-default directory was chosen. + */ + static bool pickDataDirectory(); + +private slots: + void on_browseButton_clicked(); + void on_defaultRadio_toggled(bool checked); + void updateFreeSpace(); + +private: + QRadioButton *defaultRadio; + QRadioButton *customRadio; + QLineEdit *pathEdit; + QPushButton *browseButton; + QLabel *freeSpaceLabel; + QString defaultDataDir; +}; + +#endif // INTRODIALOG_H diff --git a/src/qt/triangles.cpp b/src/qt/triangles.cpp index c081caf..23a6c88 100644 --- a/src/qt/triangles.cpp +++ b/src/qt/triangles.cpp @@ -8,6 +8,7 @@ #include "guiutil.h" #include "guiconstants.h" +#include "introdialog.h" #include "init.h" #include "ui_interface.h" #include "qtipcserver.h" @@ -131,6 +132,10 @@ int main(int argc, char *argv[]) // Command-line options take precedence: ParseParameters(argc, argv); + // Show data directory selection dialog on first run (unless -datadir was passed) + if (!IntroDialog::pickDataDirectory()) + return 0; + // ... then triangles.conf: if (!boost::filesystem::is_directory(GetDataDir(false))) { diff --git a/triangles-qt.pro b/triangles-qt.pro index 5465886..40a8874 100644 --- a/triangles-qt.pro +++ b/triangles-qt.pro @@ -202,6 +202,7 @@ HEADERS += src/qt/trianglesgui.h \ src/qt/sendcoinsdialog.h \ src/qt/addressbookpage.h \ src/qt/aboutdialog.h \ + src/qt/introdialog.h \ src/qt/editaddressdialog.h \ src/qt/trianglesaddressvalidator.h \ src/alert.h \ @@ -217,6 +218,7 @@ HEADERS += src/qt/trianglesgui.h \ src/kernel.h \ src/net_bootstrap.h \ src/tor/onion_v3.h \ + src/tor/tor_process.h \ src/tor/tor_crypto_compat.h \ src/scrypt.h \ src/pbkdf2.h \ @@ -315,6 +317,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \ src/qt/coincontroltreewidget.cpp \ src/qt/addressbookpage.cpp \ src/qt/aboutdialog.cpp \ + src/qt/introdialog.cpp \ src/qt/editaddressdialog.cpp \ src/qt/trianglesaddressvalidator.cpp \ # Old embedded Tor v2 client removed - incompatible with OpenSSL 3.x @@ -386,6 +389,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \ src/kernel.cpp \ src/net_bootstrap.cpp \ src/tor/onion_v3.cpp \ + src/tor/tor_process.cpp \ src/scrypt-arm.S \ src/scrypt-x86.S \ src/scrypt-x86_64.S \