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

IJHack / QtPass / 35985956261

24 Sep 2026 10:13AM UTC coverage: 92.908%. First build
35985956261

Pull #1917

github

web-flow
Merge 22b2f704c into 78e24880c
Pull Request #1917: MainWindow: the delete question and the Share menu are their own functions

67 of 68 new or added lines in 1 file covered. (98.53%)

8803 of 9475 relevant lines covered (92.91%)

149.52 hits per line

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

98.58
/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 <QDockWidget>
35
#include <QFileInfo>
36
#include <QHBoxLayout>
37
#include <QInputDialog>
38
#include <QLabel>
39
#include <QLineEdit>
40
#include <QMenu>
41
#include <QMenuBar>
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 <array>
53
#include <utility>
54

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

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

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

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

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

124
  updateProfileBox();
108 ✔
125

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

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

143
  searchTimer.setInterval(350);
108 ✔
144
  searchTimer.setSingleShot(true);
108 ✔
145

146
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
108 ✔
147

148
  // Install the search-box key filter once, not on every setUiElementsEnabled
149
  // call.
150
  ui->lineEdit->installEventFilter(this);
108 ✔
151
  // Both live in Breeze's "tools area" and get its header palette; see the
152
  // PaletteChange branch in eventFilter().
153
  ui->toolBar->installEventFilter(this);
108 ✔
154
  menuBar()->installEventFilter(this);
108 ✔
155

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

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

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

199
  ui->lineEdit->setClearButtonEnabled(true);
108 ✔
200
  updateGrepButtonVisibility();
108 ✔
201

202
  setUiElementsEnabled(true);
108 ✔
203

204
  ui->lineEdit->setText(searchText);
108 ✔
205

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

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

233
MainWindow::~MainWindow() {
212 ✔
234
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
235
  // window still knows its last geometry here.
236
  saveWindowState();
108 ✔
237
  delete m_qtPass;
108 ✔
238
}
320 ✔
239

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

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

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

310
/**
311
 * @brief MainWindow::event notes an application palette change: from then
312
 * on a bar palette the style stamps may be the previous theme's.
313
 * @param event
314
 * @return whether the event was consumed
315
 */
316
auto MainWindow::event(QEvent *event) -> bool {
4,580 ✔
317
  if (event->type() == QEvent::ApplicationPaletteChange) {
4,580 ✔
318
    m_themeSwitched = true;
7 ✔
319
  }
320
  return QMainWindow::event(event);
4,580 ✔
321
}
322

323
/**
324
 * @brief MainWindow::changeEvent sets focus to the search box on activation
325
 * and re-derives palette-dependent styling on a theme change.
326
 * @param event
327
 */
328
void MainWindow::changeEvent(QEvent *event) {
144 ✔
329
  QWidget::changeEvent(event);
144 ✔
330
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
144 ✔
331
    // Desktop switched light/dark (e.g. KDE day/night). Top-level widgets
332
    // receive PaletteChange for that (ApplicationPaletteChange goes to the
333
    // QApplication object). Styling that bakes palette colours into
334
    // stylesheets must be re-derived by hand.
335
    m_displayPanel->refreshPalette();
7 ✔
336
  }
337
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
144 ✔
338
      isVisible()) {
339
    // Defer one event-loop tick so the synchronous activation dispatch
340
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
341
    // unwinds before we touch widget state — calling `focusInput()`
342
    // inline from this stack has segfaulted in past iterations because
343
    // mid-rebuild ui state isn't fully wired up yet.
344
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
21 ✔
345
                              Qt::QueuedConnection);
346
  }
347
}
144 ✔
348

349
/**
350
 * @brief First-show hook: run the initial focusInput() pulse once the
351
 *        window is actually mapped. The widget's internal data is fully
352
 *        initialised by this point, so QLineEdit::selectAll() is safe.
353
 * @param event Show event passed to the base class.
354
 */
355
void MainWindow::showEvent(QShowEvent *event) {
23 ✔
356
  QMainWindow::showEvent(event);
23 ✔
357
  if (m_firstShowCompleted) {
23 ✔
358
    return;
359
  }
360
  // Queue the focus pulse for the next event-loop tick so the platform
361
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
362
  // from the config wizard path) settle before we look up the line
363
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
364
  // *after* it actually focuses, so a transient failed lookup just
365
  // re-queues on the next show rather than silently dropping.
366
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
21 ✔
367
                            Qt::QueuedConnection);
368
}
369

370
/**
371
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
372
 */
373
void MainWindow::initToolBarButtons() {
108 ✔
374
  connect(ui->actionAddPassword, &QAction::triggered, this,
108 ✔
375
          &MainWindow::addPassword);
108 ✔
376
  connect(ui->actionAddFolder, &QAction::triggered, this,
108 ✔
377
          &MainWindow::addFolder);
108 ✔
378
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
108 ✔
379
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
108 ✔
380
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
108 ✔
381
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
108 ✔
382
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
108 ✔
383
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
108 ✔
384
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
108 ✔
385

386
  // Menu-only actions. Quit ends the application, as Ctrl+Q/Cmd+Q does in
387
  // KeePassXC, Telegram and per the KDE, GNOME and macOS guidelines, tray
388
  // icon or not (#1788). Close window is what the old bare Ctrl+Q shortcut
389
  // did: close(), which closeEvent() turns into a hide when "hide on close"
390
  // is set. The X button and Alt+F4 are close() as well.
391
  connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
108 ✔
392
  connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
108 ✔
393
#ifdef Q_OS_MACOS
394
  // The menu bar is the system's up there; nothing to hide.
395
  ui->actionShowMenuBar->setVisible(false);
396
#else
397
  // The action lives in the menu it hides, so it has to be on the window as
398
  // well for Ctrl+M to keep working while the bar is gone.
399
  addAction(ui->actionShowMenuBar);
108 ✔
400
  ui->actionShowMenuBar->setChecked(QtPassSettings::load().showMenuBar);
108 ✔
401
  menuBar()->setVisible(ui->actionShowMenuBar->isChecked());
108 ✔
402
  connect(ui->actionShowMenuBar, &QAction::toggled, this, [this](bool show) {
108 ✔
403
    menuBar()->setVisible(show);
4 ✔
404
    AppSettings s = QtPassSettings::load();
4 ✔
405
    s.showMenuBar = show;
4 ✔
406
    QtPassSettings::save(s);
4 ✔
407
  });
4 ✔
408
#endif
409
  connect(ui->actionFaq, &QAction::triggered, this, [] {
109 ✔
410
    QDesktopServices::openUrl(QUrl(QStringLiteral("https://qtpass.org/faq")));
2 ✔
411
  });
1 ✔
412
  connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
108 ✔
413
  connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
108 ✔
414

415
  ui->actionAddPassword->setIcon(
108 ✔
416
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
432 ✔
417
  ui->actionAddFolder->setIcon(
108 ✔
418
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
432 ✔
419
  ui->actionEdit->setIcon(QIcon::fromTheme(
108 ✔
420
      "document-properties", QIcon(":/icons/document-properties.svg")));
324 ✔
421
  ui->actionDelete->setIcon(
108 ✔
422
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
432 ✔
423
  ui->actionPush->setIcon(
108 ✔
424
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
432 ✔
425
  ui->actionUpdate->setIcon(
108 ✔
426
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
432 ✔
427
  ui->actionUsers->setIcon(QIcon::fromTheme(
108 ✔
428
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
324 ✔
429
  ui->actionConfig->setIcon(QIcon::fromTheme(
108 ✔
430
      "applications-system", QIcon(":/icons/applications-system.svg")));
216 ✔
431
}
108 ✔
432

433
/**
434
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
435
 */
436
void MainWindow::initStatusBar() {
108 ✔
437
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
216 ✔
438

439
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
216 ✔
440
                     .scaledToHeight(statusBar()->height());
108 ✔
441
  auto *logoApp = new QLabel(statusBar());
108 ✔
442
  logoApp->setPixmap(logo);
108 ✔
443
  statusBar()->addPermanentWidget(logoApp);
108 ✔
444
}
108 ✔
445

446
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1 ✔
447
  return ui->treeView->currentIndex();
1 ✔
448
}
449

450
/**
451
 * @brief Displays the given text in the main window text browser, optionally
452
 * marking it as an error and/or rendering it as HTML.
453
 * @example
454
 * MainWindow window;
455
 * window.flashText("Operation completed.", false, false);
456
 *
457
 * @param const QString &text - The text content to display.
458
 * @param const bool isError - If true, sets the text color to red before
459
 * displaying the text; otherwise any earlier error colour is cleared.
460
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
461
 * the existing HTML content.
462
 * @return void - No return value.
463
 */
464
void MainWindow::flashText(const QString &text, const bool isError,
47 ✔
465
                           const bool isHtml) {
466
  if (isError) {
47 ✔
467
    ui->textBrowser->setTextColor(Qt::red);
7 ✔
468
  } else {
469
    // setTextColor() merges the red foreground into the browser's current
470
    // char format, and setText()/setPlainText() re-applies that format to the
471
    // whole new document. Without clearing it, a plain-text non-error message
472
    // shown after an error would still be red. Remove the property rather
473
    // than pinning a palette colour: an explicit foreground would stop the
474
    // text from following runtime light/dark palette switches (#946).
475
    QTextCharFormat format = ui->textBrowser->currentCharFormat();
40 ✔
476
    format.clearForeground();
477
    ui->textBrowser->setCurrentCharFormat(format);
40 ✔
478
  }
479

480
  if (isHtml) {
47 ✔
481
    QString _text = text;
482
    if (!ui->textBrowser->toPlainText().isEmpty()) {
76 ✔
483
      _text = ui->textBrowser->toHtml() + _text;
28 ✔
484
    }
485
    ui->textBrowser->setHtml(_text);
38 ✔
486
  } else {
487
    ui->textBrowser->setText(text);
9 ✔
488
  }
489
}
47 ✔
490

491
/**
492
 * @brief MainWindow::config pops up the configuration screen and handles all
493
 * inter-window communication
494
 */
495
void MainWindow::applyTextBrowserSettings() {
2 ✔
496
  const AppSettings s = QtPassSettings::load();
2 ✔
497
  if (s.useMonospace) {
2 ✔
498
    QFont monospace("Monospace");
1 ✔
499
    monospace.setStyleHint(QFont::Monospace);
1 ✔
500
    ui->textBrowser->setFont(monospace);
1 ✔
501
  } else {
1 ✔
502
    ui->textBrowser->setFont(QFont());
1 ✔
503
  }
504

505
  if (s.noLineWrapping) {
2 ✔
506
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
1 ✔
507
  } else {
508
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
1 ✔
509
  }
510
}
2 ✔
511

512
void MainWindow::applyWindowFlagsSettings() {
119 ✔
513
  const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
119 ✔
514
  const Qt::WindowFlags flags = windowFlags();
515
  if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
119 ✔
516
    // Nothing to change. setWindowFlags() on a top-level widget rebuilds the
517
    // native window (setParent(nullptr, flags)), which used to happen on every
518
    // Settings OK because the "off" branch never compared equal — the same
519
    // teardown that left ui->lineEdit dangling (see focusInput()).
520
    return;
521
  }
522
  // Toggle just the one hint; the old "off" branch reset to Qt::Window and
523
  // discarded every other flag with it.
524
  const bool wasVisible = isVisible();
525
  setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
4 ✔
526
                           : (flags & ~Qt::WindowStaysOnTopHint));
527
  // setWindowFlags() hides a shown window; a window that was not shown yet
528
  // (first-run wizard from the constructor) is left to its normal show.
529
  if (wasVisible) {
2 ✔
530
    show();
2 ✔
531
  }
532
}
533

534
/**
535
 * @brief Opens the configuration dialog (or, before the first usable
536
 * configuration, the first-run wizard) and applies what was accepted.
537
 * @example
538
 * if (!config()) {
539
 *   // the user cancelled
540
 * }
541
 *
542
 * @return bool - True when the dialog was accepted, false when it was
543
 * cancelled. An accepted dialog can still leave the configuration invalid
544
 * (the OK button is not gated on Util::configIsValid()); the constructor
545
 * keeps asking until the configuration is usable or this returns false.
546
 */
547
auto MainWindow::config() -> bool {
4 ✔
548
  if (m_freshStart) {
4 ✔
549
    // No usable configuration yet: the wizard finds the programs, the key
550
    // and the store, and creates the store when it has to.
551
    FirstRunWizard wizard(this);
2 ✔
552
    if (wizard.exec() != QDialog::Accepted) {
2 ✔
553
      return false;
554
    }
555
  } else {
2 ✔
556
    ConfigDialog d(this);
2 ✔
557
    d.setModal(true);
2 ✔
558
    if (d.exec() != QDialog::Accepted) {
2 ✔
559
      return false;
560
    }
561
  }
2 ✔
562

563
  applyTextBrowserSettings();
2 ✔
564
  applyWindowFlagsSettings();
2 ✔
565

566
  updateProfileBox();
2 ✔
567
  const AppSettings s = QtPassSettings::load();
2 ✔
568
  m_tree->setStore(s.passStore);
2 ✔
569
  deselect();
2 ✔
570
  ui->treeView->setCurrentIndex(QModelIndex());
2 ✔
571

572
  Pass *activePass = QtPassSettings::getPass();
2 ✔
573
  activePass->updateEnv();
2 ✔
574
  m_tree->setPass(activePass);
2 ✔
575
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
2 ✔
576
  m_qtPass->clipboard().setAutoclearTimer();
2 ✔
577

578
  updateGitButtonVisibility();
2 ✔
579
  updateOtpButtonVisibility();
2 ✔
580
  updateGrepButtonVisibility();
2 ✔
581
  updateProcessOutputVisibility();
2 ✔
582
#ifndef Q_OS_MACOS
583
  // The action's toggled handler shows or hides the bar (and saves again).
584
  ui->actionShowMenuBar->setChecked(s.showMenuBar);
2 ✔
585
#endif
586
  if (s.useTrayIcon && m_tray == nullptr) {
2 ✔
587
    initTrayIcon();
1 ✔
588
  } else if (!s.useTrayIcon && m_tray != nullptr) {
1 ✔
589
    destroyTrayIcon();
×
590
  }
591

592
  // Leave the fresh-start state in place while the accepted configuration is
593
  // still unusable (for example the user declined to create the store), so
594
  // the next attempt from the constructor's loop runs the first-run wizard
595
  // again instead of showing the bare dialog. The re-prompt itself lives
596
  // there: recursing here re-ran the dialog on this stack frame and never
597
  // reported a cancel back to the caller.
598
  if (Util::configIsValid(s)) {
2 ✔
599
    m_freshStart = false;
2 ✔
600
  }
601
  return true;
602
}
2 ✔
603

604
/**
605
 * @brief MainWindow::onUpdate do a git pull
606
 */
607
void MainWindow::onUpdate(bool block) {
2 ✔
608
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
2 ✔
609
  if (block) {
2 ✔
610
    QtPassSettings::getPass()->GitPull_b();
1 ✔
611
  } else {
612
    QtPassSettings::getPass()->GitPull();
1 ✔
613
  }
614
}
2 ✔
615

616
/**
617
 * @brief MainWindow::onPush do a git push
618
 */
619
void MainWindow::onPush() {
1 ✔
620
  if (QtPassSettings::isUseGit()) {
1 ✔
621
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
1 ✔
622
    QtPassSettings::getPass()->GitPush();
1 ✔
623
  }
624
}
1 ✔
625

626
/**
627
 * @brief MainWindow::getFile get the selected file path
628
 * @param index
629
 * @param forPass returns relative path without '.gpg' extension
630
 * @return path
631
 * @return
632
 */
633
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
32 ✔
634
  return m_tree->fileFor(index, forPass);
32 ✔
635
}
636

637
/**
638
 * @brief MainWindow::on_treeView_clicked read the selected password file
639
 * @param index
640
 */
641
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
10 ✔
642
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
10 ✔
643
  QString file = getFile(index, true);
10 ✔
644
  ui->passwordName->setText(file);
10 ✔
645
  if (!file.isEmpty() && !cleared) {
10 ✔
646
    // Remember what the panel is about to show, so onOtp() can tell whether the
647
    // code it can see belongs to the entry that is currently selected.
648
    m_shownFile = file;
9 ✔
649
    QtPassSettings::getPass()->Show(file);
18 ✔
650
  } else {
651
    m_shownFile.clear();
1 ✔
652
    clearPanel(false);
1 ✔
653
    ui->actionEdit->setEnabled(false);
1 ✔
654
    ui->actionDelete->setEnabled(true);
1 ✔
655
  }
656
}
10 ✔
657

658
/**
659
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
660
 * TreeViewItem, open the edit Window
661
 * @param index
662
 */
663
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
3 ✔
664
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
3 ✔
665

666
  if (fileOrFolder.isFile()) {
3 ✔
667
    editPassword(getFile(index, true));
4 ✔
668
  }
669
}
3 ✔
670

671
/**
672
 * @brief MainWindow::deselect clear the selection, password and copy buffer
673
 */
674
void MainWindow::deselect() {
8 ✔
675
  m_shownFile.clear();
8 ✔
676
  cancelOtpRequest();
8 ✔
677
  m_qtPass->clipboard().clear();
8 ✔
678
  ui->treeView->clearSelection();
8 ✔
679
  ui->actionEdit->setEnabled(false);
8 ✔
680
  ui->actionDelete->setEnabled(false);
8 ✔
681
  ui->passwordName->setText("");
8 ✔
682
  clearPanel(false);
8 ✔
683
}
8 ✔
684

685
void MainWindow::executeWrapperStarted() {
27 ✔
686
  m_displayPanel->clear();
27 ✔
687
  ui->textBrowser->clear();
27 ✔
688
  setUiElementsEnabled(false);
27 ✔
689
  clearPanelTimer.stop();
27 ✔
690
  if (QtPassSettings::isShowProcessOutput()) {
27 ✔
691
    m_processOutput->setVisible(true);
22 ✔
692
  }
693
}
27 ✔
694

695
/**
696
 * @brief Handles displaying parsed password entry content in the main window:
697
 * the panel shows the parsed fields, the text browser what is left over.
698
 * @param p_output - The raw output text containing the password entry data.
699
 * @param file - The entry the output belongs to; anything but the entry most
700
 * recently asked for is dropped.
701
 */
702
void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
17 ✔
703
  // Only the entry we most recently asked to show may repaint the panel; a
704
  // slower decrypt of an entry the user has since left is dropped.
705
  if (!file.isEmpty() && !m_shownFile.isEmpty() && file != m_shownFile) {
17 ✔
706
    return;
1 ✔
707
  }
708
  const AppSettings s = QtPassSettings::load();
16 ✔
709
  QStringList templ =
710
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
16 ✔
711
  bool allFields = s.useTemplate && s.templateAllFields;
16 ✔
712
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
16 ✔
713
  QString output = p_output;
714
  // Display variant: empty when the password line is itself an otpauth URI, so
715
  // the shared secret is neither rendered nor copied to the clipboard.
716
  QString password = fileContent.getPasswordForDisplay();
16 ✔
717

718
  // set clipped text
719
  //
720
  // Skipped for an OTP request: the user asked for a one-time code, not the
721
  // password, and writing both in one event-loop turn can leave the Windows
722
  // clipboard empty (two OleSetClipboard calls back to back).
723
  if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
16 ✔
724
    m_qtPass->clipboard().copyIfAlways(password, p_output);
14 ✔
725
  }
726

727
  // first clear the current view:
728
  m_displayPanel->clear();
16 ✔
729

730
  // show what is needed:
731
  if (s.hideContent) {
16 ✔
732
    output = "***" + tr("Content hidden") + "***";
4 ✔
733
  } else if (!s.displayAsIs) {
14 ✔
734
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
28 ✔
735
                                  s.useOtp ? fileContent.getOtpUri()
14 ✔
736
                                           : QString());
737
    output = fileContent.getRemainingDataForDisplay();
28 ✔
738
  }
739

740
  if (s.useAutoclearPanel) {
16 ✔
741
    clearPanelTimer.start();
2 ✔
742
  }
743

744
  flashText(QtPass::formatOutput(output), false, true);
32 ✔
745
  setUiElementsEnabled(true);
16 ✔
746
}
32 ✔
747

748
/**
749
 * @brief Generates a one-time password from a decrypted entry and copies it.
750
 *
751
 * Connected to Pass::finishedShow for good; reacts only to the entry onOtp()
752
 * asked for. passShowHandler is connected first, so by the time this runs the
753
 * panel has already been repainted and the UI re-enabled.
754
 *
755
 * @param p_output - The decrypted entry content.
756
 * @return void - This function does not return a value.
757
 */
758
void MainWindow::otpFromFileToClipboard(const QString &p_output,
17 ✔
759
                                        const QString &file) {
760
  // Permanently connected: only the decrypt of the entry onOtp() asked for
761
  // is ours, everything else belongs to a tree click or a Ctrl+C.
762
  if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
17 ✔
763
    return;
14 ✔
764
  }
765
  m_otpRequestFile.clear();
5 ✔
766

767
  if (p_output.isEmpty()) {
5 ✔
768
    // Distinguish "could not read the entry" from "entry has no OTP".
769
    flashText(tr("Could not decrypt this password entry"), true);
1 ✔
770
    setUiElementsEnabled(true);
1 ✔
771
    return;
1 ✔
772
  }
773

774
  // passShowHandler is connected first, so it has already repainted the panel
775
  // for this same finishedShow. When it rendered the OTP row the current code
776
  // is derived and cached, so reuse it instead of re-loading settings and
777
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
778
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
779
  const QString shown = m_displayPanel->currentOtpCode();
4 ✔
780
  if (!shown.isEmpty()) {
4 ✔
781
    m_qtPass->clipboard().copyText(shown);
1 ✔
782
    showStatusMessage(tr("OTP code copied to clipboard"));
1 ✔
783
    setUiElementsEnabled(true);
1 ✔
784
    return;
785
  }
786

787
  const AppSettings s = QtPassSettings::load();
3 ✔
788
  // Parse with the same template settings passShowHandler uses, so an OTP
789
  // field is recognised identically in both paths.
790
  const QStringList templ =
791
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
3 ✔
792
  const bool allFields = s.useTemplate && s.templateAllFields;
3 ✔
793
  const FileContent fileContent =
794
      FileContent::parse(p_output, templ, allFields);
3 ✔
795

796
  const std::optional<Totp::Settings> settings =
797
      Totp::parse(fileContent.getOtpUri());
6 ✔
798
  if (settings.has_value()) {
3 ✔
799
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
4 ✔
800
    showStatusMessage(tr("OTP code copied to clipboard"));
4 ✔
801
  } else {
802
    flashText(tr("No OTP code found in this password entry"), true);
2 ✔
803
  }
804
  setUiElementsEnabled(true);
3 ✔
805
}
6 ✔
806

807
/**
808
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
809
 */
810
void MainWindow::clearPanel(bool notify) {
29 ✔
811
  m_displayPanel->clear();
29 ✔
812
  const bool grepWasVisible = ui->grepResultsList->isVisible();
29 ✔
813
  ui->grepResultsList->clear();
29 ✔
814
  if (grepWasVisible) {
29 ✔
815
    ui->grepResultsList->setVisible(false);
2 ✔
816
    ui->treeView->setVisible(true);
2 ✔
817
    if (m_grep.inGrepMode()) {
2 ✔
818
      m_grep.clearGrepMode();
819
      ui->grepButton->blockSignals(true);
2 ✔
820
      ui->grepButton->setChecked(false);
2 ✔
821
      ui->grepButton->blockSignals(false);
2 ✔
822
      ui->lineEdit->blockSignals(true);
2 ✔
823
      ui->lineEdit->clear();
2 ✔
824
      ui->lineEdit->blockSignals(false);
2 ✔
825
      ui->lineEdit->setPlaceholderText(tr("Search password"));
4 ✔
826
    }
827
  }
828
  if (notify) {
29 ✔
829
    QString output = "***" + tr("Password and content hidden") + "***";
4 ✔
830
    ui->textBrowser->setHtml(output);
2 ✔
831
  } else {
832
    ui->textBrowser->setHtml("");
54 ✔
833
  }
834
}
29 ✔
835

836
/**
837
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
838
 * elements
839
 * @param state
840
 */
841
void MainWindow::setUiElementsEnabled(bool state) {
212 ✔
842
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
843
  // the user's escape hatch and endReencryptPath() releases the interface, so
844
  // neither an unrelated completion nor the watchdog may re-enable it early.
845
  if (state && m_reencryptRunning) {
212 ✔
846
    m_uiWatchdog.stop();
1 ✔
847
    return;
1 ✔
848
  }
849
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
850
  if (state) {
851
    m_uiWatchdog.stop();
165 ✔
852
  } else {
853
    m_uiWatchdog.start();
46 ✔
854
  }
855
  ui->treeView->setEnabled(state);
211 ✔
856
  ui->lineEdit->setEnabled(state);
211 ✔
857
  ui->actionAddPassword->setEnabled(state);
211 ✔
858
  ui->actionAddFolder->setEnabled(state);
211 ✔
859
  ui->actionUsers->setEnabled(state);
211 ✔
860
  ui->actionConfig->setEnabled(state);
211 ✔
861
  const bool uiEnabled = state;
862
  // is a file selected?
863
  state &= ui->treeView->currentIndex().isValid();
422 ✔
864
  ui->actionDelete->setEnabled(state);
211 ✔
865
  ui->actionEdit->setEnabled(state);
211 ✔
866
  updateGitButtonVisibility(uiEnabled);
211 ✔
867
  // `state` is now "UI enabled AND a file is selected", which is exactly when
868
  // generating an OTP makes sense.
869
  updateOtpButtonVisibility(state);
211 ✔
870
}
871

872
/**
873
 * @brief Restores the main window geometry, state, position, size, and
874
 * tray/icon settings from saved application settings.
875
 * @example
876
 * MainWindow window;
877
 * window.restoreWindow();
878
 *
879
 * @return void - This function does not return a value.
880
 */
881
void MainWindow::restoreWindow() {
117 ✔
882
  // saveGeometry() covers position, size, maximized state and screen; the
883
  // separate pos/size/maximized keys that used to be applied on top of it
884
  // are what discarded the restored position, and the unconditional
885
  // re-centre in main() then discarded the rest. Centre only when nothing
886
  // was saved.
887
  const QByteArray geometry = QtPassSettings::getGeometry();
234 ✔
888
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
117 ✔
889
    WindowStateStore::centreOnCursorScreen(*this);
4 ✔
890
  }
891
  restoreState(QtPassSettings::getSavestate(saveState()));
117 ✔
892
  const AppSettings s = QtPassSettings::load();
117 ✔
893

894
  applyWindowFlagsSettings();
117 ✔
895

896
  if (s.useTrayIcon && m_tray == nullptr) {
117 ✔
897
    initTrayIcon();
1 ✔
898
    if (s.startMinimized) {
1 ✔
899
      // since we are still in constructor, can't directly hide
900
      QTimer::singleShot(10, this, &MainWindow::hide);
2 ✔
901
    }
902
  } else if (!s.useTrayIcon && m_tray != nullptr) {
116 ✔
903
    destroyTrayIcon();
×
904
  }
905
}
234 ✔
906

907
/**
908
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
909
 */
910
void MainWindow::onConfig() { config(); }
1 ✔
911

912
/**
913
 * @brief MainWindow::showAbout version, link and licence in a standard box.
914
 */
915
void MainWindow::showAbout() {
1 ✔
916
  QMessageBox::about(
1 ✔
917
      this, tr("About QtPass"),
1 ✔
918
      tr("<h3>QtPass %1</h3>"
1 ✔
919
         "<p>A multi-platform GUI for <a "
920
         "href=\"https://www.passwordstore.org/\">"
921
         "pass</a>, the standard Unix password manager.</p>"
922
         "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
923
         "<a href=\"https://github.com/IJHack/QtPass\">Source and "
924
         "issues</a></p>"
925
         "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
926
         "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
927
         "version 3</a> or later.</p>")
928
          .arg(QStringLiteral(VERSION),
2 ✔
929
               QString::number(QDate::currentDate().year())));
2 ✔
930
}
1 ✔
931

932
/**
933
 * @brief Executes when the string in the search box changes, collapses the
934
 * TreeView
935
 * @param arg1
936
 */
937
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
23 ✔
938
  if (m_grep.inGrepMode())
23 ✔
939
    return;
940
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
36 ✔
941
  ui->treeView->expandAll();
18 ✔
942
  clearPanel(false);
18 ✔
943
  ui->passwordName->setText("");
36 ✔
944
  ui->actionEdit->setEnabled(false);
18 ✔
945
  ui->actionDelete->setEnabled(false);
18 ✔
946
  searchTimer.start();
18 ✔
947
}
948

949
/**
950
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
951
 * time from two keypresses is elapsed
952
 */
953
void MainWindow::onTimeoutSearch() {
17 ✔
954
  QString query = ui->lineEdit->text();
17 ✔
955

956
  if (query.isEmpty()) {
17 ✔
957
    ui->treeView->collapseAll();
1 ✔
958
    deselect();
1 ✔
959
  }
960

961
  // The words are matched literally, in order, with anything in between:
962
  // "work vpn" finds work/acme/vpn. Regular expressions belong to the
963
  // content search; here a typed "[" or "(" must not switch filtering off.
964
  QStringList words;
17 ✔
965
  for (const QString &word :
966
       query.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
51 ✔
967
    words << QRegularExpression::escape(word);
17 ✔
968
  }
969
  QRegularExpression regExp(words.join(QStringLiteral(".*")),
34 ✔
970
                            QRegularExpression::CaseInsensitiveOption);
17 ✔
971
  m_tree->setFilter(regExp);
17 ✔
972

973
  if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
17 ✔
974
    selectFirstFile();
14 ✔
975
  } else {
976
    ui->actionEdit->setEnabled(false);
3 ✔
977
    ui->actionDelete->setEnabled(false);
3 ✔
978
  }
979
}
34 ✔
980

981
/**
982
 * @brief MainWindow::on_lineEdit_returnPressed get searching
983
 *
984
 * Select the first possible file in the tree
985
 */
986
void MainWindow::on_lineEdit_returnPressed() {
6 ✔
987
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
6 ✔
988
                    << m_tree->proxy().rowCount();
×
989

990
  if (m_grep.inGrepMode()) {
6 ✔
991
    const QString query = ui->lineEdit->text();
4 ✔
992
    if (!query.isEmpty()) {
4 ✔
993
      ui->grepResultsList->clear();
3 ✔
994
      ui->statusBar->showMessage(tr("Searching…"));
6 ✔
995
      if (m_grep.beginSearch()) {
996
        QApplication::setOverrideCursor(Qt::WaitCursor);
3 ✔
997
      }
998
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
6 ✔
999
    } else {
1000
      if (m_grep.cancelSearch()) {
1001
        QApplication::restoreOverrideCursor();
1 ✔
1002
      }
1003
      ui->grepResultsList->clear();
1 ✔
1004
      ui->grepResultsList->setVisible(false);
1 ✔
1005
      ui->treeView->setVisible(true);
1 ✔
1006
    }
1007
    return;
1008
  }
1009

1010
  if (m_tree->proxy().rowCount() > 0) {
2 ✔
1011
    selectFirstFile();
2 ✔
1012
    on_treeView_clicked(ui->treeView->currentIndex());
2 ✔
1013
  }
1014
}
1015

1016
/**
1017
 * @brief Toggle grep (content search) mode.
1018
 */
1019
void MainWindow::on_grepButton_toggled(bool checked) {
10 ✔
1020
  const AppSettings s = QtPassSettings::load();
10 ✔
1021
  if (checked) {
10 ✔
1022
    m_grep.enterGrepMode();
1023
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
12 ✔
1024
    // The regex dialect depends on the backend (see Pass::Grep): the pass
1025
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
1026
    ui->lineEdit->setToolTip(
6 ✔
1027
        s.usePass
6 ✔
1028
            ? tr("Content search uses POSIX basic regular expressions "
12 ✔
1029
                 "(pass grep).")
1030
            : tr("Content search uses Perl-compatible regular expressions "
1031
                 "(PCRE)."));
1032
    ui->lineEdit->clear();
6 ✔
1033
    searchTimer.stop();
6 ✔
1034
    m_tree->setFilter(QRegularExpression());
6 ✔
1035
    ui->grepResultsList->setVisible(false);
6 ✔
1036
    // Keep treeView visible until results arrive
1037
  } else {
1038
    if (m_grep.leaveGrepMode()) {
1039
      QApplication::restoreOverrideCursor();
1 ✔
1040
    }
1041
    searchTimer.stop();
4 ✔
1042
    ui->lineEdit->blockSignals(true);
4 ✔
1043
    ui->lineEdit->clear();
4 ✔
1044
    ui->lineEdit->blockSignals(false);
4 ✔
1045
    ui->lineEdit->setPlaceholderText(tr("Search password"));
8 ✔
1046
    ui->lineEdit->setToolTip(QString());
8 ✔
1047
    ui->grepResultsList->clear();
4 ✔
1048
    ui->grepResultsList->setVisible(false);
4 ✔
1049
    ui->treeView->setVisible(true);
4 ✔
1050
    m_tree->setFilter(QRegularExpression());
4 ✔
1051
  }
1052
}
10 ✔
1053

1054
/**
1055
 * @brief Display grep results in grepResultsList.
1056
 */
1057
void MainWindow::onGrepFinished(
6 ✔
1058
    const QList<QPair<QString, QStringList>> &results) {
1059
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
1060
  if (outcome.restoreCursor) {
6 ✔
1061
    QApplication::restoreOverrideCursor();
1 ✔
1062
  }
1063
  // Re-enable the UI before the discard check so a cancelled search can never
1064
  // leave controls disabled.
1065
  setUiElementsEnabled(true);
6 ✔
1066
  if (outcome.discard) {
6 ✔
1067
    return;
3 ✔
1068
  }
1069
  if (!m_grep.inGrepMode())
4 ✔
1070
    return;
1071
  ui->grepResultsList->clear();
4 ✔
1072
  if (results.isEmpty()) {
4 ✔
1073
    ui->statusBar->showMessage(tr("No matches found."), 3000);
2 ✔
1074
    ui->grepResultsList->setVisible(false);
1 ✔
1075
    ui->treeView->setVisible(true);
1 ✔
1076
    return;
1 ✔
1077
  }
1078
  const AppSettings s = QtPassSettings::load();
3 ✔
1079
  const bool hideContent = s.hideContent;
3 ✔
1080
  int totalLines = 0;
1081
  for (const auto &pair : results) {
23 ✔
1082
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
20 ✔
1083
    entryItem->setText(0, pair.first);
20 ✔
1084
    entryItem->setData(0, Qt::UserRole, pair.first);
20 ✔
1085
    for (const QString &line : pair.second) {
40 ✔
1086
      auto *lineItem = new QTreeWidgetItem(entryItem);
20 ✔
1087
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
60 ✔
1088
                                       : line);
1089
      lineItem->setData(0, Qt::UserRole, pair.first);
20 ✔
1090
      ++totalLines;
20 ✔
1091
    }
1092
  }
1093
  ui->grepResultsList->expandAll();
3 ✔
1094
  ui->treeView->setVisible(false);
3 ✔
1095
  ui->grepResultsList->setVisible(true);
3 ✔
1096
  ui->statusBar->showMessage(
6 ✔
1097
      tr("Found %n match(es)", nullptr, totalLines) + " " +
6 ✔
1098
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
3 ✔
1099
      3000);
1100
  if (s.useAutoclearPanel)
3 ✔
1101
    clearPanelTimer.start();
1 ✔
1102
}
3 ✔
1103

1104
/**
1105
 * @brief Navigate to the password entry when a grep result is clicked.
1106
 */
1107
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
1 ✔
1108
                                                int /*column*/) {
1109
  const AppSettings s = QtPassSettings::load();
1 ✔
1110
  const QString entry = item->data(0, Qt::UserRole).toString();
1 ✔
1111
  if (entry.isEmpty())
1 ✔
1112
    return;
1113
  const QString fullPath =
1114
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
2 ✔
1115
  const QModelIndex proxyIndex = m_tree->indexFor(fullPath);
1 ✔
1116
  if (!proxyIndex.isValid())
1117
    return;
1118
  ui->treeView->setCurrentIndex(proxyIndex);
1 ✔
1119
  on_treeView_clicked(proxyIndex);
1 ✔
1120
  if (s.hideContent || s.useAutoclearPanel)
1 ✔
1121
    ui->grepResultsList->clear();
1 ✔
1122
  ui->grepResultsList->setVisible(false);
1 ✔
1123
  ui->treeView->setVisible(true);
1 ✔
1124
  ui->treeView->scrollTo(proxyIndex);
1 ✔
1125
  ui->treeView->setFocus();
1 ✔
1126
}
1 ✔
1127

1128
/**
1129
 * @brief MainWindow::selectFirstFile select the first possible file in the
1130
 * tree
1131
 */
1132
void MainWindow::selectFirstFile() {
16 ✔
1133
  QModelIndex index = firstFile(m_tree->rootIndex());
16 ✔
1134
  ui->treeView->setCurrentIndex(index);
16 ✔
1135
}
16 ✔
1136

1137
/**
1138
 * @brief MainWindow::firstFile return location of first possible file
1139
 * @param parentIndex
1140
 * @return QModelIndex
1141
 */
1142
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
24 ✔
1143
  StoreModel &proxy = m_tree->proxy();
24 ✔
1144
  int numRows = proxy.rowCount(parentIndex);
24 ✔
1145
  for (int row = 0; row < numRows; ++row) {
34 ✔
1146
    QModelIndex index = proxy.index(row, 0, parentIndex);
24 ✔
1147
    if (m_tree->fileInfo(index).isFile()) {
24 ✔
1148
      return index;
14 ✔
1149
    }
1150
    if (proxy.hasChildren(index)) {
15 ✔
1151
      QModelIndex childFile = firstFile(index);
8 ✔
1152
      if (childFile.isValid())
1153
        return childFile;
5 ✔
1154
    }
1155
  }
1156
  return QModelIndex();
1157
}
1158

1159
/**
1160
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1161
 * the password store and warn the user.
1162
 *
1163
 * Used before file/folder creation, move, and rename to stop user-typed
1164
 * names like "../../etc/passwd" or absolute paths from escaping the
1165
 * configured store root via the input dialogs.
1166
 *
1167
 * @param candidate Absolute candidate path to validate.
1168
 * @return true if the path is inside the password store; false otherwise (a
1169
 * warning dialog is shown in that case).
1170
 */
1171
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
8 ✔
1172
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
16 ✔
1173
    return true;
1174
  }
1175
  QMessageBox::warning(this, tr("Invalid name"),
6 ✔
1176
                       tr("That name would resolve outside the password "
3 ✔
1177
                          "store. Please choose a different name."));
1178
  return false;
3 ✔
1179
}
1180

1181
/**
1182
 * @brief MainWindow::setPassword open passworddialog
1183
 * @param file which pgp file
1184
 * @param isNew insert (not update)
1185
 */
1186
void MainWindow::setPassword(const QString &file, bool isNew) {
4 ✔
1187
  const AppSettings s = QtPassSettings::load();
4 ✔
1188
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
4 ✔
1189

1190
  if (isNew) {
4 ✔
1191
    const QString storePath = s.passStore;
1192
    QString folder = m_tree->currentDir(false);
1 ✔
1193
    if (folder.isEmpty()) {
1 ✔
1194
      folder = storePath;
×
1195
    }
1196
    // The dialog names the entry itself: every folder of the store to pick
1197
    // from, the tree's current one preselected.
1198
    // Real folders of the store only: a linked or junctioned folder would
1199
    // offer (and write the new entry to) somewhere outside it.
1200
    QStringList folders{QString()};
2 ✔
1201
    const QStringList found = Util::directoriesUnder(storePath);
1 ✔
1202
    for (const QString &path : found) {
15 ✔
1203
      const QString rel = QDir(storePath).relativeFilePath(path);
14 ✔
1204
      if (!rel.startsWith(u'.') && !rel.contains(QStringLiteral("/."))) {
42 ✔
1205
        folders << rel;
1206
      }
1207
    }
1208
    std::sort(folders.begin() + 1, folders.end());
1 ✔
1209
    d.setNewEntryLocation(storePath, folders,
1 ✔
1210
                          QDir(storePath).relativeFilePath(folder));
2 ✔
1211
    QHash<QString, QStringList> templates =
1212
        TemplateIO::readTemplates(storePath);
1 ✔
1213
    if (!templates.isEmpty()) {
1214
      QString defaultTemplate =
1215
          TemplateIO::getFolderTemplate(folder, storePath);
1 ✔
1216
      d.setAvailableTemplates(templates, defaultTemplate);
1 ✔
1217
    }
1218
  }
1 ✔
1219

1220
  if (!d.exec()) {
4 ✔
1221
    ui->treeView->setFocus();
3 ✔
1222
  }
1223
}
4 ✔
1224

1225
/**
1226
 * @brief MainWindow::addPassword add a new password: one dialog with the
1227
 * folder, the name and the content.
1228
 */
1229
void MainWindow::addPassword() { setPassword(QString()); }
2 ✔
1230

1231
/**
1232
 * @brief MainWindow::onDelete remove password, if you are
1233
 * sure.
1234
 */
1235
auto MainWindow::confirmDeletion(const QString &file, bool isDir) -> bool {
4 ✔
1236
  QString dirMessage = tr(" and the whole content?");
1237
  if (isDir) {
4 ✔
1238
    // A link, junction or special file inside is as unexpected as a stray
1239
    // plain file: the walker leaves them out of content and reports them.
1240
    QStringList skipped;
2 ✔
1241
    const QString folder =
1242
        m_tree->fileSystem().rootPath() + QDir::separator() + file;
4 ✔
1243
    const QStringList content =
1244
        Util::regularFilesUnder(folder, {QStringLiteral("*")}, &skipped);
6 ✔
1245
    const bool unexpected =
1246
        !skipped.isEmpty() ||
4 ✔
1247
        std::any_of(content.cbegin(), content.cend(), [](const QString &path) {
4 ✔
1248
          return QFileInfo(path).suffix() != QLatin1String("gpg");
4 ✔
1249
        });
1250
    if (unexpected) {
1251
      dirMessage = tr(" and the whole content? <br><strong>Attention: "
1 ✔
1252
                      "there are unexpected files in the given folder, "
1253
                      "check them before continue.</strong>");
1254
    }
1255
  }
1256
  return QMessageBox::question(
8 ✔
1257
             this, isDir ? tr("Delete folder?") : tr("Delete password?"),
8 ✔
1258
             tr("Are you sure you want to delete %1%2?")
4 ✔
1259
                 .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
10 ✔
1260
             QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
8 ✔
1261
}
1262

1263
auto MainWindow::confirmLinkRemoval(const QString &file) -> bool {
1 ✔
1264
  // Only the link goes (ImitatePass::Remove unlinks it); nothing behind it
1265
  // is looked at or mentioned.
1266
  return QMessageBox::question(
2 ✔
1267
             this, tr("Delete link?"),
1 ✔
1268
             tr("%1 is a symbolic link or junction. Remove the link? What it "
1 ✔
1269
                "points to is left alone.")
1270
                 .arg(QDir::separator() + file),
2 ✔
1271
             QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
2 ✔
1272
}
1273

1274
void MainWindow::onDelete() {
8 ✔
1275
  const QModelIndex currentIndex = ui->treeView->currentIndex();
8 ✔
1276
  if (!currentIndex.isValid()) {
1277
    // This fixes https://github.com/IJHack/QtPass/issues/556
1278
    // Otherwise the entire password directory would be deleted if
1279
    // nothing is selected in the tree view.
1280
    return;
5 ✔
1281
  }
1282

1283
  const bool isDir = !m_tree->fileInfo(currentIndex).isFile();
6 ✔
1284
  const QString file =
1285
      isDir ? m_tree->currentDir(true) : getFile(currentIndex, true);
6 ✔
1286
  const QString folder =
1287
      m_tree->fileSystem().rootPath() + QDir::separator() + file;
6 ✔
1288
  const bool linkedFolder = isDir && Util::isLinkedFolder(folder);
6 ✔
1289

1290
  if (!linkedFolder && refuseLinkedFolder(folder)) {
5 ✔
1291
    // Behind a link: deleting would reach outside the store.
1292
    return;
1293
  }
1294
  const bool confirmed =
1295
      linkedFolder ? confirmLinkRemoval(file) : confirmDeletion(file, isDir);
5 ✔
1296
  if (!confirmed) {
5 ✔
1297
    return;
1298
  }
1299
  QtPassSettings::getPass()->Remove(file, isDir);
6 ✔
1300
}
1301

1302
/**
1303
 * @brief MainWindow::cancelOtpRequest forget the pending OTP and copy requests.
1304
 *
1305
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1306
 * decrypt never emits finishedShow; if the request stayed pending, the next
1307
 * successful decrypt of that same entry (the user clicking it again) would be
1308
 * taken as the answer and copy something the user no longer asked for.
1309
 */
1310
void MainWindow::cancelOtpRequest() {
25 ✔
1311
  m_otpRequestFile.clear();
25 ✔
1312
  m_copyRequestFile.clear();
25 ✔
1313
}
25 ✔
1314

1315
void MainWindow::onOperationError() {
16 ✔
1316
  cancelOtpRequest();
16 ✔
1317
  // executeWrapperStarted() emptied the panel and the browser when the
1318
  // operation started (a refusal says so too); what is left to forget is
1319
  // which entry the panel was about to show.
1320
  m_shownFile.clear();
16 ✔
1321
}
16 ✔
1322

1323
/**
1324
 * @brief MainWindow::onOtp generate the selected entry's OTP code and copy it.
1325
 *
1326
 * Decrypts the entry once and derives the code in-process, so this works with
1327
 * either backend and on every platform. The code itself is produced by
1328
 * otpFromFileToClipboard once Pass::finishedShow arrives.
1329
 */
1330
void MainWindow::onOtp() {
10 ✔
1331
  QString file = getFile(ui->treeView->currentIndex(), true);
10 ✔
1332
  if (file.isEmpty()) {
10 ✔
1333
    flashText(tr("No password selected for OTP generation"), true);
1 ✔
1334
    return;
1 ✔
1335
  }
1336
  if (!QtPassSettings::isUseOtp()) {
9 ✔
1337
    // Normally unreachable (the action is hidden), but never fail silently.
1338
    flashText(tr("No OTP code found in this password entry"), true);
1 ✔
1339
    return;
1 ✔
1340
  }
1341

1342
  // Fast path: the panel already shows a live code for the selected entry, so
1343
  // there is nothing to decrypt. This also keeps the clipboard write single —
1344
  // going through Show() would let passShowHandler copy the password first, and
1345
  // two QClipboard::setMimeData calls in one event-loop turn can leave the
1346
  // Windows clipboard empty.
1347
  //
1348
  // Only when the panel is showing *this* entry: arrow-key navigation and
1349
  // right-clicking move the tree's currentIndex without emitting
1350
  // QTreeView::clicked, so the visible code can belong to a different account.
1351
  const QString shown =
1352
      (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
8 ✔
1353
  if (!shown.isEmpty()) {
8 ✔
1354
    m_qtPass->clipboard().copyText(shown);
1 ✔
1355
    showStatusMessage(tr("OTP code copied to clipboard"));
2 ✔
1356
    return;
1357
  }
1358

1359
  // Fallback: no OTP row on screen, so decrypt once. The request file stops
1360
  // passShowHandler putting the password on the clipboard for a request that
1361
  // only asked for a code, and tells otpFromFileToClipboard which decrypt is
1362
  // its answer.
1363
  m_otpRequestFile = file;
7 ✔
1364
  setUiElementsEnabled(false);
7 ✔
1365
  // passShowHandler repaints the panel for this Show too, so keep the marker in
1366
  // step or a second request would decrypt again instead of taking the fast
1367
  // path. Safe to set now: executeWrapperStarted() clears the panel on every
1368
  // command, so a failed decrypt leaves currentOtpCode() empty.
1369
  m_shownFile = file;
7 ✔
1370
  QtPassSettings::getPass()->Show(file);
14 ✔
1371
}
1372

1373
/**
1374
 * @brief MainWindow::onEdit try and edit (selected) password.
1375
 */
1376
void MainWindow::onEdit() {
1 ✔
1377
  QString file = getFile(ui->treeView->currentIndex(), true);
1 ✔
1378
  editPassword(file);
1 ✔
1379
}
1 ✔
1380

1381
/**
1382
 * @brief MainWindow::onUsers edit users for the current
1383
 * folder,
1384
 * gets lists and opens UserDialog.
1385
 */
1386
void MainWindow::onUsers() {
2 ✔
1387
  const QString dir = m_tree->currentDir(false);
2 ✔
1388
  if (refuseLinkedFolder(dir)) {
2 ✔
1389
    return;
1390
  }
1391

1392
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
1 ✔
1393
  if (!d.exec()) {
1 ✔
1394
    ui->treeView->setFocus();
1 ✔
1395
  }
1396
}
1 ✔
1397

1398
/**
1399
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1400
 * @param message
1401
 */
1402
void MainWindow::messageAvailable(const QString &message) {
2 ✔
1403
  show();
2 ✔
1404
  raise();
2 ✔
1405
  if (message.isEmpty()) {
2 ✔
1406
    focusInput();
1 ✔
1407
  } else {
1408
    ui->treeView->expandAll();
1 ✔
1409
    ui->lineEdit->setText(message);
1 ✔
1410
    on_lineEdit_returnPressed();
1 ✔
1411
  }
1412
}
2 ✔
1413

1414
/**
1415
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1416
 * select a more appropriate one to view too
1417
 */
1418
void MainWindow::updateProfileBox() {
110 ✔
1419
  const Profiles profiles = QtPassSettings::getProfiles();
110 ✔
1420

1421
  if (profiles.isEmpty()) {
1 ✔
1422
    ui->profileWidget->hide();
109 ✔
1423
  } else {
1424
    ui->profileWidget->show();
1 ✔
1425
    ui->profileBox->setEnabled(profiles.size() > 1);
2 ✔
1426
    ui->profileBox->clear();
1 ✔
1427
    for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
3 ✔
1428
      if (!i.key().isEmpty()) {
2 ✔
1429
        ui->profileBox->addItem(i.key());
2 ✔
1430
      }
1431
    }
1432
    ui->profileBox->model()->sort(0);
1 ✔
1433
  }
1434
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
220 ✔
1435
  if (index != -1) { //  -1 for not found
110 ✔
1436
    ui->profileBox->setCurrentIndex(index);
1 ✔
1437
  }
1438
}
110 ✔
1439

1440
/**
1441
 * @brief Handles changes to the selected profile in the profile combo box.
1442
 * @details Ignores the event during a fresh start or when the selected profile
1443
 * matches the current profile. Otherwise, it clears the password field, updates
1444
 * the active profile and related settings, refreshes the environment, and
1445
 * resets the tree view and action states to reflect the newly selected profile.
1446
 *
1447
 * @param name - The newly selected profile name.
1448
 * @return void - This function does not return a value.
1449
 *
1450
 */
1451
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
3 ✔
1452
  if (m_freshStart || name == QtPassSettings::getProfile()) {
9 ✔
1453
    return;
2 ✔
1454
  }
1455

1456
  ui->lineEdit->clear();
1 ✔
1457

1458
  const Profile prof = QtPassSettings::getProfiles().value(name);
2 ✔
1459
  AppSettings s = QtPassSettings::load();
1 ✔
1460
  s.activeProfile = name;
1 ✔
1461
  s.passStore = prof.path;
1 ✔
1462
  s.passSigningKey = prof.signingKey;
1 ✔
1463
  // A profile without its own Git flags keeps the global values.
1464
  s.useGit = prof.useGit.value_or(s.useGit);
1 ✔
1465
  s.autoPush = prof.autoPush.value_or(s.autoPush);
1 ✔
1466
  s.autoPull = prof.autoPull.value_or(s.autoPull);
1 ✔
1467
  QtPassSettings::save(s);
1 ✔
1468
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
2 ✔
1469

1470
  QtPassSettings::getPass()->updateEnv();
1 ✔
1471

1472
  m_tree->setStore(QtPassSettings::getPassStore());
2 ✔
1473
  deselect();
1 ✔
1474
  ui->treeView->setCurrentIndex(QModelIndex());
1 ✔
1475
}
1 ✔
1476

1477
/**
1478
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1479
 * support
1480
 * it
1481
 */
1482
void MainWindow::initTrayIcon() {
2 ✔
1483
  m_tray = new TrayIcon(this);
2 ✔
1484
  if (!m_tray->getIsAllocated()) {
2 ✔
1485
    destroyTrayIcon();
2 ✔
1486
  }
1487
}
2 ✔
1488

1489
/**
1490
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1491
 */
1492
void MainWindow::destroyTrayIcon() {
2 ✔
1493
  delete m_tray;
2 ✔
1494
  m_tray = nullptr;
2 ✔
1495
}
2 ✔
1496

1497
/**
1498
 * @brief MainWindow::closeEvent hide or quit
1499
 * @param event
1500
 */
1501
void MainWindow::closeEvent(QCloseEvent *event) {
2 ✔
1502
  // Save in both branches: a tray user only ever hides the window, and
1503
  // used to quit through the tray menu with nothing ever written.
1504
  saveWindowState();
2 ✔
1505
  if (QtPassSettings::isHideOnClose()) {
2 ✔
1506
    this->hide();
2 ✔
1507
    event->ignore();
1508
  } else {
1509
    m_qtPass->clipboard().clear();
×
1510
    event->accept();
1511
    // A visible QSystemTrayIcon keeps the application alive after the last
1512
    // window closes, so quitOnLastWindowClosed never fires and the window
1513
    // merely vanishes into the tray. Quit explicitly so closing the window
1514
    // actually exits when "hide on close" is disabled.
1515
    QApplication::quit();
×
1516
  }
1517
}
2 ✔
1518

1519
/**
1520
 * @brief MainWindow::eventFilter filter out some events and focus the
1521
 * treeview
1522
 * @param obj
1523
 * @param event
1524
 * @return
1525
 */
1526
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
4,448 ✔
1527
  if ((obj == ui->toolBar || obj == menuBar()) &&
4,448 ✔
1528
      event->type() == QEvent::PaletteChange) {
1529
    // KDE's Breeze style gives the menu bar and top toolbars a "header"
1530
    // palette of its own. After a runtime light/dark switch it recomputes
1531
    // that palette from a cached kdeglobals and stamps the previous theme's
1532
    // colours back on, so both bars stay dark on a light window (or vice
1533
    // versa), the toolbar's icons, which follow the new palette, invisible
1534
    // against it. A header the theme meant is close to the window colour;
1535
    // the other theme is not. When the two disagree after a switch, drop
1536
    // the imposed palette so the bar follows the application palette again.
1537
    // Deferred so we never re-enter the style while it is still applying its
1538
    // palette.
1539
    QTimer::singleShot(0, this, &MainWindow::dropStaleToolsAreaPalettes);
24 ✔
1540
  }
1541
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
4,448 ✔
1542
    auto *key = dynamic_cast<QKeyEvent *>(event);
1 ✔
1543
    if (key != nullptr && key->key() == Qt::Key_Down) {
1 ✔
1544
      ui->treeView->setFocus();
1 ✔
1545
    }
1546
  }
1547
  return QObject::eventFilter(obj, event);
4,448 ✔
1548
}
1549

1550
/**
1551
 * @brief Reset the menu bar and toolbar palettes when a style left one of
1552
 * them in the wrong theme.
1553
 *
1554
 * Only once the application palette has changed in this window's lifetime:
1555
 * before that the style's colours are as fresh as the window's, and a
1556
 * scheme may put its header far from the window colour on purpose. After a
1557
 * switch, compares the lightness of each bar's active Window colour with
1558
 * the application palette's; a difference above kStaleToolsAreaLightness
1559
 * means the bar shows the other theme. See the PaletteChange branch in
1560
 * eventFilter().
1561
 */
1562
void MainWindow::dropStaleToolsAreaPalettes() {
16 ✔
1563
  if (!m_themeSwitched) {
16 ✔
1564
    return;
1565
  }
1566
  constexpr int kStaleToolsAreaLightness = 64;
1567
  // The active group on both sides: a bar's palette() reads the inactive
1568
  // group while another window has focus (the settings app, during a theme
1569
  // switch), and Breeze gives the header set different inactive colours.
1570
  const int appLightness = QApplication::palette()
28 ✔
1571
                               .color(QPalette::Active, QPalette::Window)
1572
                               .lightness();
14 ✔
1573
  for (QWidget *bar : {static_cast<QWidget *>(ui->toolBar),
56 ✔
1574
                       static_cast<QWidget *>(menuBar())}) {
42 ✔
1575
    if (!bar->testAttribute(Qt::WA_SetPalette)) {
28 ✔
1576
      continue;
24 ✔
1577
    }
1578
    const int barLightness =
1579
        bar->palette().color(QPalette::Active, QPalette::Window).lightness();
8 ✔
1580
    if (qAbs(barLightness - appLightness) <= kStaleToolsAreaLightness) {
4 ✔
1581
      continue;
2 ✔
1582
    }
1583
    bar->setPalette(QPalette());
2 ✔
1584
    // The style also paints the tools-area background on the main window
1585
    // from the same stale palette, and both bars are transparent by default;
1586
    // paint our own background so the reset is actually visible. Their
1587
    // background role is Button, which is not the window colour in either
1588
    // theme; paint Window, which is also what a header palette the style
1589
    // stamps back later, rightly this time, puts in that role.
1590
    bar->setBackgroundRole(QPalette::Window);
2 ✔
1591
    bar->setAutoFillBackground(true);
2 ✔
1592
  }
1593
}
1594

1595
/**
1596
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1597
 * @param event
1598
 */
1599
void MainWindow::keyPressEvent(QKeyEvent *event) {
5 ✔
1600
  switch (event->key()) {
5 ✔
1601
  case Qt::Key_Delete:
1 ✔
1602
    onDelete();
1 ✔
1603
    break;
1 ✔
1604
  case Qt::Key_Return:
1 ✔
1605
  case Qt::Key_Enter:
1606
    if (m_tree->proxy().rowCount() > 0) {
1 ✔
1607
      on_treeView_clicked(ui->treeView->currentIndex());
1 ✔
1608
    }
1609
    break;
1610
  case Qt::Key_Escape:
1 ✔
1611
    ui->lineEdit->clear();
1 ✔
1612
    break;
1 ✔
1613
  default:
1614
    break;
1615
  }
1616
}
5 ✔
1617

1618
/**
1619
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1620
 * menu
1621
 * @param pos
1622
 */
1623
void MainWindow::addShareMenu(QMenu &contextMenu, const AppSettings &s) {
9 ✔
1624
  const QString dirPath = QDir::cleanPath(m_tree->currentDir(false));
9 ✔
1625
  auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
9 ✔
1626
  contextMenu.addMenu(shareMenu);
9 ✔
1627

1628
  const QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
9 ✔
1629
  const bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
9 ✔
1630
  const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
9 ✔
1631
  const bool gpgAvailable =
1632
      !exePath.isEmpty() &&
18 ✔
1633
      (Executor::parseWslCommand(exePath) || QFile(exePath).exists());
18 ✔
1634

1635
  QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
9 ✔
1636
  reencrypt->setEnabled(gpgIdExists && gpgAvailable);
9 ✔
1637
  connect(reencrypt, &QAction::triggered, this,
9 ✔
1638
          [this, dirPath]() { reencryptPath(dirPath); });
30 ✔
1639

1640
  QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
9 ✔
1641
  exportKey->setEnabled(gpgAvailable);
9 ✔
1642
  connect(exportKey, &QAction::triggered, this, &MainWindow::exportPublicKey);
9 ✔
1643

1644
  QAction *addRecipientAction = shareMenu->addAction(tr("Add recipient..."));
9 ✔
1645
  addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
9 ✔
1646
  connect(addRecipientAction, &QAction::triggered, this,
9 ✔
1647
          [this, dirPath]() { addRecipient(dirPath); });
29 ✔
1648

1649
  QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
9 ✔
1650
  connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
9 ✔
1651
}
9 ✔
1652

1653
void MainWindow::showContextMenu(const QPoint &pos) {
11 ✔
1654
  const AppSettings s = QtPassSettings::load();
11 ✔
1655
  const QModelIndex index = ui->treeView->indexAt(pos);
11 ✔
1656
  const bool selected = index.isValid();
1657
  if (!selected) {
1658
    ui->treeView->clearSelection();
1 ✔
1659
    ui->actionDelete->setEnabled(false);
1 ✔
1660
    ui->actionEdit->setEnabled(false);
1 ✔
1661
  }
1662
  ui->treeView->setCurrentIndex(index);
11 ✔
1663
  const QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
11 ✔
1664
  const QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
11 ✔
1665
  const bool isDir = fileOrFolder.isDir();
11 ✔
1666

1667
  QMenu contextMenu;
11 ✔
1668
  if (!selected || isDir) {
11 ✔
1669
    const std::array<std::pair<QString, void (MainWindow::*)()>, 4> onFolder{{
1670
        {tr("Open folder with file manager"), &MainWindow::openFolder},
10 ✔
1671
        {tr("Add folder"), &MainWindow::addFolder},
10 ✔
1672
        {tr("Add password"), &MainWindow::addPassword},
10 ✔
1673
        {tr("Users"), &MainWindow::onUsers},
10 ✔
NEW
1674
    }};
×
1675
    for (const auto &[text, slot] : onFolder) {
50 ✔
1676
      connect(contextMenu.addAction(text), &QAction::triggered, this, slot);
40 ✔
1677
    }
1678
  } else if (fileOrFolder.isFile()) {
1 ✔
1679
    connect(contextMenu.addAction(tr("Edit")), &QAction::triggered, this,
1 ✔
1680
            &MainWindow::onEdit);
2 ✔
1681
  }
1682
  if (selected) {
11 ✔
1683
    contextMenu.addSeparator();
10 ✔
1684
    if (isDir) {
10 ✔
1685
      connect(contextMenu.addAction(tr("Rename folder")), &QAction::triggered,
9 ✔
1686
              this, &MainWindow::renameFolder);
18 ✔
1687
    } else if (fileOrFolder.isFile()) {
1 ✔
1688
      connect(contextMenu.addAction(tr("Rename password")), &QAction::triggered,
1 ✔
1689
              this, &MainWindow::renamePassword);
2 ✔
1690
    }
1691
    connect(contextMenu.addAction(tr("Delete")), &QAction::triggered, this,
10 ✔
1692
            &MainWindow::onDelete);
10 ✔
1693
    if (isDir) {
10 ✔
1694
      addShareMenu(contextMenu, s);
9 ✔
1695
    }
1696
  }
1697
  contextMenu.exec(globalPos);
11 ✔
1698
}
11 ✔
1699

1700
/**
1701
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1702
 * password window
1703
 * @param pos
1704
 */
1705
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
1 ✔
1706
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
1 ✔
1707
  // createStandardContextMenu() parents the menu to textBrowser. A stylesheet
1708
  // on the browser (it used to carry "background: palette(base)") cascades to
1709
  // the child QMenu and breaks its opaque native background. Reparent to the
1710
  // main window so the menu paints solid regardless of browser styling.
1711
  contextMenu->setParent(this, contextMenu->windowFlags());
1 ✔
1712
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
1 ✔
1713

1714
  contextMenu->exec(globalPos);
1 ✔
1715
  delete contextMenu;
1 ✔
1716
}
1 ✔
1717

1718
/**
1719
 * @brief MainWindow::openFolder open the folder in the default file manager
1720
 */
1721
void MainWindow::openFolder() {
1 ✔
1722
  QString dir = m_tree->currentDir(false);
1 ✔
1723

1724
  QString path = QDir::toNativeSeparators(dir);
1 ✔
1725
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
1 ✔
1726
}
1 ✔
1727

1728
/**
1729
 * @brief MainWindow::addFolder add a new folder to store passwords in
1730
 */
1731
void MainWindow::addFolder() {
5 ✔
1732
  const AppSettings s = QtPassSettings::load();
5 ✔
1733
  bool ok;
1734
  QString dir = m_tree->currentDir(false);
5 ✔
1735
  QString newdir =
1736
      QInputDialog::getText(this, tr("New folder"),
5 ✔
1737
                            tr("New folder: \n(Will be placed in %1 )")
5 ✔
1738
                                .arg(s.passStore + m_tree->currentDir(true)),
10 ✔
1739
                            QLineEdit::Normal, "", &ok);
10 ✔
1740
  if (!ok || newdir.isEmpty()) {
5 ✔
1741
    return;
1742
  }
1743
  newdir.prepend(dir);
1744
  if (!confirmPathInStore(newdir)) {
4 ✔
1745
    return;
1746
  }
1747
  if (!QDir().mkdir(newdir)) {
6 ✔
1748
    QMessageBox::warning(this, tr("Error"),
2 ✔
1749
                         tr("Failed to create folder: %1").arg(newdir));
2 ✔
1750
    return;
1 ✔
1751
  }
1752
  // A .gpg-id only counts when it is signed once a signing key is configured:
1753
  // ImitatePass::loadVerifiedRecipients and pass (PASSWORD_STORE_SIGNING_KEY)
1754
  // both refuse to encrypt into a folder whose .gpg-id has no matching
1755
  // .gpg-id.sig. Signing here would need the signing secret key and a
1756
  // passphrase prompt just to create a folder, so in that case leave the
1757
  // folder without its own .gpg-id: it inherits the parent's signed list,
1758
  // which encrypts to the same recipients. UsersDialog (Init) is the place to
1759
  // give it a distinct, signed list.
1760
  const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
2 ✔
1761
  if (s.addGPGId && !signingConfigured) {
2 ✔
1762
    // Seed the new folder's .gpg-id from the recipients that are currently
1763
    // in effect for its parent. The previous implementation walked
1764
    // listKeys("", true) and wrote every key whose `enabled` flag was set,
1765
    // but that flag is only toggled inside UsersDialog, so the loop wrote
1766
    // nothing and left a zero-byte .gpg-id that shadowed the parent's
1767
    // recipients (see #1682).
1768
    if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
2 ✔
1769
      QMessageBox::warning(
1 ✔
1770
          this, tr("Error"),
1 ✔
1771
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
2 ✔
1772
      return;
1 ✔
1773
    }
1774
  }
1775
}
5 ✔
1776

1777
/**
1778
 * @brief MainWindow::renameFolder rename an existing folder
1779
 */
1780
void MainWindow::renameFolder() {
3 ✔
1781
  bool ok;
1782
  QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
3 ✔
1783
  QString srcDirName = QDir(srcDir).dirName();
3 ✔
1784
  QString newName =
1785
      QInputDialog::getText(this, tr("Rename folder"), tr("Rename folder to: "),
3 ✔
1786
                            QLineEdit::Normal, srcDirName, &ok);
3 ✔
1787
  if (!ok || newName.isEmpty()) {
3 ✔
1788
    return;
1789
  }
1790
  QString destDir = srcDir;
1791
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
2 ✔
1792
  if (!confirmPathInStore(destDir)) {
2 ✔
1793
    return;
1794
  }
1795
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
3 ✔
1796
}
1797

1798
/**
1799
 * @brief MainWindow::editPassword read password and open edit window via
1800
 * MainWindow::onEdit()
1801
 */
1802
void MainWindow::editPassword(const QString &file) {
3 ✔
1803
  if (!file.isEmpty()) {
3 ✔
1804
    const AppSettings s = QtPassSettings::load();
3 ✔
1805
    if (s.useGit && s.autoPull) {
3 ✔
1806
      onUpdate(true);
1 ✔
1807
    }
1808
    setPassword(file, false);
3 ✔
1809
  }
3 ✔
1810
}
3 ✔
1811

1812
/**
1813
 * @brief MainWindow::renamePassword rename an existing password
1814
 */
1815
void MainWindow::renamePassword() {
2 ✔
1816
  bool ok;
1817
  QString file = getFile(ui->treeView->currentIndex(), false);
2 ✔
1818
  QString filePath = QFileInfo(file).path();
2 ✔
1819
  QString fileName = QFileInfo(file).fileName();
2 ✔
1820
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
4 ✔
1821
    fileName.chop(4);
2 ✔
1822
  }
1823

1824
  QString newName =
1825
      QInputDialog::getText(this, tr("Rename file"), tr("Rename file to: "),
2 ✔
1826
                            QLineEdit::Normal, fileName, &ok);
2 ✔
1827
  if (!ok || newName.isEmpty()) {
2 ✔
1828
    return;
1829
  }
1830
  QString newFile = QDir(filePath).filePath(newName);
2 ✔
1831
  if (!confirmPathInStore(newFile)) {
2 ✔
1832
    return;
1833
  }
1834
  QtPassSettings::getPass()->Move(file, newFile, false);
3 ✔
1835
}
1836

1837
/**
1838
 * @brief Copies the password of the selected file from the tree view to the
1839
 * clipboard.
1840
 * @example
1841
 * MainWindow::copyPasswordFromTreeview();
1842
 *
1843
 * @return void - This function does not return a value.
1844
 */
1845
void MainWindow::copyPasswordFromTreeview() {
4 ✔
1846
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
4 ✔
1847

1848
  if (fileOrFolder.isFile()) {
4 ✔
1849
    QString file = getFile(ui->treeView->currentIndex(), true);
4 ✔
1850
    // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
1851
    // an earlier request is simply not this one's.
1852
    m_copyRequestFile = file;
4 ✔
1853
    // This Show repaints the panel as well; see onOtp().
1854
    m_shownFile = file;
4 ✔
1855
    QtPassSettings::getPass()->Show(file);
8 ✔
1856
  }
1857
}
4 ✔
1858

1859
void MainWindow::passwordFromFileToClipboard(const QString &text,
14 ✔
1860
                                             const QString &file) {
1861
  if (m_copyRequestFile.isEmpty() || file != m_copyRequestFile) {
14 ✔
1862
    return;
12 ✔
1863
  }
1864
  m_copyRequestFile.clear();
3 ✔
1865
  const QStringList tokens = text.split('\n');
3 ✔
1866
  if (tokens.isEmpty()) {
3 ✔
1867
    return;
1868
  }
1869
  // An entry created by `pass otp insert` has the otpauth:// URI as its first
1870
  // line. That URI carries the shared TOTP secret (the seed, not a code), so
1871
  // copying it to the clipboard would leak 2FA material. Skip it, matching the
1872
  // display path (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1873
  if (FileContent::isOtpUriValue(tokens[0])) {
3 ✔
1874
    flashText(tr("This entry holds an OTP secret, not a password"), true);
1 ✔
1875
    return;
1 ✔
1876
  }
1877
  m_qtPass->clipboard().copyText(tokens[0]);
2 ✔
1878
}
1879

1880
/**
1881
 * @brief Displays message in status bar
1882
 *
1883
 * @param msg     text to be displayed
1884
 * @param timeout time for which msg shall be visible
1885
 */
1886
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
19 ✔
1887
  ui->statusBar->showMessage(msg, timeout);
19 ✔
1888
}
19 ✔
1889

1890
/**
1891
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1892
 * @param dir Directory path to re-encrypt
1893
 */
1894
void MainWindow::reencryptPath(const QString &dir) {
3 ✔
1895
  QDir checkDir(dir);
3 ✔
1896
  if (!checkDir.exists()) {
3 ✔
1897
    QMessageBox::critical(this, tr("Error"),
2 ✔
1898
                          tr("Directory does not exist: %1").arg(dir));
2 ✔
1899
    return;
1 ✔
1900
  }
1901
  if (refuseLinkedFolder(dir)) {
2 ✔
1902
    return;
1903
  }
1904

1905
  int ret = QMessageBox::question(
4 ✔
1906
      this, tr("Re-encrypt passwords"),
2 ✔
1907
      tr("Re-encrypt all passwords in %1?\n\n"
2 ✔
1908
         "This will re-encrypt ALL password files in this folder "
1909
         "using the current recipients defined in .gpg-id.\n\n"
1910
         "This may rewrite many files and cannot be undone easily.\n\n"
1911
         "Continue?")
1912
          .arg(QDir(dir).dirName()),
4 ✔
1913
      QMessageBox::Yes | QMessageBox::No);
1914

1915
  if (ret != QMessageBox::Yes)
2 ✔
1916
    return;
1917

1918
  // Disable preemptively. ImitatePass::reencryptPath emits
1919
  // startReencryptPath asynchronously and the slot would re-run this,
1920
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1921
  startReencryptPath();
1 ✔
1922

1923
  QtPassSettings::getImitatePass()->reencryptPath(
2 ✔
1924
      QDir::cleanPath(QDir(dir).absolutePath()));
2 ✔
1925
}
3 ✔
1926

1927
/**
1928
 * @brief MainWindow::startReencryptPath disable ui elements and treeview and
1929
 * show the progress dialog. Idempotent: MainWindow::reencryptPath calls it
1930
 * before ImitatePass emits startReencryptPath.
1931
 */
1932
void MainWindow::startReencryptPath() {
6 ✔
1933
  m_reencryptRunning = true;
6 ✔
1934
  setUiElementsEnabled(false);
6 ✔
1935
  // The worker always ends with endReencryptPath(), and Cancel is available
1936
  // throughout, so the generic watchdog is not needed for this operation.
1937
  m_uiWatchdog.stop();
6 ✔
1938
  ui->treeView->setDisabled(true);
6 ✔
1939
  if (m_reencryptProgress)
6 ✔
1940
    return;
1941
  auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
4 ✔
1942
                                       tr("Cancel"), 0, 0, this);
8 ✔
1943
  progress->setWindowTitle(tr("Re-encrypt passwords"));
4 ✔
1944
  progress->setWindowModality(Qt::WindowModal);
4 ✔
1945
  progress->setAutoClose(false);
4 ✔
1946
  progress->setAutoReset(false);
4 ✔
1947
  progress->setMinimumDuration(0);
4 ✔
1948
  connect(progress, &QProgressDialog::canceled, this, [this]() {
4 ✔
1949
    QtPassSettings::getImitatePass()->cancelReencryptPath();
1 ✔
1950
    showStatusMessage(tr("Cancelling re-encryption"), 5000);
1 ✔
1951
  });
1 ✔
1952
  m_reencryptProgress = progress;
1953
  progress->show();
4 ✔
1954
}
1955

1956
/**
1957
 * @brief MainWindow::reencryptProgress show how many files were checked
1958
 * @param current files checked so far
1959
 * @param total files found under the folder
1960
 */
1961
void MainWindow::reencryptProgress(int current, int total) {
5 ✔
1962
  if (!m_reencryptProgress)
5 ✔
1963
    return;
1964
  m_reencryptProgress->setMaximum(total);
4 ✔
1965
  m_reencryptProgress->setLabelText(
4 ✔
1966
      tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
8 ✔
1967
  // On a modal QProgressDialog setValue() calls processEvents(), which can
1968
  // deliver the queued completion: endReencryptPath() then hides the dialog
1969
  // and resets m_reencryptProgress under our feet. Keep it last and touch
1970
  // nothing afterwards.
1971
  m_reencryptProgress->setValue(current);
4 ✔
1972
}
1973

1974
/**
1975
 * @brief MainWindow::endReencryptPath re-enable ui elements
1976
 */
1977
void MainWindow::endReencryptPath() {
4 ✔
1978
  m_reencryptRunning = false;
4 ✔
1979
  if (m_reencryptProgress) {
4 ✔
1980
    m_reencryptProgress->hide();
4 ✔
1981
    m_reencryptProgress->deleteLater();
4 ✔
1982
    m_reencryptProgress = nullptr;
1983
  }
1984
  setUiElementsEnabled(true);
4 ✔
1985
}
4 ✔
1986

1987
/**
1988
 * @brief MainWindow::exportPublicKey export the configured signing key in
1989
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1990
 *
1991
 * Falls back to a help dialog when no signing key is configured or gpg is
1992
 * unavailable, so the user still gets actionable guidance.
1993
 */
1994
void MainWindow::exportPublicKey() {
3 ✔
1995
  const AppSettings s = QtPassSettings::load();
3 ✔
1996
  const QString identity = s.passSigningKey;
1997
  if (identity.isEmpty()) {
3 ✔
1998
    QMessageBox::information(
1 ✔
1999
        this, tr("Export public key"),
1 ✔
2000
        tr("<h3>Export your public key</h3>"
×
2001
           "<p>No signing key is configured. Set one in QtPass Settings "
2002
           "&gt; GPG keys, or run this in a terminal:</p>"
2003
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
2004
           "&gt;</pre>"
2005
           "<p>Then send the file to your teammates.</p>"));
2006
    return;
1 ✔
2007
  }
2008
  QString gpgExe = s.gpgExecutable;
2009
  if (gpgExe.isEmpty()) {
2 ✔
2010
    gpgExe = QStringLiteral("gpg");
×
2011
  }
2012
  QStringList args = {"--armor", "--export"};
6 ✔
2013
  args.append(identity.split(' ', Qt::SkipEmptyParts));
2 ✔
2014
  QString stdOut;
2 ✔
2015
  QString stdErr;
2 ✔
2016
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
2 ✔
2017
  if (exitCode != 0 || stdOut.isEmpty()) {
2 ✔
2018
    QMessageBox::warning(this, tr("Export public key"),
2 ✔
2019
                         tr("Could not export public key for %1.\n\n%2")
1 ✔
2020
                             .arg(identity, stdErr.isEmpty()
2 ✔
2021
                                                ? tr("No output from gpg.")
2 ✔
2022
                                                : stdErr));
2023
    return;
2024
  }
2025
  ExportPublicKeyDialog dialog(identity, stdOut, this);
1 ✔
2026
  dialog.exec();
1 ✔
2027
}
6 ✔
2028

2029
/**
2030
 * @brief MainWindow::addRecipient open the recipient management dialog for
2031
 *        the supplied directory.
2032
 * @param dir Folder whose .gpg-id should be edited.
2033
 *
2034
 * Delegates to UsersDialog so users can tick/untick keys from their
2035
 * keyring as recipients of the folder; importing a foreign key into the
2036
 * keyring still has to happen via gpg (or QtPass settings) first.
2037
 */
2038
void MainWindow::addRecipient(const QString &dir) {
2 ✔
2039
  if (refuseLinkedFolder(dir)) {
2 ✔
2040
    return;
1 ✔
2041
  }
2042
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
1 ✔
2043
  d.exec();
1 ✔
2044
}
1 ✔
2045

2046
/**
2047
 * @brief A folder picked in the tree that is a symlink or junction is not a
2048
 *        folder of the store: say so instead of re-encrypting, re-keying or
2049
 *        listing whatever it points to. The store root itself may be a link
2050
 *        (a synced folder); that is configuration, not a tree pick.
2051
 * @param dir Folder as the tree names it.
2052
 * @return true when the operation must not go ahead.
2053
 */
2054
auto MainWindow::refuseLinkedFolder(const QString &dir) -> bool {
11 ✔
2055
  if (!Util::isUnderLink(dir, QtPassSettings::load().passStore)) {
11 ✔
2056
    return false;
2057
  }
2058
  QMessageBox::critical(
3 ✔
2059
      this, tr("Not a folder of the store"),
3 ✔
2060
      tr("%1 is, or lies behind, a symbolic link or junction. What that "
3 ✔
2061
         "points to is not part of the password store and is left alone.")
2062
          .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
6 ✔
2063
  return true;
3 ✔
2064
}
2065

2066
/**
2067
 * @brief MainWindow::showShareHelp show help about GPG sharing
2068
 */
2069
void MainWindow::showShareHelp() {
1 ✔
2070
  QMessageBox::information(
1 ✔
2071
      this, tr("Sharing passwords with GPG"),
1 ✔
2072
      tr("<h3>Sharing passwords with GPG</h3>"
1 ✔
2073
         "<p>To share passwords with other users:</p>"
2074
         "<ol>"
2075
         "<li><b>Export your public key</b> and send it to teammates</li>"
2076
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
2077
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
2078
         "them</li>"
2079
         "</ol>"
2080
         "<p>Only people who have a matching secret key can decrypt the "
2081
         "passwords.</p>"
2082
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
2083
         "<p>See the FAQ for more details.</p>"));
2084
}
1 ✔
2085

2086
void MainWindow::updateGitButtonVisibility(bool uiEnabled) {
213 ✔
2087
  const AppSettings s = QtPassSettings::load();
213 ✔
2088
  // Without Git there is nothing to push or pull, ever: the buttons go, not
2089
  // grey out. While an operation runs they stay and are disabled.
2090
  const bool usable =
2091
      s.useGit && !(s.gitExecutable.isEmpty() && s.passExecutable.isEmpty());
213 ✔
2092
  ui->actionPush->setVisible(usable);
213 ✔
2093
  ui->actionUpdate->setVisible(usable);
213 ✔
2094
  // The separator in front of them would otherwise sit next to the next one.
2095
  const QList<QAction *> actions = ui->toolBar->actions();
213 ✔
2096
  const qsizetype at = actions.indexOf(ui->actionPush);
213 ✔
2097
  if (at > 0 && actions.at(at - 1)->isSeparator()) {
213 ✔
2098
    actions.at(at - 1)->setVisible(usable);
213 ✔
2099
  }
2100
  enableGitButtons(usable && uiEnabled);
213 ✔
2101
}
213 ✔
2102

2103
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
213 ✔
2104
  // No platform gating any more: codes are generated in-process, so this works
2105
  // on Windows and macOS and with either backend. It used to be hidden there
2106
  // because the pass-otp extension is Unix-only.
2107
  //
2108
  // Kept visible but disabled when OTP is off, as it was before: hiding it
2109
  // removed the only discoverable trace of the feature, and since an OTP field
2110
  // is suppressed from the panel unconditionally (its value is a secret), a
2111
  // user with the setting off would see nothing at all where their OTP data
2112
  // used to be.
2113
  //
2114
  // Enablement must honour uiEnabled: this is called from
2115
  // setUiElementsEnabled(), and ignoring the argument re-enabled the action
2116
  // while a decrypt was in flight, letting the user stack Show calls.
2117
  ui->actionOtp->setVisible(true);
213 ✔
2118
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
213 ✔
2119
}
213 ✔
2120

2121
void MainWindow::updateGrepButtonVisibility() {
110 ✔
2122
  const bool enabled = QtPassSettings::isUseGrepSearch();
110 ✔
2123
  ui->grepButton->setVisible(enabled);
110 ✔
2124
  ui->grepCaseButton->setVisible(enabled);
110 ✔
2125
  if (!enabled && m_grep.inGrepMode()) {
110 ✔
2126
    ui->grepButton->setChecked(false);
1 ✔
2127
  }
2128
}
110 ✔
2129

2130
void MainWindow::enableGitButtons(const bool &state) {
213 ✔
2131
  ui->actionPush->setEnabled(state);
213 ✔
2132
  ui->actionUpdate->setEnabled(state);
213 ✔
2133
}
213 ✔
2134

2135
/**
2136
 * @brief MainWindow::critical critical message popup wrapper.
2137
 * @param title
2138
 * @param msg
2139
 */
2140
void MainWindow::critical(const QString &title, const QString &msg) {
2 ✔
2141
  QMessageBox::critical(this, title, msg);
2 ✔
2142
}
2 ✔
2143

2144
/**
2145
 * @brief Route a process's output to the console, unless the panel is
2146
 *        switched off in the settings.
2147
 * @param output The stdout/stderr text from the process.
2148
 * @param isError true if this is error output (stderr).
2149
 * @param pid The process that ran, for the line prefix.
2150
 */
2151
void MainWindow::onProcessOutput(const QString &output, bool isError,
6 ✔
2152
                                 Enums::PROCESS pid) {
2153
  if (!QtPassSettings::isShowProcessOutput()) {
6 ✔
2154
    return;
2155
  }
2156
  m_processOutput->append(output, isError,
5 ✔
2157
                          ProcessOutputPanel::processName(pid));
10 ✔
2158
}
2159

2160
/**
2161
 * @brief Show or hide the console per the showProcessOutput setting.
2162
 */
2163
void MainWindow::updateProcessOutputVisibility() {
2 ✔
2164
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
2 ✔
2165
}
2 ✔
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