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

IJHack / QtPass / 35286047312

17 Sep 2026 11:15PM UTC coverage: 77.32% (-0.009%) from 77.329%
35286047312

push

github

web-flow
String freeze for 2.0: lupdate and best-effort translations for the new strings (#1821)

* FirstRunWizard: whole sentences for the summary, not a spliced fragment

" and put under Git" was inserted into "It will be set up for the ticked
keys%1.", which translators cannot reorder. Two full sentences instead.

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

* lupdate: profiles editor, first-run wizard, add-password dialog, menu bar toggle

72 new source strings since #1805 (454 -> 526 per locale): the Profiles
tab (#1815), the FirstRunWizard pages (#1816), the folder/name row of the
PasswordDialog (#1817) and the Show menu bar action (#1819). Nine
vanished with the old wizard message boxes and the New file prompt.

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

* Best-effort translations for the 63 new strings in 61 locales

Same recipe as #1808: one model pass per locale reading the file for
terminology and mnemonic style, everything left type="unfinished" for
Weblate review; placeholders, %n per plural form (the Arabic zero, one
and two forms may spell the number), HTML, unique accelerators and the
Ctrl+M shortcut string checked mechanically; lrelease6 compiles all 64
files silently. 3846 strings.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

4 of 5 new or added lines in 1 file covered. (80.0%)

9 existing lines in 1 file now uncovered.

6457 of 8351 relevant lines covered (77.32%)

73.47 hits per line

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

86.58
/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 ✔
48
    const QString chosen = pick();
×
49
    if (!chosen.isEmpty()) {
×
50
      edit->setText(chosen);
×
51
      emit edit->textEdited(chosen);
×
52
    }
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 ✔
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 ✔
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 ✔
136
      QMessageBox::warning(
×
137
          this, tr("Error"),
×
138
          tr("Failed to create password-store at: %1").arg(store));
×
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.
153
      QFile::remove(QDir(store).filePath(QStringLiteral(".gpg-id")));
×
154
      QFile::remove(QDir(store).filePath(QStringLiteral(".gpg-id.sig")));
×
155
      QMessageBox::warning(this, tr("Password store not initialised"), note);
×
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.
161
      QMessageBox::information(
×
162
          this, tr("Password store"),
×
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.")
166
              .arg(QDir::toNativeSeparators(store)));
×
167
    }
168
  } else if (m_settings.useGit && !QDir(store).exists(QStringLiteral(".git"))) {
1 ✔
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.
172
      QDir(QDir(store).filePath(QStringLiteral(".git"))).removeRecursively();
×
173
      QMessageBox::warning(this, tr("Password store not initialised"), note);
×
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 ✔
195
  auto pickExe = [this]() {
×
196
    return QFileDialog::getOpenFileName(this, tr("Select executable"),
×
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 ✔
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 ✔
306
      why = tr("expired");
×
307
    } else if (!key.isValid()) {
5 ✔
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 ✔
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

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.
331
  ImitatePass pass;
×
332
  pass.init(m_wizard->m_settings);
×
333
  KeygenDialog d(m_wizard->m_settings.gpgExecutable, &pass, this);
×
334
  d.exec();
×
335
  reload();
×
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(
360
                       this, tr("Choose the password store folder"),
×
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 ✔
419
      it.next();
×
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
  if (FirstRunWizard::isStore(s.passStore)) {
3 ✔
481
    lines << tr("It is already a password store and is used as it is.");
1 ✔
482
  } else if (s.useGit) {
2 ✔
NEW
483
    lines << tr("It will be set up for the ticked keys and put under Git.");
×
484
  } else {
485
    lines << tr("It will be set up for the ticked keys.");
2 ✔
486
  }
487
  lines << tr("GnuPG: %1").arg(s.gpgExecutable.toHtmlEscaped());
6 ✔
488
  lines << (s.usePass ? tr("Operations run through pass.")
3 ✔
489
                      : tr("Operations run through gpg and git directly."));
490
  m_summary->setText(lines.join(QStringLiteral("<br>")));
6 ✔
491
  m_hidePassword->setChecked(true);
3 ✔
492
  const bool tray = QSystemTrayIcon::isSystemTrayAvailable();
3 ✔
493
  m_trayIcon->setVisible(tray);
3 ✔
494
  m_trayIcon->setChecked(tray && s.useTrayIcon);
3 ✔
495
}
3 ✔
496

497
auto DoneWizardPage::validatePage() -> bool {
6 ✔
498
  m_wizard->m_settings.hidePassword = m_hidePassword->isChecked();
6 ✔
499
  m_wizard->m_settings.useTrayIcon =
6 ✔
500
      m_trayIcon->isVisible() && m_trayIcon->isChecked();
6 ✔
501
  return true;
6 ✔
502
}
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