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

IJHack / QtPass / 24882072367

24 Apr 2026 09:18AM UTC coverage: 26.849%. First build
24882072367

Pull #1150

github

web-flow
Merge dd26909b9 into 6d61ab3d3
Pull Request #1150: fix: address Copilot review findings

9 of 16 new or added lines in 3 files covered. (56.25%)

1641 of 6112 relevant lines covered (26.85%)

28.95 hits per line

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

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

5
#ifdef QT_DEBUG
6
#include "debughelper.h"
7
#endif
8

9
#include "configdialog.h"
10
#include "filecontent.h"
11
#include "passworddialog.h"
12
#include "qpushbuttonasqrcode.h"
13
#include "qpushbuttonshowpassword.h"
14
#include "qpushbuttonwithclipboard.h"
15
#include "qtpass.h"
16
#include "qtpasssettings.h"
17
#include "trayicon.h"
18
#include "ui_mainwindow.h"
19
#include "usersdialog.h"
20
#include "util.h"
21
#include <QApplication>
22
#include <QCloseEvent>
23
#include <QDesktopServices>
24
#include <QDialog>
25
#include <QDirIterator>
26
#include <QFileInfo>
27
#include <QInputDialog>
28
#include <QLabel>
29
#include <QMenu>
30
#include <QMessageBox>
31
#include <QShortcut>
32
#include <QTimer>
33
#include <QTreeWidget>
34
#include <utility>
35

36
/**
37
 * @brief MainWindow::MainWindow handles all of the main functionality and also
38
 * the main window.
39
 * @param searchText for searching from cli
40
 * @param parent pointer
41
 */
42
MainWindow::MainWindow(const QString &searchText, QWidget *parent)
×
NEW
43
    : QMainWindow(parent), ui(new Ui::MainWindow), keygenDialog(nullptr),
×
44
      tray(nullptr) {
×
45
#ifdef __APPLE__
46
  // extra treatment for mac os
47
  // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
48
  qt_set_sequence_auto_mnemonic(true);
49
#endif
50
  ui->setupUi(this);
×
51

52
  m_qtPass = new QtPass(this);
×
53

54
  // register shortcut ctrl/cmd + Q to close the main window
55
  new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
×
56
  // register shortcut ctrl/cmd + C to copy the currently selected password
57
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this,
×
58
                SLOT(copyPasswordFromTreeview()));
×
59

60
  model.setNameFilters(QStringList() << "*.gpg");
×
61
  model.setNameFilterDisables(false);
×
62

63
  /*
64
   * I added this to solve Windows bug but now on GNU/Linux the main folder,
65
   * if hidden, disappear
66
   *
67
   * model.setFilter(QDir::NoDot);
68
   */
69

70
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
×
71

72
  QModelIndex rootDir = model.setRootPath(passStore);
×
73
  model.fetchMore(rootDir);
×
74

75
  proxyModel.setModelAndStore(&model, passStore);
×
76
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
×
77

78
  ui->treeView->setModel(&proxyModel);
×
79
  ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir));
×
80
  ui->treeView->setColumnHidden(1, true);
×
81
  ui->treeView->setColumnHidden(2, true);
×
82
  ui->treeView->setColumnHidden(3, true);
×
83
  ui->treeView->setHeaderHidden(true);
×
84
  ui->treeView->setIndentation(15);
×
85
  ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
×
86
  ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
×
87
  ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
×
88
  ui->treeView->sortByColumn(0, Qt::AscendingOrder);
×
89
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
×
90
          &MainWindow::showContextMenu);
×
91
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
×
92
          &MainWindow::deselect);
×
93

94
  if (QtPassSettings::isUseMonospace()) {
×
95
    QFont monospace("Monospace");
×
96
    monospace.setStyleHint(QFont::Monospace);
×
97
    ui->textBrowser->setFont(monospace);
×
98
  }
×
99
  if (QtPassSettings::isNoLineWrapping()) {
×
100
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
101
  }
102
  ui->textBrowser->setOpenExternalLinks(true);
×
103
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
×
104
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
×
105
          &MainWindow::showBrowserContextMenu);
×
106

107
  updateProfileBox();
×
108

109
  QtPassSettings::getPass()->updateEnv();
×
110
  clearPanelTimer.setInterval(MS_PER_SECOND *
×
111
                              QtPassSettings::getAutoclearPanelSeconds());
×
112
  clearPanelTimer.setSingleShot(true);
×
113
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
×
114

115
  searchTimer.setInterval(350);
×
116
  searchTimer.setSingleShot(true);
×
117

118
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
×
119

120
  initToolBarButtons();
×
121
  initStatusBar();
×
122

123
  ui->lineEdit->setClearButtonEnabled(true);
×
124
  updateGrepButtonVisibility();
×
125

126
  setUiElementsEnabled(true);
×
127

128
  QTimer::singleShot(10, this, SLOT(focusInput()));
129

130
  ui->lineEdit->setText(searchText);
×
131

132
  if (!m_qtPass->init()) {
×
133
    // no working config so this should just quit
134
    QApplication::quit();
×
135
  }
136
}
×
137

138
MainWindow::~MainWindow() { delete m_qtPass; }
×
139

140
/**
141
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
142
 * box and sets focus to it. Allows for easy searching, called at application
143
 * start and when receiving empty message in MainWindow::messageAvailable when
144
 * compiled with SINGLE_APP=1 (default).
145
 */
146
void MainWindow::focusInput() {
×
147
  ui->lineEdit->selectAll();
×
148
  ui->lineEdit->setFocus();
×
149
}
×
150

151
/**
152
 * @brief MainWindow::changeEvent sets focus to the search box
153
 * @param event
154
 */
155
void MainWindow::changeEvent(QEvent *event) {
×
156
  QWidget::changeEvent(event);
×
157
  if (event->type() == QEvent::ActivationChange) {
×
158
    if (isActiveWindow()) {
×
159
      focusInput();
×
160
    }
161
  }
162
}
×
163

164
/**
165
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
166
 */
167
void MainWindow::initToolBarButtons() {
×
168
  connect(ui->actionAddPassword, &QAction::triggered, this,
×
169
          &MainWindow::addPassword);
×
170
  connect(ui->actionAddFolder, &QAction::triggered, this,
×
171
          &MainWindow::addFolder);
×
172
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
×
173
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
×
174
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
×
175
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
×
176
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
×
177
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
×
178
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
×
179

180
  ui->actionAddPassword->setIcon(
×
181
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
×
182
  ui->actionAddFolder->setIcon(
×
183
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
×
184
  ui->actionEdit->setIcon(QIcon::fromTheme(
×
185
      "document-properties", QIcon(":/icons/document-properties.svg")));
×
186
  ui->actionDelete->setIcon(
×
187
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
×
188
  ui->actionPush->setIcon(
×
189
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
×
190
  ui->actionUpdate->setIcon(
×
191
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
×
192
  ui->actionUsers->setIcon(QIcon::fromTheme(
×
193
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
×
194
  ui->actionConfig->setIcon(QIcon::fromTheme(
×
195
      "applications-system", QIcon(":/icons/applications-system.svg")));
×
196
}
×
197

198
/**
199
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
200
 */
201
void MainWindow::initStatusBar() {
×
202
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
×
203

204
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
×
205
                     .scaledToHeight(statusBar()->height());
×
206
  auto *logoApp = new QLabel(statusBar());
×
207
  logoApp->setPixmap(logo);
×
208
  statusBar()->addPermanentWidget(logoApp);
×
209
}
×
210

211
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
212
  return ui->treeView->currentIndex();
×
213
}
214

215
void MainWindow::cleanKeygenDialog() {
×
NEW
216
  if (this->keygenDialog != nullptr) {
×
NEW
217
    this->keygenDialog->close();
×
218
  }
NEW
219
  this->keygenDialog = nullptr;
×
220
}
×
221

222
/**
223
 * @brief Displays the given text in the main window text browser, optionally
224
 * marking it as an error and/or rendering it as HTML.
225
 * @example
226
 * MainWindow window;
227
 * window.flashText("Operation completed.", false, false);
228
 *
229
 * @param const QString &text - The text content to display.
230
 * @param const bool isError - If true, sets the text color to red before
231
 * displaying the text.
232
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
233
 * the existing HTML content.
234
 * @return void - No return value.
235
 */
236
void MainWindow::flashText(const QString &text, const bool isError,
×
237
                           const bool isHtml) {
238
  if (isError) {
×
239
    ui->textBrowser->setTextColor(Qt::red);
×
240
  }
241

242
  if (isHtml) {
×
243
    QString _text = text;
244
    if (!ui->textBrowser->toPlainText().isEmpty()) {
×
245
      _text = ui->textBrowser->toHtml() + _text;
×
246
    }
247
    ui->textBrowser->setHtml(_text);
×
248
  } else {
249
    ui->textBrowser->setText(text);
×
250
  }
251
}
×
252

253
/**
254
 * @brief MainWindow::config pops up the configuration screen and handles all
255
 * inter-window communication
256
 */
257
void MainWindow::applyTextBrowserSettings() {
×
258
  if (QtPassSettings::isUseMonospace()) {
×
259
    QFont monospace("Monospace");
×
260
    monospace.setStyleHint(QFont::Monospace);
×
261
    ui->textBrowser->setFont(monospace);
×
262
  } else {
×
263
    ui->textBrowser->setFont(QFont());
×
264
  }
265

266
  if (QtPassSettings::isNoLineWrapping()) {
×
267
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
268
  } else {
269
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
270
  }
271
}
×
272

273
void MainWindow::applyWindowFlagsSettings() {
×
274
  if (QtPassSettings::isAlwaysOnTop()) {
×
275
    Qt::WindowFlags flags = windowFlags();
276
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
277
  } else {
278
    this->setWindowFlags(Qt::Window);
×
279
  }
280
  this->show();
×
281
}
×
282

283
/**
284
 * @brief Opens and processes the application configuration dialog, then applies
285
 * any accepted settings.
286
 * @example
287
 * config();
288
 *
289
 * @return void - This function does not return a value.
290
 */
291
void MainWindow::config() {
×
292
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
293
  d->setModal(true);
×
294
  // Automatically default to pass if it's available
295
  if (m_qtPass->isFreshStart() &&
×
296
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
297
    QtPassSettings::setUsePass(true);
×
298
  }
299

300
  if (m_qtPass->isFreshStart()) {
×
NEW
301
    d->wizard(); // run initial setup wizard for first-time configuration
×
302
  }
303
  if (d->exec()) {
×
304
    if (d->result() == QDialog::Accepted) {
×
305
      applyTextBrowserSettings();
×
306
      applyWindowFlagsSettings();
×
307

308
      updateProfileBox();
×
309
      const QString passStore = QtPassSettings::getPassStore();
×
310
      proxyModel.setStore(passStore);
×
311
      ui->treeView->setRootIndex(
×
312
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
313
      deselect();
×
314
      ui->treeView->setCurrentIndex(QModelIndex());
×
315

316
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
317
        config();
×
318
      }
319
      QtPassSettings::getPass()->updateEnv();
×
320
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
321
                                  QtPassSettings::getAutoclearPanelSeconds());
×
322
      m_qtPass->setClipboardTimer();
×
323

324
      updateGitButtonVisibility();
×
325
      updateOtpButtonVisibility();
×
326
      updateGrepButtonVisibility();
×
327
      if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
328
        initTrayIcon();
×
329
      } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
330
        destroyTrayIcon();
×
331
      }
332
    }
333

334
    m_qtPass->setFreshStart(false);
×
335
  }
336
}
×
337

338
/**
339
 * @brief MainWindow::onUpdate do a git pull
340
 */
341
void MainWindow::onUpdate(bool block) {
×
342
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
343
  if (block) {
×
344
    QtPassSettings::getPass()->GitPull_b();
×
345
  } else {
346
    QtPassSettings::getPass()->GitPull();
×
347
  }
348
}
×
349

350
/**
351
 * @brief MainWindow::onPush do a git push
352
 */
353
void MainWindow::onPush() {
×
354
  if (QtPassSettings::isUseGit()) {
×
355
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
356
    QtPassSettings::getPass()->GitPush();
×
357
  }
358
}
×
359

360
/**
361
 * @brief MainWindow::getFile get the selected file path
362
 * @param index
363
 * @param forPass returns relative path without '.gpg' extension
364
 * @return path
365
 * @return
366
 */
367
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
368
  if (!index.isValid() ||
×
369
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
370
    return {};
371
  }
372
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
373
  if (forPass) {
×
374
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
375
    filePath.replace(Util::endsWithGpg(), "");
×
376
  }
377
  return filePath;
378
}
379

380
/**
381
 * @brief MainWindow::on_treeView_clicked read the selected password file
382
 * @param index
383
 */
384
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
385
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
386
  currentDir =
387
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
388
  // Clear any previously cached clipped text before showing new password
389
  m_qtPass->clearClippedText();
×
390
  QString file = getFile(index, true);
×
391
  ui->passwordName->setText(file);
×
392
  if (!file.isEmpty() && !cleared) {
×
393
    QtPassSettings::getPass()->Show(file);
×
394
  } else {
395
    clearPanel(false);
×
396
    ui->actionEdit->setEnabled(false);
×
397
    ui->actionDelete->setEnabled(true);
×
398
  }
399
}
×
400

401
/**
402
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
403
 * TreeViewItem, open the edit Window
404
 * @param index
405
 */
406
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
407
  QFileInfo fileOrFolder =
408
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
409

410
  if (fileOrFolder.isFile()) {
×
411
    editPassword(getFile(index, true));
×
412
  }
413
}
×
414

415
/**
416
 * @brief MainWindow::deselect clear the selection, password and copy buffer
417
 */
418
void MainWindow::deselect() {
×
419
  currentDir = "";
×
420
  m_qtPass->clearClipboard();
×
421
  ui->treeView->clearSelection();
×
422
  ui->actionEdit->setEnabled(false);
×
423
  ui->actionDelete->setEnabled(false);
×
424
  ui->passwordName->setText("");
×
425
  clearPanel(false);
×
426
}
×
427

428
void MainWindow::executeWrapperStarted() {
×
429
  clearTemplateWidgets();
×
430
  ui->textBrowser->clear();
×
431
  setUiElementsEnabled(false);
×
432
  clearPanelTimer.stop();
×
433
}
×
434

435
/**
436
 * @brief Handles displaying parsed password entry content in the main window.
437
 * @example
438
 * void result = MainWindow::passShowHandler(p_output);
439
 * // Updates the UI with parsed fields and emits
440
 * passShowHandlerFinished(output)
441
 *
442
 * @param p_output - The raw output text containing the password entry data.
443
 * @return void - This function does not return a value.
444
 */
445
void MainWindow::passShowHandler(const QString &p_output) {
×
446
  QStringList templ = QtPassSettings::isUseTemplate()
×
447
                          ? QtPassSettings::getPassTemplate().split("\n")
×
448
                          : QStringList();
×
449
  bool allFields =
450
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
451
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
452
  QString output = p_output;
453
  QString password = fileContent.getPassword();
×
454

455
  // set clipped text
456
  m_qtPass->setClippedText(password, p_output);
×
457

458
  // first clear the current view:
459
  clearTemplateWidgets();
×
460

461
  // show what is needed:
462
  if (QtPassSettings::isHideContent()) {
×
463
    output = "***" + tr("Content hidden") + "***";
×
464
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
465
    if (!password.isEmpty()) {
×
466
      // set the password, it is hidden if needed in addToGridLayout
467
      addToGridLayout(0, tr("Password"), password);
×
468
    }
469

470
    NamedValues namedValues = fileContent.getNamedValues();
×
471
    for (int j = 0; j < namedValues.length(); ++j) {
×
472
      const NamedValue &nv = namedValues.at(j);
473
      addToGridLayout(j + 1, nv.name, nv.value);
×
474
    }
475
    if (ui->gridLayout->count() == 0) {
×
476
      ui->verticalLayoutPassword->setSpacing(0);
×
477
    } else {
478
      ui->verticalLayoutPassword->setSpacing(6);
×
479
    }
480

481
    output = fileContent.getRemainingDataForDisplay();
×
482
  }
483

484
  if (QtPassSettings::isUseAutoclearPanel()) {
×
485
    clearPanelTimer.start();
×
486
  }
487

488
  emit passShowHandlerFinished(output);
×
489
  setUiElementsEnabled(true);
×
490
}
×
491

492
/**
493
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
494
 * and updating the UI state.
495
 * @example
496
 * void MainWindow::passOtpHandler(const QString &p_output);
497
 *
498
 * @param const QString &p_output - The OTP code text to process; if empty, an
499
 * error message is shown instead.
500
 * @return void - This function does not return a value.
501
 */
502
void MainWindow::passOtpHandler(const QString &p_output) {
×
503
  if (!p_output.isEmpty()) {
×
504
    addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
×
505
    m_qtPass->copyTextToClipboard(p_output);
×
506
    showStatusMessage(tr("OTP code copied to clipboard"));
×
507
  } else {
508
    flashText(tr("No OTP code found in this password entry"), true);
×
509
  }
510
  if (QtPassSettings::isUseAutoclearPanel()) {
×
511
    clearPanelTimer.start();
×
512
  }
513
  setUiElementsEnabled(true);
×
514
}
×
515

516
/**
517
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
518
 */
519
void MainWindow::clearPanel(bool notify) {
×
520
  while (ui->gridLayout->count() > 0) {
×
521
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
522
    delete item->widget();
×
523
    delete item;
×
524
  }
525
  const bool grepWasVisible = ui->grepResultsList->isVisible();
×
526
  ui->grepResultsList->clear();
×
527
  if (grepWasVisible) {
×
528
    ui->grepResultsList->setVisible(false);
×
529
    ui->treeView->setVisible(true);
×
530
    if (m_grepMode) {
×
531
      m_grepMode = false;
×
532
      ui->grepButton->blockSignals(true);
×
533
      ui->grepButton->setChecked(false);
×
534
      ui->grepButton->blockSignals(false);
×
535
      ui->lineEdit->blockSignals(true);
×
536
      ui->lineEdit->clear();
×
537
      ui->lineEdit->blockSignals(false);
×
538
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
539
    }
540
  }
541
  if (notify) {
×
542
    QString output = "***" + tr("Password and Content hidden") + "***";
×
543
    ui->textBrowser->setHtml(output);
×
544
  } else {
545
    ui->textBrowser->setHtml("");
×
546
  }
547
}
×
548

549
/**
550
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
551
 * elements
552
 * @param state
553
 */
554
void MainWindow::setUiElementsEnabled(bool state) {
×
555
  ui->treeView->setEnabled(state);
×
556
  ui->lineEdit->setEnabled(state);
×
557
  ui->lineEdit->installEventFilter(this);
×
558
  ui->actionAddPassword->setEnabled(state);
×
559
  ui->actionAddFolder->setEnabled(state);
×
560
  ui->actionUsers->setEnabled(state);
×
561
  ui->actionConfig->setEnabled(state);
×
562
  // is a file selected?
563
  state &= ui->treeView->currentIndex().isValid();
×
564
  ui->actionDelete->setEnabled(state);
×
565
  ui->actionEdit->setEnabled(state);
×
566
  updateGitButtonVisibility();
×
567
  updateOtpButtonVisibility();
×
568
}
×
569

570
/**
571
 * @brief Restores the main window geometry, state, position, size, and
572
 * tray/icon settings from saved application settings.
573
 * @example
574
 * MainWindow window;
575
 * window.restoreWindow();
576
 *
577
 * @return void - This function does not return a value.
578
 */
579
void MainWindow::restoreWindow() {
×
580
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
×
581
  restoreGeometry(geometry);
×
582
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
×
583
  restoreState(savestate);
×
584
  QPoint position = QtPassSettings::getPos(pos());
×
585
  move(position);
×
586
  QSize newSize = QtPassSettings::getSize(size());
×
587
  resize(newSize);
×
588
  if (QtPassSettings::isMaximized(isMaximized())) {
×
589
    showMaximized();
×
590
  }
591

592
  if (QtPassSettings::isAlwaysOnTop()) {
×
593
    Qt::WindowFlags flags = windowFlags();
594
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
595
    show();
×
596
  }
597

598
  if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
599
    initTrayIcon();
×
600
    if (QtPassSettings::isStartMinimized()) {
×
601
      // since we are still in constructor, can't directly hide
602
      QTimer::singleShot(10, this, SLOT(hide()));
×
603
    }
604
  } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
605
    destroyTrayIcon();
×
606
  }
607
}
×
608

609
/**
610
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
611
 */
612
void MainWindow::onConfig() { config(); }
×
613

614
/**
615
 * @brief Executes when the string in the search box changes, collapses the
616
 * TreeView
617
 * @param arg1
618
 */
619
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
620
  if (m_grepMode)
×
621
    return;
622
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
623
  ui->treeView->expandAll();
×
624
  clearPanel(false);
×
625
  ui->passwordName->setText("");
×
626
  ui->actionEdit->setEnabled(false);
×
627
  ui->actionDelete->setEnabled(false);
×
628
  searchTimer.start();
×
629
}
630

631
/**
632
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
633
 * time from two keypresses is elapsed
634
 */
635
void MainWindow::onTimeoutSearch() {
×
636
  QString query = ui->lineEdit->text();
×
637

638
  if (query.isEmpty()) {
×
639
    ui->treeView->collapseAll();
×
640
    deselect();
×
641
  }
642

643
  query.replace(QStringLiteral(" "), ".*");
×
644
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
645
  proxyModel.setFilterRegularExpression(regExp);
×
646
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
647
      model.setRootPath(QtPassSettings::getPassStore())));
×
648

649
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
650
    selectFirstFile();
×
651
  } else {
652
    ui->actionEdit->setEnabled(false);
×
653
    ui->actionDelete->setEnabled(false);
×
654
  }
655
}
×
656

657
/**
658
 * @brief MainWindow::on_lineEdit_returnPressed get searching
659
 *
660
 * Select the first possible file in the tree
661
 */
662
void MainWindow::on_lineEdit_returnPressed() {
×
663
#ifdef QT_DEBUG
664
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
665
#endif
666

667
  if (m_grepMode) {
×
668
    const QString query = ui->lineEdit->text();
×
669
    if (!query.isEmpty()) {
×
670
      m_grepCancelled = false;
×
671
      ui->grepResultsList->clear();
×
672
      ui->statusBar->showMessage(tr("Searching…"));
×
673
      if (!m_grepBusy) {
×
674
        m_grepBusy = true;
×
675
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
676
      }
677
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
678
    } else {
679
      m_grepCancelled = true;
×
680
      if (m_grepBusy) {
×
681
        m_grepBusy = false;
×
682
        QApplication::restoreOverrideCursor();
×
683
      }
684
      ui->grepResultsList->clear();
×
685
      ui->grepResultsList->setVisible(false);
×
686
      ui->treeView->setVisible(true);
×
687
    }
688
    return;
689
  }
690

691
  if (proxyModel.rowCount() > 0) {
×
692
    selectFirstFile();
×
693
    on_treeView_clicked(ui->treeView->currentIndex());
×
694
  }
695
}
696

697
/**
698
 * @brief Toggle grep (content search) mode.
699
 */
700
void MainWindow::on_grepButton_toggled(bool checked) {
×
701
  m_grepMode = checked;
×
702
  if (checked) {
×
703
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
704
    ui->lineEdit->clear();
×
705
    searchTimer.stop();
×
706
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
707
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
708
        model.setRootPath(QtPassSettings::getPassStore())));
×
709
    ui->grepResultsList->setVisible(false);
×
710
    // Keep treeView visible until results arrive
711
  } else {
712
    if (m_grepBusy) {
×
713
      m_grepBusy = false;
×
714
      m_grepCancelled = true;
×
715
      QApplication::restoreOverrideCursor();
×
716
    }
717
    searchTimer.stop();
×
718
    ui->lineEdit->blockSignals(true);
×
719
    ui->lineEdit->clear();
×
720
    ui->lineEdit->blockSignals(false);
×
721
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
722
    ui->grepResultsList->clear();
×
723
    ui->grepResultsList->setVisible(false);
×
724
    ui->treeView->setVisible(true);
×
725
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
726
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
727
        model.setRootPath(QtPassSettings::getPassStore())));
×
728
  }
729
}
×
730

731
/**
732
 * @brief Display grep results in grepResultsList.
733
 */
734
void MainWindow::onGrepFinished(
×
735
    const QList<QPair<QString, QStringList>> &results) {
736
  if (m_grepBusy) {
×
737
    m_grepBusy = false;
×
738
    QApplication::restoreOverrideCursor();
×
739
  }
740
  if (m_grepCancelled) {
×
741
    m_grepCancelled = false;
×
742
    return;
×
743
  }
744
  setUiElementsEnabled(true);
×
745
  if (!m_grepMode)
×
746
    return;
747
  ui->grepResultsList->clear();
×
748
  if (results.isEmpty()) {
×
749
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
750
    ui->grepResultsList->setVisible(false);
×
751
    ui->treeView->setVisible(true);
×
752
    return;
×
753
  }
754
  const bool hideContent = QtPassSettings::isHideContent();
×
755
  int totalLines = 0;
756
  for (const auto &pair : results) {
×
757
    QTreeWidgetItem *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
758
    entryItem->setText(0, pair.first);
×
759
    entryItem->setData(0, Qt::UserRole, pair.first);
×
760
    for (const QString &line : pair.second) {
×
761
      QTreeWidgetItem *lineItem = new QTreeWidgetItem(entryItem);
×
762
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
763
                                       : line);
764
      lineItem->setData(0, Qt::UserRole, pair.first);
×
765
      ++totalLines;
×
766
    }
767
  }
768
  ui->grepResultsList->expandAll();
×
769
  ui->treeView->setVisible(false);
×
770
  ui->grepResultsList->setVisible(true);
×
771
  ui->statusBar->showMessage(
×
772
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
773
          tr("in %n entr(ies).", nullptr, results.size()),
×
774
      3000);
775
  if (QtPassSettings::isUseAutoclearPanel())
×
776
    clearPanelTimer.start();
×
777
}
778

779
/**
780
 * @brief Navigate to the password entry when a grep result is clicked.
781
 */
782
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
783
                                                int /*column*/) {
784
  const QString entry = item->data(0, Qt::UserRole).toString();
×
785
  if (entry.isEmpty())
×
786
    return;
787
  const QString fullPath = QDir::cleanPath(
788
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
789
  QModelIndex srcIndex = model.index(fullPath);
×
790
  if (!srcIndex.isValid())
791
    return;
792
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
793
  if (!proxyIndex.isValid())
794
    return;
795
  ui->treeView->setCurrentIndex(proxyIndex);
×
796
  on_treeView_clicked(proxyIndex);
×
797
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
798
    ui->grepResultsList->clear();
×
799
  ui->grepResultsList->setVisible(false);
×
800
  ui->treeView->setVisible(true);
×
801
  ui->treeView->scrollTo(proxyIndex);
×
802
  ui->treeView->setFocus();
×
803
}
804

805
/**
806
 * @brief MainWindow::selectFirstFile select the first possible file in the
807
 * tree
808
 */
809
void MainWindow::selectFirstFile() {
×
810
  QModelIndex index = proxyModel.mapFromSource(
×
811
      model.setRootPath(QtPassSettings::getPassStore()));
×
812
  index = firstFile(index);
×
813
  ui->treeView->setCurrentIndex(index);
×
814
}
×
815

816
/**
817
 * @brief MainWindow::firstFile return location of first possible file
818
 * @param parentIndex
819
 * @return QModelIndex
820
 */
821
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
822
  QModelIndex index = parentIndex;
×
823
  int numRows = proxyModel.rowCount(parentIndex);
×
824
  for (int row = 0; row < numRows; ++row) {
×
825
    index = proxyModel.index(row, 0, parentIndex);
×
826
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
827
      return index;
×
828
    }
829
    if (proxyModel.hasChildren(index)) {
×
830
      return firstFile(index);
×
831
    }
832
  }
833
  return index;
×
834
}
835

836
/**
837
 * @brief MainWindow::setPassword open passworddialog
838
 * @param file which pgp file
839
 * @param isNew insert (not update)
840
 */
841
void MainWindow::setPassword(const QString &file, bool isNew) {
×
842
  PasswordDialog d(file, isNew, this);
×
843

844
  if (isNew) {
×
845
    QString storePath = QtPassSettings::getPassStore();
×
846
    QString folder =
847
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
848
    if (folder.isEmpty()) {
×
849
      folder = storePath;
×
850
    }
851
    QHash<QString, QStringList> templates = Util::readTemplates(storePath);
×
852
    if (!templates.isEmpty()) {
853
      QString defaultTemplate = Util::getFolderTemplate(folder, storePath);
×
854
      d.setAvailableTemplates(templates, defaultTemplate);
×
855
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
856
                    [&d]() { d.cycleTemplate(); });
×
857
    }
858
  }
×
859

860
  if (!d.exec()) {
×
861
    ui->treeView->setFocus();
×
862
  }
863
}
×
864

865
/**
866
 * @brief MainWindow::addPassword add a new password by showing a
867
 * number of dialogs.
868
 */
869
void MainWindow::addPassword() {
×
870
  bool ok;
871
  QString dir =
872
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
873
  QString file =
874
      QInputDialog::getText(this, tr("New file"),
×
875
                            tr("New password file: \n(Will be placed in %1 )")
×
876
                                .arg(QtPassSettings::getPassStore() +
×
877
                                     Util::getDir(ui->treeView->currentIndex(),
×
878
                                                  true, model, proxyModel)),
879
                            QLineEdit::Normal, "", &ok);
×
880
  if (!ok || file.isEmpty()) {
×
881
    return;
882
  }
883
  file = dir + file;
×
884
  setPassword(file);
×
885
}
886

887
/**
888
 * @brief MainWindow::onDelete remove password, if you are
889
 * sure.
890
 */
891
void MainWindow::onDelete() {
×
892
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
893
  if (!currentIndex.isValid()) {
894
    // This fixes https://github.com/IJHack/QtPass/issues/556
895
    // Otherwise the entire password directory would be deleted if
896
    // nothing is selected in the tree view.
897
    return;
×
898
  }
899

900
  QFileInfo fileOrFolder =
901
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
902
  QString file = "";
×
903
  bool isDir = false;
904

905
  if (fileOrFolder.isFile()) {
×
906
    file = getFile(ui->treeView->currentIndex(), true);
×
907
  } else {
908
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
909
    isDir = true;
910
  }
911

912
  QString dirMessage = tr(" and the whole content?");
913
  if (isDir) {
×
914
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
915
                    QDirIterator::Subdirectories);
×
916
    bool okDir = true;
917
    while (it.hasNext() && okDir) {
×
918
      it.next();
×
919
      if (QFileInfo(it.filePath()).isFile()) {
×
920
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
921
          okDir = false;
922
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
923
                          "there are unexpected files in the given folder, "
924
                          "check them before continue.</strong>");
925
        }
926
      }
927
    }
928
  }
×
929

930
  if (QMessageBox::question(
×
931
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
932
          tr("Are you sure you want to delete %1%2?")
×
933
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
934
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
935
    return;
936
  }
937

938
  QtPassSettings::getPass()->Remove(file, isDir);
×
939
}
×
940

941
/**
942
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
943
 */
944
void MainWindow::onOtp() {
×
945
  QString file = getFile(ui->treeView->currentIndex(), true);
×
946
  if (!file.isEmpty()) {
×
947
    if (QtPassSettings::isUseOtp()) {
×
948
      setUiElementsEnabled(false);
×
949
      QtPassSettings::getPass()->OtpGenerate(file);
×
950
    }
951
  } else {
952
    flashText(tr("No password selected for OTP generation"), true);
×
953
  }
954
}
×
955

956
/**
957
 * @brief MainWindow::onEdit try and edit (selected) password.
958
 */
959
void MainWindow::onEdit() {
×
960
  QString file = getFile(ui->treeView->currentIndex(), true);
×
961
  editPassword(file);
×
962
}
×
963

964
/**
965
 * @brief MainWindow::userDialog see MainWindow::onUsers()
966
 * @param dir folder to edit users for.
967
 */
968
void MainWindow::userDialog(const QString &dir) {
×
969
  if (!dir.isEmpty()) {
×
970
    currentDir = dir;
×
971
  }
972
  onUsers();
×
973
}
×
974

975
/**
976
 * @brief MainWindow::onUsers edit users for the current
977
 * folder,
978
 * gets lists and opens UserDialog.
979
 */
980
void MainWindow::onUsers() {
×
981
  QString dir =
982
      currentDir.isEmpty()
983
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
984
          : currentDir;
×
985

986
  UsersDialog d(dir, this);
×
987
  if (!d.exec()) {
×
988
    ui->treeView->setFocus();
×
989
  }
990
}
×
991

992
/**
993
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
994
 * @param message
995
 */
996
void MainWindow::messageAvailable(const QString &message) {
×
997
  if (message.isEmpty()) {
×
998
    focusInput();
×
999
  } else {
1000
    ui->treeView->expandAll();
×
1001
    ui->lineEdit->setText(message);
×
1002
    on_lineEdit_returnPressed();
×
1003
  }
1004
  show();
×
1005
  raise();
×
1006
}
×
1007

1008
/**
1009
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1010
 * @param batch
1011
 * @param keygenWindow
1012
 */
1013
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
NEW
1014
  keygenDialog = keygenWindow;
×
1015
  emit generateGPGKeyPair(batch);
×
1016
}
×
1017

1018
/**
1019
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1020
 * select a more appropriate one to view too
1021
 */
1022
void MainWindow::updateProfileBox() {
×
1023
  QHash<QString, QHash<QString, QString>> profiles =
1024
      QtPassSettings::getProfiles();
×
1025

1026
  if (profiles.isEmpty()) {
1027
    ui->profileWidget->hide();
×
1028
  } else {
1029
    ui->profileWidget->show();
×
1030
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1031
    ui->profileBox->clear();
×
1032
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
×
1033
    while (i.hasNext()) {
×
1034
      i.next();
1035
      if (!i.key().isEmpty()) {
×
1036
        ui->profileBox->addItem(i.key());
×
1037
      }
1038
    }
1039
    ui->profileBox->model()->sort(0);
×
1040
  }
1041
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
×
1042
  if (index != -1) { //  -1 for not found
×
1043
    ui->profileBox->setCurrentIndex(index);
×
1044
  }
1045
}
×
1046

1047
/**
1048
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1049
 * correct "profile"
1050
 * @param name
1051
 */
1052
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1053
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1054
#else
1055
/**
1056
 * @brief Handles changes to the selected profile in the profile combo box.
1057
 * @details Ignores the event during a fresh start or when the selected profile
1058
 * matches the current profile. Otherwise, it clears the password field, updates
1059
 * the active profile and related settings, refreshes the environment, and
1060
 * resets the tree view and action states to reflect the newly selected profile.
1061
 *
1062
 * @param name - The newly selected profile name.
1063
 * @return void - This function does not return a value.
1064
 *
1065
 */
1066
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1067
#endif
1068
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
1069
    return;
×
1070
  }
1071

1072
  ui->lineEdit->clear();
×
1073

1074
  QtPassSettings::setProfile(name);
×
1075

1076
  QtPassSettings::setPassStore(
×
1077
      QtPassSettings::getProfiles().value(name).value("path"));
×
1078
  QtPassSettings::setPassSigningKey(
×
1079
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1080
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1081

1082
  QtPassSettings::getPass()->updateEnv();
×
1083

1084
  const QString passStore = QtPassSettings::getPassStore();
×
1085
  proxyModel.setStore(passStore);
×
1086
  ui->treeView->setRootIndex(
×
1087
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1088
  deselect();
×
1089
  ui->treeView->setCurrentIndex(QModelIndex());
×
1090
}
1091

1092
/**
1093
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1094
 * support
1095
 * it
1096
 */
1097
void MainWindow::initTrayIcon() {
×
1098
  this->tray = new TrayIcon(this);
×
1099
  // Setup tray icon
1100

1101
  if (tray == nullptr) {
1102
#ifdef QT_DEBUG
1103
    dbg() << "Allocating tray icon failed.";
1104
#endif
1105
    return;
1106
  }
1107

1108
  if (!tray->getIsAllocated()) {
×
1109
    destroyTrayIcon();
×
1110
  }
1111
}
1112

1113
/**
1114
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1115
 */
1116
void MainWindow::destroyTrayIcon() {
×
1117
  delete this->tray;
×
1118
  tray = nullptr;
×
1119
}
×
1120

1121
/**
1122
 * @brief MainWindow::closeEvent hide or quit
1123
 * @param event
1124
 */
1125
void MainWindow::closeEvent(QCloseEvent *event) {
×
1126
  if (QtPassSettings::isHideOnClose()) {
×
1127
    this->hide();
×
1128
    event->ignore();
1129
  } else {
1130
    m_qtPass->clearClipboard();
×
1131

1132
    QtPassSettings::setGeometry(saveGeometry());
×
1133
    QtPassSettings::setSavestate(saveState());
×
1134
    QtPassSettings::setMaximized(isMaximized());
×
1135
    if (!isMaximized()) {
×
1136
      QtPassSettings::setPos(pos());
×
1137
      QtPassSettings::setSize(size());
×
1138
    }
1139
    event->accept();
1140
  }
1141
}
×
1142

1143
/**
1144
 * @brief MainWindow::eventFilter filter out some events and focus the
1145
 * treeview
1146
 * @param obj
1147
 * @param event
1148
 * @return
1149
 */
1150
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
×
1151
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
×
1152
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1153
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1154
      ui->treeView->setFocus();
×
1155
    }
1156
  }
1157
  return QObject::eventFilter(obj, event);
×
1158
}
1159

1160
/**
1161
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1162
 * @param event
1163
 */
1164
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1165
  switch (event->key()) {
×
1166
  case Qt::Key_Delete:
×
1167
    onDelete();
×
1168
    break;
×
1169
  case Qt::Key_Return:
×
1170
  case Qt::Key_Enter:
1171
    if (proxyModel.rowCount() > 0) {
×
1172
      on_treeView_clicked(ui->treeView->currentIndex());
×
1173
    }
1174
    break;
1175
  case Qt::Key_Escape:
×
1176
    ui->lineEdit->clear();
×
1177
    break;
×
1178
  default:
1179
    break;
1180
  }
1181
}
×
1182

1183
/**
1184
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1185
 * menu
1186
 * @param pos
1187
 */
1188
void MainWindow::showContextMenu(const QPoint &pos) {
×
1189
  QModelIndex index = ui->treeView->indexAt(pos);
×
1190
  bool selected = true;
1191
  if (!index.isValid()) {
1192
    ui->treeView->clearSelection();
×
1193
    ui->actionDelete->setEnabled(false);
×
1194
    ui->actionEdit->setEnabled(false);
×
1195
    currentDir = "";
×
1196
    selected = false;
1197
  }
1198

1199
  ui->treeView->setCurrentIndex(index);
×
1200

1201
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1202

1203
  QFileInfo fileOrFolder =
1204
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1205

1206
  QMenu contextMenu;
×
1207
  if (!selected || fileOrFolder.isDir()) {
×
1208
    QAction *openFolder =
1209
        contextMenu.addAction(tr("Open folder with file manager"));
×
1210
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1211
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1212
    QAction *users = contextMenu.addAction(tr("Users"));
×
1213
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1214
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1215
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1216
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1217
  } else if (fileOrFolder.isFile()) {
×
1218
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1219
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1220
  }
1221
  if (selected) {
×
1222
    contextMenu.addSeparator();
×
1223
    if (fileOrFolder.isDir()) {
×
1224
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1225
      connect(renameFolder, &QAction::triggered, this,
×
1226
              &MainWindow::renameFolder);
×
1227
    } else if (fileOrFolder.isFile()) {
×
1228
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1229
      connect(renamePassword, &QAction::triggered, this,
×
1230
              &MainWindow::renamePassword);
×
1231
    }
1232
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1233
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1234
    if (fileOrFolder.isDir()) {
×
1235
      QString dirPath = QDir::cleanPath(
1236
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1237

1238
      QMenu *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1239
      contextMenu.addMenu(shareMenu);
×
1240

1241
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1242
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1243

1244
      QString exePath = QtPassSettings::isUsePass()
×
1245
                            ? QtPassSettings::getPassExecutable()
×
1246
                            : QtPassSettings::getGpgExecutable();
×
1247
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1248
                                                 QFile(exePath).exists());
×
1249

1250
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1251
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1252
      connect(reencrypt, &QAction::triggered, this,
×
1253
              [this, dirPath]() { reencryptPath(dirPath); });
×
1254

1255
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1256
      exportKey->setEnabled(gpgAvailable);
×
1257
      connect(exportKey, &QAction::triggered, this,
×
1258
              &MainWindow::exportPublicKey);
×
1259

1260
      QAction *addRecipientAction =
1261
          shareMenu->addAction(tr("Add recipient..."));
×
1262
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1263
      connect(addRecipientAction, &QAction::triggered, this,
×
1264
              [this, dirPath]() { addRecipient(dirPath); });
×
1265

1266
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1267
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1268
    }
1269
  }
1270
  contextMenu.exec(globalPos);
×
1271
}
×
1272

1273
/**
1274
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1275
 * password window
1276
 * @param pos
1277
 */
1278
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1279
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1280
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1281

1282
  contextMenu->exec(globalPos);
×
1283
  delete contextMenu;
×
1284
}
×
1285

1286
/**
1287
 * @brief MainWindow::openFolder open the folder in the default file manager
1288
 */
1289
void MainWindow::openFolder() {
×
1290
  QString dir =
1291
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1292

1293
  QString path = QDir::toNativeSeparators(dir);
×
1294
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1295
}
×
1296

1297
/**
1298
 * @brief MainWindow::addFolder add a new folder to store passwords in
1299
 */
1300
void MainWindow::addFolder() {
×
1301
  bool ok;
1302
  QString dir =
1303
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1304
  QString newdir =
1305
      QInputDialog::getText(this, tr("New file"),
×
1306
                            tr("New Folder: \n(Will be placed in %1 )")
×
1307
                                .arg(QtPassSettings::getPassStore() +
×
1308
                                     Util::getDir(ui->treeView->currentIndex(),
×
1309
                                                  true, model, proxyModel)),
1310
                            QLineEdit::Normal, "", &ok);
×
1311
  if (!ok || newdir.isEmpty()) {
×
1312
    return;
1313
  }
1314
  newdir.prepend(dir);
1315
  if (!QDir().mkdir(newdir)) {
×
1316
    QMessageBox::warning(this, tr("Error"),
×
1317
                         tr("Failed to create folder: %1").arg(newdir));
×
1318
    return;
×
1319
  }
1320
  if (QtPassSettings::isAddGPGId(true)) {
×
1321
    QString gpgIdFile = newdir + "/.gpg-id";
×
1322
    QFile gpgId(gpgIdFile);
×
1323
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1324
      QMessageBox::warning(
×
1325
          this, tr("Error"),
×
1326
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1327
      return;
1328
    }
1329
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1330
    for (const UserInfo &user : users) {
×
1331
      if (user.enabled) {
×
1332
        gpgId.write((user.key_id + "\n").toUtf8());
×
1333
      }
1334
    }
1335
    gpgId.close();
×
1336
  }
×
1337
}
1338

1339
/**
1340
 * @brief MainWindow::renameFolder rename an existing folder
1341
 */
1342
void MainWindow::renameFolder() {
×
1343
  bool ok;
1344
  QString srcDir = QDir::cleanPath(
1345
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1346
  QString srcDirName = QDir(srcDir).dirName();
×
1347
  QString newName =
1348
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1349
                            QLineEdit::Normal, srcDirName, &ok);
×
1350
  if (!ok || newName.isEmpty()) {
×
1351
    return;
1352
  }
1353
  QString destDir = srcDir;
1354
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1355
  QtPassSettings::getPass()->Move(srcDir, destDir);
×
1356
}
1357

1358
/**
1359
 * @brief MainWindow::editPassword read password and open edit window via
1360
 * MainWindow::onEdit()
1361
 */
1362
void MainWindow::editPassword(const QString &file) {
×
1363
  if (!file.isEmpty()) {
×
1364
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1365
      onUpdate(true);
×
1366
    }
1367
    setPassword(file, false);
×
1368
  }
1369
}
×
1370

1371
/**
1372
 * @brief MainWindow::renamePassword rename an existing password
1373
 */
1374
void MainWindow::renamePassword() {
×
1375
  bool ok;
1376
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1377
  QString filePath = QFileInfo(file).path();
×
1378
  QString fileName = QFileInfo(file).fileName();
×
1379
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1380
    fileName.chop(4);
×
1381
  }
1382

1383
  QString newName =
1384
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1385
                            QLineEdit::Normal, fileName, &ok);
×
1386
  if (!ok || newName.isEmpty()) {
×
1387
    return;
1388
  }
1389
  QString newFile = QDir(filePath).filePath(newName);
×
1390
  QtPassSettings::getPass()->Move(file, newFile);
×
1391
}
1392

1393
/**
1394
 * @brief MainWindow::clearTemplateWidgets empty the template widget fields in
1395
 * the UI
1396
 */
1397
void MainWindow::clearTemplateWidgets() {
×
1398
  while (ui->gridLayout->count() > 0) {
×
1399
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
1400
    delete item->widget();
×
1401
    delete item;
×
1402
  }
1403
  ui->verticalLayoutPassword->setSpacing(0);
×
1404
}
×
1405

1406
/**
1407
 * @brief Copies the password of the selected file from the tree view to the
1408
 * clipboard.
1409
 * @example
1410
 * MainWindow::copyPasswordFromTreeview();
1411
 *
1412
 * @return void - This function does not return a value.
1413
 */
1414
void MainWindow::copyPasswordFromTreeview() {
×
1415
  QFileInfo fileOrFolder =
1416
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1417

1418
  if (fileOrFolder.isFile()) {
×
1419
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1420
    // Disconnect any previous connection to avoid accumulation
1421
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1422
               &MainWindow::passwordFromFileToClipboard);
1423
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1424
            &MainWindow::passwordFromFileToClipboard);
×
1425
    QtPassSettings::getPass()->Show(file);
×
1426
  }
1427
}
×
1428

1429
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1430
  QStringList tokens = text.split('\n');
×
1431
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1432
}
×
1433

1434
/**
1435
 * @brief MainWindow::addToGridLayout add a field to the template grid
1436
 * @param position
1437
 * @param field
1438
 * @param value
1439
 */
1440
void MainWindow::addToGridLayout(int position, const QString &field,
×
1441
                                 const QString &value) {
1442
  QString trimmedField = field.trimmed();
1443
  QString trimmedValue = value.trimmed();
1444

1445
  const QString buttonStyle =
1446
      "border-style: none; background: transparent; padding: 0; margin: 0; "
1447
      "icon-size: 16px; color: inherit;";
×
1448

1449
  // Combine the Copy button and the line edit in one widget
1450
  auto *frame = new QFrame();
×
1451
  QLayout *ly = new QHBoxLayout();
×
1452
  ly->setContentsMargins(5, 2, 2, 2);
×
1453
  ly->setSpacing(0);
×
1454
  frame->setLayout(ly);
×
1455
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER) {
×
1456
    auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
×
1457
    connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
×
1458
            &QtPass::copyTextToClipboard);
×
1459

1460
    fieldLabel->setStyleSheet(buttonStyle);
×
1461
    frame->layout()->addWidget(fieldLabel);
×
1462
  }
1463

1464
  if (QtPassSettings::isUseQrencode()) {
×
1465
    auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
×
1466
    connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
×
1467
            &QtPass::showTextAsQRCode);
×
1468
    qrbutton->setStyleSheet(buttonStyle);
×
1469
    frame->layout()->addWidget(qrbutton);
×
1470
  }
1471

1472
  // set the echo mode to password, if the field is "password"
1473
  const QString lineStyle =
1474
      QtPassSettings::isUseMonospace()
×
1475
          ? "border-style: none; background: transparent; font-family: "
1476
            "monospace;"
1477
          : "border-style: none; background: transparent;";
×
1478

1479
  if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
×
1480
    auto *line = new QLineEdit();
×
1481
    line->setObjectName(trimmedField);
×
1482
    line->setText(trimmedValue);
×
1483
    line->setReadOnly(true);
×
1484
    line->setStyleSheet(lineStyle);
×
1485
    line->setContentsMargins(0, 0, 0, 0);
×
1486
    line->setEchoMode(QLineEdit::Password);
×
1487
    auto *showButton = new QPushButtonShowPassword(line, this);
×
1488
    showButton->setStyleSheet(buttonStyle);
×
1489
    showButton->setContentsMargins(0, 0, 0, 0);
×
1490
    frame->layout()->addWidget(showButton);
×
1491
    frame->layout()->addWidget(line);
×
1492
  } else {
1493
    auto *line = new QTextBrowser();
×
1494
    line->setOpenExternalLinks(true);
×
1495
    line->setOpenLinks(true);
×
1496
    line->setMaximumHeight(26);
×
1497
    line->setMinimumHeight(26);
×
1498
    line->setSizePolicy(
×
1499
        QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1500
    line->setObjectName(trimmedField);
×
1501
    trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
1502
    line->setText(trimmedValue);
×
1503
    line->setReadOnly(true);
×
1504
    line->setStyleSheet(lineStyle);
×
1505
    line->setContentsMargins(0, 0, 0, 0);
×
1506
    frame->layout()->addWidget(line);
×
1507
  }
1508

1509
  frame->setStyleSheet(
×
1510
      ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1511

1512
  // set into the layout
1513
  ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
×
1514
  ui->gridLayout->addWidget(frame, position, 1);
×
1515
}
×
1516

1517
/**
1518
 * @brief Displays message in status bar
1519
 *
1520
 * @param msg     text to be displayed
1521
 * @param timeout time for which msg shall be visible
1522
 */
1523
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
×
1524
  ui->statusBar->showMessage(msg, timeout);
×
1525
}
×
1526

1527
/**
1528
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1529
 * @param dir Directory path to re-encrypt
1530
 */
1531
void MainWindow::reencryptPath(const QString &dir) {
×
1532
  QDir checkDir(dir);
×
1533
  if (!checkDir.exists()) {
×
1534
    QMessageBox::critical(this, tr("Error"),
×
1535
                          tr("Directory does not exist: %1").arg(dir));
×
1536
    return;
×
1537
  }
1538

1539
  int ret = QMessageBox::question(
×
1540
      this, tr("Re-encrypt passwords"),
×
1541
      tr("Re-encrypt all passwords in %1?\n\n"
×
1542
         "This will re-encrypt ALL password files in this folder "
1543
         "using the current recipients defined in .gpg-id.\n\n"
1544
         "This may rewrite many files and cannot be undone easily.\n\n"
1545
         "Continue?")
1546
          .arg(QDir(dir).dirName()),
×
1547
      QMessageBox::Yes | QMessageBox::No);
1548

1549
  if (ret != QMessageBox::Yes)
×
1550
    return;
1551

1552
  // Prevent double execution - use same method as startReencryptPath
1553
  setUiElementsEnabled(false);
×
1554
  ui->treeView->setDisabled(true);
×
1555

1556
  QtPassSettings::getImitatePass()->reencryptPath(
×
1557
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1558
}
×
1559

1560
/**
1561
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1562
 */
1563
void MainWindow::startReencryptPath() {
×
1564
  setUiElementsEnabled(false);
×
1565
  ui->treeView->setDisabled(true);
×
1566
}
×
1567

1568
/**
1569
 * @brief MainWindow::endReencryptPath re-enable ui elements
1570
 */
1571
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1572

1573
/**
1574
 * @brief MainWindow::exportPublicKey export current user's public key
1575
 * Shows instructions to export key via command line
1576
 * TODO: Wire to real gpg export via Executor - see issue #422
1577
 */
1578
void MainWindow::exportPublicKey() {
×
1579
  QString identity = QtPassSettings::getPassSigningKey();
×
1580
  if (identity.isEmpty()) {
×
1581
    identity = "<your-key-id>";
×
1582
  }
1583
  identity = identity.toHtmlEscaped();
×
1584
  QMessageBox::information(
×
1585
      this, tr("Export Public Key"),
×
1586
      tr("<h3>Export Your Public Key</h3>"
×
1587
         "<p>To export your public GPG key, run this in terminal:</p>"
1588
         "<pre>gpg --armor --export --output my_key.asc %1</pre>"
1589
         "<p>Then send the file to your teammates.</p>"
1590
         "<p>Your key ID: You can find it in QtPass Settings &gt; GPG "
1591
         "keys.</p>")
1592
          .arg(identity));
×
1593
}
×
1594

1595
/**
1596
 * @brief MainWindow::addRecipient add a new recipient's public key
1597
 * @param dir Directory path
1598
 */
1599
void MainWindow::addRecipient(const QString &dir) {
×
1600
  QString gpgIdPath = Pass::getGpgIdPath(dir).toHtmlEscaped();
×
1601
  QMessageBox::information(
×
1602
      this, tr("Add Recipient"),
×
1603
      tr("<h3>Add Recipient</h3>"
×
1604
         "<p>To add a teammate's public key:</p>"
1605
         "<ol>"
1606
         "<li>Save their public key as a .asc file</li>"
1607
         "<li>Copy the key text</li>"
1608
         "<li>Open %1</li>"
1609
         "<li>Add the new key ID to the file</li>"
1610
         "<li>Re-encrypt passwords to share with them</li>"
1611
         "</ol>"
1612
         "<p>Use the full fingerprint to ensure accuracy.</p>")
1613
          .arg(gpgIdPath));
×
1614
}
×
1615

1616
/**
1617
 * @brief MainWindow::showShareHelp show help about GPG sharing
1618
 */
1619
void MainWindow::showShareHelp() {
×
1620
  QMessageBox::information(
×
1621
      this, tr("Sharing Passwords with GPG"),
×
1622
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1623
         "<p>To share passwords with other users:</p>"
1624
         "<ol>"
1625
         "<li><b>Export your public key</b> and send it to teammates</li>"
1626
         "<li><b>Import teammates' public keys</b> to their own folders</li>"
1627
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1628
         "them</li>"
1629
         "</ol>"
1630
         "<p>Only people who have a matching secret key can decrypt the "
1631
         "passwords.</p>"
1632
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1633
         "<p>See the FAQ for more details.</p>"));
1634
}
×
1635

1636
void MainWindow::updateGitButtonVisibility() {
×
1637
  if (!QtPassSettings::isUseGit() ||
×
1638
      (QtPassSettings::getGitExecutable().isEmpty() &&
×
1639
       QtPassSettings::getPassExecutable().isEmpty())) {
×
1640
    enableGitButtons(false);
×
1641
  } else {
1642
    enableGitButtons(true);
×
1643
  }
1644
}
×
1645

1646
void MainWindow::updateOtpButtonVisibility() {
×
1647
#if defined(Q_OS_WIN) || defined(__APPLE__)
1648
  ui->actionOtp->setVisible(false);
1649
#endif
1650
  if (!QtPassSettings::isUseOtp()) {
×
1651
    ui->actionOtp->setEnabled(false);
×
1652
  } else {
1653
    ui->actionOtp->setEnabled(true);
×
1654
  }
1655
}
×
1656

1657
void MainWindow::updateGrepButtonVisibility() {
×
1658
  const bool enabled = QtPassSettings::isUseGrepSearch();
×
1659
  ui->grepButton->setVisible(enabled);
×
1660
  ui->grepCaseButton->setVisible(enabled);
×
1661
  if (!enabled && m_grepMode) {
×
1662
    ui->grepButton->setChecked(false);
×
1663
  }
1664
}
×
1665

1666
void MainWindow::enableGitButtons(const bool &state) {
×
1667
  // Following GNOME guidelines is preferable disable buttons instead of hide
1668
  ui->actionPush->setEnabled(state);
×
1669
  ui->actionUpdate->setEnabled(state);
×
1670
}
×
1671

1672
/**
1673
 * @brief MainWindow::critical critical message popup wrapper.
1674
 * @param title
1675
 * @param msg
1676
 */
1677
void MainWindow::critical(const QString &title, const QString &msg) {
×
1678
  QMessageBox::critical(this, title, msg);
×
1679
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc