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

IJHack / QtPass / 24938165024

25 Apr 2026 06:54PM UTC coverage: 27.433% (+0.6%) from 26.828%
24938165024

push

github

web-flow
feat: wire Share submenu actions to real operations (#422) (#1162)

* feat: wire Share submenu actions to real operations (#422)

Replace the help-only stubs added in #1144 with real behavior:

- Export my public key now runs `gpg --armor --export <signing-key>`
  via Executor and shows the armored output in a new dedicated
  ExportPublicKeyDialog, with Copy-to-Clipboard and Save-to-File
  actions. Falls back to the original guidance dialog when no signing
  key is configured.
- Add recipient now opens the existing UsersDialog for the folder so
  recipients can be ticked/unticked through the standard UI.

Part of issue #422.

* fix: address review on share submenu actions

- Use real years (2026) in SPDX headers of new files; matches the
  rest of the repo and replaces the YYYY placeholder.
- Split getPassSigningKey() on whitespace before passing key IDs to
  gpg --export so multi-key signing configurations work, mirroring the
  pattern used in imitatepass.cpp / pass.cpp.
- Switch the save path in ExportPublicKeyDialog to QSaveFile and check
  QTextStream::status() and commit() so a partial/failed write surfaces
  to the user instead of silently truncating.
- Give the Copy button transient feedback by relabelling it to
  "Copied!" for 1.5s via QTimer::singleShot.

* fix: harden ExportPublicKeyDialog against unsafe key IDs

- Sanitize the key ID before using it as the default save name: take
  only the first whitespace-separated token, then strip everything
  outside [A-Za-z0-9_-]. With the multi-key fix in fa77fa9 the stored
  identity can hold several space-separated IDs, so the raw value is
  no longer a clean filename.
- Force keyIdLabel to Qt::PlainText so a settings-controlled key ID
  can never be rendered as rich text. Matches the textFormat property
  used on UsersDialog's label.

* test: cover ExportPublicKeyDialog and harden against unsafe key IDs

Restores patch coverage for #1162 (codecov was 0% for the new files)
and addresses review feed... (continued)

25 of 66 new or added lines in 3 files covered. (37.88%)

11 existing lines in 2 files now uncovered.

1711 of 6237 relevant lines covered (27.43%)

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

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

53
  m_qtPass = new QtPass(this);
×
54

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

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

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

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

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

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

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

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

108
  updateProfileBox();
×
109

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

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

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

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

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

127
  setUiElementsEnabled(true);
×
128

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1075
  QtPassSettings::setProfile(name);
×
1076

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1574
/**
1575
 * @brief MainWindow::exportPublicKey export the configured signing key in
1576
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1577
 *
1578
 * Falls back to a help dialog when no signing key is configured or gpg is
1579
 * unavailable, so the user still gets actionable guidance.
1580
 */
1581
void MainWindow::exportPublicKey() {
×
1582
  QString identity = QtPassSettings::getPassSigningKey();
×
1583
  if (identity.isEmpty()) {
×
NEW
1584
    QMessageBox::information(
×
NEW
1585
        this, tr("Export Public Key"),
×
NEW
1586
        tr("<h3>Export Your Public Key</h3>"
×
1587
           "<p>No signing key is configured. Set one in QtPass Settings "
1588
           "&gt; GPG keys, or run this in a terminal:</p>"
1589
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1590
           "&gt;</pre>"
1591
           "<p>Then send the file to your teammates.</p>"));
NEW
1592
    return;
×
1593
  }
NEW
1594
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
NEW
1595
  if (gpgExe.isEmpty()) {
×
NEW
1596
    gpgExe = QStringLiteral("gpg");
×
1597
  }
NEW
1598
  QStringList args = {"--armor", "--export"};
×
NEW
1599
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
NEW
1600
  QString stdOut;
×
NEW
1601
  QString stdErr;
×
1602
  int exitCode =
NEW
1603
      Executor::executeBlocking(gpgExe, args, QString(), &stdOut, &stdErr);
×
NEW
1604
  if (exitCode != 0 || stdOut.isEmpty()) {
×
NEW
1605
    QMessageBox::warning(this, tr("Export Public Key"),
×
NEW
1606
                         tr("Could not export public key for %1.\n\n%2")
×
NEW
1607
                             .arg(identity, stdErr.isEmpty()
×
NEW
1608
                                                ? tr("No output from gpg.")
×
1609
                                                : stdErr));
1610
    return;
1611
  }
NEW
1612
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
NEW
1613
  dialog.exec();
×
UNCOV
1614
}
×
1615

1616
/**
1617
 * @brief MainWindow::addRecipient open the recipient management dialog for
1618
 *        the supplied directory.
1619
 * @param dir Folder whose .gpg-id should be edited.
1620
 *
1621
 * Delegates to UsersDialog so users can tick/untick keys from their
1622
 * keyring as recipients of the folder; importing a foreign key into the
1623
 * keyring still has to happen via gpg (or QtPass settings) first.
1624
 */
1625
void MainWindow::addRecipient(const QString &dir) {
×
NEW
1626
  UsersDialog d(dir, this);
×
NEW
1627
  d.exec();
×
UNCOV
1628
}
×
1629

1630
/**
1631
 * @brief MainWindow::showShareHelp show help about GPG sharing
1632
 */
1633
void MainWindow::showShareHelp() {
×
1634
  QMessageBox::information(
×
1635
      this, tr("Sharing Passwords with GPG"),
×
1636
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1637
         "<p>To share passwords with other users:</p>"
1638
         "<ol>"
1639
         "<li><b>Export your public key</b> and send it to teammates</li>"
1640
         "<li><b>Import teammates' public keys</b> to their own folders</li>"
1641
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1642
         "them</li>"
1643
         "</ol>"
1644
         "<p>Only people who have a matching secret key can decrypt the "
1645
         "passwords.</p>"
1646
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1647
         "<p>See the FAQ for more details.</p>"));
1648
}
×
1649

1650
void MainWindow::updateGitButtonVisibility() {
×
1651
  if (!QtPassSettings::isUseGit() ||
×
1652
      (QtPassSettings::getGitExecutable().isEmpty() &&
×
1653
       QtPassSettings::getPassExecutable().isEmpty())) {
×
1654
    enableGitButtons(false);
×
1655
  } else {
1656
    enableGitButtons(true);
×
1657
  }
1658
}
×
1659

1660
void MainWindow::updateOtpButtonVisibility() {
×
1661
#if defined(Q_OS_WIN) || defined(__APPLE__)
1662
  ui->actionOtp->setVisible(false);
1663
#endif
1664
  if (!QtPassSettings::isUseOtp()) {
×
1665
    ui->actionOtp->setEnabled(false);
×
1666
  } else {
1667
    ui->actionOtp->setEnabled(true);
×
1668
  }
1669
}
×
1670

1671
void MainWindow::updateGrepButtonVisibility() {
×
1672
  const bool enabled = QtPassSettings::isUseGrepSearch();
×
1673
  ui->grepButton->setVisible(enabled);
×
1674
  ui->grepCaseButton->setVisible(enabled);
×
1675
  if (!enabled && m_grepMode) {
×
1676
    ui->grepButton->setChecked(false);
×
1677
  }
1678
}
×
1679

1680
void MainWindow::enableGitButtons(const bool &state) {
×
1681
  // Following GNOME guidelines is preferable disable buttons instead of hide
1682
  ui->actionPush->setEnabled(state);
×
1683
  ui->actionUpdate->setEnabled(state);
×
1684
}
×
1685

1686
/**
1687
 * @brief MainWindow::critical critical message popup wrapper.
1688
 * @param title
1689
 * @param msg
1690
 */
1691
void MainWindow::critical(const QString &title, const QString &msg) {
×
1692
  QMessageBox::critical(this, title, msg);
×
1693
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc