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 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 02:25:26 -07:00
parent 3ed9a42b3c
commit 5530920b25
4 changed files with 256 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
#include "introdialog.h"
#include "util.h"
#include <QSettings>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFileDialog>
#include <QDir>
#include <QMessageBox>
#include <QDialogButtonBox>
#include <boost/filesystem.hpp>
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;
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef INTRODIALOG_H
#define INTRODIALOG_H
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QRadioButton>
#include <QPushButton>
/** 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
+5
View File
@@ -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)))
{
+4
View File
@@ -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 \