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

IJHack / QtPass / 35437047893

19 Sep 2026 10:18AM UTC coverage: 77.417%. Remained the same
35437047893

push

github

web-flow
Sentence case for the remaining Title Case strings (#1835)

* Sentence case for the remaining Title Case strings

Twenty-one strings still capitalised every word (Export Public Key,
Password Length:, OTP Code, Rename Folder To:, Search Password, ...) while
the rest of the interface uses sentence case, and each was its own string
to translate. lupdate turns a case change into a new empty message next to
the vanished one; since the translation of a re-cased English string is the
same translation, it is copied over (1489 messages in 64 files) with its
finished state intact, so Weblate has nothing new to review for this.

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

* Carry over obsolete entries too, keep English identity translations in step

Review of the case pass: en_US and en_GB keep their predecessors as
type="obsolete" rather than "vanished", so their translations were not
copied, and a translation that only echoes the English source must follow
the source's new case instead of preserving the old one (else en_US would
still show "Export Public Key"). Also the help heading "Export Your Public
Key" that the first pass missed.

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

* Redo the catalogs on a clean tree; re-case only English identity strings

The second lupdate ran after a build and picked up ui_*.h locations; the
catalogs are regenerated from a clean tree. The identity re-casing now
applies to en_US and en_GB only (a German "Version" is a translation, not
an echo of the source) and keeps en_GB's typographic ellipsis, so "Save
to File…" becomes "Save to file…".

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>

5 of 25 new or added lines in 7 files covered. (20.0%)

9 existing lines in 1 file now uncovered.

6647 of 8586 relevant lines covered (77.42%)

73.76 hits per line

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

48.69
/src/mainwindow.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "mainwindow.h"
4

5
#include "qtpasslogging.h"
6

7
#include "configdialog.h"
8
#include "enums.h"
9
#include "executor.h"
10
#include "exportpublickeydialog.h"
11
#include "filecontent.h"
12
#include "firstrunwizard.h"
13
#include "passworddialog.h"
14
#include "passworddisplaypanel.h"
15
#include "pathvalidator.h"
16
#include "processoutputpanel.h"
17
#include "qpushbuttonasqrcode.h"
18
#include "qpushbuttonshowpassword.h"
19
#include "qpushbuttonwithclipboard.h"
20
#include "qtpass.h"
21
#include "qtpasssettings.h"
22
#include "templateio.h"
23
#include "totp.h"
24
#include "trayicon.h"
25
#include "ui_mainwindow.h"
26
#include "usersdialog.h"
27
#include "util.h"
28
#include "windowstatestore.h"
29
#include <QApplication>
30
#include <QCloseEvent>
31
#include <QDate>
32
#include <QDesktopServices>
33
#include <QDialog>
34
#include <QDirIterator>
35
#include <QDockWidget>
36
#include <QFileInfo>
37
#include <QHBoxLayout>
38
#include <QInputDialog>
39
#include <QLabel>
40
#include <QLineEdit>
41
#include <QMenu>
42
#include <QMessageBox>
43
#include <QProgressDialog>
44
#include <QPushButton>
45
#include <QShortcut>
46
#include <QTextCharFormat>
47
#include <QTextStream>
48
#include <QTimer>
49
#include <QTreeWidget>
50
#include <QUrl>
51
#include <algorithm>
52
#include <utility>
53

54
/**
55
 * @brief MainWindow::MainWindow handles all of the main functionality and also
56
 * the main window.
57
 * @param searchText for searching from cli
58
 * @param parent pointer
59
 */
60
MainWindow::MainWindow(const QString &searchText, QWidget *parent)
43 ✔
61
    : QMainWindow(parent), ui(new Ui::MainWindow) {
43 ✔
62
#ifdef __APPLE__
63
  // extra treatment for mac os
64
  // see https://doc.qt.io/qt-6/qkeysequence.html#qt_set_sequence_auto_mnemonic
65
  qt_set_sequence_auto_mnemonic(true);
66
#endif
67
  ui->setupUi(this);
43 ✔
68

69
  m_qtPass = new QtPass(this);
43 ✔
70
  restoreWindow();
43 ✔
71
  connect(m_qtPass, &QtPass::outputReady, this,
43 ✔
72
          [this](const QString &html) { flashText(html, false, true); });
55 ✔
73
  connect(m_qtPass, &QtPass::operationFinished, this,
43 ✔
74
          [this]() { setUiElementsEnabled(true); });
55 ✔
75
  connect(m_qtPass, &QtPass::statusMessage, this,
43 ✔
76
          &MainWindow::showStatusMessage);
43 ✔
77
  connect(m_qtPass, &QtPass::pushRequested, this, &MainWindow::onPush);
43 ✔
78
  connect(m_qtPass, &QtPass::entryInserted, this,
43 ✔
79
          [this]() { on_treeView_clicked(getCurrentTreeViewIndex()); });
44 ✔
80
  connect(&m_qtPass->clipboard(), &ClipboardManager::statusMessage, this,
43 ✔
81
          [this](const QString &message) { showStatusMessage(message); });
50 ✔
82
  // Both backends: the active one can change through the configuration
83
  // dialog without a restart.
84
  connectPassSignals(QtPassSettings::getRealPass());
43 ✔
85
  connectPassSignals(QtPassSettings::getImitatePass());
43 ✔
86
  ImitatePass *ipass = QtPassSettings::getImitatePass();
43 ✔
87
  connect(ipass, &ImitatePass::startReencryptPath, this,
43 ✔
88
          &MainWindow::startReencryptPath);
43 ✔
89
  connect(ipass, &ImitatePass::reencryptProgress, this,
43 ✔
90
          &MainWindow::reencryptProgress);
43 ✔
91
  connect(ipass, &ImitatePass::endReencryptPath, this,
43 ✔
92
          &MainWindow::endReencryptPath);
43 ✔
93

94
  // register shortcut ctrl/cmd + C to copy the currently selected password
95
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
43 ✔
96
                &MainWindow::copyPasswordFromTreeview);
43 ✔
97

98
  m_tree = new StoreTree(ui->treeView, this);
43 ✔
99
  m_tree->setStore(QtPassSettings::getPassStore(Util::findPasswordStore()));
43 ✔
100
  m_tree->setPass(QtPassSettings::getPass());
43 ✔
101
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
43 ✔
102
          &MainWindow::showContextMenu);
43 ✔
103
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
43 ✔
104
          &MainWindow::deselect);
43 ✔
105

106
  {
107
    const AppSettings s = QtPassSettings::load();
43 ✔
108
    if (s.useMonospace) {
43 ✔
109
      QFont monospace("Monospace");
×
110
      monospace.setStyleHint(QFont::Monospace);
×
111
      ui->textBrowser->setFont(monospace);
×
112
    }
×
113
    if (s.noLineWrapping) {
43 ✔
114
      ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
115
    }
116
    clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
43 ✔
117
  }
43 ✔
118
  ui->textBrowser->setOpenExternalLinks(true);
43 ✔
119
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
43 ✔
120
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
43 ✔
121
          &MainWindow::showBrowserContextMenu);
43 ✔
122

123
  updateProfileBox();
43 ✔
124

125
  m_displayPanel = new PasswordDisplayPanel(
43 ✔
126
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
43 ✔
127
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
43 ✔
128
          &m_qtPass->clipboard(), &ClipboardManager::copyText);
43 ✔
129
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
43 ✔
130
          &QtPass::showTextAsQRCode);
43 ✔
131
  // Double-click on what is shown edits it, as in the tree.
132
  connect(m_displayPanel, &PasswordDisplayPanel::editRequested, this, [this] {
43 ✔
133
    if (ui->actionEdit->isEnabled()) {
×
134
      onEdit();
×
135
    }
136
  });
×
137

138
  QtPassSettings::getPass()->updateEnv();
43 ✔
139
  clearPanelTimer.setSingleShot(true);
43 ✔
140
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
43 ✔
141

142
  searchTimer.setInterval(350);
43 ✔
143
  searchTimer.setSingleShot(true);
43 ✔
144

145
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
43 ✔
146

147
  // Install the search-box key filter once, not on every setUiElementsEnabled
148
  // call.
149
  ui->lineEdit->installEventFilter(this);
43 ✔
150
  ui->toolBar->installEventFilter(this);
43 ✔
151

152
  // Safety net: if a backend operation disables the UI but never signals
153
  // completion, re-enable after a timeout so the window can't get stuck.
154
  m_uiWatchdog.setSingleShot(true);
43 ✔
155
  m_uiWatchdog.setInterval(UiWatchdogMs);
43 ✔
156
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
43 ✔
157
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
158
    // Drop any in-flight OTP request so a late finishedShow cannot be mistaken
159
    // for the answer to it.
160
    cancelOtpRequest();
×
161
    setUiElementsEnabled(true);
×
162
  });
×
163

164
  initToolBarButtons();
43 ✔
165
  initStatusBar();
43 ✔
166
  m_processOutput = new ProcessOutputPanel(this);
43 ✔
167
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
43 ✔
168
  // setVisible after addDockWidget so our explicit preference wins even if
169
  // QMainWindow applies any cached state when the dock is attached.
170
  // restoreWindow() ran right after the QtPass construction above, so the
171
  // saved layout has already been processed.
172
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
43 ✔
173

174
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
43 ✔
175
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
86 ✔
176
            // Never route potentially-secret output through the panel:
177
            // - PASS_SHOW goes via a dedicated signal to
178
            //   the main text browser (which clears on a timer).
179
            // - PASS_GREP returns lines from password files; #252 must
180
            //   not leak those into a long-lived panel.
181
            // - PASS_INSERT's stdin is the password; stdout normally
182
            //   carries gpg/git progress only, but exclude defensively
183
            //   in case a future code path uses --echo or similar.
184
            if (ProcessOutputPanel::isSensitiveProcess(pid)) {
×
185
              return;
186
            }
187
            if (!out.isEmpty()) {
×
188
              onProcessOutput(out, false, pid);
×
189
            }
190
            if (!err.isEmpty()) {
×
191
              onProcessOutput(err, true, pid);
×
192
            }
193
          });
194

195
  ui->lineEdit->setClearButtonEnabled(true);
43 ✔
196
  updateGrepButtonVisibility();
43 ✔
197

198
  setUiElementsEnabled(true);
43 ✔
199

200
  ui->lineEdit->setText(searchText);
43 ✔
201

202
  // Record whether startup configuration succeeded instead of calling
203
  // QApplication::quit() here: quit() before the event loop runs (exec() is
204
  // still ahead in main()) is a documented no-op, so a cancelled first-run
205
  // wizard used to leave the half-configured window showing anyway. main()
206
  // consults initSucceeded() and exits before show() when this is false.
207
  m_qtPass->init();
43 ✔
208
  // Ask again until the configuration is usable or the user gives up. The
209
  // dialog's OK button is not gated on Util::configIsValid(), so an accepted
210
  // first-run configuration can still point at a store without a .gpg-id
211
  // (the user declined to create it); only a cancel ends the loop.
212
  while (!Util::configIsValid(QtPassSettings::load())) {
44 ✔
213
    if (!config()) {
2 ✔
214
      return;
215
    }
216
  }
217
  m_freshStart = false;
42 ✔
218
  m_initSucceeded = true;
42 ✔
219

220
  // Initial focus is handled in showEvent() once the window is actually
221
  // mapped. Scheduling it here via a 10 ms QTimer was racy: if the timer
222
  // fires while the window has not yet been realised — e.g. an
223
  // ActivationChange queued by main()'s `activateWindow()` call before
224
  // `show()`, or a nested QDialog::exec() inside init() — the
225
  // QLineEdit's internal text engine hasn't been wired up and
226
  // selectAll() segfaults inside Qt (see #1187, #1188).
227
}
×
228

229
MainWindow::~MainWindow() {
82 ✔
230
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
231
  // window still knows its last geometry here.
232
  saveWindowState();
43 ✔
233
  delete m_qtPass;
43 ✔
234
}
125 ✔
235

236
/**
237
 * @brief Connect the signals of one backend that the window answers itself:
238
 * decrypt results, errors, status text and the grep results.
239
 * @param pass The backend.
240
 */
241
void MainWindow::connectPassSignals(Pass *pass) {
86 ✔
242
  // A failed decrypt never emits finishedShow; drop the pending OTP/copy
243
  // request so a later decrypt of the same entry is not taken as its answer.
244
  connect(pass, &Pass::processErrorExit, this, &MainWindow::cancelOtpRequest);
86 ✔
245
  connect(pass, &Pass::critical, this, &MainWindow::critical);
86 ✔
246
  connect(pass, &Pass::startingExecuteWrapper, this,
86 ✔
247
          &MainWindow::executeWrapperStarted);
86 ✔
248
  connect(pass, &Pass::statusMsg, this, &MainWindow::showStatusMessage);
86 ✔
249
  connect(pass, &Pass::finishedShow, this, &MainWindow::passShowHandler);
86 ✔
250
  // Both check the file against their own pending request and ignore the
251
  // rest, so they can stay connected for good.
252
  connect(pass, &Pass::finishedShow, this, &MainWindow::otpFromFileToClipboard);
86 ✔
253
  connect(pass, &Pass::finishedShow, this,
86 ✔
254
          &MainWindow::passwordFromFileToClipboard);
86 ✔
255
  connect(pass, &Pass::finishedGrep, this, &MainWindow::onGrepFinished);
86 ✔
256
}
86 ✔
257

258
/**
259
 * @brief MainWindow::saveWindowState persist geometry and dock/toolbar
260
 *        layout. Idempotent, cheap, called from closeEvent() and the
261
 *        destructor.
262
 */
263
void MainWindow::saveWindowState() {
45 ✔
264
  QtPassSettings::setGeometry(saveGeometry());
45 ✔
265
  QtPassSettings::setSavestate(saveState());
45 ✔
266
}
45 ✔
267

268
/**
269
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
270
 * box and sets focus to it. Allows for easy searching, called at application
271
 * start and when receiving empty message in MainWindow::messageAvailable when
272
 * compiled with SINGLE_APP=1 (default).
273
 */
274
void MainWindow::focusInput() {
14 ✔
275
  // Resolve the QLineEdit through the live widget tree rather than the
276
  // cached `ui->lineEdit` pointer.
277
  //
278
  // On a fresh-config first launch the constructor calls
279
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
280
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
281
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
282
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
283
  // we observed the QLineEdit attached to the centralWidget gets
284
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
285
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
286
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
287
  // walks the current hierarchy and returns null cleanly when the
288
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
289
  // use-after-free.
290
  if (!isVisible()) {
14 ✔
291
    return;
292
  }
293
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
14 ✔
294
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
14 ✔
295
    return;
296
  }
297
  lineEdit->selectAll();
14 ✔
298
  lineEdit->setFocus();
14 ✔
299
  // Only mark the first-show focus pulse as done once it's actually
300
  // landed; setting it eagerly in showEvent() would consume the
301
  // one-shot if focusInput returned early (mid-rebuild widget state)
302
  // and we'd never retry.
303
  m_firstShowCompleted = true;
14 ✔
304
}
305

306
/**
307
 * @brief MainWindow::changeEvent sets focus to the search box on activation
308
 * and re-derives palette-dependent styling on a theme change.
309
 * @param event
310
 */
311
void MainWindow::changeEvent(QEvent *event) {
62 ✔
312
  QWidget::changeEvent(event);
62 ✔
313
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
62 ✔
314
    // Desktop switched light/dark (e.g. KDE day/night). Top-level widgets
315
    // receive PaletteChange for that (ApplicationPaletteChange goes to the
316
    // QApplication object). Styling that bakes palette colours into
317
    // stylesheets must be re-derived by hand.
318
    m_displayPanel->refreshPalette();
5 ✔
319
  }
320
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
62 ✔
321
      isVisible()) {
322
    // Defer one event-loop tick so the synchronous activation dispatch
323
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
324
    // unwinds before we touch widget state — calling `focusInput()`
325
    // inline from this stack has segfaulted in past iterations because
326
    // mid-rebuild ui state isn't fully wired up yet.
327
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
10 ✔
328
                              Qt::QueuedConnection);
329
  }
330
}
62 ✔
331

332
/**
333
 * @brief First-show hook: run the initial focusInput() pulse once the
334
 *        window is actually mapped. The widget's internal data is fully
335
 *        initialised by this point, so QLineEdit::selectAll() is safe.
336
 * @param event Show event passed to the base class.
337
 */
338
void MainWindow::showEvent(QShowEvent *event) {
12 ✔
339
  QMainWindow::showEvent(event);
12 ✔
340
  if (m_firstShowCompleted) {
12 ✔
341
    return;
342
  }
343
  // Queue the focus pulse for the next event-loop tick so the platform
344
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
345
  // from the config wizard path) settle before we look up the line
346
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
347
  // *after* it actually focuses, so a transient failed lookup just
348
  // re-queues on the next show rather than silently dropping.
349
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
10 ✔
350
                            Qt::QueuedConnection);
351
}
352

353
/**
354
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
355
 */
356
void MainWindow::initToolBarButtons() {
43 ✔
357
  connect(ui->actionAddPassword, &QAction::triggered, this,
43 ✔
358
          &MainWindow::addPassword);
43 ✔
359
  connect(ui->actionAddFolder, &QAction::triggered, this,
43 ✔
360
          &MainWindow::addFolder);
43 ✔
361
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
43 ✔
362
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
43 ✔
363
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
43 ✔
364
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
43 ✔
365
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
43 ✔
366
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
43 ✔
367
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
43 ✔
368

369
  // Menu-only actions. Quit ends the application, as Ctrl+Q/Cmd+Q does in
370
  // KeePassXC, Telegram and per the KDE, GNOME and macOS guidelines, tray
371
  // icon or not (#1788). Close window is what the old bare Ctrl+Q shortcut
372
  // did: close(), which closeEvent() turns into a hide when "hide on close"
373
  // is set. The X button and Alt+F4 are close() as well.
374
  connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
43 ✔
375
  connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
43 ✔
376
#ifdef Q_OS_MACOS
377
  // The menu bar is the system's up there; nothing to hide.
378
  ui->actionShowMenuBar->setVisible(false);
379
#else
380
  // The action lives in the menu it hides, so it has to be on the window as
381
  // well for Ctrl+M to keep working while the bar is gone.
382
  addAction(ui->actionShowMenuBar);
43 ✔
383
  ui->actionShowMenuBar->setChecked(QtPassSettings::load().showMenuBar);
43 ✔
384
  menuBar()->setVisible(ui->actionShowMenuBar->isChecked());
43 ✔
385
  connect(ui->actionShowMenuBar, &QAction::toggled, this, [this](bool show) {
43 ✔
386
    menuBar()->setVisible(show);
3 ✔
387
    AppSettings s = QtPassSettings::load();
3 ✔
388
    s.showMenuBar = show;
3 ✔
389
    QtPassSettings::save(s);
3 ✔
390
  });
3 ✔
391
#endif
392
  connect(ui->actionFaq, &QAction::triggered, this, [] {
43 ✔
393
    QDesktopServices::openUrl(QUrl(QStringLiteral("https://qtpass.org/faq")));
×
394
  });
×
395
  connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
43 ✔
396
  connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
43 ✔
397

398
  ui->actionAddPassword->setIcon(
43 ✔
399
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
172 ✔
400
  ui->actionAddFolder->setIcon(
43 ✔
401
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
172 ✔
402
  ui->actionEdit->setIcon(QIcon::fromTheme(
43 ✔
403
      "document-properties", QIcon(":/icons/document-properties.svg")));
129 ✔
404
  ui->actionDelete->setIcon(
43 ✔
405
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
172 ✔
406
  ui->actionPush->setIcon(
43 ✔
407
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
172 ✔
408
  ui->actionUpdate->setIcon(
43 ✔
409
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
172 ✔
410
  ui->actionUsers->setIcon(QIcon::fromTheme(
43 ✔
411
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
129 ✔
412
  ui->actionConfig->setIcon(QIcon::fromTheme(
43 ✔
413
      "applications-system", QIcon(":/icons/applications-system.svg")));
86 ✔
414
}
43 ✔
415

416
/**
417
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
418
 */
419
void MainWindow::initStatusBar() {
43 ✔
420
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
86 ✔
421

422
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
86 ✔
423
                     .scaledToHeight(statusBar()->height());
43 ✔
424
  auto *logoApp = new QLabel(statusBar());
43 ✔
425
  logoApp->setPixmap(logo);
43 ✔
426
  statusBar()->addPermanentWidget(logoApp);
43 ✔
427
}
43 ✔
428

429
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1 ✔
430
  return ui->treeView->currentIndex();
1 ✔
431
}
432

433
/**
434
 * @brief Displays the given text in the main window text browser, optionally
435
 * marking it as an error and/or rendering it as HTML.
436
 * @example
437
 * MainWindow window;
438
 * window.flashText("Operation completed.", false, false);
439
 *
440
 * @param const QString &text - The text content to display.
441
 * @param const bool isError - If true, sets the text color to red before
442
 * displaying the text; otherwise any earlier error colour is cleared.
443
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
444
 * the existing HTML content.
445
 * @return void - No return value.
446
 */
447
void MainWindow::flashText(const QString &text, const bool isError,
24 ✔
448
                           const bool isHtml) {
449
  if (isError) {
24 ✔
450
    ui->textBrowser->setTextColor(Qt::red);
3 ✔
451
  } else {
452
    // setTextColor() merges the red foreground into the browser's current
453
    // char format, and setText()/setPlainText() re-applies that format to the
454
    // whole new document. Without clearing it, a plain-text non-error message
455
    // shown after an error would still be red. Remove the property rather
456
    // than pinning a palette colour: an explicit foreground would stop the
457
    // text from following runtime light/dark palette switches (#946).
458
    QTextCharFormat format = ui->textBrowser->currentCharFormat();
21 ✔
459
    format.clearForeground();
460
    ui->textBrowser->setCurrentCharFormat(format);
21 ✔
461
  }
462

463
  if (isHtml) {
24 ✔
464
    QString _text = text;
465
    if (!ui->textBrowser->toPlainText().isEmpty()) {
38 ✔
466
      _text = ui->textBrowser->toHtml() + _text;
16 ✔
467
    }
468
    ui->textBrowser->setHtml(_text);
19 ✔
469
  } else {
470
    ui->textBrowser->setText(text);
5 ✔
471
  }
472
}
24 ✔
473

474
/**
475
 * @brief MainWindow::config pops up the configuration screen and handles all
476
 * inter-window communication
477
 */
478
void MainWindow::applyTextBrowserSettings() {
1 ✔
479
  const AppSettings s = QtPassSettings::load();
1 ✔
480
  if (s.useMonospace) {
1 ✔
481
    QFont monospace("Monospace");
×
482
    monospace.setStyleHint(QFont::Monospace);
×
483
    ui->textBrowser->setFont(monospace);
×
484
  } else {
×
485
    ui->textBrowser->setFont(QFont());
1 ✔
486
  }
487

488
  if (s.noLineWrapping) {
1 ✔
489
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
490
  } else {
491
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
1 ✔
492
  }
493
}
1 ✔
494

495
void MainWindow::applyWindowFlagsSettings() {
51 ✔
496
  const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
51 ✔
497
  const Qt::WindowFlags flags = windowFlags();
498
  if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
51 ✔
499
    // Nothing to change. setWindowFlags() on a top-level widget rebuilds the
500
    // native window (setParent(nullptr, flags)), which used to happen on every
501
    // Settings OK because the "off" branch never compared equal — the same
502
    // teardown that left ui->lineEdit dangling (see focusInput()).
503
    return;
504
  }
505
  // Toggle just the one hint; the old "off" branch reset to Qt::Window and
506
  // discarded every other flag with it.
507
  const bool wasVisible = isVisible();
508
  setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
4 ✔
509
                           : (flags & ~Qt::WindowStaysOnTopHint));
510
  // setWindowFlags() hides a shown window; a window that was not shown yet
511
  // (first-run wizard from the constructor) is left to its normal show.
512
  if (wasVisible) {
2 ✔
513
    show();
2 ✔
514
  }
515
}
516

517
/**
518
 * @brief Opens the configuration dialog (or, before the first usable
519
 * configuration, the first-run wizard) and applies what was accepted.
520
 * @example
521
 * if (!config()) {
522
 *   // the user cancelled
523
 * }
524
 *
525
 * @return bool - True when the dialog was accepted, false when it was
526
 * cancelled. An accepted dialog can still leave the configuration invalid
527
 * (the OK button is not gated on Util::configIsValid()); the constructor
528
 * keeps asking until the configuration is usable or this returns false.
529
 */
530
auto MainWindow::config() -> bool {
2 ✔
531
  if (m_freshStart) {
2 ✔
532
    // No usable configuration yet: the wizard finds the programs, the key
533
    // and the store, and creates the store when it has to.
534
    FirstRunWizard wizard(this);
2 ✔
535
    if (wizard.exec() != QDialog::Accepted) {
2 ✔
536
      return false;
537
    }
538
  } else {
2 ✔
539
    ConfigDialog d(this);
×
540
    d.setModal(true);
×
541
    if (d.exec() != QDialog::Accepted) {
×
542
      return false;
543
    }
544
  }
×
545

546
  applyTextBrowserSettings();
1 ✔
547
  applyWindowFlagsSettings();
1 ✔
548

549
  updateProfileBox();
1 ✔
550
  const AppSettings s = QtPassSettings::load();
1 ✔
551
  m_tree->setStore(s.passStore);
1 ✔
552
  deselect();
1 ✔
553
  ui->treeView->setCurrentIndex(QModelIndex());
1 ✔
554

555
  Pass *activePass = QtPassSettings::getPass();
1 ✔
556
  activePass->updateEnv();
1 ✔
557
  m_tree->setPass(activePass);
1 ✔
558
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
1 ✔
559
  m_qtPass->clipboard().setAutoclearTimer();
1 ✔
560

561
  updateGitButtonVisibility();
1 ✔
562
  updateOtpButtonVisibility();
1 ✔
563
  updateGrepButtonVisibility();
1 ✔
564
  updateProcessOutputVisibility();
1 ✔
565
#ifndef Q_OS_MACOS
566
  // The action's toggled handler shows or hides the bar (and saves again).
567
  ui->actionShowMenuBar->setChecked(s.showMenuBar);
1 ✔
568
#endif
569
  if (s.useTrayIcon && m_tray == nullptr) {
1 ✔
570
    initTrayIcon();
×
571
  } else if (!s.useTrayIcon && m_tray != nullptr) {
1 ✔
572
    destroyTrayIcon();
×
573
  }
574

575
  // Leave the fresh-start state in place while the accepted configuration is
576
  // still unusable (for example the user declined to create the store), so
577
  // the next attempt from the constructor's loop runs the first-run wizard
578
  // again instead of showing the bare dialog. The re-prompt itself lives
579
  // there: recursing here re-ran the dialog on this stack frame and never
580
  // reported a cancel back to the caller.
581
  if (Util::configIsValid(s)) {
1 ✔
582
    m_freshStart = false;
1 ✔
583
  }
584
  return true;
585
}
1 ✔
586

587
/**
588
 * @brief MainWindow::onUpdate do a git pull
589
 */
590
void MainWindow::onUpdate(bool block) {
×
591
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
592
  if (block) {
×
593
    QtPassSettings::getPass()->GitPull_b();
×
594
  } else {
595
    QtPassSettings::getPass()->GitPull();
×
596
  }
597
}
×
598

599
/**
600
 * @brief MainWindow::onPush do a git push
601
 */
602
void MainWindow::onPush() {
×
603
  if (QtPassSettings::isUseGit()) {
×
604
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
605
    QtPassSettings::getPass()->GitPush();
×
606
  }
607
}
×
608

609
/**
610
 * @brief MainWindow::getFile get the selected file path
611
 * @param index
612
 * @param forPass returns relative path without '.gpg' extension
613
 * @return path
614
 * @return
615
 */
616
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
12 ✔
617
  return m_tree->fileFor(index, forPass);
12 ✔
618
}
619

620
/**
621
 * @brief MainWindow::on_treeView_clicked read the selected password file
622
 * @param index
623
 */
624
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
3 ✔
625
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
3 ✔
626
  QString file = getFile(index, true);
3 ✔
627
  ui->passwordName->setText(file);
3 ✔
628
  if (!file.isEmpty() && !cleared) {
3 ✔
629
    // Remember what the panel is about to show, so onOtp() can tell whether the
630
    // code it can see belongs to the entry that is currently selected.
631
    m_shownFile = file;
3 ✔
632
    QtPassSettings::getPass()->Show(file);
6 ✔
633
  } else {
634
    m_shownFile.clear();
×
635
    clearPanel(false);
×
636
    ui->actionEdit->setEnabled(false);
×
637
    ui->actionDelete->setEnabled(true);
×
638
  }
639
}
3 ✔
640

641
/**
642
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
643
 * TreeViewItem, open the edit Window
644
 * @param index
645
 */
646
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
1 ✔
647
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
1 ✔
648

649
  if (fileOrFolder.isFile()) {
1 ✔
650
    editPassword(getFile(index, true));
2 ✔
651
  }
652
}
1 ✔
653

654
/**
655
 * @brief MainWindow::deselect clear the selection, password and copy buffer
656
 */
657
void MainWindow::deselect() {
2 ✔
658
  m_shownFile.clear();
2 ✔
659
  cancelOtpRequest();
2 ✔
660
  m_qtPass->clipboard().clear();
2 ✔
661
  ui->treeView->clearSelection();
2 ✔
662
  ui->actionEdit->setEnabled(false);
2 ✔
663
  ui->actionDelete->setEnabled(false);
2 ✔
664
  ui->passwordName->setText("");
2 ✔
665
  clearPanel(false);
2 ✔
666
}
2 ✔
667

668
void MainWindow::executeWrapperStarted() {
13 ✔
669
  m_displayPanel->clear();
13 ✔
670
  ui->textBrowser->clear();
13 ✔
671
  setUiElementsEnabled(false);
13 ✔
672
  clearPanelTimer.stop();
13 ✔
673
  if (QtPassSettings::isShowProcessOutput()) {
13 ✔
674
    m_processOutput->setVisible(true);
8 ✔
675
  }
676
}
13 ✔
677

678
/**
679
 * @brief Handles displaying parsed password entry content in the main window:
680
 * the panel shows the parsed fields, the text browser what is left over.
681
 * @param p_output - The raw output text containing the password entry data.
682
 * @param file - The entry the output belongs to; anything but the entry most
683
 * recently asked for is dropped.
684
 */
685
void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
6 ✔
686
  // Only the entry we most recently asked to show may repaint the panel; a
687
  // slower decrypt of an entry the user has since left is dropped.
688
  if (!file.isEmpty() && !m_shownFile.isEmpty() && file != m_shownFile) {
6 ✔
689
    return;
×
690
  }
691
  const AppSettings s = QtPassSettings::load();
6 ✔
692
  QStringList templ =
693
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
6 ✔
694
  bool allFields = s.useTemplate && s.templateAllFields;
6 ✔
695
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
6 ✔
696
  QString output = p_output;
697
  // Display variant: empty when the password line is itself an otpauth URI, so
698
  // the shared secret is neither rendered nor copied to the clipboard.
699
  QString password = fileContent.getPasswordForDisplay();
6 ✔
700

701
  // set clipped text
702
  //
703
  // Skipped for an OTP request: the user asked for a one-time code, not the
704
  // password, and writing both in one event-loop turn can leave the Windows
705
  // clipboard empty (two OleSetClipboard calls back to back).
706
  if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
6 ✔
707
    m_qtPass->clipboard().copyIfAlways(password, p_output);
5 ✔
708
  }
709

710
  // first clear the current view:
711
  m_displayPanel->clear();
6 ✔
712

713
  // show what is needed:
714
  if (s.hideContent) {
6 ✔
715
    output = "***" + tr("Content hidden") + "***";
×
716
  } else if (!s.displayAsIs) {
6 ✔
717
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
12 ✔
718
                                  s.useOtp ? fileContent.getOtpUri()
6 ✔
719
                                           : QString());
720
    output = fileContent.getRemainingDataForDisplay();
12 ✔
721
  }
722

723
  if (s.useAutoclearPanel) {
6 ✔
724
    clearPanelTimer.start();
×
725
  }
726

727
  flashText(QtPass::formatOutput(output), false, true);
12 ✔
728
  setUiElementsEnabled(true);
6 ✔
729
}
12 ✔
730

731
/**
732
 * @brief Generates a one-time password from a decrypted entry and copies it.
733
 *
734
 * Connected to Pass::finishedShow for good; reacts only to the entry onOtp()
735
 * asked for. passShowHandler is connected first, so by the time this runs the
736
 * panel has already been repainted and the UI re-enabled.
737
 *
738
 * @param p_output - The decrypted entry content.
739
 * @return void - This function does not return a value.
740
 */
741
void MainWindow::otpFromFileToClipboard(const QString &p_output,
9 ✔
742
                                        const QString &file) {
743
  // Permanently connected: only the decrypt of the entry onOtp() asked for
744
  // is ours, everything else belongs to a tree click or a Ctrl+C.
745
  if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
9 ✔
746
    return;
8 ✔
747
  }
748
  m_otpRequestFile.clear();
2 ✔
749

750
  if (p_output.isEmpty()) {
2 ✔
751
    // Distinguish "could not read the entry" from "entry has no OTP".
752
    flashText(tr("Could not decrypt this password entry"), true);
×
753
    setUiElementsEnabled(true);
×
754
    return;
×
755
  }
756

757
  // passShowHandler is connected first, so it has already repainted the panel
758
  // for this same finishedShow. When it rendered the OTP row the current code
759
  // is derived and cached, so reuse it instead of re-loading settings and
760
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
761
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
762
  const QString shown = m_displayPanel->currentOtpCode();
2 ✔
763
  if (!shown.isEmpty()) {
2 ✔
764
    m_qtPass->clipboard().copyText(shown);
1 ✔
765
    showStatusMessage(tr("OTP code copied to clipboard"));
1 ✔
766
    setUiElementsEnabled(true);
1 ✔
767
    return;
768
  }
769

770
  const AppSettings s = QtPassSettings::load();
1 ✔
771
  // Parse with the same template settings passShowHandler uses, so an OTP
772
  // field is recognised identically in both paths.
773
  const QStringList templ =
774
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
1 ✔
775
  const bool allFields = s.useTemplate && s.templateAllFields;
1 ✔
776
  const FileContent fileContent =
777
      FileContent::parse(p_output, templ, allFields);
1 ✔
778

779
  const std::optional<Totp::Settings> settings =
780
      Totp::parse(fileContent.getOtpUri());
2 ✔
781
  if (settings.has_value()) {
1 ✔
782
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
2 ✔
783
    showStatusMessage(tr("OTP code copied to clipboard"));
2 ✔
784
  } else {
785
    flashText(tr("No OTP code found in this password entry"), true);
×
786
  }
787
  setUiElementsEnabled(true);
1 ✔
788
}
2 ✔
789

790
/**
791
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
792
 */
793
void MainWindow::clearPanel(bool notify) {
2 ✔
794
  m_displayPanel->clear();
2 ✔
795
  const bool grepWasVisible = ui->grepResultsList->isVisible();
2 ✔
796
  ui->grepResultsList->clear();
2 ✔
797
  if (grepWasVisible) {
2 ✔
798
    ui->grepResultsList->setVisible(false);
×
799
    ui->treeView->setVisible(true);
×
800
    if (m_grep.inGrepMode()) {
×
801
      m_grep.clearGrepMode();
802
      ui->grepButton->blockSignals(true);
×
803
      ui->grepButton->setChecked(false);
×
804
      ui->grepButton->blockSignals(false);
×
805
      ui->lineEdit->blockSignals(true);
×
806
      ui->lineEdit->clear();
×
807
      ui->lineEdit->blockSignals(false);
×
NEW
808
      ui->lineEdit->setPlaceholderText(tr("Search password"));
×
809
    }
810
  }
811
  if (notify) {
2 ✔
NEW
812
    QString output = "***" + tr("Password and content hidden") + "***";
×
813
    ui->textBrowser->setHtml(output);
×
814
  } else {
815
    ui->textBrowser->setHtml("");
4 ✔
816
  }
817
}
2 ✔
818

819
/**
820
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
821
 * elements
822
 * @param state
823
 */
824
void MainWindow::setUiElementsEnabled(bool state) {
95 ✔
825
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
826
  // the user's escape hatch and endReencryptPath() releases the interface, so
827
  // neither an unrelated completion nor the watchdog may re-enable it early.
828
  if (state && m_reencryptRunning) {
95 ✔
829
    m_uiWatchdog.stop();
1 ✔
830
    return;
1 ✔
831
  }
832
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
833
  if (state) {
834
    m_uiWatchdog.stop();
69 ✔
835
  } else {
836
    m_uiWatchdog.start();
25 ✔
837
  }
838
  ui->treeView->setEnabled(state);
94 ✔
839
  ui->lineEdit->setEnabled(state);
94 ✔
840
  ui->actionAddPassword->setEnabled(state);
94 ✔
841
  ui->actionAddFolder->setEnabled(state);
94 ✔
842
  ui->actionUsers->setEnabled(state);
94 ✔
843
  ui->actionConfig->setEnabled(state);
94 ✔
844
  const bool uiEnabled = state;
845
  // is a file selected?
846
  state &= ui->treeView->currentIndex().isValid();
188 ✔
847
  ui->actionDelete->setEnabled(state);
94 ✔
848
  ui->actionEdit->setEnabled(state);
94 ✔
849
  updateGitButtonVisibility(uiEnabled);
94 ✔
850
  // `state` is now "UI enabled AND a file is selected", which is exactly when
851
  // generating an OTP makes sense.
852
  updateOtpButtonVisibility(state);
94 ✔
853
}
854

855
/**
856
 * @brief Restores the main window geometry, state, position, size, and
857
 * tray/icon settings from saved application settings.
858
 * @example
859
 * MainWindow window;
860
 * window.restoreWindow();
861
 *
862
 * @return void - This function does not return a value.
863
 */
864
void MainWindow::restoreWindow() {
50 ✔
865
  // saveGeometry() covers position, size, maximized state and screen; the
866
  // separate pos/size/maximized keys that used to be applied on top of it
867
  // are what discarded the restored position, and the unconditional
868
  // re-centre in main() then discarded the rest. Centre only when nothing
869
  // was saved.
870
  const QByteArray geometry = QtPassSettings::getGeometry();
100 ✔
871
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
50 ✔
872
    WindowStateStore::centreOnCursorScreen(*this);
4 ✔
873
  }
874
  restoreState(QtPassSettings::getSavestate(saveState()));
50 ✔
875
  const AppSettings s = QtPassSettings::load();
50 ✔
876

877
  applyWindowFlagsSettings();
50 ✔
878

879
  if (s.useTrayIcon && m_tray == nullptr) {
50 ✔
880
    initTrayIcon();
×
881
    if (s.startMinimized) {
×
882
      // since we are still in constructor, can't directly hide
883
      QTimer::singleShot(10, this, &MainWindow::hide);
×
884
    }
885
  } else if (!s.useTrayIcon && m_tray != nullptr) {
50 ✔
886
    destroyTrayIcon();
×
887
  }
888
}
100 ✔
889

890
/**
891
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
892
 */
893
void MainWindow::onConfig() { config(); }
×
894

895
/**
896
 * @brief MainWindow::showAbout version, link and licence in a standard box.
897
 */
898
void MainWindow::showAbout() {
×
899
  QMessageBox::about(
×
900
      this, tr("About QtPass"),
×
901
      tr("<h3>QtPass %1</h3>"
×
902
         "<p>A multi-platform GUI for <a "
903
         "href=\"https://www.passwordstore.org/\">"
904
         "pass</a>, the standard Unix password manager.</p>"
905
         "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
906
         "<a href=\"https://github.com/IJHack/QtPass\">Source and "
907
         "issues</a></p>"
908
         "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
909
         "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
910
         "version 3</a> or later.</p>")
911
          .arg(QStringLiteral(VERSION),
×
912
               QString::number(QDate::currentDate().year())));
×
913
}
×
914

915
/**
916
 * @brief Executes when the string in the search box changes, collapses the
917
 * TreeView
918
 * @param arg1
919
 */
920
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
921
  if (m_grep.inGrepMode())
×
922
    return;
923
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
924
  ui->treeView->expandAll();
×
925
  clearPanel(false);
×
926
  ui->passwordName->setText("");
×
927
  ui->actionEdit->setEnabled(false);
×
928
  ui->actionDelete->setEnabled(false);
×
929
  searchTimer.start();
×
930
}
931

932
/**
933
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
934
 * time from two keypresses is elapsed
935
 */
936
void MainWindow::onTimeoutSearch() {
×
937
  QString query = ui->lineEdit->text();
×
938

939
  if (query.isEmpty()) {
×
940
    ui->treeView->collapseAll();
×
941
    deselect();
×
942
  }
943

944
  query.replace(QStringLiteral(" "), ".*");
×
945
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
946
  if (!regExp.isValid())
×
947
    return;
948
  m_tree->setFilter(regExp);
×
949

950
  if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
×
951
    selectFirstFile();
×
952
  } else {
953
    ui->actionEdit->setEnabled(false);
×
954
    ui->actionDelete->setEnabled(false);
×
955
  }
956
}
×
957

958
/**
959
 * @brief MainWindow::on_lineEdit_returnPressed get searching
960
 *
961
 * Select the first possible file in the tree
962
 */
963
void MainWindow::on_lineEdit_returnPressed() {
×
964
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
×
965
                    << m_tree->proxy().rowCount();
×
966

967
  if (m_grep.inGrepMode()) {
×
968
    const QString query = ui->lineEdit->text();
×
969
    if (!query.isEmpty()) {
×
970
      ui->grepResultsList->clear();
×
971
      ui->statusBar->showMessage(tr("Searching…"));
×
972
      if (m_grep.beginSearch()) {
973
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
974
      }
975
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
976
    } else {
977
      if (m_grep.cancelSearch()) {
978
        QApplication::restoreOverrideCursor();
×
979
      }
980
      ui->grepResultsList->clear();
×
981
      ui->grepResultsList->setVisible(false);
×
982
      ui->treeView->setVisible(true);
×
983
    }
984
    return;
985
  }
986

987
  if (m_tree->proxy().rowCount() > 0) {
×
988
    selectFirstFile();
×
989
    on_treeView_clicked(ui->treeView->currentIndex());
×
990
  }
991
}
992

993
/**
994
 * @brief Toggle grep (content search) mode.
995
 */
996
void MainWindow::on_grepButton_toggled(bool checked) {
×
997
  const AppSettings s = QtPassSettings::load();
×
998
  if (checked) {
×
999
    m_grep.enterGrepMode();
1000
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
1001
    // The regex dialect depends on the backend (see Pass::Grep): the pass
1002
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
1003
    ui->lineEdit->setToolTip(
×
1004
        s.usePass
×
1005
            ? tr("Content search uses POSIX basic regular expressions "
×
1006
                 "(pass grep).")
1007
            : tr("Content search uses Perl-compatible regular expressions "
1008
                 "(PCRE)."));
1009
    ui->lineEdit->clear();
×
1010
    searchTimer.stop();
×
1011
    m_tree->setFilter(QRegularExpression());
×
1012
    ui->grepResultsList->setVisible(false);
×
1013
    // Keep treeView visible until results arrive
1014
  } else {
1015
    if (m_grep.leaveGrepMode()) {
1016
      QApplication::restoreOverrideCursor();
×
1017
    }
1018
    searchTimer.stop();
×
1019
    ui->lineEdit->blockSignals(true);
×
1020
    ui->lineEdit->clear();
×
1021
    ui->lineEdit->blockSignals(false);
×
NEW
1022
    ui->lineEdit->setPlaceholderText(tr("Search password"));
×
1023
    ui->lineEdit->setToolTip(QString());
×
1024
    ui->grepResultsList->clear();
×
1025
    ui->grepResultsList->setVisible(false);
×
1026
    ui->treeView->setVisible(true);
×
1027
    m_tree->setFilter(QRegularExpression());
×
1028
  }
1029
}
×
1030

1031
/**
1032
 * @brief Display grep results in grepResultsList.
1033
 */
1034
void MainWindow::onGrepFinished(
×
1035
    const QList<QPair<QString, QStringList>> &results) {
1036
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
1037
  if (outcome.restoreCursor) {
×
1038
    QApplication::restoreOverrideCursor();
×
1039
  }
1040
  // Re-enable the UI before the discard check so a cancelled search can never
1041
  // leave controls disabled.
1042
  setUiElementsEnabled(true);
×
1043
  if (outcome.discard) {
×
1044
    return;
×
1045
  }
1046
  if (!m_grep.inGrepMode())
×
1047
    return;
1048
  ui->grepResultsList->clear();
×
1049
  if (results.isEmpty()) {
×
1050
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
1051
    ui->grepResultsList->setVisible(false);
×
1052
    ui->treeView->setVisible(true);
×
1053
    return;
×
1054
  }
1055
  const AppSettings s = QtPassSettings::load();
×
1056
  const bool hideContent = s.hideContent;
×
1057
  int totalLines = 0;
1058
  for (const auto &pair : results) {
×
1059
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
1060
    entryItem->setText(0, pair.first);
×
1061
    entryItem->setData(0, Qt::UserRole, pair.first);
×
1062
    for (const QString &line : pair.second) {
×
1063
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
1064
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
1065
                                       : line);
1066
      lineItem->setData(0, Qt::UserRole, pair.first);
×
1067
      ++totalLines;
×
1068
    }
1069
  }
1070
  ui->grepResultsList->expandAll();
×
1071
  ui->treeView->setVisible(false);
×
1072
  ui->grepResultsList->setVisible(true);
×
1073
  ui->statusBar->showMessage(
×
1074
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
1075
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
1076
      3000);
1077
  if (s.useAutoclearPanel)
×
1078
    clearPanelTimer.start();
×
1079
}
×
1080

1081
/**
1082
 * @brief Navigate to the password entry when a grep result is clicked.
1083
 */
1084
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
1085
                                                int /*column*/) {
1086
  const AppSettings s = QtPassSettings::load();
×
1087
  const QString entry = item->data(0, Qt::UserRole).toString();
×
1088
  if (entry.isEmpty())
×
1089
    return;
1090
  const QString fullPath =
1091
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
×
1092
  const QModelIndex proxyIndex = m_tree->indexFor(fullPath);
×
1093
  if (!proxyIndex.isValid())
1094
    return;
1095
  ui->treeView->setCurrentIndex(proxyIndex);
×
1096
  on_treeView_clicked(proxyIndex);
×
1097
  if (s.hideContent || s.useAutoclearPanel)
×
1098
    ui->grepResultsList->clear();
×
1099
  ui->grepResultsList->setVisible(false);
×
1100
  ui->treeView->setVisible(true);
×
1101
  ui->treeView->scrollTo(proxyIndex);
×
1102
  ui->treeView->setFocus();
×
1103
}
×
1104

1105
/**
1106
 * @brief MainWindow::selectFirstFile select the first possible file in the
1107
 * tree
1108
 */
1109
void MainWindow::selectFirstFile() {
×
1110
  QModelIndex index = firstFile(m_tree->rootIndex());
×
1111
  ui->treeView->setCurrentIndex(index);
×
1112
}
×
1113

1114
/**
1115
 * @brief MainWindow::firstFile return location of first possible file
1116
 * @param parentIndex
1117
 * @return QModelIndex
1118
 */
1119
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
1120
  StoreModel &proxy = m_tree->proxy();
×
1121
  int numRows = proxy.rowCount(parentIndex);
×
1122
  for (int row = 0; row < numRows; ++row) {
×
1123
    QModelIndex index = proxy.index(row, 0, parentIndex);
×
1124
    if (m_tree->fileInfo(index).isFile()) {
×
1125
      return index;
×
1126
    }
1127
    if (proxy.hasChildren(index)) {
×
1128
      QModelIndex childFile = firstFile(index);
×
1129
      if (childFile.isValid())
1130
        return childFile;
×
1131
    }
1132
  }
1133
  return QModelIndex();
1134
}
1135

1136
/**
1137
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1138
 * the password store and warn the user.
1139
 *
1140
 * Used before file/folder creation, move, and rename to stop user-typed
1141
 * names like "../../etc/passwd" or absolute paths from escaping the
1142
 * configured store root via the input dialogs.
1143
 *
1144
 * @param candidate Absolute candidate path to validate.
1145
 * @return true if the path is inside the password store; false otherwise (a
1146
 * warning dialog is shown in that case).
1147
 */
1148
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1149
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1150
    return true;
1151
  }
1152
  QMessageBox::warning(this, tr("Invalid name"),
×
1153
                       tr("That name would resolve outside the password "
×
1154
                          "store. Please choose a different name."));
1155
  return false;
×
1156
}
1157

1158
/**
1159
 * @brief MainWindow::setPassword open passworddialog
1160
 * @param file which pgp file
1161
 * @param isNew insert (not update)
1162
 */
1163
void MainWindow::setPassword(const QString &file, bool isNew) {
1 ✔
1164
  const AppSettings s = QtPassSettings::load();
1 ✔
1165
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
1 ✔
1166

1167
  if (isNew) {
1 ✔
1168
    const QString storePath = s.passStore;
1169
    QString folder = m_tree->currentDir(false);
×
1170
    if (folder.isEmpty()) {
×
1171
      folder = storePath;
×
1172
    }
1173
    // The dialog names the entry itself: every folder of the store to pick
1174
    // from, the tree's current one preselected.
1175
    QStringList folders{QString()};
×
1176
    QDirIterator it(storePath, QDir::Dirs | QDir::NoDotAndDotDot,
1177
                    QDirIterator::Subdirectories);
×
1178
    while (it.hasNext()) {
×
1179
      const QString rel = QDir(storePath).relativeFilePath(it.next());
×
1180
      if (!rel.startsWith(u'.') && !rel.contains(QStringLiteral("/."))) {
×
1181
        folders << rel;
1182
      }
1183
    }
1184
    std::sort(folders.begin() + 1, folders.end());
×
1185
    d.setNewEntryLocation(storePath, folders,
×
1186
                          QDir(storePath).relativeFilePath(folder));
×
1187
    QHash<QString, QStringList> templates =
1188
        TemplateIO::readTemplates(storePath);
×
1189
    if (!templates.isEmpty()) {
1190
      QString defaultTemplate =
1191
          TemplateIO::getFolderTemplate(folder, storePath);
×
1192
      d.setAvailableTemplates(templates, defaultTemplate);
×
1193
    }
1194
  }
×
1195

1196
  if (!d.exec()) {
1 ✔
1197
    ui->treeView->setFocus();
×
1198
  }
1199
}
1 ✔
1200

1201
/**
1202
 * @brief MainWindow::addPassword add a new password: one dialog with the
1203
 * folder, the name and the content.
1204
 */
1205
void MainWindow::addPassword() { setPassword(QString()); }
×
1206

1207
/**
1208
 * @brief MainWindow::onDelete remove password, if you are
1209
 * sure.
1210
 */
1211
void MainWindow::onDelete() {
×
1212
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1213
  if (!currentIndex.isValid()) {
1214
    // This fixes https://github.com/IJHack/QtPass/issues/556
1215
    // Otherwise the entire password directory would be deleted if
1216
    // nothing is selected in the tree view.
1217
    return;
×
1218
  }
1219

1220
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
×
1221
  QString file = "";
×
1222
  bool isDir = false;
1223

1224
  if (fileOrFolder.isFile()) {
×
1225
    file = getFile(ui->treeView->currentIndex(), true);
×
1226
  } else {
1227
    file = m_tree->currentDir(true);
×
1228
    isDir = true;
1229
  }
1230

1231
  QString dirMessage = tr(" and the whole content?");
1232
  if (isDir) {
×
1233
    QDirIterator it(m_tree->fileSystem().rootPath() + QDir::separator() + file,
×
1234
                    QDirIterator::Subdirectories);
×
1235
    bool okDir = true;
1236
    while (it.hasNext() && okDir) {
×
1237
      it.next();
×
1238
      if (QFileInfo(it.filePath()).isFile()) {
×
1239
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1240
          okDir = false;
1241
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1242
                          "there are unexpected files in the given folder, "
1243
                          "check them before continue.</strong>");
1244
        }
1245
      }
1246
    }
1247
  }
×
1248

1249
  if (QMessageBox::question(
×
1250
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1251
          tr("Are you sure you want to delete %1%2?")
×
1252
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1253
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1254
    return;
1255
  }
1256

1257
  QtPassSettings::getPass()->Remove(file, isDir);
×
1258
}
×
1259

1260
/**
1261
 * @brief MainWindow::cancelOtpRequest forget the pending OTP and copy requests.
1262
 *
1263
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1264
 * decrypt never emits finishedShow; if the request stayed pending, the next
1265
 * successful decrypt of that same entry (the user clicking it again) would be
1266
 * taken as the answer and copy something the user no longer asked for.
1267
 */
1268
void MainWindow::cancelOtpRequest() {
12 ✔
1269
  m_otpRequestFile.clear();
12 ✔
1270
  m_copyRequestFile.clear();
12 ✔
1271
}
12 ✔
1272

1273
/**
1274
 * @brief MainWindow::onOtp generate the selected entry's OTP code and copy it.
1275
 *
1276
 * Decrypts the entry once and derives the code in-process, so this works with
1277
 * either backend and on every platform. The code itself is produced by
1278
 * otpFromFileToClipboard once Pass::finishedShow arrives.
1279
 */
1280
void MainWindow::onOtp() {
4 ✔
1281
  QString file = getFile(ui->treeView->currentIndex(), true);
4 ✔
1282
  if (file.isEmpty()) {
4 ✔
1283
    flashText(tr("No password selected for OTP generation"), true);
×
1284
    return;
×
1285
  }
1286
  if (!QtPassSettings::isUseOtp()) {
4 ✔
1287
    // Normally unreachable (the action is hidden), but never fail silently.
1288
    flashText(tr("No OTP code found in this password entry"), true);
×
1289
    return;
×
1290
  }
1291

1292
  // Fast path: the panel already shows a live code for the selected entry, so
1293
  // there is nothing to decrypt. This also keeps the clipboard write single —
1294
  // going through Show() would let passShowHandler copy the password first, and
1295
  // two QClipboard::setMimeData calls in one event-loop turn can leave the
1296
  // Windows clipboard empty.
1297
  //
1298
  // Only when the panel is showing *this* entry: arrow-key navigation and
1299
  // right-clicking move the tree's currentIndex without emitting
1300
  // QTreeView::clicked, so the visible code can belong to a different account.
1301
  const QString shown =
1302
      (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
4 ✔
1303
  if (!shown.isEmpty()) {
4 ✔
1304
    m_qtPass->clipboard().copyText(shown);
1 ✔
1305
    showStatusMessage(tr("OTP code copied to clipboard"));
2 ✔
1306
    return;
1307
  }
1308

1309
  // Fallback: no OTP row on screen, so decrypt once. The request file stops
1310
  // passShowHandler putting the password on the clipboard for a request that
1311
  // only asked for a code, and tells otpFromFileToClipboard which decrypt is
1312
  // its answer.
1313
  m_otpRequestFile = file;
3 ✔
1314
  setUiElementsEnabled(false);
3 ✔
1315
  // passShowHandler repaints the panel for this Show too, so keep the marker in
1316
  // step or a second request would decrypt again instead of taking the fast
1317
  // path. Safe to set now: executeWrapperStarted() clears the panel on every
1318
  // command, so a failed decrypt leaves currentOtpCode() empty.
1319
  m_shownFile = file;
3 ✔
1320
  QtPassSettings::getPass()->Show(file);
6 ✔
1321
}
1322

1323
/**
1324
 * @brief MainWindow::onEdit try and edit (selected) password.
1325
 */
1326
void MainWindow::onEdit() {
×
1327
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1328
  editPassword(file);
×
1329
}
×
1330

1331
/**
1332
 * @brief MainWindow::onUsers edit users for the current
1333
 * folder,
1334
 * gets lists and opens UserDialog.
1335
 */
1336
void MainWindow::onUsers() {
×
1337
  const QString dir = m_tree->currentDir(false);
×
1338

1339
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1340
  if (!d.exec()) {
×
1341
    ui->treeView->setFocus();
×
1342
  }
1343
}
×
1344

1345
/**
1346
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1347
 * @param message
1348
 */
1349
void MainWindow::messageAvailable(const QString &message) {
×
1350
  show();
×
1351
  raise();
×
1352
  if (message.isEmpty()) {
×
1353
    focusInput();
×
1354
  } else {
1355
    ui->treeView->expandAll();
×
1356
    ui->lineEdit->setText(message);
×
1357
    on_lineEdit_returnPressed();
×
1358
  }
1359
}
×
1360

1361
/**
1362
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1363
 * select a more appropriate one to view too
1364
 */
1365
void MainWindow::updateProfileBox() {
44 ✔
1366
  const Profiles profiles = QtPassSettings::getProfiles();
44 ✔
1367

1368
  if (profiles.isEmpty()) {
×
1369
    ui->profileWidget->hide();
44 ✔
1370
  } else {
1371
    ui->profileWidget->show();
×
1372
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1373
    ui->profileBox->clear();
×
1374
    for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
×
1375
      if (!i.key().isEmpty()) {
×
1376
        ui->profileBox->addItem(i.key());
×
1377
      }
1378
    }
1379
    ui->profileBox->model()->sort(0);
×
1380
  }
1381
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
88 ✔
1382
  if (index != -1) { //  -1 for not found
44 ✔
1383
    ui->profileBox->setCurrentIndex(index);
×
1384
  }
1385
}
44 ✔
1386

1387
/**
1388
 * @brief Handles changes to the selected profile in the profile combo box.
1389
 * @details Ignores the event during a fresh start or when the selected profile
1390
 * matches the current profile. Otherwise, it clears the password field, updates
1391
 * the active profile and related settings, refreshes the environment, and
1392
 * resets the tree view and action states to reflect the newly selected profile.
1393
 *
1394
 * @param name - The newly selected profile name.
1395
 * @return void - This function does not return a value.
1396
 *
1397
 */
1398
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1399
  if (m_freshStart || name == QtPassSettings::getProfile()) {
×
1400
    return;
×
1401
  }
1402

1403
  ui->lineEdit->clear();
×
1404

1405
  const Profile prof = QtPassSettings::getProfiles().value(name);
×
1406
  AppSettings s = QtPassSettings::load();
×
1407
  s.activeProfile = name;
×
1408
  s.passStore = prof.path;
×
1409
  s.passSigningKey = prof.signingKey;
×
1410
  // A profile without its own Git flags keeps the global values.
1411
  s.useGit = prof.useGit.value_or(s.useGit);
×
1412
  s.autoPush = prof.autoPush.value_or(s.autoPush);
×
1413
  s.autoPull = prof.autoPull.value_or(s.autoPull);
×
1414
  QtPassSettings::save(s);
×
1415
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1416

1417
  QtPassSettings::getPass()->updateEnv();
×
1418

1419
  m_tree->setStore(QtPassSettings::getPassStore());
×
1420
  deselect();
×
1421
  ui->treeView->setCurrentIndex(QModelIndex());
×
1422
}
×
1423

1424
/**
1425
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1426
 * support
1427
 * it
1428
 */
1429
void MainWindow::initTrayIcon() {
×
1430
  m_tray = new TrayIcon(this);
×
1431
  if (!m_tray->getIsAllocated()) {
×
1432
    destroyTrayIcon();
×
1433
  }
1434
}
×
1435

1436
/**
1437
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1438
 */
1439
void MainWindow::destroyTrayIcon() {
×
1440
  delete m_tray;
×
1441
  m_tray = nullptr;
×
1442
}
×
1443

1444
/**
1445
 * @brief MainWindow::closeEvent hide or quit
1446
 * @param event
1447
 */
1448
void MainWindow::closeEvent(QCloseEvent *event) {
2 ✔
1449
  // Save in both branches: a tray user only ever hides the window, and
1450
  // used to quit through the tray menu with nothing ever written.
1451
  saveWindowState();
2 ✔
1452
  if (QtPassSettings::isHideOnClose()) {
2 ✔
1453
    this->hide();
2 ✔
1454
    event->ignore();
1455
  } else {
1456
    m_qtPass->clipboard().clear();
×
1457
    event->accept();
1458
    // A visible QSystemTrayIcon keeps the application alive after the last
1459
    // window closes, so quitOnLastWindowClosed never fires and the window
1460
    // merely vanishes into the tray. Quit explicitly so closing the window
1461
    // actually exits when "hide on close" is disabled.
1462
    QApplication::quit();
×
1463
  }
1464
}
2 ✔
1465

1466
/**
1467
 * @brief MainWindow::eventFilter filter out some events and focus the
1468
 * treeview
1469
 * @param obj
1470
 * @param event
1471
 * @return
1472
 */
1473
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
1,421 ✔
1474
  if (obj == ui->toolBar && event->type() == QEvent::PaletteChange) {
1,421 ✔
1475
    // KDE's Breeze style gives top toolbars a "header" palette of its own.
1476
    // After a runtime light/dark switch it recomputes that palette from a
1477
    // cached kdeglobals and stamps the previous theme's colours back on, so
1478
    // the toolbar stays dark on a light window (or vice versa) with matching
1479
    // invisible icons. A legitimate header tint is close to the window
1480
    // colour; a stale theme is not. When the two disagree, drop the imposed
1481
    // palette so the toolbar follows the application palette again. Deferred
1482
    // so we never re-enter the style while it is still applying its palette.
1483
    QTimer::singleShot(0, this, &MainWindow::dropStaleToolBarPalette);
6 ✔
1484
  }
1485
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
1,421 ✔
1486
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1487
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1488
      ui->treeView->setFocus();
×
1489
    }
1490
  }
1491
  return QObject::eventFilter(obj, event);
1,421 ✔
1492
}
1493

1494
/**
1495
 * @brief Reset the toolbar palette when a style left it in the wrong theme.
1496
 *
1497
 * Compares the lightness of the toolbar's Window colour with the application
1498
 * palette; a difference above kStaleToolBarLightness means the toolbar shows
1499
 * the other theme. See the PaletteChange branch in eventFilter().
1500
 */
1501
void MainWindow::dropStaleToolBarPalette() {
3 ✔
1502
  if (!ui->toolBar->testAttribute(Qt::WA_SetPalette)) {
3 ✔
1503
    return;
1504
  }
1505
  constexpr int kStaleToolBarLightness = 64;
1506
  const int barLightness =
1507
      ui->toolBar->palette().color(QPalette::Window).lightness();
4 ✔
1508
  const int appLightness =
1509
      QApplication::palette().color(QPalette::Window).lightness();
4 ✔
1510
  if (qAbs(barLightness - appLightness) > kStaleToolBarLightness) {
2 ✔
1511
    ui->toolBar->setPalette(QPalette());
1 ✔
1512
    // The style also paints the tools-area background on the main window
1513
    // from the same stale palette, and the toolbar is transparent by default;
1514
    // paint our own background so the reset is actually visible.
1515
    ui->toolBar->setAutoFillBackground(true);
1 ✔
1516
  }
1517
}
1518

1519
/**
1520
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1521
 * @param event
1522
 */
1523
void MainWindow::keyPressEvent(QKeyEvent *event) {
1 ✔
1524
  switch (event->key()) {
1 ✔
1525
  case Qt::Key_Delete:
×
1526
    onDelete();
×
1527
    break;
×
1528
  case Qt::Key_Return:
×
1529
  case Qt::Key_Enter:
1530
    if (m_tree->proxy().rowCount() > 0) {
×
1531
      on_treeView_clicked(ui->treeView->currentIndex());
×
1532
    }
1533
    break;
1534
  case Qt::Key_Escape:
×
1535
    ui->lineEdit->clear();
×
1536
    break;
×
1537
  default:
1538
    break;
1539
  }
1540
}
1 ✔
1541

1542
/**
1543
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1544
 * menu
1545
 * @param pos
1546
 */
1547
void MainWindow::showContextMenu(const QPoint &pos) {
×
1548
  const AppSettings s = QtPassSettings::load();
×
1549
  QModelIndex index = ui->treeView->indexAt(pos);
×
1550
  bool selected = true;
1551
  if (!index.isValid()) {
1552
    ui->treeView->clearSelection();
×
1553
    ui->actionDelete->setEnabled(false);
×
1554
    ui->actionEdit->setEnabled(false);
×
1555
    selected = false;
1556
  }
1557

1558
  ui->treeView->setCurrentIndex(index);
×
1559

1560
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1561

1562
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
×
1563

1564
  QMenu contextMenu;
×
1565
  if (!selected || fileOrFolder.isDir()) {
×
1566
    QAction *openFolder =
1567
        contextMenu.addAction(tr("Open folder with file manager"));
×
1568
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1569
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1570
    QAction *users = contextMenu.addAction(tr("Users"));
×
1571
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1572
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1573
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1574
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1575
  } else if (fileOrFolder.isFile()) {
×
1576
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1577
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1578
  }
1579
  if (selected) {
×
1580
    contextMenu.addSeparator();
×
1581
    if (fileOrFolder.isDir()) {
×
1582
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1583
      connect(renameFolder, &QAction::triggered, this,
×
1584
              &MainWindow::renameFolder);
×
1585
    } else if (fileOrFolder.isFile()) {
×
1586
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1587
      connect(renamePassword, &QAction::triggered, this,
×
1588
              &MainWindow::renamePassword);
×
1589
    }
1590
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1591
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1592
    if (fileOrFolder.isDir()) {
×
1593
      QString dirPath = QDir::cleanPath(m_tree->currentDir(false));
×
1594

1595
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1596
      contextMenu.addMenu(shareMenu);
×
1597

1598
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1599
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1600

1601
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1602
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1603
                                                 QFile(exePath).exists());
×
1604

1605
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1606
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1607
      connect(reencrypt, &QAction::triggered, this,
×
1608
              [this, dirPath]() { reencryptPath(dirPath); });
×
1609

1610
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1611
      exportKey->setEnabled(gpgAvailable);
×
1612
      connect(exportKey, &QAction::triggered, this,
×
1613
              &MainWindow::exportPublicKey);
×
1614

1615
      QAction *addRecipientAction =
1616
          shareMenu->addAction(tr("Add recipient..."));
×
1617
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1618
      connect(addRecipientAction, &QAction::triggered, this,
×
1619
              [this, dirPath]() { addRecipient(dirPath); });
×
1620

1621
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1622
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1623
    }
1624
  }
1625
  contextMenu.exec(globalPos);
×
1626
}
×
1627

1628
/**
1629
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1630
 * password window
1631
 * @param pos
1632
 */
1633
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1634
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1635
  // createStandardContextMenu() parents the menu to textBrowser. A stylesheet
1636
  // on the browser (it used to carry "background: palette(base)") cascades to
1637
  // the child QMenu and breaks its opaque native background. Reparent to the
1638
  // main window so the menu paints solid regardless of browser styling.
1639
  contextMenu->setParent(this, contextMenu->windowFlags());
×
1640
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1641

1642
  contextMenu->exec(globalPos);
×
1643
  delete contextMenu;
×
1644
}
×
1645

1646
/**
1647
 * @brief MainWindow::openFolder open the folder in the default file manager
1648
 */
1649
void MainWindow::openFolder() {
×
1650
  QString dir = m_tree->currentDir(false);
×
1651

1652
  QString path = QDir::toNativeSeparators(dir);
×
1653
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1654
}
×
1655

1656
/**
1657
 * @brief MainWindow::addFolder add a new folder to store passwords in
1658
 */
1659
void MainWindow::addFolder() {
×
1660
  const AppSettings s = QtPassSettings::load();
×
1661
  bool ok;
1662
  QString dir = m_tree->currentDir(false);
×
1663
  QString newdir =
1664
      QInputDialog::getText(this, tr("New folder"),
×
NEW
1665
                            tr("New folder: \n(Will be placed in %1 )")
×
1666
                                .arg(s.passStore + m_tree->currentDir(true)),
×
1667
                            QLineEdit::Normal, "", &ok);
×
1668
  if (!ok || newdir.isEmpty()) {
×
1669
    return;
1670
  }
1671
  newdir.prepend(dir);
1672
  if (!confirmPathInStore(newdir)) {
×
1673
    return;
1674
  }
1675
  if (!QDir().mkdir(newdir)) {
×
1676
    QMessageBox::warning(this, tr("Error"),
×
1677
                         tr("Failed to create folder: %1").arg(newdir));
×
1678
    return;
×
1679
  }
1680
  // A .gpg-id only counts when it is signed once a signing key is configured:
1681
  // ImitatePass::verifyGpgIdFile and pass (PASSWORD_STORE_SIGNING_KEY) both
1682
  // refuse to encrypt into a folder whose .gpg-id has no matching
1683
  // .gpg-id.sig. Signing here would need the signing secret key and a
1684
  // passphrase prompt just to create a folder, so in that case leave the
1685
  // folder without its own .gpg-id: it inherits the parent's signed list,
1686
  // which encrypts to the same recipients. UsersDialog (Init) is the place to
1687
  // give it a distinct, signed list.
1688
  const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
×
1689
  if (s.addGPGId && !signingConfigured) {
×
1690
    // Seed the new folder's .gpg-id from the recipients that are currently
1691
    // in effect for its parent. The previous implementation walked
1692
    // listKeys("", true) and wrote every key whose `enabled` flag was set,
1693
    // but that flag is only toggled inside UsersDialog, so the loop wrote
1694
    // nothing and left a zero-byte .gpg-id that shadowed the parent's
1695
    // recipients (see #1682).
1696
    if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
×
1697
      QMessageBox::warning(
×
1698
          this, tr("Error"),
×
1699
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1700
      return;
×
1701
    }
1702
  }
1703
}
×
1704

1705
/**
1706
 * @brief MainWindow::renameFolder rename an existing folder
1707
 */
1708
void MainWindow::renameFolder() {
×
1709
  bool ok;
1710
  QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
×
1711
  QString srcDirName = QDir(srcDir).dirName();
×
1712
  QString newName =
NEW
1713
      QInputDialog::getText(this, tr("Rename folder"), tr("Rename folder to: "),
×
1714
                            QLineEdit::Normal, srcDirName, &ok);
×
1715
  if (!ok || newName.isEmpty()) {
×
1716
    return;
1717
  }
1718
  QString destDir = srcDir;
1719
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1720
  if (!confirmPathInStore(destDir)) {
×
1721
    return;
1722
  }
1723
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1724
}
1725

1726
/**
1727
 * @brief MainWindow::editPassword read password and open edit window via
1728
 * MainWindow::onEdit()
1729
 */
1730
void MainWindow::editPassword(const QString &file) {
1 ✔
1731
  if (!file.isEmpty()) {
1 ✔
1732
    const AppSettings s = QtPassSettings::load();
1 ✔
1733
    if (s.useGit && s.autoPull) {
1 ✔
1734
      onUpdate(true);
×
1735
    }
1736
    setPassword(file, false);
1 ✔
1737
  }
1 ✔
1738
}
1 ✔
1739

1740
/**
1741
 * @brief MainWindow::renamePassword rename an existing password
1742
 */
1743
void MainWindow::renamePassword() {
×
1744
  bool ok;
1745
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1746
  QString filePath = QFileInfo(file).path();
×
1747
  QString fileName = QFileInfo(file).fileName();
×
1748
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1749
    fileName.chop(4);
×
1750
  }
1751

1752
  QString newName =
NEW
1753
      QInputDialog::getText(this, tr("Rename file"), tr("Rename file to: "),
×
1754
                            QLineEdit::Normal, fileName, &ok);
×
1755
  if (!ok || newName.isEmpty()) {
×
1756
    return;
1757
  }
1758
  QString newFile = QDir(filePath).filePath(newName);
×
1759
  if (!confirmPathInStore(newFile)) {
×
1760
    return;
1761
  }
1762
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1763
}
1764

1765
/**
1766
 * @brief Copies the password of the selected file from the tree view to the
1767
 * clipboard.
1768
 * @example
1769
 * MainWindow::copyPasswordFromTreeview();
1770
 *
1771
 * @return void - This function does not return a value.
1772
 */
1773
void MainWindow::copyPasswordFromTreeview() {
4 ✔
1774
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
4 ✔
1775

1776
  if (fileOrFolder.isFile()) {
4 ✔
1777
    QString file = getFile(ui->treeView->currentIndex(), true);
4 ✔
1778
    // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
1779
    // an earlier request is simply not this one's.
1780
    m_copyRequestFile = file;
4 ✔
1781
    // This Show repaints the panel as well; see onOtp().
1782
    m_shownFile = file;
4 ✔
1783
    QtPassSettings::getPass()->Show(file);
8 ✔
1784
  }
1785
}
4 ✔
1786

1787
void MainWindow::passwordFromFileToClipboard(const QString &text,
9 ✔
1788
                                             const QString &file) {
1789
  if (m_copyRequestFile.isEmpty() || file != m_copyRequestFile) {
9 ✔
1790
    return;
7 ✔
1791
  }
1792
  m_copyRequestFile.clear();
3 ✔
1793
  const QStringList tokens = text.split('\n');
3 ✔
1794
  if (tokens.isEmpty()) {
3 ✔
1795
    return;
1796
  }
1797
  // An entry created by `pass otp insert` has the otpauth:// URI as its first
1798
  // line. That URI carries the shared TOTP secret (the seed, not a code), so
1799
  // copying it to the clipboard would leak 2FA material. Skip it, matching the
1800
  // display path (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1801
  if (FileContent::isOtpUriValue(tokens[0])) {
3 ✔
1802
    flashText(tr("This entry holds an OTP secret, not a password"), true);
1 ✔
1803
    return;
1 ✔
1804
  }
1805
  m_qtPass->clipboard().copyText(tokens[0]);
2 ✔
1806
}
1807

1808
/**
1809
 * @brief Displays message in status bar
1810
 *
1811
 * @param msg     text to be displayed
1812
 * @param timeout time for which msg shall be visible
1813
 */
1814
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
13 ✔
1815
  ui->statusBar->showMessage(msg, timeout);
13 ✔
1816
}
13 ✔
1817

1818
/**
1819
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1820
 * @param dir Directory path to re-encrypt
1821
 */
1822
void MainWindow::reencryptPath(const QString &dir) {
×
1823
  QDir checkDir(dir);
×
1824
  if (!checkDir.exists()) {
×
1825
    QMessageBox::critical(this, tr("Error"),
×
1826
                          tr("Directory does not exist: %1").arg(dir));
×
1827
    return;
×
1828
  }
1829

1830
  int ret = QMessageBox::question(
×
1831
      this, tr("Re-encrypt passwords"),
×
1832
      tr("Re-encrypt all passwords in %1?\n\n"
×
1833
         "This will re-encrypt ALL password files in this folder "
1834
         "using the current recipients defined in .gpg-id.\n\n"
1835
         "This may rewrite many files and cannot be undone easily.\n\n"
1836
         "Continue?")
1837
          .arg(QDir(dir).dirName()),
×
1838
      QMessageBox::Yes | QMessageBox::No);
1839

1840
  if (ret != QMessageBox::Yes)
×
1841
    return;
1842

1843
  // Disable preemptively. ImitatePass::reencryptPath emits
1844
  // startReencryptPath asynchronously and the slot would re-run this,
1845
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1846
  startReencryptPath();
×
1847

1848
  QtPassSettings::getImitatePass()->reencryptPath(
×
1849
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1850
}
×
1851

1852
/**
1853
 * @brief MainWindow::startReencryptPath disable ui elements and treeview and
1854
 * show the progress dialog. Idempotent: MainWindow::reencryptPath calls it
1855
 * before ImitatePass emits startReencryptPath.
1856
 */
1857
void MainWindow::startReencryptPath() {
3 ✔
1858
  m_reencryptRunning = true;
3 ✔
1859
  setUiElementsEnabled(false);
3 ✔
1860
  // The worker always ends with endReencryptPath(), and Cancel is available
1861
  // throughout, so the generic watchdog is not needed for this operation.
1862
  m_uiWatchdog.stop();
3 ✔
1863
  ui->treeView->setDisabled(true);
3 ✔
1864
  if (m_reencryptProgress)
3 ✔
1865
    return;
1866
  auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
2 ✔
1867
                                       tr("Cancel"), 0, 0, this);
4 ✔
1868
  progress->setWindowTitle(tr("Re-encrypt passwords"));
2 ✔
1869
  progress->setWindowModality(Qt::WindowModal);
2 ✔
1870
  progress->setAutoClose(false);
2 ✔
1871
  progress->setAutoReset(false);
2 ✔
1872
  progress->setMinimumDuration(0);
2 ✔
1873
  connect(progress, &QProgressDialog::canceled, this, [this]() {
2 ✔
1874
    QtPassSettings::getImitatePass()->cancelReencryptPath();
×
1875
    showStatusMessage(tr("Cancelling re-encryption"), 5000);
×
1876
  });
×
1877
  m_reencryptProgress = progress;
1878
  progress->show();
2 ✔
1879
}
1880

1881
/**
1882
 * @brief MainWindow::reencryptProgress show how many files were checked
1883
 * @param current files checked so far
1884
 * @param total files found under the folder
1885
 */
1886
void MainWindow::reencryptProgress(int current, int total) {
4 ✔
1887
  if (!m_reencryptProgress)
4 ✔
1888
    return;
1889
  m_reencryptProgress->setMaximum(total);
3 ✔
1890
  m_reencryptProgress->setLabelText(
3 ✔
1891
      tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
6 ✔
1892
  // On a modal QProgressDialog setValue() calls processEvents(), which can
1893
  // deliver the queued completion: endReencryptPath() then hides the dialog
1894
  // and resets m_reencryptProgress under our feet. Keep it last and touch
1895
  // nothing afterwards.
1896
  m_reencryptProgress->setValue(current);
3 ✔
1897
}
1898

1899
/**
1900
 * @brief MainWindow::endReencryptPath re-enable ui elements
1901
 */
1902
void MainWindow::endReencryptPath() {
2 ✔
1903
  m_reencryptRunning = false;
2 ✔
1904
  if (m_reencryptProgress) {
2 ✔
1905
    m_reencryptProgress->hide();
2 ✔
1906
    m_reencryptProgress->deleteLater();
2 ✔
1907
    m_reencryptProgress = nullptr;
1908
  }
1909
  setUiElementsEnabled(true);
2 ✔
1910
}
2 ✔
1911

1912
/**
1913
 * @brief MainWindow::exportPublicKey export the configured signing key in
1914
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1915
 *
1916
 * Falls back to a help dialog when no signing key is configured or gpg is
1917
 * unavailable, so the user still gets actionable guidance.
1918
 */
1919
void MainWindow::exportPublicKey() {
×
1920
  const AppSettings s = QtPassSettings::load();
×
1921
  const QString identity = s.passSigningKey;
1922
  if (identity.isEmpty()) {
×
1923
    QMessageBox::information(
×
NEW
1924
        this, tr("Export public key"),
×
NEW
1925
        tr("<h3>Export your public key</h3>"
×
1926
           "<p>No signing key is configured. Set one in QtPass Settings "
1927
           "&gt; GPG keys, or run this in a terminal:</p>"
1928
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1929
           "&gt;</pre>"
1930
           "<p>Then send the file to your teammates.</p>"));
1931
    return;
×
1932
  }
1933
  QString gpgExe = s.gpgExecutable;
1934
  if (gpgExe.isEmpty()) {
×
1935
    gpgExe = QStringLiteral("gpg");
×
1936
  }
1937
  QStringList args = {"--armor", "--export"};
×
1938
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1939
  QString stdOut;
×
1940
  QString stdErr;
×
1941
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
×
1942
  if (exitCode != 0 || stdOut.isEmpty()) {
×
NEW
1943
    QMessageBox::warning(this, tr("Export public key"),
×
1944
                         tr("Could not export public key for %1.\n\n%2")
×
1945
                             .arg(identity, stdErr.isEmpty()
×
1946
                                                ? tr("No output from gpg.")
×
1947
                                                : stdErr));
1948
    return;
1949
  }
1950
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1951
  dialog.exec();
×
1952
}
×
1953

1954
/**
1955
 * @brief MainWindow::addRecipient open the recipient management dialog for
1956
 *        the supplied directory.
1957
 * @param dir Folder whose .gpg-id should be edited.
1958
 *
1959
 * Delegates to UsersDialog so users can tick/untick keys from their
1960
 * keyring as recipients of the folder; importing a foreign key into the
1961
 * keyring still has to happen via gpg (or QtPass settings) first.
1962
 */
1963
void MainWindow::addRecipient(const QString &dir) {
×
1964
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1965
  d.exec();
×
1966
}
×
1967

1968
/**
1969
 * @brief MainWindow::showShareHelp show help about GPG sharing
1970
 */
1971
void MainWindow::showShareHelp() {
×
1972
  QMessageBox::information(
×
NEW
1973
      this, tr("Sharing passwords with GPG"),
×
NEW
1974
      tr("<h3>Sharing passwords with GPG</h3>"
×
1975
         "<p>To share passwords with other users:</p>"
1976
         "<ol>"
1977
         "<li><b>Export your public key</b> and send it to teammates</li>"
1978
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1979
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1980
         "them</li>"
1981
         "</ol>"
1982
         "<p>Only people who have a matching secret key can decrypt the "
1983
         "passwords.</p>"
1984
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1985
         "<p>See the FAQ for more details.</p>"));
1986
}
×
1987

1988
void MainWindow::updateGitButtonVisibility(bool uiEnabled) {
95 ✔
1989
  const AppSettings s = QtPassSettings::load();
95 ✔
1990
  // Without Git there is nothing to push or pull, ever: the buttons go, not
1991
  // grey out. While an operation runs they stay and are disabled.
1992
  const bool usable =
1993
      s.useGit && !(s.gitExecutable.isEmpty() && s.passExecutable.isEmpty());
95 ✔
1994
  ui->actionPush->setVisible(usable);
95 ✔
1995
  ui->actionUpdate->setVisible(usable);
95 ✔
1996
  // The separator in front of them would otherwise sit next to the next one.
1997
  const QList<QAction *> actions = ui->toolBar->actions();
95 ✔
1998
  const qsizetype at = actions.indexOf(ui->actionPush);
95 ✔
1999
  if (at > 0 && actions.at(at - 1)->isSeparator()) {
95 ✔
2000
    actions.at(at - 1)->setVisible(usable);
95 ✔
2001
  }
2002
  enableGitButtons(usable && uiEnabled);
95 ✔
2003
}
95 ✔
2004

2005
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
95 ✔
2006
  // No platform gating any more: codes are generated in-process, so this works
2007
  // on Windows and macOS and with either backend. It used to be hidden there
2008
  // because the pass-otp extension is Unix-only.
2009
  //
2010
  // Kept visible but disabled when OTP is off, as it was before: hiding it
2011
  // removed the only discoverable trace of the feature, and since an OTP field
2012
  // is suppressed from the panel unconditionally (its value is a secret), a
2013
  // user with the setting off would see nothing at all where their OTP data
2014
  // used to be.
2015
  //
2016
  // Enablement must honour uiEnabled: this is called from
2017
  // setUiElementsEnabled(), and ignoring the argument re-enabled the action
2018
  // while a decrypt was in flight, letting the user stack Show calls.
2019
  ui->actionOtp->setVisible(true);
95 ✔
2020
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
95 ✔
2021
}
95 ✔
2022

2023
void MainWindow::updateGrepButtonVisibility() {
44 ✔
2024
  const bool enabled = QtPassSettings::isUseGrepSearch();
44 ✔
2025
  ui->grepButton->setVisible(enabled);
44 ✔
2026
  ui->grepCaseButton->setVisible(enabled);
44 ✔
2027
  if (!enabled && m_grep.inGrepMode()) {
44 ✔
2028
    ui->grepButton->setChecked(false);
×
2029
  }
2030
}
44 ✔
2031

2032
void MainWindow::enableGitButtons(const bool &state) {
95 ✔
2033
  ui->actionPush->setEnabled(state);
95 ✔
2034
  ui->actionUpdate->setEnabled(state);
95 ✔
2035
}
95 ✔
2036

2037
/**
2038
 * @brief MainWindow::critical critical message popup wrapper.
2039
 * @param title
2040
 * @param msg
2041
 */
2042
void MainWindow::critical(const QString &title, const QString &msg) {
×
2043
  QMessageBox::critical(this, title, msg);
×
2044
}
×
2045

2046
/**
2047
 * @brief Route a process's output to the console, unless the panel is
2048
 *        switched off in the settings.
2049
 * @param output The stdout/stderr text from the process.
2050
 * @param isError true if this is error output (stderr).
2051
 * @param pid The process that ran, for the line prefix.
2052
 */
2053
void MainWindow::onProcessOutput(const QString &output, bool isError,
2 ✔
2054
                                 Enums::PROCESS pid) {
2055
  if (!QtPassSettings::isShowProcessOutput()) {
2 ✔
2056
    return;
2057
  }
2058
  m_processOutput->append(output, isError,
1 ✔
2059
                          ProcessOutputPanel::processName(pid));
2 ✔
2060
}
2061

2062
/**
2063
 * @brief Show or hide the console per the showProcessOutput setting.
2064
 */
2065
void MainWindow::updateProcessOutputVisibility() {
1 ✔
2066
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
1 ✔
2067
}
1 ✔
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