QtPass Developer documentation
QtPass test coverage
Current view: top level - src/mainwindow.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 97.8 % 1064 1041
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 98 98

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements MainWindow; the backend signal wiring lives in
       6              :  *        connectBackends() and connectPassSignals(), the decrypted-entry
       7              :  *        rendering in passShowHandler().
       8              :  */
       9              : #include "mainwindow.h"
      10              : 
      11              : #include "qtpasslogging.h"
      12              : 
      13              : #include "configdialog.h"
      14              : #include "enums.h"
      15              : #include "executor.h"
      16              : #include "exportpublickeydialog.h"
      17              : #include "filecontent.h"
      18              : #include "firstrunwizard.h"
      19              : #include "menubarpeek.h"
      20              : #include "nativegrep.h"
      21              : #include "passworddialog.h"
      22              : #include "passworddisplaypanel.h"
      23              : #include "pathvalidator.h"
      24              : #include "plainmessagebox.h"
      25              : #include "processoutputpanel.h"
      26              : #include "qpushbuttonasqrcode.h"
      27              : #include "qpushbuttonshowpassword.h"
      28              : #include "qpushbuttonwithclipboard.h"
      29              : #include "qtpass.h"
      30              : #include "qtpasssettings.h"
      31              : #include "templateio.h"
      32              : #include "totp.h"
      33              : #include "trayicon.h"
      34              : #include "ui_mainwindow.h"
      35              : #include "usersdialog.h"
      36              : #include "util.h"
      37              : #include "windowstatestore.h"
      38              : #include <QApplication>
      39              : #include <QCloseEvent>
      40              : #include <QDate>
      41              : #include <QDesktopServices>
      42              : #include <QDialog>
      43              : #include <QDockWidget>
      44              : #include <QFileInfo>
      45              : #include <QHBoxLayout>
      46              : #include <QInputDialog>
      47              : #include <QLabel>
      48              : #include <QLineEdit>
      49              : #include <QMenu>
      50              : #include <QMenuBar>
      51              : #include <QMessageBox>
      52              : #include <QProgressDialog>
      53              : #include <QPushButton>
      54              : #include <QShortcut>
      55              : #include <QTextCharFormat>
      56              : #include <QTextStream>
      57              : #include <QTimer>
      58              : #include <QTreeWidget>
      59              : #include <QUrl>
      60              : #include <algorithm>
      61              : #include <array>
      62              : #include <utility>
      63              : 
      64          131 : MainWindow::MainWindow(const QString &searchText, QWidget *parent)
      65          131 :     : QMainWindow(parent), ui(new Ui::MainWindow) {
      66              : #ifdef __APPLE__
      67              :   // extra treatment for mac os
      68              :   // see https://doc.qt.io/qt-6/qkeysequence.html#qt_set_sequence_auto_mnemonic
      69              :   qt_set_sequence_auto_mnemonic(true);
      70              : #endif
      71          131 :   ui->setupUi(this);
      72              : 
      73          131 :   m_qtPass = new QtPass(this);
      74          131 :   restoreWindow();
      75          131 :   connectBackends();
      76              : 
      77          131 :   new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
      78          131 :                 &MainWindow::copyPasswordFromTreeview);
      79              : 
      80          131 :   m_tree = new StoreTree(ui->treeView, this);
      81          131 :   m_tree->setStore(QtPassSettings::getPassStore(Util::findPasswordStore()));
      82          131 :   m_tree->setPass(QtPassSettings::getPass());
      83          131 :   connect(ui->treeView, &QWidget::customContextMenuRequested, this,
      84          131 :           &MainWindow::showContextMenu);
      85          131 :   connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
      86          131 :           &MainWindow::deselect);
      87              : 
      88          131 :   initTextBrowser();
      89              : 
      90          131 :   updateProfileBox();
      91              : 
      92          131 :   initDisplayPanel();
      93              : 
      94          131 :   QtPassSettings::getPass()->updateEnv();
      95              : 
      96              :   // Install the search-box key filter once, not on every setUiElementsEnabled
      97              :   // call.
      98          131 :   ui->lineEdit->installEventFilter(this);
      99              :   // Both live in Breeze's "tools area" and get its header palette; see the
     100              :   // PaletteChange branch in eventFilter().
     101          131 :   ui->toolBar->installEventFilter(this);
     102          131 :   menuBar()->installEventFilter(this);
     103              : 
     104          131 :   initTimers();
     105              : 
     106          131 :   initToolBarButtons();
     107          131 :   initStatusBar();
     108          131 :   initProcessOutput();
     109              : 
     110          131 :   ui->lineEdit->setClearButtonEnabled(true);
     111          131 :   updateGrepButtonVisibility();
     112              : 
     113          131 :   setUiElementsEnabled(true);
     114              : 
     115          131 :   ui->lineEdit->setText(searchText);
     116              : 
     117              :   // No QApplication::quit() here: before exec() it is a documented no-op.
     118              :   // main() checks initSucceeded() and exits before show() instead.
     119          131 :   m_qtPass->init();
     120              :   // OK is not gated on Util::configIsValid(): an accepted configuration can
     121              :   // still lack a .gpg-id, so ask again; only a cancel ends the loop.
     122          132 :   while (!Util::configIsValid(QtPassSettings::load())) {
     123            2 :     if (!config()) {
     124              :       return;
     125              :     }
     126              :   }
     127          130 :   m_freshStart = false;
     128          130 :   m_initSucceeded = true;
     129              : 
     130              :   // Initial focus is set in showEvent(): a 10 ms QTimer here could fire
     131              :   // before the window was realised (queued ActivationChange, nested exec() in
     132              :   // init()) and selectAll() segfaulted inside Qt (#1187, #1188).
     133            0 : }
     134              : 
     135          131 : void MainWindow::connectBackends() {
     136          131 :   connect(m_qtPass, &QtPass::outputReady, this,
     137          154 :           [this](const QString &html) { flashText(html, false, true); });
     138          131 :   connect(m_qtPass, &QtPass::operationFinished, this,
     139          155 :           [this]() { setUiElementsEnabled(true); });
     140          131 :   connect(m_qtPass, &QtPass::statusMessage, this,
     141          131 :           &MainWindow::showStatusMessage);
     142          131 :   connect(m_qtPass, &QtPass::pushRequested, this, &MainWindow::onPush);
     143          131 :   connect(m_qtPass, &QtPass::entryInserted, this,
     144          132 :           [this]() { on_treeView_clicked(getCurrentTreeViewIndex()); });
     145          131 :   connect(&m_qtPass->clipboard(), &ClipboardManager::statusMessage, this,
     146          141 :           [this](const QString &message) { showStatusMessage(message); });
     147              :   // Both backends: the active one can change through the configuration
     148              :   // dialog without a restart.
     149          131 :   connectPassSignals(QtPassSettings::getRealPass());
     150          131 :   connectPassSignals(QtPassSettings::getImitatePass());
     151          131 :   ImitatePass *ipass = QtPassSettings::getImitatePass();
     152          131 :   connect(ipass, &ImitatePass::startReencryptPath, this,
     153          131 :           &MainWindow::startReencryptPath);
     154          131 :   connect(ipass, &ImitatePass::reencryptProgress, this,
     155          131 :           &MainWindow::reencryptProgress);
     156          131 :   connect(ipass, &ImitatePass::endReencryptPath, this,
     157          131 :           &MainWindow::endReencryptPath);
     158          131 : }
     159              : 
     160          131 : void MainWindow::initTextBrowser() {
     161              :   {
     162          131 :     const AppSettings s = QtPassSettings::load();
     163          131 :     if (s.useMonospace) {
     164            1 :       QFont monospace("Monospace");
     165            1 :       monospace.setStyleHint(QFont::Monospace);
     166            1 :       ui->textBrowser->setFont(monospace);
     167            1 :     }
     168          131 :     if (s.noLineWrapping) {
     169            1 :       ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
     170              :     }
     171          131 :     clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
     172          131 :   }
     173          131 :   ui->textBrowser->setOpenExternalLinks(true);
     174          131 :   ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
     175          131 :   connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
     176          131 :           &MainWindow::showBrowserContextMenu);
     177          131 : }
     178              : 
     179          131 : void MainWindow::initDisplayPanel() {
     180          131 :   m_displayPanel = new PasswordDisplayPanel(
     181          131 :       ui->gridLayout, ui->verticalLayoutPassword, this, this);
     182          131 :   connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
     183          131 :           &m_qtPass->clipboard(), &ClipboardManager::copyText);
     184          131 :   connect(m_displayPanel, &PasswordDisplayPanel::textCopied,
     185          131 :           &m_qtPass->clipboard(), &ClipboardManager::trackCopied);
     186          131 :   connect(ui->textBrowser, &SecretTextBrowser::copied, &m_qtPass->clipboard(),
     187          131 :           &ClipboardManager::trackCopied);
     188          131 :   connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
     189          131 :           &QtPass::showTextAsQRCode);
     190              :   // Double-click on what is shown edits it, as in the tree.
     191          131 :   connect(m_displayPanel, &PasswordDisplayPanel::editRequested, this, [this] {
     192            3 :     if (ui->actionEdit->isEnabled()) {
     193            2 :       onEdit();
     194              :     }
     195            3 :   });
     196          131 : }
     197              : 
     198          131 : void MainWindow::initTimers() {
     199          131 :   clearPanelTimer.setSingleShot(true);
     200          133 :   connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
     201              : 
     202          131 :   searchTimer.setInterval(350);
     203          131 :   searchTimer.setSingleShot(true);
     204              : 
     205          131 :   connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
     206              : 
     207              :   // Safety net: if a backend operation disables the UI but never signals
     208              :   // completion, re-enable after a timeout so the window can't get stuck.
     209          131 :   m_uiWatchdog.setSingleShot(true);
     210          131 :   m_uiWatchdog.setInterval(UiWatchdogMs);
     211          131 :   connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
     212            0 :     showStatusMessage(tr("Operation timed out; re-enabling interface."));
     213              :     // Drop any in-flight OTP request so a late finishedShow cannot be mistaken
     214              :     // for the answer to it.
     215            0 :     cancelOtpRequest();
     216            0 :     setUiElementsEnabled(true);
     217            0 :   });
     218          131 : }
     219              : 
     220          131 : void MainWindow::initProcessOutput() {
     221          131 :   m_processOutput = new ProcessOutputPanel(this);
     222          131 :   addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
     223              :   // After addDockWidget so our preference wins over any cached state
     224              :   // QMainWindow applies; restoreWindow() already processed the saved layout.
     225          131 :   m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
     226              :   // From the setting, not isVisible(): the window is not shown yet, so no
     227              :   // child of it reports visible.
     228          131 :   ui->actionShowProcessOutput->setChecked(
     229          131 :       QtPassSettings::isShowProcessOutput());
     230          131 :   connect(ui->actionShowProcessOutput, &QAction::toggled, this,
     231          131 :           [this](bool show) {
     232            2 :             m_processOutput->setVisible(show);
     233            2 :             AppSettings s = QtPassSettings::load();
     234            2 :             if (s.showProcessOutput != show) {
     235            2 :               s.showProcessOutput = show;
     236            2 :               QtPassSettings::save(s);
     237              :             }
     238            2 :           });
     239              : 
     240          131 :   connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
     241          131 :           [this](const QString &out, const QString &err, Enums::PROCESS pid) {
     242              :             // Never route potentially-secret output through the panel:
     243              :             // PASS_SHOW goes to the text browser (cleared on a timer when
     244              :             // "Autoclear panel" is set),
     245              :             // PASS_GREP would return password-file lines (#252; it is no
     246              :             // longer started, the check stays), and PASS_INSERT
     247              :             // is excluded defensively in case a future path uses --echo.
     248            5 :             if (ProcessOutputPanel::isSensitiveProcess(pid)) {
     249              :               return;
     250              :             }
     251            4 :             if (!out.isEmpty()) {
     252            1 :               onProcessOutput(out, false, pid);
     253              :             }
     254            4 :             if (!err.isEmpty()) {
     255            3 :               onProcessOutput(err, true, pid);
     256              :             }
     257              :           });
     258          131 : }
     259              : 
     260          258 : MainWindow::~MainWindow() {
     261              :   // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
     262              :   // window still knows its last geometry here.
     263          131 :   saveWindowState();
     264          131 :   delete m_qtPass;
     265          389 : }
     266              : 
     267              : // The signals of one backend that the window answers itself.
     268          262 : void MainWindow::connectPassSignals(Pass *pass) {
     269              :   // A failed decrypt never emits finishedShow; drop the pending OTP/copy
     270              :   // request so a later decrypt of the same entry is not taken as its answer.
     271          262 :   connect(pass, &Pass::processErrorExit, this, &MainWindow::onOperationError);
     272          262 :   connect(pass, &Pass::critical, this, &MainWindow::critical);
     273          262 :   connect(pass, &Pass::startingExecuteWrapper, this,
     274          262 :           &MainWindow::executeWrapperStarted);
     275              :   // The profile box comes back when the queue is done, also after the
     276              :   // watchdog released the rest of the interface early.
     277          262 :   connect(pass, &Pass::idle, this, &MainWindow::updateProfileBoxEnabled);
     278          262 :   connect(pass, &Pass::statusMsg, this, &MainWindow::showStatusMessage);
     279          262 :   connect(pass, &Pass::finishedShow, this, &MainWindow::passShowHandler);
     280              :   // Both check the file against their own pending request and ignore the
     281              :   // rest, so they can stay connected for good.
     282          262 :   connect(pass, &Pass::finishedShow, this, &MainWindow::otpFromFileToClipboard);
     283          262 :   connect(pass, &Pass::finishedShow, this,
     284          262 :           &MainWindow::passwordFromFileToClipboard);
     285          262 :   connect(pass, &Pass::finishedGrep, this, &MainWindow::onGrepFinished);
     286          262 : }
     287              : 
     288              : // Idempotent; called from closeEvent() and the destructor.
     289          133 : void MainWindow::saveWindowState() {
     290          133 :   QtPassSettings::setGeometry(saveGeometry());
     291          133 :   QtPassSettings::setSavestate(saveState());
     292          133 : }
     293              : 
     294           43 : void MainWindow::focusInput() {
     295              :   // findChild() rather than ui->lineEdit: on a fresh-config first launch
     296              :   // config() calls setWindowFlags(), which rebuilds the native window, and
     297              :   // under Qt 6.11 the QLineEdit was destroyed while ui->lineEdit kept its old
     298              :   // address (SIGSEGV in QWidget::testAttribute). findChild() returns null.
     299           43 :   if (!isVisible()) {
     300              :     return;
     301              :   }
     302           43 :   auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
     303           43 :   if (lineEdit == nullptr || !lineEdit->isVisible()) {
     304              :     return;
     305              :   }
     306           43 :   lineEdit->selectAll();
     307           43 :   lineEdit->setFocus();
     308              :   // Only once focus has landed: set in showEvent(), an early return above
     309              :   // would consume the one-shot and never retry.
     310           43 :   m_firstShowCompleted = true;
     311              : }
     312              : 
     313         5798 : auto MainWindow::event(QEvent *event) -> bool {
     314         5798 :   if (event->type() == QEvent::ApplicationPaletteChange) {
     315           11 :     m_themeSwitched = true;
     316              :     // Breeze may re-stamp the bars before this arrives (Plasma 6 does):
     317              :     // their PaletteChange then found no switch yet, so check them now too.
     318           11 :     QTimer::singleShot(0, this, &MainWindow::dropStaleToolsAreaPalettes);
     319              :   }
     320         5798 :   return QMainWindow::event(event);
     321              : }
     322              : 
     323          175 : void MainWindow::changeEvent(QEvent *event) {
     324          175 :   QWidget::changeEvent(event);
     325          175 :   if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
     326              :     // Desktop switched light/dark. Top-level widgets get PaletteChange
     327              :     // (ApplicationPaletteChange goes to QApplication); stylesheets with
     328              :     // baked-in palette colours must be re-derived by hand.
     329           10 :     m_displayPanel->refreshPalette();
     330              :   }
     331          175 :   if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
     332              :       isVisible()) {
     333              :     // Defer a tick so the activation dispatch chain unwinds first: calling
     334              :     // focusInput() inline has segfaulted on half-rebuilt ui state.
     335           25 :     QMetaObject::invokeMethod(this, &MainWindow::focusInput,
     336              :                               Qt::QueuedConnection);
     337              :   }
     338          175 : }
     339              : 
     340           27 : void MainWindow::showEvent(QShowEvent *event) {
     341           27 :   QMainWindow::showEvent(event);
     342           27 :   if (m_firstShowCompleted) {
     343              :     return;
     344              :   }
     345              :   // Queued so the platform map and pending rebuilds (setWindowFlags on the
     346              :   // wizard path) settle first. focusInput() sets the latch only on success,
     347              :   // so a failed lookup retries on the next show.
     348           25 :   QMetaObject::invokeMethod(this, &MainWindow::focusInput,
     349              :                             Qt::QueuedConnection);
     350              : }
     351              : 
     352          131 : void MainWindow::initToolBarButtons() {
     353          131 :   connect(ui->actionAddPassword, &QAction::triggered, this,
     354          131 :           &MainWindow::addPassword);
     355          131 :   connect(ui->actionAddFolder, &QAction::triggered, this,
     356          131 :           &MainWindow::addFolder);
     357          131 :   connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
     358          131 :   connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
     359          131 :   connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
     360          131 :   connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
     361          131 :   connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
     362          131 :   connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
     363          131 :   connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
     364              : 
     365              :   // Menu-only. Quit ends the application, tray icon or not, as in KeePassXC,
     366              :   // Telegram and the KDE, GNOME and macOS guidelines (#1788). Close is
     367              :   // close(), a hide when "hide on close" is set, like the X button and Alt+F4.
     368          131 :   connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
     369          131 :   connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
     370              : #ifdef Q_OS_MACOS
     371              :   // The menu bar is the system's up there; nothing to hide.
     372              :   ui->actionShowMenuBar->setVisible(false);
     373              : #else
     374              :   // The action lives in the menu it hides, so it has to be on the window as
     375              :   // well for Ctrl+M to keep working while the bar is gone.
     376          131 :   addAction(ui->actionShowMenuBar);
     377          131 :   ui->actionShowMenuBar->setChecked(QtPassSettings::load().showMenuBar);
     378          131 :   menuBar()->setVisible(ui->actionShowMenuBar->isChecked());
     379          131 :   connect(ui->actionShowMenuBar, &QAction::toggled, this, [this](bool show) {
     380            5 :     menuBar()->setVisible(show);
     381            5 :     AppSettings s = QtPassSettings::load();
     382            5 :     s.showMenuBar = show;
     383            5 :     QtPassSettings::save(s);
     384            5 :   });
     385          131 :   new MenuBarPeek(this, ui->actionShowMenuBar);
     386              : #endif
     387          132 :   connect(ui->actionFaq, &QAction::triggered, this, [] {
     388            2 :     QDesktopServices::openUrl(QUrl(QStringLiteral("https://qtpass.org/faq")));
     389            1 :   });
     390          131 :   connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
     391          131 :   connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
     392              : 
     393          131 :   ui->actionAddPassword->setIcon(
     394          524 :       QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
     395          131 :   ui->actionAddFolder->setIcon(
     396          524 :       QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
     397          131 :   ui->actionEdit->setIcon(QIcon::fromTheme(
     398          393 :       "document-properties", QIcon(":/icons/document-properties.svg")));
     399          131 :   ui->actionDelete->setIcon(
     400          524 :       QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
     401          131 :   ui->actionPush->setIcon(
     402          524 :       QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
     403          131 :   ui->actionUpdate->setIcon(
     404          524 :       QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
     405          131 :   ui->actionUsers->setIcon(QIcon::fromTheme(
     406          393 :       "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
     407          131 :   ui->actionConfig->setIcon(QIcon::fromTheme(
     408          262 :       "applications-system", QIcon(":/icons/applications-system.svg")));
     409          131 : }
     410              : 
     411          131 : void MainWindow::initStatusBar() {
     412          262 :   ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
     413              : 
     414          262 :   QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
     415          131 :                      .scaledToHeight(statusBar()->height());
     416          131 :   auto *logoApp = new QLabel(statusBar());
     417          131 :   logoApp->setPixmap(logo);
     418          131 :   statusBar()->addPermanentWidget(logoApp);
     419          131 : }
     420              : 
     421            1 : auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
     422            1 :   return ui->treeView->currentIndex();
     423              : }
     424              : 
     425           49 : void MainWindow::flashText(const QString &text, const bool isError,
     426              :                            const bool isHtml) {
     427           49 :   if (isError) {
     428            7 :     ui->textBrowser->setTextColor(Qt::red);
     429              :   } else {
     430              :     // setText() re-applies the current char format, red after an error, to
     431              :     // the whole document. Clear the foreground rather than pin a palette
     432              :     // colour, so text follows runtime light/dark switches (#946).
     433           42 :     QTextCharFormat format = ui->textBrowser->currentCharFormat();
     434              :     format.clearForeground();
     435           42 :     ui->textBrowser->setCurrentCharFormat(format);
     436              :   }
     437              : 
     438           49 :   if (isHtml) {
     439              :     QString _text = text;
     440           80 :     if (!ui->textBrowser->toPlainText().isEmpty()) {
     441           26 :       _text = ui->textBrowser->toHtml() + _text;
     442              :     }
     443           40 :     ui->textBrowser->setHtml(_text);
     444              :   } else {
     445            9 :     ui->textBrowser->setText(text);
     446              :   }
     447           49 : }
     448              : 
     449            2 : void MainWindow::applyTextBrowserSettings() {
     450            2 :   const AppSettings s = QtPassSettings::load();
     451            2 :   if (s.useMonospace) {
     452            1 :     QFont monospace("Monospace");
     453            1 :     monospace.setStyleHint(QFont::Monospace);
     454            1 :     ui->textBrowser->setFont(monospace);
     455            1 :   } else {
     456            1 :     ui->textBrowser->setFont(QFont());
     457              :   }
     458              : 
     459            2 :   if (s.noLineWrapping) {
     460            1 :     ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
     461              :   } else {
     462            1 :     ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
     463              :   }
     464            2 : }
     465              : 
     466          142 : void MainWindow::applyWindowFlagsSettings() {
     467          142 :   const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
     468              :   const Qt::WindowFlags flags = windowFlags();
     469          142 :   if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
     470              :     // setWindowFlags() rebuilds the native window, the teardown that left
     471              :     // ui->lineEdit dangling (see focusInput()); skip it when nothing changes.
     472              :     return;
     473              :   }
     474              :   // Toggle just this hint; resetting to Qt::Window discarded every other flag.
     475              :   const bool wasVisible = isVisible();
     476            4 :   setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
     477              :                            : (flags & ~Qt::WindowStaysOnTopHint));
     478              :   // setWindowFlags() hides a shown window; a window that was not shown yet
     479              :   // (first-run wizard from the constructor) is left to its normal show.
     480            2 :   if (wasVisible) {
     481            2 :     show();
     482              :   }
     483              : }
     484              : 
     485            4 : auto MainWindow::config() -> bool {
     486            4 :   if (m_freshStart) {
     487              :     // No usable configuration yet: the wizard finds the programs, the key
     488              :     // and the store, and creates the store when it has to.
     489            2 :     FirstRunWizard wizard(this);
     490            2 :     if (wizard.exec() != QDialog::Accepted) {
     491              :       return false;
     492              :     }
     493            2 :   } else {
     494            2 :     ConfigDialog d(this);
     495            2 :     d.setModal(true);
     496            2 :     if (d.exec() != QDialog::Accepted) {
     497              :       return false;
     498              :     }
     499            2 :   }
     500              : 
     501            2 :   applyTextBrowserSettings();
     502            2 :   applyWindowFlagsSettings();
     503              : 
     504            2 :   updateProfileBox();
     505            2 :   const AppSettings s = QtPassSettings::load();
     506            2 :   m_tree->setStore(s.passStore);
     507            2 :   deselect();
     508            2 :   ui->treeView->setCurrentIndex(QModelIndex());
     509              : 
     510            2 :   Pass *activePass = QtPassSettings::getPass();
     511            2 :   activePass->updateEnv();
     512            2 :   m_tree->setPass(activePass);
     513            2 :   clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
     514            2 :   m_qtPass->clipboard().setAutoclearTimer();
     515              : 
     516            2 :   updateGitButtonVisibility();
     517            2 :   updateOtpButtonVisibility();
     518            2 :   updateGrepButtonVisibility();
     519            2 :   updateProcessOutputVisibility();
     520              : #ifndef Q_OS_MACOS
     521              :   // The action's toggled handler shows or hides the bar (and saves again).
     522            2 :   ui->actionShowMenuBar->setChecked(s.showMenuBar);
     523              : #endif
     524            2 :   if (s.useTrayIcon && m_tray == nullptr) {
     525            1 :     initTrayIcon();
     526            1 :   } else if (!s.useTrayIcon && m_tray != nullptr) {
     527            0 :     destroyTrayIcon();
     528              :   }
     529              : 
     530              :   // Stay fresh-start while the configuration is unusable, so the
     531              :   // constructor's loop re-runs the wizard. The re-prompt lives there:
     532              :   // recursing here never reported a cancel back to the caller.
     533            2 :   if (Util::configIsValid(s)) {
     534            2 :     m_freshStart = false;
     535              :   }
     536              :   return true;
     537            2 : }
     538              : 
     539            2 : void MainWindow::onUpdate(bool block) {
     540            2 :   ui->statusBar->showMessage(tr("Updating password-store"), 2000);
     541            2 :   if (block) {
     542            1 :     QtPassSettings::getPass()->GitPull_b();
     543              :   } else {
     544            1 :     QtPassSettings::getPass()->GitPull();
     545              :   }
     546            2 : }
     547              : 
     548            1 : void MainWindow::onPush() {
     549            1 :   if (QtPassSettings::isUseGit()) {
     550            1 :     ui->statusBar->showMessage(tr("Updating password-store"), 2000);
     551            1 :     QtPassSettings::getPass()->GitPush();
     552              :   }
     553            1 : }
     554              : 
     555           37 : auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
     556           37 :   return m_tree->fileFor(index, forPass);
     557              : }
     558              : 
     559           13 : void MainWindow::on_treeView_clicked(const QModelIndex &index) {
     560           13 :   bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
     561           13 :   QString file = getFile(index, true);
     562           13 :   ui->passwordName->setText(file);
     563           13 :   if (!file.isEmpty() && !cleared) {
     564              :     // Remember what the panel is about to show, so onOtp() can tell whether the
     565              :     // code it can see belongs to the entry that is currently selected.
     566           12 :     m_shownFile = file;
     567           24 :     QtPassSettings::getPass()->Show(file);
     568              :   } else {
     569            1 :     m_shownFile.clear();
     570            1 :     clearPanel(false);
     571            1 :     ui->actionEdit->setEnabled(false);
     572            1 :     ui->actionDelete->setEnabled(true);
     573              :   }
     574           13 : }
     575              : 
     576            4 : void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
     577            4 :   QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
     578              : 
     579            4 :   if (fileOrFolder.isFile()) {
     580            6 :     editPassword(getFile(index, true));
     581              :   }
     582            4 : }
     583              : 
     584           13 : void MainWindow::deselect() {
     585           13 :   m_shownFile.clear();
     586           13 :   cancelOtpRequest();
     587           13 :   m_qtPass->clipboard().clear();
     588           13 :   ui->treeView->clearSelection();
     589           13 :   ui->actionEdit->setEnabled(false);
     590           13 :   ui->actionDelete->setEnabled(false);
     591           13 :   ui->passwordName->setText("");
     592           13 :   clearPanel(false);
     593           13 : }
     594              : 
     595           34 : void MainWindow::executeWrapperStarted() {
     596           34 :   m_displayPanel->clear();
     597           34 :   ui->textBrowser->clear();
     598           34 :   setUiElementsEnabled(false);
     599           34 :   clearPanelTimer.stop();
     600           34 :   if (QtPassSettings::isShowProcessOutput()) {
     601           29 :     m_processOutput->setVisible(true);
     602              :   }
     603           34 : }
     604              : 
     605           24 : void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
     606              :   // Only the entry we most recently asked to show may repaint the panel; a
     607              :   // slower decrypt of an entry the user has since left is dropped, and so is
     608              :   // one that arrives after the selection was cleared (deselect, a profile
     609              :   // switch): nothing is asked to be shown then.
     610           24 :   if (!file.isEmpty() && file != m_shownFile) {
     611              :     // A newer show is pending and releases the interface when it lands;
     612              :     // with none (the selection was cleared) nothing else would until the
     613              :     // watchdog. setUiElementsEnabled() keeps a re-encryption's lock.
     614            8 :     if (m_shownFile.isEmpty()) {
     615            7 :       setUiElementsEnabled(true);
     616              :     }
     617            8 :     return;
     618              :   }
     619           16 :   const AppSettings s = QtPassSettings::load();
     620              :   // What a template marks secret, in the settings or the store's
     621              :   // .templates; read per entry shown, so an edit there counts at once.
     622           16 :   if (s.hidePassword) {
     623            2 :     m_displayPanel->setMarkedSecrets(
     624            4 :         TemplateIO::secretFieldsIn(s.passTemplate) +
     625            6 :         TemplateIO::readSecretFields(s.passStore));
     626              :   }
     627              :   QStringList templ =
     628           16 :       s.useTemplate ? TemplateIO::fieldsIn(s.passTemplate) : QStringList();
     629           16 :   bool allFields = s.useTemplate && s.templateAllFields;
     630              :   // A secret the panel hides is a field even when the template doesn't list
     631              :   // it: left in the text below the fields, it would show in plain view.
     632              :   FileContent fileContent = FileContent::parse(
     633           16 :       p_output, templ, allFields, [this, &s](const QString &name) {
     634            7 :         return m_displayPanel->isSecret(name, s);
     635           16 :       });
     636              :   QString output = p_output;
     637              :   // Display variant: empty when the password line is itself an otpauth URI, so
     638              :   // the shared secret is neither rendered nor copied to the clipboard.
     639           16 :   QString password = fileContent.getPasswordForDisplay();
     640              : 
     641              :   // Skipped for an OTP request: the user asked for a code, and two
     642              :   // OleSetClipboard calls in one turn can leave the Windows clipboard empty.
     643           16 :   if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
     644           14 :     m_qtPass->clipboard().copyIfAlways(password, p_output);
     645              :   }
     646              : 
     647           16 :   m_displayPanel->clear();
     648              : 
     649           16 :   if (s.hideContent) {
     650            4 :     output = "***" + tr("Content hidden") + "***";
     651           14 :   } else if (s.displayAsIs) {
     652              :     // As it is, but not a block per line of millions.
     653            0 :     output = FileContent::boundedForDisplay(p_output);
     654              :   } else {
     655           28 :     m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
     656           14 :                                   s.useOtp ? fileContent.getOtpUri()
     657              :                                            : QString());
     658           28 :     output = fileContent.getRemainingDataForDisplay();
     659              :   }
     660              : 
     661           16 :   if (s.useAutoclearPanel) {
     662            2 :     clearPanelTimer.start();
     663              :   }
     664              : 
     665           32 :   flashText(QtPass::formatOutput(output), false, true);
     666           16 :   setUiElementsEnabled(true);
     667           32 : }
     668              : 
     669           20 : void MainWindow::otpFromFileToClipboard(const QString &p_output,
     670              :                                         const QString &file) {
     671              :   // Permanently connected: only the decrypt of the entry onOtp() asked for
     672              :   // is ours, everything else belongs to a tree click or a Ctrl+C.
     673           20 :   if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
     674           17 :     return;
     675              :   }
     676            5 :   m_otpRequestFile.clear();
     677              : 
     678            5 :   if (p_output.isEmpty()) {
     679              :     // Distinguish "could not read the entry" from "entry has no OTP".
     680            1 :     flashText(tr("Could not decrypt this password entry"), true);
     681            1 :     setUiElementsEnabled(true);
     682            1 :     return;
     683              :   }
     684              : 
     685              :   // passShowHandler ran first for this finishedShow; if it rendered the OTP
     686              :   // row, reuse its code (as onOtp()'s fast path does). Otherwise parse:
     687              :   // hideContent, displayAsIs or no OTP field.
     688            4 :   const QString shown = m_displayPanel->currentOtpCode();
     689            4 :   if (!shown.isEmpty()) {
     690            1 :     m_qtPass->clipboard().copyText(shown);
     691            1 :     showStatusMessage(tr("OTP code copied to clipboard"));
     692            1 :     setUiElementsEnabled(true);
     693              :     return;
     694              :   }
     695              : 
     696            3 :   const AppSettings s = QtPassSettings::load();
     697              :   // Parse with the same template settings passShowHandler uses, so an OTP
     698              :   // field is recognised identically in both paths.
     699              :   const QStringList templ =
     700            3 :       s.useTemplate ? TemplateIO::fieldsIn(s.passTemplate) : QStringList();
     701            3 :   const bool allFields = s.useTemplate && s.templateAllFields;
     702              :   const FileContent fileContent =
     703            3 :       FileContent::parse(p_output, templ, allFields);
     704              : 
     705              :   const std::optional<Totp::Settings> settings =
     706            6 :       Totp::parse(fileContent.getOtpUri());
     707            3 :   if (settings.has_value()) {
     708            4 :     m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
     709            4 :     showStatusMessage(tr("OTP code copied to clipboard"));
     710              :   } else {
     711            2 :     flashText(tr("No OTP code found in this password entry"), true);
     712              :   }
     713            3 :   setUiElementsEnabled(true);
     714            6 : }
     715              : 
     716           35 : void MainWindow::clearPanel(bool notify) {
     717           35 :   m_displayPanel->clear();
     718           35 :   const bool grepWasVisible = ui->grepResultsList->isVisible();
     719           35 :   ui->grepResultsList->clear();
     720           35 :   if (grepWasVisible) {
     721            2 :     ui->grepResultsList->setVisible(false);
     722            2 :     ui->treeView->setVisible(true);
     723            2 :     if (m_grep.inGrepMode()) {
     724              :       m_grep.clearGrepMode();
     725            2 :       ui->grepButton->blockSignals(true);
     726            2 :       ui->grepButton->setChecked(false);
     727            2 :       ui->grepButton->blockSignals(false);
     728            2 :       ui->lineEdit->blockSignals(true);
     729            2 :       ui->lineEdit->clear();
     730            2 :       ui->lineEdit->blockSignals(false);
     731            4 :       ui->lineEdit->setPlaceholderText(tr("Search password"));
     732              :     }
     733              :   }
     734           35 :   if (notify) {
     735            4 :     QString output = "***" + tr("Password and content hidden") + "***";
     736            2 :     ui->textBrowser->setHtml(output);
     737              :   } else {
     738           66 :     ui->textBrowser->setHtml("");
     739              :   }
     740           35 : }
     741              : 
     742          257 : void MainWindow::setUiElementsEnabled(bool state) {
     743              :   // A running re-encryption owns the UI state: the progress dialog's Cancel is
     744              :   // the user's escape hatch and endReencryptPath() releases the interface, so
     745              :   // neither an unrelated completion nor the watchdog may re-enable it early.
     746          257 :   if (state && m_reencryptRunning) {
     747            1 :     m_uiWatchdog.stop();
     748            1 :     return;
     749              :   }
     750              :   // Arm the watchdog while the UI is disabled; disarm once re-enabled.
     751              :   if (state) {
     752          202 :     m_uiWatchdog.stop();
     753              :   } else {
     754           54 :     m_uiWatchdog.start();
     755              :   }
     756          256 :   ui->treeView->setEnabled(state);
     757          256 :   ui->lineEdit->setEnabled(state);
     758          256 :   ui->actionAddPassword->setEnabled(state);
     759          256 :   ui->actionAddFolder->setEnabled(state);
     760          256 :   ui->actionUsers->setEnabled(state);
     761          256 :   ui->actionConfig->setEnabled(state);
     762          256 :   m_uiEnabled = state;
     763          256 :   updateProfileBoxEnabled();
     764              :   const bool uiEnabled = state;
     765              :   // is a file selected?
     766          512 :   state &= ui->treeView->currentIndex().isValid();
     767          256 :   ui->actionDelete->setEnabled(state);
     768          256 :   ui->actionEdit->setEnabled(state);
     769          256 :   updateGitButtonVisibility(uiEnabled);
     770              :   // `state` is now "UI enabled AND a file is selected", which is exactly when
     771              :   // generating an OTP makes sense.
     772          256 :   updateOtpButtonVisibility(state);
     773              : }
     774              : 
     775          140 : void MainWindow::restoreWindow() {
     776              :   // saveGeometry() covers position, size, maximized state and screen;
     777              :   // anything applied on top discards it. Centre only when nothing was saved.
     778          280 :   const QByteArray geometry = QtPassSettings::getGeometry();
     779          140 :   if (geometry.isEmpty() || !restoreGeometry(geometry)) {
     780            4 :     WindowStateStore::centreOnCursorScreen(*this);
     781              :   }
     782          140 :   restoreState(QtPassSettings::getSavestate(saveState()));
     783          140 :   const AppSettings s = QtPassSettings::load();
     784              : 
     785          140 :   applyWindowFlagsSettings();
     786              : 
     787          140 :   if (s.useTrayIcon && m_tray == nullptr) {
     788            1 :     initTrayIcon();
     789            1 :     if (s.startMinimized) {
     790              :       // since we are still in constructor, can't directly hide
     791            2 :       QTimer::singleShot(10, this, &MainWindow::hide);
     792              :     }
     793          139 :   } else if (!s.useTrayIcon && m_tray != nullptr) {
     794            0 :     destroyTrayIcon();
     795              :   }
     796          280 : }
     797              : 
     798            1 : void MainWindow::onConfig() { config(); }
     799              : 
     800            1 : void MainWindow::showAbout() {
     801            1 :   QMessageBox::about(
     802            1 :       this, tr("About QtPass"),
     803            1 :       tr("<h3>QtPass %1</h3>"
     804              :          "<p>A multi-platform GUI for <a "
     805              :          "href=\"https://www.passwordstore.org/\">"
     806              :          "pass</a>, the standard Unix password manager.</p>"
     807              :          "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
     808              :          "<a href=\"https://github.com/IJHack/QtPass\">Source and "
     809              :          "issues</a></p>"
     810              :          "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
     811              :          "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
     812              :          "version 3</a> or later.</p>")
     813            2 :           .arg(QStringLiteral(VERSION),
     814            2 :                QString::number(QDate::currentDate().year())));
     815            1 : }
     816              : 
     817           24 : void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
     818           24 :   if (m_grep.inGrepMode())
     819              :     return;
     820           38 :   ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
     821           19 :   ui->treeView->expandAll();
     822              :   // Drop any decrypt still pending for the entry that was shown: otherwise it
     823              :   // keeps its right to the panel and, with "Always copy", the clipboard, and
     824              :   // when it lands late it shows that entry's password under the new heading.
     825           19 :   m_shownFile.clear();
     826           19 :   cancelOtpRequest();
     827           19 :   clearPanel(false);
     828           38 :   ui->passwordName->setText("");
     829           19 :   ui->actionEdit->setEnabled(false);
     830           19 :   ui->actionDelete->setEnabled(false);
     831           19 :   searchTimer.start();
     832              : }
     833              : 
     834           17 : void MainWindow::onTimeoutSearch() {
     835           17 :   QString query = ui->lineEdit->text();
     836              : 
     837           17 :   if (query.isEmpty()) {
     838            1 :     ui->treeView->collapseAll();
     839            1 :     deselect();
     840              :   }
     841              : 
     842              :   // The words are matched literally, in order, with anything in between:
     843              :   // "work vpn" finds work/acme/vpn. Regular expressions belong to the
     844              :   // content search; here a typed "[" or "(" must not switch filtering off.
     845           17 :   QStringList words;
     846              :   for (const QString &word :
     847           51 :        query.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
     848           17 :     words << QRegularExpression::escape(word);
     849              :   }
     850           34 :   QRegularExpression regExp(words.join(QStringLiteral(".*")),
     851           17 :                             QRegularExpression::CaseInsensitiveOption);
     852           17 :   m_tree->setFilter(regExp);
     853              : 
     854           17 :   if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
     855           14 :     selectFirstFile();
     856              :   } else {
     857            3 :     ui->actionEdit->setEnabled(false);
     858            3 :     ui->actionDelete->setEnabled(false);
     859              :   }
     860           34 : }
     861              : 
     862            6 : void MainWindow::on_lineEdit_returnPressed() {
     863            6 :   qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
     864            0 :                     << m_tree->proxy().rowCount();
     865              : 
     866            6 :   if (m_grep.inGrepMode()) {
     867            4 :     const QString query = ui->lineEdit->text();
     868            4 :     if (!query.isEmpty()) {
     869            3 :       ui->grepResultsList->clear();
     870            6 :       ui->statusBar->showMessage(tr("Searching…"));
     871              :       if (m_grep.beginSearch()) {
     872            3 :         QApplication::setOverrideCursor(Qt::WaitCursor);
     873              :       }
     874            6 :       QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
     875              :     } else {
     876              :       if (m_grep.cancelSearch()) {
     877            1 :         QApplication::restoreOverrideCursor();
     878              :       }
     879            1 :       ui->grepResultsList->clear();
     880            1 :       ui->grepResultsList->setVisible(false);
     881            1 :       ui->treeView->setVisible(true);
     882              :     }
     883              :     return;
     884              :   }
     885              : 
     886            2 :   if (m_tree->proxy().rowCount() > 0) {
     887            2 :     selectFirstFile();
     888            2 :     on_treeView_clicked(ui->treeView->currentIndex());
     889              :   }
     890              : }
     891              : 
     892           12 : void MainWindow::on_grepButton_toggled(bool checked) {
     893           12 :   const AppSettings s = QtPassSettings::load();
     894           12 :   if (checked) {
     895              :     m_grep.enterGrepMode();
     896           14 :     ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
     897              :     // Both backends search through NativeGrep (QRegularExpression), so one
     898              :     // dialect: PCRE.
     899            7 :     ui->lineEdit->setToolTip(
     900            7 :         tr("Content search uses Perl-compatible regular expressions (PCRE)."));
     901            7 :     ui->lineEdit->clear();
     902            7 :     searchTimer.stop();
     903            7 :     m_tree->setFilter(QRegularExpression());
     904            7 :     ui->grepResultsList->setVisible(false);
     905              :     // Keep treeView visible until results arrive
     906              :   } else {
     907              :     if (m_grep.leaveGrepMode()) {
     908            1 :       QApplication::restoreOverrideCursor();
     909              :     }
     910            5 :     searchTimer.stop();
     911            5 :     ui->lineEdit->blockSignals(true);
     912            5 :     ui->lineEdit->clear();
     913            5 :     ui->lineEdit->blockSignals(false);
     914           10 :     ui->lineEdit->setPlaceholderText(tr("Search password"));
     915           10 :     ui->lineEdit->setToolTip(QString());
     916            5 :     ui->grepResultsList->clear();
     917            5 :     ui->grepResultsList->setVisible(false);
     918            5 :     ui->treeView->setVisible(true);
     919            5 :     m_tree->setFilter(QRegularExpression());
     920              :   }
     921           12 : }
     922              : 
     923            6 : void MainWindow::onGrepFinished(
     924              :     const QList<QPair<QString, QStringList>> &results) {
     925              :   const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
     926            6 :   if (outcome.restoreCursor) {
     927            1 :     QApplication::restoreOverrideCursor();
     928              :   }
     929              :   // Re-enable the UI before the discard check so a cancelled search can never
     930              :   // leave controls disabled.
     931            6 :   setUiElementsEnabled(true);
     932            6 :   if (outcome.discard) {
     933            3 :     return;
     934              :   }
     935            4 :   if (!m_grep.inGrepMode())
     936              :     return;
     937            4 :   ui->grepResultsList->clear();
     938            4 :   if (results.isEmpty()) {
     939            2 :     ui->statusBar->showMessage(tr("No matches found."), 3000);
     940            1 :     ui->grepResultsList->setVisible(false);
     941            1 :     ui->treeView->setVisible(true);
     942            1 :     return;
     943              :   }
     944            3 :   const AppSettings s = QtPassSettings::load();
     945            3 :   const bool hideContent = s.hideContent;
     946              :   int totalLines = 0;
     947              :   int entries = 0;
     948              :   bool stopped = false;
     949           24 :   for (const auto &pair : results) {
     950           21 :     if (NativeGrep::isStoppedMarker(pair)) {
     951              :       stopped = true;
     952            0 :       continue;
     953              :     }
     954           21 :     ++entries;
     955           21 :     auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
     956           21 :     entryItem->setText(0, pair.first);
     957           21 :     entryItem->setData(0, Qt::UserRole, pair.first);
     958           42 :     for (const QString &line : pair.second) {
     959           21 :       auto *lineItem = new QTreeWidgetItem(entryItem);
     960              :       // The marker is no content: shown as it is with hidden content too,
     961              :       // and not counted as a match.
     962           21 :       if (NativeGrep::isMoreMarker(line)) {
     963            0 :         lineItem->setText(0, tr("… more matches in this entry"));
     964              :       } else {
     965           63 :         lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
     966              :                                          : line);
     967           21 :         ++totalLines;
     968              :       }
     969           21 :       lineItem->setData(0, Qt::UserRole, pair.first);
     970              :     }
     971              :   }
     972            3 :   ui->grepResultsList->expandAll();
     973            3 :   ui->treeView->setVisible(false);
     974            3 :   ui->grepResultsList->setVisible(true);
     975            3 :   if (stopped) {
     976              :     // The search stopped there; more entries may match.
     977            0 :     ui->statusBar->showMessage(
     978            0 :         tr("Showing the first %n match(es); search for something narrower "
     979              :            "to see the rest.",
     980              :            nullptr, totalLines),
     981              :         10000);
     982              :   } else {
     983            9 :     ui->statusBar->showMessage(tr("Found %n match(es)", nullptr, totalLines) +
     984            3 :                                    " " +
     985            6 :                                    tr("in %n entr(ies).", nullptr, entries),
     986              :                                3000);
     987              :   }
     988            3 :   if (s.useAutoclearPanel)
     989            1 :     clearPanelTimer.start();
     990            3 : }
     991              : 
     992            1 : void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
     993              :                                                 int /*column*/) {
     994            1 :   const AppSettings s = QtPassSettings::load();
     995            1 :   const QString entry = item->data(0, Qt::UserRole).toString();
     996            1 :   if (entry.isEmpty())
     997              :     return;
     998              :   const QString fullPath =
     999            2 :       QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
    1000            1 :   const QModelIndex proxyIndex = m_tree->indexFor(fullPath);
    1001              :   if (!proxyIndex.isValid())
    1002              :     return;
    1003            1 :   ui->treeView->setCurrentIndex(proxyIndex);
    1004            1 :   on_treeView_clicked(proxyIndex);
    1005            1 :   if (s.hideContent || s.useAutoclearPanel)
    1006            1 :     ui->grepResultsList->clear();
    1007            1 :   ui->grepResultsList->setVisible(false);
    1008            1 :   ui->treeView->setVisible(true);
    1009            1 :   ui->treeView->scrollTo(proxyIndex);
    1010            1 :   ui->treeView->setFocus();
    1011            1 : }
    1012              : 
    1013           16 : void MainWindow::selectFirstFile() {
    1014           16 :   QModelIndex index = firstFile(m_tree->rootIndex());
    1015           16 :   ui->treeView->setCurrentIndex(index);
    1016           16 : }
    1017              : 
    1018           24 : auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
    1019           24 :   StoreModel &proxy = m_tree->proxy();
    1020           24 :   int numRows = proxy.rowCount(parentIndex);
    1021           34 :   for (int row = 0; row < numRows; ++row) {
    1022           24 :     QModelIndex index = proxy.index(row, 0, parentIndex);
    1023           24 :     if (m_tree->fileInfo(index).isFile()) {
    1024           14 :       return index;
    1025              :     }
    1026           15 :     if (proxy.hasChildren(index)) {
    1027            8 :       QModelIndex childFile = firstFile(index);
    1028              :       if (childFile.isValid())
    1029            5 :         return childFile;
    1030              :     }
    1031              :   }
    1032           10 :   return QModelIndex();
    1033              : }
    1034              : 
    1035              : // Guards the input dialogs (create, move, rename) against names like
    1036              : // "../../etc/passwd" or absolute paths escaping the store root.
    1037            9 : auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
    1038           18 :   if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
    1039              :     return true;
    1040              :   }
    1041            9 :   QMessageBox::warning(this, tr("Invalid name"),
    1042            3 :                        tr("That name would resolve outside the password "
    1043              :                           "store. Please choose a different name."));
    1044            3 :   return false;
    1045              : }
    1046              : 
    1047            6 : void MainWindow::setPassword(const QString &file, bool isNew) {
    1048            6 :   const AppSettings s = QtPassSettings::load();
    1049            6 :   PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
    1050              :   // A copy out of the editor is autocleared like one from the display.
    1051            6 :   connect(&d, &PasswordDialog::copied, &m_qtPass->clipboard(),
    1052            6 :           &ClipboardManager::trackCopied);
    1053              : 
    1054            6 :   if (isNew) {
    1055              :     const QString storePath = s.passStore;
    1056            1 :     QString folder = m_tree->currentDir(false);
    1057            1 :     if (folder.isEmpty()) {
    1058            0 :       folder = storePath;
    1059              :     }
    1060              :     // Folders to pick from, the tree's current one preselected. Real folders
    1061              :     // only: a linked or junctioned one would write the entry outside the store.
    1062            2 :     QStringList folders{QString()};
    1063            1 :     const QStringList found = Util::directoriesUnder(storePath);
    1064           15 :     for (const QString &path : found) {
    1065           14 :       const QString rel = QDir(storePath).relativeFilePath(path);
    1066           42 :       if (!rel.startsWith(u'.') && !rel.contains(QStringLiteral("/."))) {
    1067              :         folders << rel;
    1068              :       }
    1069              :     }
    1070            1 :     std::sort(folders.begin() + 1, folders.end());
    1071            1 :     d.setNewEntryLocation(storePath, folders,
    1072            2 :                           QDir(storePath).relativeFilePath(folder));
    1073              :     QHash<QString, QStringList> templates =
    1074            1 :         TemplateIO::readTemplates(storePath);
    1075              :     if (!templates.isEmpty()) {
    1076              :       QString defaultTemplate =
    1077            1 :           TemplateIO::getFolderTemplate(folder, storePath);
    1078            1 :       d.setAvailableTemplates(templates, defaultTemplate);
    1079              :     }
    1080            1 :   }
    1081              : 
    1082            6 :   if (!d.exec()) {
    1083            5 :     ui->treeView->setFocus();
    1084              :   }
    1085            6 : }
    1086              : 
    1087            2 : void MainWindow::addPassword() { setPassword(QString()); }
    1088              : 
    1089            4 : auto MainWindow::confirmDeletion(const QString &file, bool isDir) -> bool {
    1090            4 :   const QString path = QDir::separator() + file;
    1091            4 :   if (!isDir) {
    1092            2 :     return PlainMessageBox::askYes(
    1093            2 :         this, tr("Delete password?"),
    1094            4 :         tr("Are you sure you want to delete %1?").arg(path));
    1095              :   }
    1096              :   // Whole sentences, not a question stitched from pieces: a verb-final
    1097              :   // language cannot put the rest of the question after the path.
    1098              :   // Plain text, as every box quoting the store: the path is a name other
    1099              :   // writers control, and as rich text "a  b" would also show as "a b".
    1100              :   QString question =
    1101            2 :       tr("Are you sure you want to delete %1 and the whole content?").arg(path);
    1102              :   // A link, junction or special file inside is as unexpected as a stray
    1103              :   // plain file: the walker leaves them out of content and reports them.
    1104            2 :   QStringList skipped;
    1105            6 :   const QStringList content = Util::regularFilesUnder(
    1106            6 :       m_tree->fileSystem().rootPath() + path, {QStringLiteral("*")}, &skipped);
    1107              :   const bool unexpected =
    1108            2 :       !skipped.isEmpty() ||
    1109            2 :       std::any_of(content.cbegin(), content.cend(), [](const QString &entry) {
    1110            4 :         return QFileInfo(entry).suffix() != QLatin1String("gpg");
    1111              :       });
    1112              :   if (unexpected) {
    1113            1 :     question += QStringLiteral("\n\n") +
    1114            1 :                 tr("Attention: there are unexpected files in the given "
    1115            1 :                    "folder, check them before continuing.");
    1116              :   }
    1117            4 :   return PlainMessageBox::askYes(this, tr("Delete folder?"), question);
    1118              : }
    1119              : 
    1120            1 : auto MainWindow::confirmLinkRemoval(const QString &file) -> bool {
    1121              :   // Only the link goes (ImitatePass::Remove unlinks it); nothing behind it
    1122              :   // is looked at or mentioned.
    1123            1 :   return PlainMessageBox::askYes(
    1124            1 :       this, tr("Delete link?"),
    1125            1 :       tr("%1 is a symbolic link or junction. Remove the link? What it "
    1126              :          "points to is left alone.")
    1127            2 :           .arg(QDir::separator() + file));
    1128              : }
    1129              : 
    1130            8 : void MainWindow::onDelete() {
    1131            8 :   const QModelIndex currentIndex = ui->treeView->currentIndex();
    1132              :   if (!currentIndex.isValid()) {
    1133              :     // Otherwise the whole password directory would be deleted (#556).
    1134            5 :     return;
    1135              :   }
    1136              : 
    1137            6 :   const bool isDir = !m_tree->fileInfo(currentIndex).isFile();
    1138              :   const QString file =
    1139            6 :       isDir ? m_tree->currentDir(true) : getFile(currentIndex, true);
    1140              :   const QString folder =
    1141            6 :       m_tree->fileSystem().rootPath() + QDir::separator() + file;
    1142            6 :   const bool linkedFolder = isDir && Util::isLinkedFolder(folder);
    1143              : 
    1144            5 :   if (!linkedFolder && refuseLinkedFolder(folder)) {
    1145              :     // Behind a link: deleting would reach outside the store.
    1146              :     return;
    1147              :   }
    1148              :   const bool confirmed =
    1149            5 :       linkedFolder ? confirmLinkRemoval(file) : confirmDeletion(file, isDir);
    1150            5 :   if (!confirmed) {
    1151              :     return;
    1152              :   }
    1153            6 :   QtPassSettings::getPass()->Remove(file, isDir);
    1154              : }
    1155              : 
    1156           51 : void MainWindow::cancelOtpRequest() {
    1157           51 :   m_otpRequestFile.clear();
    1158           51 :   m_copyRequestFile.clear();
    1159           51 : }
    1160              : 
    1161           18 : void MainWindow::onOperationError() {
    1162           18 :   cancelOtpRequest();
    1163              :   // executeWrapperStarted() emptied the panel and the browser when the
    1164              :   // operation started (a refusal says so too); what is left to forget is
    1165              :   // which entry the panel was about to show.
    1166           18 :   m_shownFile.clear();
    1167           18 : }
    1168              : 
    1169              : // Derives the code in-process from one decrypt, so it works with either
    1170              : // backend on every platform; otpFromFileToClipboard() finishes the job.
    1171           10 : void MainWindow::onOtp() {
    1172           10 :   QString file = getFile(ui->treeView->currentIndex(), true);
    1173           10 :   if (file.isEmpty()) {
    1174            1 :     flashText(tr("No password selected for OTP generation"), true);
    1175            1 :     return;
    1176              :   }
    1177            9 :   if (!QtPassSettings::isUseOtp()) {
    1178              :     // Normally unreachable (the action is hidden), but never fail silently.
    1179            1 :     flashText(tr("No OTP code found in this password entry"), true);
    1180            1 :     return;
    1181              :   }
    1182              : 
    1183              :   // Fast path: the panel shows a live code, so no decrypt, and one clipboard
    1184              :   // write (two setMimeData calls in one turn can empty the Windows
    1185              :   // clipboard). Only for *this* entry: arrow keys and right-click move
    1186              :   // currentIndex without QTreeView::clicked, so the code may be another's.
    1187              :   const QString shown =
    1188            8 :       (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
    1189            8 :   if (!shown.isEmpty()) {
    1190            1 :     m_qtPass->clipboard().copyText(shown);
    1191            2 :     showStatusMessage(tr("OTP code copied to clipboard"));
    1192              :     return;
    1193              :   }
    1194              : 
    1195              :   // Fallback: decrypt once. The request file keeps passShowHandler from
    1196              :   // copying the password and tells otpFromFileToClipboard its answer.
    1197            7 :   m_otpRequestFile = file;
    1198            7 :   setUiElementsEnabled(false);
    1199              :   // Keep the marker in step so a second request takes the fast path; safe as
    1200              :   // executeWrapperStarted() clears the panel, so a failed decrypt shows none.
    1201            7 :   m_shownFile = file;
    1202           14 :   QtPassSettings::getPass()->Show(file);
    1203              : }
    1204              : 
    1205            2 : void MainWindow::onEdit() {
    1206            2 :   QString file = getFile(ui->treeView->currentIndex(), true);
    1207            2 :   editPassword(file);
    1208            2 : }
    1209              : 
    1210            2 : void MainWindow::onUsers() {
    1211            2 :   const QString dir = m_tree->currentDir(false);
    1212            2 :   if (refuseLinkedFolder(dir)) {
    1213              :     return;
    1214              :   }
    1215              : 
    1216            1 :   UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
    1217            1 :   if (!d.exec()) {
    1218            1 :     ui->treeView->setFocus();
    1219              :   }
    1220            1 : }
    1221              : 
    1222            2 : void MainWindow::messageAvailable(const QString &message) {
    1223            2 :   show();
    1224            2 :   raise();
    1225            2 :   if (message.isEmpty()) {
    1226            1 :     focusInput();
    1227              :   } else {
    1228            1 :     ui->treeView->expandAll();
    1229            1 :     ui->lineEdit->setText(message);
    1230            1 :     on_lineEdit_returnPressed();
    1231              :   }
    1232            2 : }
    1233              : 
    1234          137 : void MainWindow::updateProfileBox() {
    1235          137 :   const Profiles profiles = QtPassSettings::getProfiles();
    1236              :   bool activeGone = false;
    1237              : 
    1238            7 :   if (profiles.isEmpty()) {
    1239          130 :     ui->profileWidget->hide();
    1240              :   } else {
    1241            7 :     ui->profileWidget->show();
    1242              :     // A refill is no choice of the user's: clearing and adding items moves
    1243              :     // the current text, which would otherwise switch profiles.
    1244            7 :     const QSignalBlocker quiet(ui->profileBox);
    1245            7 :     ui->profileBox->clear();
    1246           19 :     for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
    1247           12 :       if (!i.key().isEmpty()) {
    1248           12 :         ui->profileBox->addItem(i.key());
    1249              :       }
    1250              :     }
    1251            7 :     ui->profileBox->model()->sort(0);
    1252           14 :     int index = ui->profileBox->findText(QtPassSettings::getProfile());
    1253            7 :     if (index != -1) { //  -1 for not found
    1254            6 :       ui->profileBox->setCurrentIndex(index);
    1255            1 :     } else if (ui->profileBox->count() > 0) {
    1256              :       // The active profile is gone (removed in Settings): the box shows the
    1257              :       // first one, and the settings and backend must follow it, or the
    1258              :       // removed profile's store stays in use behind it.
    1259            1 :       ui->profileBox->setCurrentIndex(0);
    1260              :       activeGone = true;
    1261              :     }
    1262            7 :   }
    1263            7 :   if (activeGone) {
    1264            2 :     on_profileBox_currentTextChanged(ui->profileBox->currentText());
    1265              :   }
    1266          137 :   updateProfileBoxEnabled();
    1267          137 : }
    1268              : 
    1269          417 : void MainWindow::updateProfileBoxEnabled() {
    1270              :   // No profile switch while anything of the store runs or waits: the
    1271              :   // backend's store and environment would change under it, and what is
    1272              :   // still queued (a push after a save, say) would finish for the other
    1273              :   // profile (#2071). Asked of the backend, not only of the interface: the
    1274              :   // watchdog releases the interface while a slow gpg or git still runs.
    1275              :   // Every backend: one kept from before a switch of backend may still
    1276              :   // run what was queued on it.
    1277          835 :   ui->profileBox->setEnabled(m_uiEnabled && ui->profileBox->count() > 1 &&
    1278           11 :                              QtPassSettings::backendsIdle());
    1279          417 : }
    1280              : 
    1281            6 : void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
    1282           18 :   if (m_freshStart || name == QtPassSettings::getProfile()) {
    1283            1 :     return;
    1284              :   }
    1285              :   // The box is disabled while anything runs; should a change come through
    1286              :   // anyway, the active profile stays and the box says so again.
    1287            5 :   if (!m_uiEnabled || !QtPassSettings::backendsIdle()) {
    1288            0 :     const QSignalBlocker quiet(ui->profileBox);
    1289            0 :     ui->profileBox->setCurrentText(QtPassSettings::getProfile());
    1290              :     return;
    1291            0 :   }
    1292              :   // A content search runs outside the queue: leaving grep mode cancels it,
    1293              :   // so its results for the old store are dropped, not shown for the new.
    1294            5 :   if (m_grep.inGrepMode()) {
    1295            1 :     ui->grepButton->setChecked(false);
    1296              :   }
    1297              : 
    1298            5 :   ui->lineEdit->clear();
    1299              : 
    1300           10 :   const Profile prof = QtPassSettings::getProfiles().value(name);
    1301            5 :   AppSettings s = QtPassSettings::load();
    1302            5 :   s.activeProfile = name;
    1303            5 :   s.passStore = prof.path;
    1304            5 :   s.passSigningKey = prof.signingKey;
    1305              :   // A profile without its own Git flags keeps the global values.
    1306            5 :   s.useGit = prof.useGit.value_or(s.useGit);
    1307            5 :   s.autoPush = prof.autoPush.value_or(s.autoPush);
    1308            5 :   s.autoPull = prof.autoPull.value_or(s.autoPull);
    1309            5 :   QtPassSettings::save(s);
    1310           10 :   ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
    1311              : 
    1312            5 :   QtPassSettings::getPass()->updateEnv();
    1313              : 
    1314           10 :   m_tree->setStore(QtPassSettings::getPassStore());
    1315            5 :   deselect();
    1316            5 :   ui->treeView->setCurrentIndex(QModelIndex());
    1317            5 : }
    1318              : 
    1319            2 : void MainWindow::initTrayIcon() {
    1320            2 :   m_tray = new TrayIcon(this);
    1321            2 :   if (!m_tray->getIsAllocated()) {
    1322            2 :     destroyTrayIcon();
    1323            2 :     return;
    1324              :   }
    1325              : #ifndef Q_OS_MACOS
    1326            0 :   m_tray->addToggle(ui->actionShowMenuBar);
    1327              : #endif
    1328              : }
    1329              : 
    1330            2 : void MainWindow::destroyTrayIcon() {
    1331            2 :   delete m_tray;
    1332            2 :   m_tray = nullptr;
    1333            2 : }
    1334              : 
    1335            2 : void MainWindow::closeEvent(QCloseEvent *event) {
    1336              :   // Save in both branches: a tray user only ever hides the window, and
    1337              :   // used to quit through the tray menu with nothing ever written.
    1338            2 :   saveWindowState();
    1339            2 :   if (QtPassSettings::isHideOnClose()) {
    1340            2 :     this->hide();
    1341              :     event->ignore();
    1342              :   } else {
    1343            0 :     m_qtPass->clipboard().clear();
    1344              :     event->accept();
    1345              :     // A visible QSystemTrayIcon keeps the application alive, so
    1346              :     // quitOnLastWindowClosed never fires; quit explicitly.
    1347            0 :     QApplication::quit();
    1348              :   }
    1349            2 : }
    1350              : 
    1351         5225 : auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
    1352         5225 :   if ((obj == ui->toolBar || obj == menuBar()) &&
    1353              :       event->type() == QEvent::PaletteChange) {
    1354              :     // Breeze gives these bars a "header" palette and, after a runtime
    1355              :     // light/dark switch, stamps the previous theme's colours back from a
    1356              :     // cached kdeglobals, hiding the toolbar icons. Deferred so we never
    1357              :     // re-enter the style while it is still applying its palette.
    1358           36 :     QTimer::singleShot(0, this, &MainWindow::dropStaleToolsAreaPalettes);
    1359              :   }
    1360         5225 :   if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
    1361            1 :     auto *key = dynamic_cast<QKeyEvent *>(event);
    1362            1 :     if (key != nullptr && key->key() == Qt::Key_Down) {
    1363            1 :       ui->treeView->setFocus();
    1364              :     }
    1365              :   }
    1366         5225 :   return QObject::eventFilter(obj, event);
    1367              : }
    1368              : 
    1369              : // Only once the application palette changed (see m_themeSwitched): a bar
    1370              : // whose active Window lightness is far from the application's shows the
    1371              : // other theme.
    1372           31 : void MainWindow::dropStaleToolsAreaPalettes() {
    1373           31 :   if (!m_themeSwitched) {
    1374              :     return;
    1375              :   }
    1376              :   constexpr int kStaleToolsAreaLightness = 64;
    1377              :   // The active group on both sides: a bar's palette() reads the inactive
    1378              :   // group while another window has focus (the settings app, during a theme
    1379              :   // switch), and Breeze gives the header set different inactive colours.
    1380           54 :   const int appLightness = QApplication::palette()
    1381              :                                .color(QPalette::Active, QPalette::Window)
    1382           27 :                                .lightness();
    1383          108 :   for (QWidget *bar : {static_cast<QWidget *>(ui->toolBar),
    1384           81 :                        static_cast<QWidget *>(menuBar())}) {
    1385           54 :     if (!bar->testAttribute(Qt::WA_SetPalette)) {
    1386           48 :       continue;
    1387              :     }
    1388              :     const int barLightness =
    1389           12 :         bar->palette().color(QPalette::Active, QPalette::Window).lightness();
    1390            6 :     if (qAbs(barLightness - appLightness) <= kStaleToolsAreaLightness) {
    1391            2 :       continue;
    1392              :     }
    1393            4 :     bar->setPalette(QPalette());
    1394              :     // Both bars are transparent and the style paints the tools area from the
    1395              :     // stale palette, so fill our own. Window, not their Button role, which is
    1396              :     // not the window colour in either theme.
    1397            4 :     bar->setBackgroundRole(QPalette::Window);
    1398            4 :     bar->setAutoFillBackground(true);
    1399              :   }
    1400              : }
    1401              : 
    1402           18 : void MainWindow::keyPressEvent(QKeyEvent *event) {
    1403           18 :   switch (event->key()) {
    1404            1 :   case Qt::Key_Delete:
    1405            1 :     onDelete();
    1406            1 :     break;
    1407            1 :   case Qt::Key_Return:
    1408              :   case Qt::Key_Enter:
    1409            1 :     if (m_tree->proxy().rowCount() > 0) {
    1410            1 :       on_treeView_clicked(ui->treeView->currentIndex());
    1411              :     }
    1412              :     break;
    1413            3 :   case Qt::Key_Escape:
    1414            3 :     ui->lineEdit->clear();
    1415            3 :     break;
    1416              :   default:
    1417              :     break;
    1418              :   }
    1419           18 : }
    1420              : 
    1421            9 : void MainWindow::addShareMenu(QMenu &contextMenu, const AppSettings &s) {
    1422            9 :   const QString dirPath = QDir::cleanPath(m_tree->currentDir(false));
    1423            9 :   auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
    1424            9 :   contextMenu.addMenu(shareMenu);
    1425              : 
    1426            9 :   const QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
    1427            9 :   const bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
    1428            9 :   const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
    1429              :   const bool gpgAvailable =
    1430           18 :       !exePath.isEmpty() &&
    1431           18 :       (Executor::parseWslCommand(exePath) || QFile(exePath).exists());
    1432              : 
    1433            9 :   QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
    1434            9 :   reencrypt->setEnabled(gpgIdExists && gpgAvailable);
    1435            9 :   connect(reencrypt, &QAction::triggered, this,
    1436           30 :           [this, dirPath]() { reencryptPath(dirPath); });
    1437              : 
    1438            9 :   QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
    1439            9 :   exportKey->setEnabled(gpgAvailable);
    1440            9 :   connect(exportKey, &QAction::triggered, this, &MainWindow::exportPublicKey);
    1441              : 
    1442            9 :   QAction *addRecipientAction = shareMenu->addAction(tr("Add recipient..."));
    1443            9 :   addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
    1444            9 :   connect(addRecipientAction, &QAction::triggered, this,
    1445           29 :           [this, dirPath]() { addRecipient(dirPath); });
    1446              : 
    1447            9 :   QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
    1448            9 :   connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
    1449            9 : }
    1450              : 
    1451           11 : void MainWindow::showContextMenu(const QPoint &pos) {
    1452           11 :   const AppSettings s = QtPassSettings::load();
    1453           11 :   const QModelIndex index = ui->treeView->indexAt(pos);
    1454              :   const bool selected = index.isValid();
    1455              :   if (!selected) {
    1456            1 :     ui->treeView->clearSelection();
    1457            1 :     ui->actionDelete->setEnabled(false);
    1458            1 :     ui->actionEdit->setEnabled(false);
    1459              :   }
    1460           11 :   ui->treeView->setCurrentIndex(index);
    1461           11 :   const QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
    1462           11 :   const QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
    1463           11 :   const bool isDir = fileOrFolder.isDir();
    1464              : 
    1465           11 :   QMenu contextMenu;
    1466           11 :   if (!selected || isDir) {
    1467              :     const std::array<std::pair<QString, void (MainWindow::*)()>, 4> onFolder{{
    1468           10 :         {tr("Open folder with file manager"), &MainWindow::openFolder},
    1469           10 :         {tr("Add folder"), &MainWindow::addFolder},
    1470           10 :         {tr("Add password"), &MainWindow::addPassword},
    1471           10 :         {tr("Users"), &MainWindow::onUsers},
    1472            0 :     }};
    1473           50 :     for (const auto &[text, slot] : onFolder) {
    1474           40 :       connect(contextMenu.addAction(text), &QAction::triggered, this, slot);
    1475              :     }
    1476            1 :   } else if (fileOrFolder.isFile()) {
    1477            1 :     connect(contextMenu.addAction(tr("Edit")), &QAction::triggered, this,
    1478            2 :             &MainWindow::onEdit);
    1479              :   }
    1480           11 :   if (selected) {
    1481           10 :     contextMenu.addSeparator();
    1482           10 :     if (isDir) {
    1483            9 :       connect(contextMenu.addAction(tr("Rename folder")), &QAction::triggered,
    1484           18 :               this, &MainWindow::renameFolder);
    1485            1 :     } else if (fileOrFolder.isFile()) {
    1486            1 :       connect(contextMenu.addAction(tr("Rename password")), &QAction::triggered,
    1487            2 :               this, &MainWindow::renamePassword);
    1488              :     }
    1489           10 :     connect(contextMenu.addAction(tr("Delete")), &QAction::triggered, this,
    1490           10 :             &MainWindow::onDelete);
    1491           10 :     if (isDir) {
    1492            9 :       addShareMenu(contextMenu, s);
    1493              :     }
    1494              :   }
    1495           11 :   contextMenu.exec(globalPos);
    1496           11 : }
    1497              : 
    1498            1 : void MainWindow::showBrowserContextMenu(const QPoint &pos) {
    1499            1 :   QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
    1500              :   // Reparent from textBrowser: a browser stylesheet cascades to the child
    1501              :   // QMenu and breaks its opaque native background.
    1502            1 :   contextMenu->setParent(this, contextMenu->windowFlags());
    1503            1 :   QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
    1504              : 
    1505            1 :   contextMenu->exec(globalPos);
    1506            1 :   delete contextMenu;
    1507            1 : }
    1508              : 
    1509            1 : void MainWindow::openFolder() {
    1510            1 :   QString dir = m_tree->currentDir(false);
    1511              : 
    1512            1 :   QString path = QDir::toNativeSeparators(dir);
    1513            1 :   QDesktopServices::openUrl(QUrl::fromLocalFile(path));
    1514            1 : }
    1515              : 
    1516            6 : void MainWindow::addFolder() {
    1517            6 :   const AppSettings s = QtPassSettings::load();
    1518              :   bool ok;
    1519            6 :   QString dir = m_tree->currentDir(false);
    1520              :   QString newdir =
    1521            6 :       QInputDialog::getText(this, tr("New folder"),
    1522            6 :                             tr("New folder: \n(Will be placed in %1 )")
    1523           12 :                                 .arg(s.passStore + m_tree->currentDir(true)),
    1524           12 :                             QLineEdit::Normal, "", &ok);
    1525            6 :   if (!ok || newdir.isEmpty()) {
    1526              :     return;
    1527              :   }
    1528              :   // Checked and created as one and the same path: the check resolves ".."
    1529              :   // lexically, the system after following links, so "link/../x" would pass
    1530              :   // as the store's x and be made beside the link's target.
    1531            5 :   newdir = QDir::cleanPath(newdir.prepend(dir));
    1532            5 :   if (!confirmPathInStore(newdir)) {
    1533              :     return;
    1534              :   }
    1535              :   // Exists already: an error, as QDir::mkdir() made it before.
    1536            4 :   if (QFileInfo::exists(newdir) || !Util::mkpathForStore(newdir)) {
    1537            2 :     PlainMessageBox::warning(this, tr("Error"),
    1538            2 :                              tr("Failed to create folder: %1").arg(newdir));
    1539            1 :     return;
    1540              :   }
    1541              :   // With a signing key, ImitatePass::loadVerifiedRecipients and pass
    1542              :   // (PASSWORD_STORE_SIGNING_KEY) refuse a .gpg-id without .gpg-id.sig, and
    1543              :   // signing needs a passphrase just to make a folder. So it inherits the
    1544              :   // parent's signed list; UsersDialog (Init) gives it its own.
    1545            3 :   const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
    1546            3 :   if (s.addGPGId && !signingConfigured) {
    1547              :     // Seed from the parent's recipients in effect, not from `enabled` keys:
    1548              :     // that flag is only set in UsersDialog, so it left a zero-byte .gpg-id
    1549              :     // shadowing the parent's recipients (#1682).
    1550            3 :     if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
    1551            1 :       PlainMessageBox::warning(
    1552            1 :           this, tr("Error"),
    1553            2 :           tr("Failed to create .gpg-id file in: %1").arg(newdir));
    1554            1 :       return;
    1555              :     }
    1556              :   }
    1557            6 : }
    1558              : 
    1559            3 : void MainWindow::renameFolder() {
    1560              :   bool ok;
    1561            3 :   QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
    1562            3 :   QString srcDirName = QDir(srcDir).dirName();
    1563              :   QString newName =
    1564            3 :       QInputDialog::getText(this, tr("Rename folder"), tr("Rename folder to: "),
    1565            6 :                             QLineEdit::Normal, srcDirName, &ok);
    1566            3 :   if (!ok || newName.isEmpty()) {
    1567              :     return;
    1568              :   }
    1569              :   QString destDir = srcDir;
    1570            2 :   destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
    1571            2 :   if (!confirmPathInStore(destDir)) {
    1572              :     return;
    1573              :   }
    1574            3 :   QtPassSettings::getPass()->Move(srcDir, destDir, false);
    1575              : }
    1576              : 
    1577            5 : void MainWindow::editPassword(const QString &file) {
    1578            5 :   if (!file.isEmpty()) {
    1579            5 :     const AppSettings s = QtPassSettings::load();
    1580            5 :     if (s.useGit && s.autoPull) {
    1581            1 :       onUpdate(true);
    1582              :     }
    1583              :     // The dialog decrypts the entry itself to fill its fields. That decrypt
    1584              :     // lands on the same finishedShow the panel uses, so drop the panel's claim
    1585              :     // to it: otherwise passShowHandler() repaints and, with "Always copy",
    1586              :     // puts the entry's old password on the clipboard - replacing whatever the
    1587              :     // user copied to paste into the dialog.
    1588            5 :     m_shownFile.clear();
    1589            5 :     setPassword(file, false);
    1590            5 :   }
    1591            5 : }
    1592              : 
    1593            2 : void MainWindow::renamePassword() {
    1594              :   bool ok;
    1595            2 :   QString file = getFile(ui->treeView->currentIndex(), false);
    1596            2 :   QString filePath = QFileInfo(file).path();
    1597            2 :   QString fileName = QFileInfo(file).fileName();
    1598            4 :   if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
    1599            2 :     fileName.chop(4);
    1600              :   }
    1601              : 
    1602              :   QString newName =
    1603            2 :       QInputDialog::getText(this, tr("Rename file"), tr("Rename file to: "),
    1604            4 :                             QLineEdit::Normal, fileName, &ok);
    1605            2 :   if (!ok || newName.isEmpty()) {
    1606              :     return;
    1607              :   }
    1608            2 :   QString newFile = QDir(filePath).filePath(newName);
    1609            2 :   if (!confirmPathInStore(newFile)) {
    1610              :     return;
    1611              :   }
    1612            3 :   QtPassSettings::getPass()->Move(file, newFile, false);
    1613              : }
    1614              : 
    1615            4 : void MainWindow::copyPasswordFromTreeview() {
    1616            4 :   QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
    1617              : 
    1618            4 :   if (fileOrFolder.isFile()) {
    1619            4 :     QString file = getFile(ui->treeView->currentIndex(), true);
    1620              :     // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
    1621              :     // an earlier request is simply not this one's.
    1622            4 :     m_copyRequestFile = file;
    1623              :     // This Show repaints the panel as well; see onOtp().
    1624            4 :     m_shownFile = file;
    1625            8 :     QtPassSettings::getPass()->Show(file);
    1626              :   }
    1627            4 : }
    1628              : 
    1629           17 : void MainWindow::passwordFromFileToClipboard(const QString &text,
    1630              :                                              const QString &file) {
    1631           17 :   if (m_copyRequestFile.isEmpty() || file != m_copyRequestFile) {
    1632           15 :     return;
    1633              :   }
    1634            3 :   m_copyRequestFile.clear();
    1635              :   // The first line, without a list of every line (an entry can expand into
    1636              :   // millions of them); left(-1) is the whole text when there is no newline.
    1637            3 :   const QString first = text.left(text.indexOf(QLatin1Char('\n')));
    1638              :   // A `pass otp insert` entry's first line is the otpauth:// URI carrying the
    1639              :   // TOTP seed; copying it would leak 2FA material. Matches the display path
    1640              :   // (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
    1641            3 :   if (FileContent::isOtpUriValue(first)) {
    1642            2 :     flashText(tr("This entry holds an OTP secret, not a password"), true);
    1643              :     return;
    1644              :   }
    1645            2 :   m_qtPass->clipboard().copyText(first);
    1646              : }
    1647              : 
    1648           21 : void MainWindow::showStatusMessage(const QString &msg, int timeout) {
    1649           21 :   ui->statusBar->showMessage(msg, timeout);
    1650           21 : }
    1651              : 
    1652            3 : void MainWindow::reencryptPath(const QString &dir) {
    1653            3 :   QDir checkDir(dir);
    1654            3 :   if (!checkDir.exists()) {
    1655            2 :     PlainMessageBox::critical(this, tr("Error"),
    1656            2 :                               tr("Directory does not exist: %1").arg(dir));
    1657            1 :     return;
    1658              :   }
    1659            2 :   if (refuseLinkedFolder(dir)) {
    1660              :     return;
    1661              :   }
    1662              : 
    1663            2 :   const bool yes = PlainMessageBox::askYes(
    1664            2 :       this, tr("Re-encrypt passwords"),
    1665            2 :       tr("Re-encrypt all passwords in %1?\n\n"
    1666              :          "This will re-encrypt ALL password files in this folder "
    1667              :          "using the current recipients defined in .gpg-id.\n\n"
    1668              :          "This may rewrite many files and cannot be undone easily.\n\n"
    1669              :          "Continue?")
    1670            4 :           .arg(QDir(dir).dirName()));
    1671              : 
    1672            2 :   if (!yes)
    1673              :     return;
    1674              : 
    1675              :   // Disable now; the asynchronous startReencryptPath signal re-runs this
    1676              :   // harmlessly.
    1677            1 :   startReencryptPath();
    1678              : 
    1679              :   // Under the current profile's settings: with the pass backend active, the
    1680              :   // native one is not re-initialised on a profile switch.
    1681            2 :   QtPassSettings::getImitatePass()->reencryptPath(
    1682            2 :       QDir::cleanPath(QDir(dir).absolutePath()), QtPassSettings::load());
    1683            3 : }
    1684              : 
    1685              : // Idempotent: reencryptPath() calls it before ImitatePass emits the signal.
    1686            6 : void MainWindow::startReencryptPath() {
    1687            6 :   m_reencryptRunning = true;
    1688            6 :   setUiElementsEnabled(false);
    1689              :   // The worker always ends with endReencryptPath(), and Cancel is available
    1690              :   // throughout, so the generic watchdog is not needed for this operation.
    1691            6 :   m_uiWatchdog.stop();
    1692            6 :   ui->treeView->setDisabled(true);
    1693            6 :   if (m_reencryptProgress)
    1694              :     return;
    1695            4 :   auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
    1696            8 :                                        tr("Cancel"), 0, 0, this);
    1697            4 :   progress->setWindowTitle(tr("Re-encrypt passwords"));
    1698            4 :   progress->setWindowModality(Qt::WindowModal);
    1699            4 :   progress->setAutoClose(false);
    1700            4 :   progress->setAutoReset(false);
    1701            4 :   progress->setMinimumDuration(0);
    1702            4 :   connect(progress, &QProgressDialog::canceled, this, [this]() {
    1703            1 :     QtPassSettings::getImitatePass()->cancelReencryptPath();
    1704            1 :     showStatusMessage(tr("Cancelling re-encryption"), 5000);
    1705            1 :   });
    1706              :   m_reencryptProgress = progress;
    1707            4 :   progress->show();
    1708              : }
    1709              : 
    1710            5 : void MainWindow::reencryptProgress(int current, int total) {
    1711            5 :   if (!m_reencryptProgress)
    1712              :     return;
    1713            4 :   m_reencryptProgress->setMaximum(total);
    1714            4 :   m_reencryptProgress->setLabelText(
    1715            8 :       tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
    1716              :   // On a modal dialog setValue() runs processEvents(), which can deliver
    1717              :   // endReencryptPath() and reset m_reencryptProgress: keep this last.
    1718            4 :   m_reencryptProgress->setValue(current);
    1719              : }
    1720              : 
    1721            4 : void MainWindow::endReencryptPath() {
    1722            4 :   m_reencryptRunning = false;
    1723            4 :   if (m_reencryptProgress) {
    1724            4 :     m_reencryptProgress->hide();
    1725            4 :     m_reencryptProgress->deleteLater();
    1726              :     m_reencryptProgress = nullptr;
    1727              :   }
    1728            4 :   setUiElementsEnabled(true);
    1729            4 : }
    1730              : 
    1731            3 : void MainWindow::exportPublicKey() {
    1732            3 :   const AppSettings s = QtPassSettings::load();
    1733              :   const QString identity = s.passSigningKey;
    1734            3 :   if (identity.isEmpty()) {
    1735            2 :     QMessageBox::information(
    1736            1 :         this, tr("Export public key"),
    1737            0 :         tr("<h3>Export your public key</h3>"
    1738              :            "<p>No signing key is configured. Set one in QtPass Settings "
    1739              :            "&gt; GPG keys, or run this in a terminal:</p>"
    1740              :            "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
    1741              :            "&gt;</pre>"
    1742              :            "<p>Then send the file to your teammates.</p>"));
    1743            1 :     return;
    1744              :   }
    1745              :   QString gpgExe = s.gpgExecutable;
    1746            2 :   if (gpgExe.isEmpty()) {
    1747            0 :     gpgExe = QStringLiteral("gpg");
    1748              :   }
    1749           10 :   QStringList args = {"--armor", "--export"};
    1750            2 :   args.append(identity.split(' ', Qt::SkipEmptyParts));
    1751            2 :   QString stdOut;
    1752            2 :   QString stdErr;
    1753            2 :   int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
    1754            2 :   if (exitCode != 0 || stdOut.isEmpty()) {
    1755            2 :     PlainMessageBox::warning(this, tr("Export public key"),
    1756            1 :                              tr("Could not export public key for %1.\n\n%2")
    1757            2 :                                  .arg(identity, stdErr.isEmpty()
    1758            2 :                                                     ? tr("No output from gpg.")
    1759              :                                                     : stdErr));
    1760              :     return;
    1761              :   }
    1762            1 :   ExportPublicKeyDialog dialog(identity, stdOut, this);
    1763            1 :   dialog.exec();
    1764            4 : }
    1765              : 
    1766              : // UsersDialog only ticks keys already in the keyring; importing a foreign
    1767              : // key still has to happen via gpg (or QtPass settings) first.
    1768            2 : void MainWindow::addRecipient(const QString &dir) {
    1769            2 :   if (refuseLinkedFolder(dir)) {
    1770            1 :     return;
    1771              :   }
    1772            1 :   UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
    1773            1 :   d.exec();
    1774            1 : }
    1775              : 
    1776              : // A tree-picked folder that is, or lies behind, a symlink or junction is not
    1777              : // a folder of the store. The store root itself may be a link (a synced
    1778              : // folder); that is configuration, not a tree pick.
    1779           11 : auto MainWindow::refuseLinkedFolder(const QString &dir) -> bool {
    1780           11 :   if (!Util::isUnderLink(dir, QtPassSettings::load().passStore)) {
    1781              :     return false;
    1782              :   }
    1783            3 :   PlainMessageBox::critical(
    1784            3 :       this, tr("Not a folder of the store"),
    1785            3 :       tr("%1 is, or lies behind, a symbolic link or junction. What that "
    1786              :          "points to is not part of the password store and is left alone.")
    1787            6 :           .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
    1788            3 :   return true;
    1789              : }
    1790              : 
    1791            1 : void MainWindow::showShareHelp() {
    1792            2 :   QMessageBox::information(
    1793            1 :       this, tr("Sharing passwords with GPG"),
    1794            1 :       tr("<h3>Sharing passwords with GPG</h3>"
    1795              :          "<p>To share passwords with other users:</p>"
    1796              :          "<ol>"
    1797              :          "<li><b>Export your public key</b> and send it to teammates</li>"
    1798              :          "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
    1799              :          "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
    1800              :          "them</li>"
    1801              :          "</ol>"
    1802              :          "<p>Only people who have a matching secret key can decrypt the "
    1803              :          "passwords.</p>"
    1804              :          "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
    1805              :          "<p>See the FAQ for more details.</p>"));
    1806            1 : }
    1807              : 
    1808          258 : void MainWindow::updateGitButtonVisibility(bool uiEnabled) {
    1809          258 :   const AppSettings s = QtPassSettings::load();
    1810              :   // Without Git there is nothing to push or pull, ever: the buttons go, not
    1811              :   // grey out. While an operation runs they stay and are disabled.
    1812              :   const bool usable =
    1813          258 :       s.useGit && !(s.gitExecutable.isEmpty() && s.passExecutable.isEmpty());
    1814          258 :   ui->actionPush->setVisible(usable);
    1815          258 :   ui->actionUpdate->setVisible(usable);
    1816              :   // The separator in front of them would otherwise sit next to the next one.
    1817          258 :   const QList<QAction *> actions = ui->toolBar->actions();
    1818          258 :   const qsizetype at = actions.indexOf(ui->actionPush);
    1819          258 :   if (at > 0 && actions.at(at - 1)->isSeparator()) {
    1820          258 :     actions.at(at - 1)->setVisible(usable);
    1821              :   }
    1822          258 :   enableGitButtons(usable && uiEnabled);
    1823          258 : }
    1824              : 
    1825          258 : void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
    1826              :   // No platform gating: codes are generated in-process. Visible but disabled
    1827              :   // when OTP is off: the panel never shows an OTP field (a secret), so hiding
    1828              :   // this too would leave no trace of the feature. Honour uiEnabled, or the
    1829              :   // user can stack Show calls while a decrypt is in flight.
    1830          258 :   ui->actionOtp->setVisible(true);
    1831          271 :   ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
    1832          258 : }
    1833              : 
    1834          133 : void MainWindow::updateGrepButtonVisibility() {
    1835          133 :   const bool enabled = QtPassSettings::isUseGrepSearch();
    1836          133 :   ui->grepButton->setVisible(enabled);
    1837          133 :   ui->grepCaseButton->setVisible(enabled);
    1838          133 :   if (!enabled && m_grep.inGrepMode()) {
    1839            1 :     ui->grepButton->setChecked(false);
    1840              :   }
    1841          133 : }
    1842              : 
    1843          258 : void MainWindow::enableGitButtons(const bool &state) {
    1844          258 :   ui->actionPush->setEnabled(state);
    1845          258 :   ui->actionUpdate->setEnabled(state);
    1846          258 : }
    1847              : 
    1848            3 : void MainWindow::critical(const QString &title, const QString &msg) {
    1849              :   // Pass's messages quote store paths; see PlainMessageBox.
    1850            3 :   PlainMessageBox::critical(this, title, msg);
    1851            3 : }
    1852              : 
    1853            6 : void MainWindow::onProcessOutput(const QString &output, bool isError,
    1854              :                                  Enums::PROCESS pid) {
    1855            6 :   if (!QtPassSettings::isShowProcessOutput()) {
    1856              :     return;
    1857              :   }
    1858            5 :   m_processOutput->append(output, isError,
    1859           10 :                           ProcessOutputPanel::processName(pid));
    1860              : }
    1861              : 
    1862            2 : void MainWindow::updateProcessOutputVisibility() {
    1863              :   // The action's toggled handler shows or hides the dock.
    1864            2 :   ui->actionShowProcessOutput->setChecked(
    1865            2 :       QtPassSettings::isShowProcessOutput());
    1866            2 :   m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
    1867            2 : }
        

Generated by: LCOV version 2.0-1