• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 35267216991

17 Sep 2026 07:50PM UTC coverage: 77.023% (+0.2%) from 76.864%
35267216991

push

github

web-flow
A first-run wizard instead of message boxes before the Settings dialog (#1816)

* A first-run wizard instead of message boxes before the Settings dialog

ConfigDialog::wizard() was a chain of modal boxes that ran before the
whole configuration dialog was shown: GnuPG missing, generate a key,
create the store?, pick recipients, then the full Settings dialog with
its four tabs for someone who had not used the program yet. Declining
"Create password-store?" left an invalid configuration and the dialog
came back. It mutated ConfigDialog widgets, drove KeygenDialog and
UsersDialog through the shared backend and re-pointed the backend at the
new store from inside the dialog.

FirstRunWizard is a QWizard with four pages after the intro: Programs
(gpg required, runnable check on the path, pass and Git optional and only
offerable when found), Your key (the secret keys the chosen gpg lists,
tickable, with a Generate button that runs KeygenDialog), Password store
(an existing store is used as it is, an empty or missing folder is
described and set up on Finish) and Ready (summary, hide passwords, tray
icon). It edits a copy of the settings; accept() creates and initialises
the store through ProfileInit for the ticked keys, saves the settings
and invalidates the backend. MainWindow::config() runs it on a fresh
start and the ConfigDialog otherwise; the constructor's loop is unchanged.
The six wizard helpers leave ConfigDialog.

tst_firstrunwizard drives the pages with a scripted gpg (page gating,
key listing and ticking, store status, Finish creating .gpg-id with the
fingerprint, an existing store left alone). tst_mainwindow's first-run
tests now drive the wizard: cancel stops startup, Finish sets up the
store and startup succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* First-run wizard: Git decided per folder, unusable keys unticked, no early writes

Review of the wizard tur... (continued)

291 of 361 new or added lines in 4 files covered. (80.61%)

22 existing lines in 3 files now uncovered.

6349 of 8243 relevant lines covered (77.02%)

72.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.86
/src/firstrunwizard.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "firstrunwizard.h"
4
#include "executor.h"
5
#include "gpgkeystate.h"
6
#include "imitatepass.h"
7
#include "keygendialog.h"
8
#include "passbackendfactory.h"
9
#include "profileinit.h"
10
#include "qtpasssettings.h"
11
#include "util.h"
12
#include <QCheckBox>
13
#include <QDateTime>
14
#include <QDir>
15
#include <QDirIterator>
16
#include <QFile>
17
#include <QFileDialog>
18
#include <QFileInfo>
19
#include <QFormLayout>
20
#include <QHBoxLayout>
21
#include <QLabel>
22
#include <QLineEdit>
23
#include <QListWidget>
24
#include <QMessageBox>
25
#include <QPushButton>
26
#include <QSystemTrayIcon>
27
#include <QToolButton>
28
#include <QVBoxLayout>
29
#include <functional>
30
#ifdef Q_OS_WIN
31
#include <windows.h>
32
#endif
33

34
namespace {
35

36
/// A line edit with a "…" button that opens a file or folder picker.
37
auto pathRow(QLineEdit *edit, QWidget *parent,
44 ✔
38
             const std::function<QString()> &pick) -> QWidget * {
39
  auto *row = new QWidget(parent);
44 ✔
40
  auto *layout = new QHBoxLayout(row);
44 ✔
41
  layout->setContentsMargins(0, 0, 0, 0);
44 ✔
42
  layout->addWidget(edit);
44 ✔
43
  auto *browse = new QToolButton(row);
44 ✔
44
  browse->setText(QStringLiteral("…"));
88 ✔
45
  browse->setToolTip(FirstRunWizard::tr("Browse"));
88 ✔
46
  layout->addWidget(browse);
44 ✔
47
  QObject::connect(browse, &QToolButton::clicked, edit, [edit, pick]() {
88 ✔
NEW
48
    const QString chosen = pick();
×
NEW
49
    if (!chosen.isEmpty()) {
×
NEW
50
      edit->setText(chosen);
×
NEW
51
      emit edit->textEdited(chosen);
×
52
    }
NEW
53
  });
×
54
  return row;
44 ✔
55
}
56

57
auto runnable(const QString &exe) -> bool {
70 ✔
58
  return exe.startsWith(QStringLiteral("wsl ")) ||
140 ✔
59
         (!exe.isEmpty() && QFileInfo(exe).isExecutable());
140 ✔
60
}
61

62
} // namespace
63

64
// --------------------------------------------------------------------------
65

66
FirstRunWizard::FirstRunWizard(QWidget *parent)
11 ✔
67
    : QWizard(parent), m_settings(QtPassSettings::load()) {
11 ✔
68
  setWindowTitle(tr("Welcome to QtPass"));
11 ✔
69
  setWizardStyle(QWizard::ModernStyle);
11 ✔
70
  setOption(QWizard::NoBackButtonOnStartPage);
11 ✔
71
  setOption(QWizard::HaveHelpButton, false);
11 ✔
72

73
  auto *intro = new QWizardPage(this);
11 ✔
74
  intro->setTitle(tr("Welcome to QtPass"));
11 ✔
75
  auto *introLayout = new QVBoxLayout(intro);
11 ✔
76
  auto *introText = new QLabel(
77
      tr("QtPass is a graphical front-end for <i>pass</i>, the standard Unix "
11 ✔
78
         "password manager: every password is a file encrypted with GnuPG, "
79
         "kept in a folder you can put under Git.<br><br>The next pages find "
80
         "GnuPG, make sure you have a key to encrypt to and pick the folder "
81
         "your passwords live in. Nothing is written until you press "
82
         "Finish."),
83
      intro);
22 ✔
84
  introText->setWordWrap(true);
11 ✔
85
  introText->setTextFormat(Qt::RichText);
11 ✔
86
  introLayout->addWidget(introText);
11 ✔
87
  setPage(IntroPage, intro);
11 ✔
88
  setPage(ProgramsPage, new ProgramsWizardPage(this));
11 ✔
89
  setPage(KeyPage, new KeyWizardPage(this));
11 ✔
90
  setPage(StorePage, new StoreWizardPage(this));
11 ✔
91
  setPage(DonePage, new DoneWizardPage(this));
11 ✔
92
  setStartId(IntroPage);
11 ✔
93
  resize(640, 480);
94
}
11 ✔
95

96
auto FirstRunWizard::secretKeys(const QString &gpgExecutable)
8 ✔
97
    -> QList<UserInfo> {
98
  if (!runnable(gpgExecutable)) {
8 ✔
99
    return {};
2 ✔
100
  }
101
  QString out;
6 ✔
102
  const int rc = Executor::executeBlocking(
36 ✔
103
      gpgExecutable,
104
      {QStringLiteral("--no-tty"), QStringLiteral("--with-colons"),
6 ✔
105
       QStringLiteral("--with-fingerprint"),
6 ✔
106
       QStringLiteral("--list-secret-keys")},
6 ✔
107
      &out);
108
  if (rc != 0) {
6 ✔
NEW
109
    return {};
×
110
  }
111
  return parseGpgColonOutput(out, true);
6 ✔
112
}
113

114
auto FirstRunWizard::isStore(const QString &path) -> bool {
71 ✔
115
  return !path.isEmpty() &&
71 ✔
116
         QFile::exists(QDir(path).filePath(QStringLiteral(".gpg-id")));
213 ✔
117
}
118

119
void FirstRunWizard::accept() {
3 ✔
120
  // The Finish button validates the page before calling this; a programmatic
121
  // accept() gets the same treatment so the last page's choices are in.
122
  if (currentPage() != nullptr && !currentPage()->validatePage()) {
3 ✔
NEW
123
    return;
×
124
  }
125
  const QString store = QDir::cleanPath(m_settings.passStore);
3 ✔
126
  QString note;
3 ✔
127
  if (!isStore(store)) {
3 ✔
128
    QList<UserInfo> recipients;
2 ✔
129
    for (const UserInfo &key : std::as_const(m_keys)) {
4 ✔
130
      if (key.enabled) {
2 ✔
131
        recipients.append(key);
132
      }
133
    }
134
    const bool created = !QDir(store).exists();
2 ✔
135
    if (created && !QDir().mkpath(store)) {
4 ✔
NEW
136
      QMessageBox::warning(
×
NEW
137
          this, tr("Error"),
×
NEW
138
          tr("Failed to create password-store at: %1").arg(store));
×
NEW
139
      return;
×
140
    }
141
#ifdef Q_OS_WIN
142
    // Only a folder made here is hidden, like ~/.password-store on Unix; a
143
    // folder the user picked keeps its attributes.
144
    if (created) {
145
      SetFileAttributes(store.toStdWString().c_str(), FILE_ATTRIBUTE_HIDDEN);
146
    }
147
#endif
148
    if (!ProfileInit::initialise(store, recipients, m_settings,
2 ✔
149
                                 m_settings.useGit, &note)) {
2 ✔
150
      // ProfileInit writes .gpg-id before it signs and commits; take it back
151
      // so a second Finish runs the whole initialisation again instead of
152
      // finding a store that looks finished.
NEW
153
      QFile::remove(QDir(store).filePath(QStringLiteral(".gpg-id")));
×
NEW
154
      QFile::remove(QDir(store).filePath(QStringLiteral(".gpg-id.sig")));
×
NEW
155
      QMessageBox::warning(this, tr("Password store not initialised"), note);
×
NEW
156
      return;
×
157
    }
158
    if (!note.isEmpty()) {
2 ✔
159
      // ProfileInit's own note talks of switching profiles; on the first run
160
      // the folder simply becomes the store.
NEW
161
      QMessageBox::information(
×
NEW
162
          this, tr("Password store"),
×
NEW
163
          tr("%1 already contains encrypted files; they were not "
×
164
             "re-encrypted to the ticked keys. Open Users after the start "
165
             "to do that.")
NEW
166
              .arg(QDir::toNativeSeparators(store)));
×
167
    }
168
  } else if (m_settings.useGit && !QDir(store).exists(QStringLiteral(".git"))) {
1 ✔
NEW
169
    if (!ProfileInit::initGit(store, m_settings, &note)) {
×
170
      // Take the repository back too, so a second Finish retries the whole
171
      // thing instead of skipping a .git that never got its first commit.
NEW
172
      QDir(QDir(store).filePath(QStringLiteral(".git"))).removeRecursively();
×
NEW
173
      QMessageBox::warning(this, tr("Password store not initialised"), note);
×
NEW
174
      return;
×
175
    }
176
  }
177
  m_settings.passStore = Util::normalizeFolderPath(store);
3 ✔
178
  QtPassSettings::save(m_settings);
3 ✔
179
  // The backend cached the old paths at construction.
180
  PassBackendFactory::invalidate();
3 ✔
181
  QWizard::accept();
3 ✔
182
}
183

184
// --------------------------------------------------------------------------
185

186
ProgramsWizardPage::ProgramsWizardPage(FirstRunWizard *wizard)
11 ✔
187
    : QWizardPage(wizard), m_wizard(wizard), m_gpg(new QLineEdit(this)),
11 ✔
188
      m_git(new QLineEdit(this)), m_pass(new QLineEdit(this)),
11 ✔
189
      m_usePass(new QCheckBox(tr("Use the pass command-line tool"), this)),
22 ✔
190
      m_gpgStatus(new QLabel(this)) {
22 ✔
191
  setTitle(tr("Programs"));
22 ✔
192
  setSubTitle(tr("GnuPG does the encrypting. pass and Git are optional; "
11 ✔
193
                 "QtPass can do their work itself."));
194
  auto *layout = new QFormLayout(this);
11 ✔
NEW
195
  auto pickExe = [this]() {
×
NEW
196
    return QFileDialog::getOpenFileName(this, tr("Select executable"),
×
NEW
197
                                        QString(), tr("All files (*)"));
×
198
  };
199
  layout->addRow(tr("GnuPG"), pathRow(m_gpg, this, pickExe));
33 ✔
200
  m_gpgStatus->setWordWrap(true);
11 ✔
201
  layout->addRow(QString(), m_gpgStatus);
22 ✔
202
  layout->addRow(tr("Git"), pathRow(m_git, this, pickExe));
33 ✔
203
  layout->addRow(tr("pass"), pathRow(m_pass, this, pickExe));
33 ✔
204
  m_usePass->setToolTip(tr("Run the pass script for every operation instead "
11 ✔
205
                           "of calling gpg and git directly"));
206
  layout->addRow(QString(), m_usePass);
11 ✔
207
  for (QLineEdit *edit : {m_gpg, m_git, m_pass}) {
44 ✔
208
    connect(edit, &QLineEdit::textEdited, this,
33 ✔
209
            &ProgramsWizardPage::updateStatus);
66 ✔
210
  }
211
}
11 ✔
212

213
void ProgramsWizardPage::initializePage() {
4 ✔
214
  AppSettings &s = m_wizard->m_settings;
4 ✔
215
  // QtPassSettings::initExecutables() fills empty paths from PATH only; a
216
  // stored path whose binary has moved would be shown as an error here, so
217
  // look it up again in that case.
218
  auto detect = [](QString &stored, const QStringList &names) {
16 ✔
219
    if (runnable(stored)) {
16 ✔
220
      return;
221
    }
222
    for (const QString &name : names) {
25 ✔
223
      const QString found = Util::findBinaryInPath(name);
13 ✔
224
      if (!found.isEmpty()) {
13 ✔
NEW
225
        stored = found;
×
226
        return;
227
      }
228
    }
229
  };
230
  detect(s.gpgExecutable, {QStringLiteral("gpg2"), QStringLiteral("gpg")});
16 ✔
231
  detect(s.gitExecutable, {QStringLiteral("git")});
12 ✔
232
  detect(s.passExecutable, {QStringLiteral("pass")});
12 ✔
233
  detect(s.pwgenExecutable, {QStringLiteral("pwgen")});
12 ✔
234
  m_gpg->setText(s.gpgExecutable);
4 ✔
235
  m_git->setText(s.gitExecutable);
4 ✔
236
  m_pass->setText(s.passExecutable);
4 ✔
237
  m_usePass->setChecked(s.usePass || runnable(s.passExecutable));
4 ✔
238
  updateStatus();
4 ✔
239
}
4 ✔
240

241
void ProgramsWizardPage::updateStatus() {
6 ✔
242
  const QString gpg = m_gpg->text().trimmed();
12 ✔
243
  if (gpg.isEmpty()) {
6 ✔
244
    m_gpgStatus->setText(tr("GnuPG was not found. Install it (gpg or gpg2) "
2 ✔
245
                            "and enter its location here."));
246
  } else if (!runnable(gpg)) {
5 ✔
247
    m_gpgStatus->setText(tr("%1 is not an executable file.").arg(gpg));
2 ✔
248
  } else {
249
    m_gpgStatus->setText(QString());
8 ✔
250
  }
251
  m_usePass->setEnabled(runnable(m_pass->text().trimmed()));
12 ✔
252
  if (!m_usePass->isEnabled()) {
6 ✔
253
    m_usePass->setChecked(false);
6 ✔
254
  }
255
  emit completeChanged();
6 ✔
256
}
6 ✔
257

258
auto ProgramsWizardPage::isComplete() const -> bool {
22 ✔
259
  return runnable(m_gpg->text().trimmed());
66 ✔
260
}
261

262
auto ProgramsWizardPage::validatePage() -> bool {
4 ✔
263
  AppSettings &s = m_wizard->m_settings;
4 ✔
264
  s.gpgExecutable = m_gpg->text().trimmed();
4 ✔
265
  s.gitExecutable = m_git->text().trimmed();
4 ✔
266
  s.passExecutable = m_pass->text().trimmed();
4 ✔
267
  s.usePass = m_usePass->isChecked();
4 ✔
268
  return true;
4 ✔
269
}
270

271
// --------------------------------------------------------------------------
272

273
KeyWizardPage::KeyWizardPage(FirstRunWizard *wizard)
11 ✔
274
    : QWizardPage(wizard), m_wizard(wizard), m_list(new QListWidget(this)),
11 ✔
275
      m_generate(new QPushButton(tr("Generate a new key pair…"), this)),
22 ✔
276
      m_status(new QLabel(this)) {
22 ✔
277
  setTitle(tr("Your key"));
22 ✔
278
  setSubTitle(tr("Passwords are encrypted to GnuPG keys. Tick the keys that "
11 ✔
279
                 "should be able to open a new store; you need at least one "
280
                 "with its secret half on this machine."));
281
  auto *layout = new QVBoxLayout(this);
11 ✔
282
  layout->addWidget(m_list);
11 ✔
283
  m_status->setWordWrap(true);
11 ✔
284
  layout->addWidget(m_status);
11 ✔
285
  auto *buttons = new QHBoxLayout;
11 ✔
286
  buttons->addStretch();
11 ✔
287
  buttons->addWidget(m_generate);
11 ✔
288
  layout->addLayout(buttons);
11 ✔
289
  connect(m_generate, &QPushButton::clicked, this, &KeyWizardPage::generate);
11 ✔
290
  connect(m_list, &QListWidget::itemChanged, this,
11 ✔
291
          [this]() { emit completeChanged(); });
22 ✔
292
}
11 ✔
293

294
void KeyWizardPage::initializePage() { reload(); }
5 ✔
295

296
void KeyWizardPage::reload() {
5 ✔
297
  m_wizard->m_keys =
5 ✔
298
      FirstRunWizard::secretKeys(m_wizard->m_settings.gpgExecutable);
5 ✔
299
  m_list->clear();
5 ✔
300
  const QDateTime now = QDateTime::currentDateTime();
5 ✔
301
  for (UserInfo &key : m_wizard->m_keys) {
10 ✔
302
    // An expired or otherwise unusable key would make every encryption to
303
    // the new store fail; list it, but do not tick it.
304
    QString why;
5 ✔
305
    if (key.expiry.isValid() && key.expiry < now) {
5 ✔
NEW
306
      why = tr("expired");
×
307
    } else if (!key.isValid()) {
5 ✔
NEW
308
      why = tr("not usable");
×
309
    }
310
    key.enabled = why.isEmpty();
5 ✔
311
    QString label = QStringLiteral("%1\n%2").arg(key.name, key.key_id);
10 ✔
312
    if (!why.isEmpty()) {
5 ✔
NEW
313
      label += QStringLiteral(" (%1)").arg(why);
×
314
    }
315
    auto *item = new QListWidgetItem(label, m_list);
5 ✔
316
    item->setFlags(key.enabled ? (item->flags() | Qt::ItemIsUserCheckable)
10 ✔
317
                               : (item->flags() & ~Qt::ItemIsEnabled));
318
    item->setCheckState(key.enabled ? Qt::Checked : Qt::Unchecked);
5 ✔
319
  }
320
  m_status->setText(m_wizard->m_keys.isEmpty()
10 ✔
321
                        ? tr("GnuPG has no secret key yet. Generate one here, "
5 ✔
322
                             "or import your existing key with gpg first.")
323
                        : QString());
324
  emit completeChanged();
5 ✔
325
}
5 ✔
326

NEW
327
void KeyWizardPage::generate() {
×
328
  // A backend of the wizard's own, set up from its copy of the settings, so
329
  // the chosen gpg is used and nothing is written before Finish. Each Pass
330
  // owns its Executor; the shared backend is not involved.
NEW
331
  ImitatePass pass;
×
NEW
332
  pass.init(m_wizard->m_settings);
×
NEW
333
  KeygenDialog d(m_wizard->m_settings.gpgExecutable, &pass, this);
×
NEW
334
  d.exec();
×
NEW
335
  reload();
×
NEW
336
}
×
337

338
auto KeyWizardPage::isComplete() const -> bool { return true; }
42 ✔
339

340
auto KeyWizardPage::validatePage() -> bool {
6 ✔
341
  for (int i = 0; i < m_list->count() && i < m_wizard->m_keys.size(); ++i) {
12 ✔
342
    m_wizard->m_keys[i].enabled = m_list->item(i)->checkState() == Qt::Checked;
6 ✔
343
  }
344
  return true;
6 ✔
345
}
346

347
// --------------------------------------------------------------------------
348

349
StoreWizardPage::StoreWizardPage(FirstRunWizard *wizard)
11 ✔
350
    : QWizardPage(wizard), m_wizard(wizard), m_path(new QLineEdit(this)),
11 ✔
351
      m_useGit(new QCheckBox(tr("Keep the store under Git"), this)),
22 ✔
352
      m_status(new QLabel(this)) {
22 ✔
353
  setTitle(tr("Password store"));
22 ✔
354
  setSubTitle(tr("The folder your passwords live in. An existing store is "
11 ✔
355
                 "used as it is; an empty or missing folder is set up for "
356
                 "the keys you ticked."));
357
  auto *layout = new QFormLayout(this);
11 ✔
358
  layout->addRow(tr("Folder"), pathRow(m_path, this, [this]() {
33 ✔
359
                   return QFileDialog::getExistingDirectory(
NEW
360
                       this, tr("Choose the password store folder"),
×
NEW
361
                       m_path->text());
×
362
                 }));
363
  m_status->setWordWrap(true);
11 ✔
364
  layout->addRow(QString(), m_status);
11 ✔
365
  layout->addRow(QString(), m_useGit);
11 ✔
366
  connect(m_path, &QLineEdit::textEdited, this, &StoreWizardPage::updateStatus);
11 ✔
367
}
11 ✔
368

369
void StoreWizardPage::initializePage() {
9 ✔
370
  QString path = m_wizard->m_settings.passStore;
9 ✔
371
  if (path.isEmpty()) {
9 ✔
372
    path = Util::findPasswordStore();
4 ✔
373
  }
374
  m_path->setText(QDir::toNativeSeparators(QDir::cleanPath(path)));
9 ✔
375
  const QString clean = QDir::cleanPath(path);
9 ✔
376
  const bool repository = QDir(clean).exists(QStringLiteral(".git"));
18 ✔
377
  bool haveGit = runnable(m_wizard->m_settings.gitExecutable);
9 ✔
378
  // A first commit needs a name and an e-mail; without them Finish would
379
  // fail at the very end. Ask git now and say what to do instead.
380
  const bool identity =
381
      haveGit && (repository || ProfileInit::gitIdentityConfigured(
13 ✔
382
                                    QDir::homePath(), m_wizard->m_settings));
4 ✔
383
  m_useGit->setToolTip(
9 ✔
384
      haveGit && !identity
9 ✔
385
          ? tr("Git has no name and e-mail to commit with yet. Run\n"
9 ✔
386
               "git config --global user.name \"Your Name\"\n"
387
               "git config --global user.email you@example.org\n"
388
               "and turn Git on in Settings afterwards.")
389
          : tr("Every change becomes a commit; a folder that is no "
390
               "repository yet gets one"));
391
  haveGit = haveGit && identity;
9 ✔
392
  m_useGit->setEnabled(haveGit);
9 ✔
393
  // On for a repository or a store still to be made, off for an existing
394
  // store that is no repository: turning Git on there would make every
395
  // remove fail in a folder git knows nothing about.
396
  m_useGit->setChecked(haveGit && (m_wizard->m_settings.useGit || repository ||
20 ✔
397
                                   !FirstRunWizard::isStore(clean)));
2 ✔
398
  updateStatus();
9 ✔
399
}
9 ✔
400

401
auto StoreWizardPage::recipients() const -> int {
34 ✔
402
  int ticked = 0;
403
  for (const UserInfo &key : std::as_const(m_wizard->m_keys)) {
60 ✔
404
    ticked += key.enabled ? 1 : 0;
34 ✔
405
  }
406
  return ticked;
34 ✔
407
}
408

409
void StoreWizardPage::updateStatus() {
13 ✔
410
  const QString path = QDir::cleanPath(m_path->text().trimmed());
26 ✔
411
  QString text;
13 ✔
412
  if (path.isEmpty()) {
13 ✔
413
    text = tr("Enter a folder.");
1 ✔
414
  } else if (FirstRunWizard::isStore(path)) {
12 ✔
415
    int entries = 0;
416
    QDirIterator it(path, {QStringLiteral("*.gpg")}, QDir::Files,
4 ✔
417
                    QDirIterator::Subdirectories);
12 ✔
418
    while (it.hasNext()) {
4 ✔
NEW
419
      it.next();
×
NEW
420
      ++entries;
×
421
    }
422
    text = tr("An existing password store with %n entries.", nullptr, entries);
4 ✔
423
  } else if (QDir(path).exists()) {
12 ✔
424
    text = QDir(path).isEmpty()
2 ✔
425
               ? tr("An empty folder; it will be set up as a password store.")
2 ✔
426
               : tr("This folder is not a password store yet; a .gpg-id for "
427
                    "the ticked keys will be written into it.");
428
  } else {
429
    text = tr("The folder does not exist yet; it will be created.");
7 ✔
430
  }
431
  if (!path.isEmpty() && !FirstRunWizard::isStore(path) && recipients() == 0) {
13 ✔
432
    text += QLatin1Char(' ') +
8 ✔
433
            tr("Go back and tick at least one key to encrypt it to.");
4 ✔
434
  }
435
  m_status->setText(text);
13 ✔
436
  emit completeChanged();
13 ✔
437
}
13 ✔
438

439
auto StoreWizardPage::isComplete() const -> bool {
42 ✔
440
  const QString path = QDir::cleanPath(m_path->text().trimmed());
126 ✔
441
  if (path.isEmpty()) {
42 ✔
442
    return false;
443
  }
444
  return FirstRunWizard::isStore(path) || recipients() > 0;
39 ✔
445
}
446

447
auto StoreWizardPage::validatePage() -> bool {
4 ✔
448
  m_wizard->m_settings.passStore =
4 ✔
449
      QDir::fromNativeSeparators(m_path->text().trimmed());
8 ✔
450
  m_wizard->m_settings.useGit = m_useGit->isEnabled() && m_useGit->isChecked();
4 ✔
451
  return true;
4 ✔
452
}
453

454
// --------------------------------------------------------------------------
455

456
DoneWizardPage::DoneWizardPage(FirstRunWizard *wizard)
11 ✔
457
    : QWizardPage(wizard), m_wizard(wizard), m_summary(new QLabel(this)),
11 ✔
458
      m_hidePassword(new QCheckBox(tr("Hide passwords until asked"), this)),
22 ✔
459
      m_trayIcon(new QCheckBox(tr("Show an icon in the system tray"), this)) {
33 ✔
460
  setTitle(tr("Ready"));
22 ✔
461
  setSubTitle(tr("Everything else can be changed later in Settings."));
11 ✔
462
  auto *layout = new QVBoxLayout(this);
11 ✔
463
  m_summary->setWordWrap(true);
11 ✔
464
  m_summary->setTextFormat(Qt::RichText);
11 ✔
465
  layout->addWidget(m_summary);
11 ✔
466
  layout->addSpacing(12);
11 ✔
467
  m_hidePassword->setToolTip(
11 ✔
468
      tr("Show the password line as dots; Show password reveals it"));
11 ✔
469
  layout->addWidget(m_hidePassword);
11 ✔
470
  layout->addWidget(m_trayIcon);
11 ✔
471
  layout->addStretch();
11 ✔
472
  setFinalPage(true);
11 ✔
473
}
11 ✔
474

475
void DoneWizardPage::initializePage() {
3 ✔
476
  const AppSettings &s = m_wizard->m_settings;
3 ✔
477
  const QString store = QDir::toNativeSeparators(QDir::cleanPath(s.passStore));
3 ✔
478
  QStringList lines;
3 ✔
479
  lines << tr("Store: %1").arg(store.toHtmlEscaped());
6 ✔
480
  lines << (FirstRunWizard::isStore(s.passStore)
3 ✔
481
                ? tr("It is already a password store and is used as it is.")
5 ✔
482
                : tr("It will be set up for the ticked keys%1.")
3 ✔
483
                      .arg(s.useGit ? tr(" and put under Git") : QString()));
7 ✔
484
  lines << tr("GnuPG: %1").arg(s.gpgExecutable.toHtmlEscaped());
6 ✔
485
  lines << (s.usePass ? tr("Operations run through pass.")
3 ✔
486
                      : tr("Operations run through gpg and git directly."));
487
  m_summary->setText(lines.join(QStringLiteral("<br>")));
6 ✔
488
  m_hidePassword->setChecked(true);
3 ✔
489
  const bool tray = QSystemTrayIcon::isSystemTrayAvailable();
3 ✔
490
  m_trayIcon->setVisible(tray);
3 ✔
491
  m_trayIcon->setChecked(tray && s.useTrayIcon);
3 ✔
492
}
3 ✔
493

494
auto DoneWizardPage::validatePage() -> bool {
6 ✔
495
  m_wizard->m_settings.hidePassword = m_hidePassword->isChecked();
6 ✔
496
  m_wizard->m_settings.useTrayIcon =
6 ✔
497
      m_trayIcon->isVisible() && m_trayIcon->isChecked();
6 ✔
498
  return true;
6 ✔
499
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc