QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
mainwindow.cpp
Go to the documentation of this file.
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"
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 <QCloseEvent>
22#include <QDesktopServices>
23#include <QDialog>
24#include <QDirIterator>
25#include <QFileInfo>
26#include <QInputDialog>
27#include <QLabel>
28#include <QMenu>
29#include <QMessageBox>
30#include <QShortcut>
31#include <QTimer>
32#include <utility>
33
40MainWindow::MainWindow(const QString &searchText, QWidget *parent)
41 : QMainWindow(parent), ui(new Ui::MainWindow), keygen(nullptr),
42 tray(nullptr) {
43#ifdef __APPLE__
44 // extra treatment for mac os
45 // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
46 qt_set_sequence_auto_mnemonic(true);
47#endif
48 ui->setupUi(this);
49
50 m_qtPass = new QtPass(this);
51
52 // register shortcut ctrl/cmd + Q to close the main window
53 new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
54 // register shortcut ctrl/cmd + C to copy the currently selected password
55 new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this,
56 SLOT(copyPasswordFromTreeview()));
57
58 model.setNameFilters(QStringList() << "*.gpg");
59 model.setNameFilterDisables(false);
60
61 /*
62 * I added this to solve Windows bug but now on GNU/Linux the main folder,
63 * if hidden, disappear
64 *
65 * model.setFilter(QDir::NoDot);
66 */
67
69
70 QModelIndex rootDir = model.setRootPath(passStore);
71 model.fetchMore(rootDir);
72
73 proxyModel.setModelAndStore(&model, passStore);
74 selectionModel.reset(new QItemSelectionModel(&proxyModel));
75
76 ui->treeView->setModel(&proxyModel);
77 ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir));
78 ui->treeView->setColumnHidden(1, true);
79 ui->treeView->setColumnHidden(2, true);
80 ui->treeView->setColumnHidden(3, true);
81 ui->treeView->setHeaderHidden(true);
82 ui->treeView->setIndentation(15);
83 ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
84 ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
85 ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
86 ui->treeView->sortByColumn(0, Qt::AscendingOrder);
87 connect(ui->treeView, &QWidget::customContextMenuRequested, this,
88 &MainWindow::showContextMenu);
89 connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
91
93 QFont monospace("Monospace");
94 monospace.setStyleHint(QFont::Monospace);
95 ui->textBrowser->setFont(monospace);
96 }
98 ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
99 }
100 ui->textBrowser->setOpenExternalLinks(true);
101 ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
102 connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
103 &MainWindow::showBrowserContextMenu);
104
105 updateProfileBox();
106
107 QtPassSettings::getPass()->updateEnv();
108 clearPanelTimer.setInterval(MS_PER_SECOND *
110 clearPanelTimer.setSingleShot(true);
111 connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
112
113 searchTimer.setInterval(350);
114 searchTimer.setSingleShot(true);
115
116 connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
117
118 initToolBarButtons();
119 initStatusBar();
120
121 ui->lineEdit->setClearButtonEnabled(true);
122
124
125 QTimer::singleShot(10, this, SLOT(focusInput()));
126
127 ui->lineEdit->setText(searchText);
128
129 if (!m_qtPass->init()) {
130 // no working config so this should just quit
131 QApplication::quit();
132 }
133}
134
135MainWindow::~MainWindow() { delete m_qtPass; }
136
143void MainWindow::focusInput() {
144 ui->lineEdit->selectAll();
145 ui->lineEdit->setFocus();
146}
147
152void MainWindow::changeEvent(QEvent *event) {
153 QWidget::changeEvent(event);
154 if (event->type() == QEvent::ActivationChange) {
155 if (isActiveWindow()) {
156 focusInput();
157 }
158 }
159}
160
164void MainWindow::initToolBarButtons() {
165 connect(ui->actionAddPassword, &QAction::triggered, this,
166 &MainWindow::addPassword);
167 connect(ui->actionAddFolder, &QAction::triggered, this,
168 &MainWindow::addFolder);
169 connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
170 connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
171 connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
172 connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
173 connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
174 connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
175 connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
176
177 ui->actionAddPassword->setIcon(
178 QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
179 ui->actionAddFolder->setIcon(
180 QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
181 ui->actionEdit->setIcon(QIcon::fromTheme(
182 "document-properties", QIcon(":/icons/document-properties.svg")));
183 ui->actionDelete->setIcon(
184 QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
185 ui->actionPush->setIcon(
186 QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
187 ui->actionUpdate->setIcon(
188 QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
189 ui->actionUsers->setIcon(QIcon::fromTheme(
190 "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
191 ui->actionConfig->setIcon(QIcon::fromTheme(
192 "applications-system", QIcon(":/icons/applications-system.svg")));
193}
194
198void MainWindow::initStatusBar() {
199 ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
200
201 QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
202 .scaledToHeight(statusBar()->height());
203 auto *logoApp = new QLabel(statusBar());
204 logoApp->setPixmap(logo);
205 statusBar()->addPermanentWidget(logoApp);
206}
207
209 return ui->treeView->currentIndex();
210}
211
213 this->keygen->close();
214 this->keygen = nullptr;
215}
216
231void MainWindow::flashText(const QString &text, const bool isError,
232 const bool isHtml) {
233 if (isError) {
234 ui->textBrowser->setTextColor(Qt::red);
235 }
236
237 if (isHtml) {
238 QString _text = text;
239 if (!ui->textBrowser->toPlainText().isEmpty()) {
240 _text = ui->textBrowser->toHtml() + _text;
241 }
242 ui->textBrowser->setHtml(_text);
243 } else {
244 ui->textBrowser->setText(text);
245 }
246}
247
252void MainWindow::applyTextBrowserSettings() {
254 QFont monospace("Monospace");
255 monospace.setStyleHint(QFont::Monospace);
256 ui->textBrowser->setFont(monospace);
257 } else {
258 ui->textBrowser->setFont(QFont());
259 }
260
262 ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
263 } else {
264 ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
265 }
266}
267
268void MainWindow::applyWindowFlagsSettings() {
270 Qt::WindowFlags flags = windowFlags();
271 this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
272 } else {
273 this->setWindowFlags(Qt::Window);
274 }
275 this->show();
276}
277
287 QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
288 d->setModal(true);
289 // Automatically default to pass if it's available
290 if (m_qtPass->isFreshStart() &&
291 QFile(QtPassSettings::getPassExecutable()).exists()) {
293 }
294
295 if (m_qtPass->isFreshStart()) {
296 d->wizard(); // does shit
297 }
298 if (d->exec()) {
299 if (d->result() == QDialog::Accepted) {
300 applyTextBrowserSettings();
301 applyWindowFlagsSettings();
302
303 updateProfileBox();
304 ui->treeView->setRootIndex(proxyModel.mapFromSource(
305 model.setRootPath(QtPassSettings::getPassStore())));
306
307 if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
308 config();
309 }
310 QtPassSettings::getPass()->updateEnv();
311 clearPanelTimer.setInterval(MS_PER_SECOND *
313 m_qtPass->setClipboardTimer();
314
315 updateGitButtonVisibility();
316 updateOtpButtonVisibility();
317 if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
318 initTrayIcon();
319 } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
320 destroyTrayIcon();
321 }
322 }
323
324 m_qtPass->setFreshStart(false);
325 }
326}
327
331void MainWindow::onUpdate(bool block) {
332 ui->statusBar->showMessage(tr("Updating password-store"), 2000);
333 if (block) {
334 QtPassSettings::getPass()->GitPull_b();
335 } else {
336 QtPassSettings::getPass()->GitPull();
337 }
338}
339
345 ui->statusBar->showMessage(tr("Updating password-store"), 2000);
346 QtPassSettings::getPass()->GitPush();
347 }
348}
349
357auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
358 if (!index.isValid() ||
359 !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
360 return {};
361 }
362 QString filePath = model.filePath(proxyModel.mapToSource(index));
363 if (forPass) {
364 filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
365 filePath.replace(Util::endsWithGpg(), "");
366 }
367 return filePath;
368}
369
374void MainWindow::on_treeView_clicked(const QModelIndex &index) {
375 bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
376 currentDir =
377 Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
378 // Clear any previously cached clipped text before showing new password
379 m_qtPass->clearClippedText();
380 QString file = getFile(index, true);
381 ui->passwordName->setText(getFile(index, true));
382 if (!file.isEmpty() && !cleared) {
383 QtPassSettings::getPass()->Show(file);
384 } else {
385 clearPanel(false);
386 ui->actionEdit->setEnabled(false);
387 ui->actionDelete->setEnabled(true);
388 }
389}
390
396void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
397 QFileInfo fileOrFolder =
398 model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
399
400 if (fileOrFolder.isFile()) {
401 editPassword(getFile(index, true));
402 }
403}
404
409 currentDir = "";
410 m_qtPass->clearClipboard();
411 ui->treeView->clearSelection();
412 ui->actionEdit->setEnabled(false);
413 ui->actionDelete->setEnabled(false);
414 ui->passwordName->setText("");
415 clearPanel(false);
416}
417
419 clearTemplateWidgets();
420 ui->textBrowser->clear();
422 clearPanelTimer.stop();
423}
424
435void MainWindow::passShowHandler(const QString &p_output) {
436 QStringList templ = QtPassSettings::isUseTemplate()
437 ? QtPassSettings::getPassTemplate().split("\n")
438 : QStringList();
439 bool allFields =
441 FileContent fileContent = FileContent::parse(p_output, templ, allFields);
442 QString output = p_output;
443 QString password = fileContent.getPassword();
444
445 // set clipped text
446 m_qtPass->setClippedText(password, p_output);
447
448 // first clear the current view:
449 clearTemplateWidgets();
450
451 // show what is needed:
453 output = "***" + tr("Content hidden") + "***";
454 } else if (!QtPassSettings::isDisplayAsIs()) {
455 if (!password.isEmpty()) {
456 // set the password, it is hidden if needed in addToGridLayout
457 addToGridLayout(0, tr("Password"), password);
458 }
459
460 NamedValues namedValues = fileContent.getNamedValues();
461 for (int j = 0; j < namedValues.length(); ++j) {
462 const NamedValue &nv = namedValues.at(j);
463 addToGridLayout(j + 1, nv.name, nv.value);
464 }
465 if (ui->gridLayout->count() == 0) {
466 ui->verticalLayoutPassword->setSpacing(0);
467 } else {
468 ui->verticalLayoutPassword->setSpacing(6);
469 }
470
471 output = fileContent.getRemainingDataForDisplay();
472 }
473
475 clearPanelTimer.start();
476 }
477
478 emit passShowHandlerFinished(output);
480}
481
492void MainWindow::passOtpHandler(const QString &p_output) {
493 if (!p_output.isEmpty()) {
494 addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
495 m_qtPass->copyTextToClipboard(p_output);
496 showStatusMessage(tr("OTP code copied to clipboard"));
497 } else {
498 flashText(tr("No OTP code found in this password entry"), true);
499 }
501 clearPanelTimer.start();
502 }
504}
505
509void MainWindow::clearPanel(bool notify) {
510 while (ui->gridLayout->count() > 0) {
511 QLayoutItem *item = ui->gridLayout->takeAt(0);
512 delete item->widget();
513 delete item;
514 }
515 if (notify) {
516 QString output = "***" + tr("Password and Content hidden") + "***";
517 ui->textBrowser->setHtml(output);
518 } else {
519 ui->textBrowser->setHtml("");
520 }
521}
522
529 ui->treeView->setEnabled(state);
530 ui->lineEdit->setEnabled(state);
531 ui->lineEdit->installEventFilter(this);
532 ui->actionAddPassword->setEnabled(state);
533 ui->actionAddFolder->setEnabled(state);
534 ui->actionUsers->setEnabled(state);
535 ui->actionConfig->setEnabled(state);
536 // is a file selected?
537 state &= ui->treeView->currentIndex().isValid();
538 ui->actionDelete->setEnabled(state);
539 ui->actionEdit->setEnabled(state);
540 updateGitButtonVisibility();
541 updateOtpButtonVisibility();
542}
543
554 QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
555 restoreGeometry(geometry);
556 QByteArray savestate = QtPassSettings::getSavestate(saveState());
557 restoreState(savestate);
558 QPoint position = QtPassSettings::getPos(pos());
559 move(position);
560 QSize newSize = QtPassSettings::getSize(size());
561 resize(newSize);
562 if (QtPassSettings::isMaximized(isMaximized())) {
563 showMaximized();
564 }
565
567 Qt::WindowFlags flags = windowFlags();
568 setWindowFlags(flags | Qt::WindowStaysOnTopHint);
569 show();
570 }
571
572 if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
573 initTrayIcon();
575 // since we are still in constructor, can't directly hide
576 QTimer::singleShot(10, this, SLOT(hide()));
577 }
578 } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
579 destroyTrayIcon();
580 }
581}
582
586void MainWindow::onConfig() { config(); }
587
593void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
594 ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
595 ui->treeView->expandAll();
596 clearPanel(false);
597 ui->passwordName->setText("");
598 ui->actionEdit->setEnabled(false);
599 ui->actionDelete->setEnabled(false);
600 searchTimer.start();
601}
602
607void MainWindow::onTimeoutSearch() {
608 QString query = ui->lineEdit->text();
609
610 if (query.isEmpty()) {
611 ui->treeView->collapseAll();
612 deselect();
613 }
614
615 query.replace(QStringLiteral(" "), ".*");
616 QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
617 proxyModel.setFilterRegularExpression(regExp);
618 ui->treeView->setRootIndex(proxyModel.mapFromSource(
619 model.setRootPath(QtPassSettings::getPassStore())));
620
621 if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
622 selectFirstFile();
623 } else {
624 ui->actionEdit->setEnabled(false);
625 ui->actionDelete->setEnabled(false);
626 }
627}
628
634void MainWindow::on_lineEdit_returnPressed() {
635#ifdef QT_DEBUG
636 dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
637#endif
638
639 if (proxyModel.rowCount() > 0) {
640 selectFirstFile();
641 on_treeView_clicked(ui->treeView->currentIndex());
642 }
643}
644
649void MainWindow::selectFirstFile() {
650 QModelIndex index = proxyModel.mapFromSource(
651 model.setRootPath(QtPassSettings::getPassStore()));
652 index = firstFile(index);
653 ui->treeView->setCurrentIndex(index);
654}
655
661auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
662 QModelIndex index = parentIndex;
663 int numRows = proxyModel.rowCount(parentIndex);
664 for (int row = 0; row < numRows; ++row) {
665 index = proxyModel.index(row, 0, parentIndex);
666 if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
667 return index;
668 }
669 if (proxyModel.hasChildren(index)) {
670 return firstFile(index);
671 }
672 }
673 return index;
674}
675
681void MainWindow::setPassword(const QString &file, bool isNew) {
682 PasswordDialog d(file, isNew, this);
683
684 if (!d.exec()) {
685 ui->treeView->setFocus();
686 }
687}
688
693void MainWindow::addPassword() {
694 bool ok;
695 QString dir =
696 Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
697 QString file =
698 QInputDialog::getText(this, tr("New file"),
699 tr("New password file: \n(Will be placed in %1 )")
701 Util::getDir(ui->treeView->currentIndex(),
702 true, model, proxyModel)),
703 QLineEdit::Normal, "", &ok);
704 if (!ok || file.isEmpty()) {
705 return;
706 }
707 file = dir + file;
708 setPassword(file);
709}
710
715void MainWindow::onDelete() {
716 QModelIndex currentIndex = ui->treeView->currentIndex();
717 if (!currentIndex.isValid()) {
718 // This fixes https://github.com/IJHack/QtPass/issues/556
719 // Otherwise the entire password directory would be deleted if
720 // nothing is selected in the tree view.
721 return;
722 }
723
724 QFileInfo fileOrFolder =
725 model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
726 QString file = "";
727 bool isDir = false;
728
729 if (fileOrFolder.isFile()) {
730 file = getFile(ui->treeView->currentIndex(), true);
731 } else {
732 file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
733 isDir = true;
734 }
735
736 QString dirMessage = tr(" and the whole content?");
737 if (isDir) {
738 QDirIterator it(model.rootPath() + QDir::separator() + file,
739 QDirIterator::Subdirectories);
740 bool okDir = true;
741 while (it.hasNext() && okDir) {
742 it.next();
743 if (QFileInfo(it.filePath()).isFile()) {
744 if (QFileInfo(it.filePath()).suffix() != "gpg") {
745 okDir = false;
746 dirMessage = tr(" and the whole content? <br><strong>Attention: "
747 "there are unexpected files in the given folder, "
748 "check them before continue.</strong>");
749 }
750 }
751 }
752 }
753
754 if (QMessageBox::question(
755 this, isDir ? tr("Delete folder?") : tr("Delete password?"),
756 tr("Are you sure you want to delete %1%2?")
757 .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
758 QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
759 return;
760 }
761
762 QtPassSettings::getPass()->Remove(file, isDir);
763}
764
768void MainWindow::onOtp() {
769 QString file = getFile(ui->treeView->currentIndex(), true);
770 if (!file.isEmpty()) {
773 QtPassSettings::getPass()->OtpGenerate(file);
774 }
775 } else {
776 flashText(tr("No password selected for OTP generation"), true);
777 }
778}
779
783void MainWindow::onEdit() {
784 QString file = getFile(ui->treeView->currentIndex(), true);
785 editPassword(file);
786}
787
792void MainWindow::userDialog(const QString &dir) {
793 if (!dir.isEmpty()) {
794 currentDir = dir;
795 }
796 onUsers();
797}
798
804void MainWindow::onUsers() {
805 QString dir =
806 currentDir.isEmpty()
807 ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
808 : currentDir;
809
810 UsersDialog d(dir, this);
811 if (!d.exec()) {
812 ui->treeView->setFocus();
813 }
814}
815
820void MainWindow::messageAvailable(const QString &message) {
821 if (message.isEmpty()) {
822 focusInput();
823 } else {
824 ui->treeView->expandAll();
825 ui->lineEdit->setText(message);
826 on_lineEdit_returnPressed();
827 }
828 show();
829 raise();
830}
831
837void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
838 keygen = keygenWindow;
839 emit generateGPGKeyPair(batch);
840}
841
846void MainWindow::updateProfileBox() {
847 QHash<QString, QHash<QString, QString>> profiles =
849
850 if (profiles.isEmpty()) {
851 ui->profileWidget->hide();
852 } else {
853 ui->profileWidget->show();
854 ui->profileBox->setEnabled(profiles.size() > 1);
855 ui->profileBox->clear();
856 QHashIterator<QString, QHash<QString, QString>> i(profiles);
857 while (i.hasNext()) {
858 i.next();
859 if (!i.key().isEmpty()) {
860 ui->profileBox->addItem(i.key());
861 }
862 }
863 ui->profileBox->model()->sort(0);
864 }
865 int index = ui->profileBox->findText(QtPassSettings::getProfile());
866 if (index != -1) { // -1 for not found
867 ui->profileBox->setCurrentIndex(index);
868 }
869}
870
876#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
877void MainWindow::on_profileBox_currentIndexChanged(QString name) {
878#else
890void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
891#endif
892 if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
893 return;
894 }
895
896 ui->lineEdit->clear();
897
899
901 QtPassSettings::getProfiles().value(name).value("path"));
903 QtPassSettings::getProfiles().value(name).value("signingKey"));
904 ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
905
906 QtPassSettings::getPass()->updateEnv();
907
908 ui->treeView->selectionModel()->clear();
909 ui->treeView->setRootIndex(proxyModel.mapFromSource(
910 model.setRootPath(QtPassSettings::getPassStore())));
911
912 ui->actionEdit->setEnabled(false);
913 ui->actionDelete->setEnabled(false);
914}
915
921void MainWindow::initTrayIcon() {
922 this->tray = new TrayIcon(this);
923 // Setup tray icon
924
925 if (tray == nullptr) {
926#ifdef QT_DEBUG
927 dbg() << "Allocating tray icon failed.";
928#endif
929 }
930
931 if (!tray->getIsAllocated()) {
932 destroyTrayIcon();
933 }
934}
935
939void MainWindow::destroyTrayIcon() {
940 delete this->tray;
941 tray = nullptr;
942}
943
948void MainWindow::closeEvent(QCloseEvent *event) {
950 this->hide();
951 event->ignore();
952 } else {
953 m_qtPass->clearClipboard();
954
955 QtPassSettings::setGeometry(saveGeometry());
956 QtPassSettings::setSavestate(saveState());
957 QtPassSettings::setMaximized(isMaximized());
958 if (!isMaximized()) {
961 }
962 event->accept();
963 }
964}
965
973auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
974 if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
975 auto *key = dynamic_cast<QKeyEvent *>(event);
976 if (key != nullptr && key->key() == Qt::Key_Down) {
977 ui->treeView->setFocus();
978 }
979 }
980 return QObject::eventFilter(obj, event);
981}
982
987void MainWindow::keyPressEvent(QKeyEvent *event) {
988 switch (event->key()) {
989 case Qt::Key_Delete:
990 onDelete();
991 break;
992 case Qt::Key_Return:
993 case Qt::Key_Enter:
994 if (proxyModel.rowCount() > 0) {
995 on_treeView_clicked(ui->treeView->currentIndex());
996 }
997 break;
998 case Qt::Key_Escape:
999 ui->lineEdit->clear();
1000 break;
1001 default:
1002 break;
1003 }
1004}
1005
1011void MainWindow::showContextMenu(const QPoint &pos) {
1012 QModelIndex index = ui->treeView->indexAt(pos);
1013 bool selected = true;
1014 if (!index.isValid()) {
1015 ui->treeView->clearSelection();
1016 ui->actionDelete->setEnabled(false);
1017 ui->actionEdit->setEnabled(false);
1018 currentDir = "";
1019 selected = false;
1020 }
1021
1022 ui->treeView->setCurrentIndex(index);
1023
1024 QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
1025
1026 QFileInfo fileOrFolder =
1027 model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
1028
1029 QMenu contextMenu;
1030 if (!selected || fileOrFolder.isDir()) {
1031 QAction *openFolder =
1032 contextMenu.addAction(tr("Open folder with file manager"));
1033 QAction *addFolder = contextMenu.addAction(tr("Add folder"));
1034 QAction *addPassword = contextMenu.addAction(tr("Add password"));
1035 QAction *users = contextMenu.addAction(tr("Users"));
1036 connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
1037 connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
1038 connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
1039 connect(users, &QAction::triggered, this, &MainWindow::onUsers);
1040 } else if (fileOrFolder.isFile()) {
1041 QAction *edit = contextMenu.addAction(tr("Edit"));
1042 connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
1043 }
1044 if (selected) {
1045 contextMenu.addSeparator();
1046 if (fileOrFolder.isDir()) {
1047 QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
1048 connect(renameFolder, &QAction::triggered, this,
1049 &MainWindow::renameFolder);
1050 } else if (fileOrFolder.isFile()) {
1051 QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
1052 connect(renamePassword, &QAction::triggered, this,
1053 &MainWindow::renamePassword);
1054 }
1055 QAction *deleteItem = contextMenu.addAction(tr("Delete"));
1056 connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
1057 }
1058 contextMenu.exec(globalPos);
1059}
1060
1066void MainWindow::showBrowserContextMenu(const QPoint &pos) {
1067 QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
1068 QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
1069
1070 contextMenu->exec(globalPos);
1071 delete contextMenu;
1072}
1073
1077void MainWindow::openFolder() {
1078 QString dir =
1079 Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
1080
1081 QString path = QDir::toNativeSeparators(dir);
1082 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
1083}
1084
1088void MainWindow::addFolder() {
1089 bool ok;
1090 QString dir =
1091 Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
1092 QString newdir =
1093 QInputDialog::getText(this, tr("New file"),
1094 tr("New Folder: \n(Will be placed in %1 )")
1096 Util::getDir(ui->treeView->currentIndex(),
1097 true, model, proxyModel)),
1098 QLineEdit::Normal, "", &ok);
1099 if (!ok || newdir.isEmpty()) {
1100 return;
1101 }
1102 newdir.prepend(dir);
1103 if (!QDir().mkdir(newdir)) {
1104 QMessageBox::warning(this, tr("Error"),
1105 tr("Failed to create folder: %1").arg(newdir));
1106 return;
1107 }
1108 if (QtPassSettings::isAddGPGId(true)) {
1109 QString gpgIdFile = newdir + "/.gpg-id";
1110 QFile gpgId(gpgIdFile);
1111 if (!gpgId.open(QIODevice::WriteOnly)) {
1112 QMessageBox::warning(
1113 this, tr("Error"),
1114 tr("Failed to create .gpg-id file in: %1").arg(newdir));
1115 return;
1116 }
1117 QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
1118 for (const UserInfo &user : users) {
1119 if (user.enabled) {
1120 gpgId.write((user.key_id + "\n").toUtf8());
1121 }
1122 }
1123 gpgId.close();
1124 }
1125}
1126
1130void MainWindow::renameFolder() {
1131 bool ok;
1132 QString srcDir = QDir::cleanPath(
1133 Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
1134 QString srcDirName = QDir(srcDir).dirName();
1135 QString newName =
1136 QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
1137 QLineEdit::Normal, srcDirName, &ok);
1138 if (!ok || newName.isEmpty()) {
1139 return;
1140 }
1141 QString destDir = srcDir;
1142 destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
1143 QtPassSettings::getPass()->Move(srcDir, destDir);
1144}
1145
1150void MainWindow::editPassword(const QString &file) {
1151 if (!file.isEmpty()) {
1153 onUpdate(true);
1154 }
1155 setPassword(file, false);
1156 }
1157}
1158
1162void MainWindow::renamePassword() {
1163 bool ok;
1164 QString file = getFile(ui->treeView->currentIndex(), false);
1165 QString filePath = QFileInfo(file).path();
1166 QString fileName = QFileInfo(file).fileName();
1167 if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
1168 fileName.chop(4);
1169 }
1170
1171 QString newName =
1172 QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
1173 QLineEdit::Normal, fileName, &ok);
1174 if (!ok || newName.isEmpty()) {
1175 return;
1176 }
1177 QString newFile = QDir(filePath).filePath(newName);
1178 QtPassSettings::getPass()->Move(file, newFile);
1179}
1180
1185void MainWindow::clearTemplateWidgets() {
1186 while (ui->gridLayout->count() > 0) {
1187 QLayoutItem *item = ui->gridLayout->takeAt(0);
1188 delete item->widget();
1189 delete item;
1190 }
1191 ui->verticalLayoutPassword->setSpacing(0);
1192}
1193
1202void MainWindow::copyPasswordFromTreeview() {
1203 QFileInfo fileOrFolder =
1204 model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
1205
1206 if (fileOrFolder.isFile()) {
1207 QString file = getFile(ui->treeView->currentIndex(), true);
1208 // Disconnect any previous connection to avoid accumulation
1209 disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
1210 &MainWindow::passwordFromFileToClipboard);
1212 &MainWindow::passwordFromFileToClipboard);
1213 QtPassSettings::getPass()->Show(file);
1214 }
1215}
1216
1217void MainWindow::passwordFromFileToClipboard(const QString &text) {
1218 QStringList tokens = text.split('\n');
1219 m_qtPass->copyTextToClipboard(tokens[0]);
1220}
1221
1228void MainWindow::addToGridLayout(int position, const QString &field,
1229 const QString &value) {
1230 QString trimmedField = field.trimmed();
1231 QString trimmedValue = value.trimmed();
1232
1233 const QString buttonStyle =
1234 "border-style: none; background: transparent; padding: 0; margin: 0; "
1235 "icon-size: 16px; color: inherit;";
1236
1237 // Combine the Copy button and the line edit in one widget
1238 auto *frame = new QFrame();
1239 QLayout *ly = new QHBoxLayout();
1240 ly->setContentsMargins(5, 2, 2, 2);
1241 ly->setSpacing(0);
1242 frame->setLayout(ly);
1244 auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
1245 connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
1247
1248 fieldLabel->setStyleSheet(buttonStyle);
1249 frame->layout()->addWidget(fieldLabel);
1250 }
1251
1253 auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
1254 connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
1256 qrbutton->setStyleSheet(buttonStyle);
1257 frame->layout()->addWidget(qrbutton);
1258 }
1259
1260 // set the echo mode to password, if the field is "password"
1261 const QString lineStyle =
1263 ? "border-style: none; background: transparent; font-family: "
1264 "monospace;"
1265 : "border-style: none; background: transparent;";
1266
1267 if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
1268 auto *line = new QLineEdit();
1269 line->setObjectName(trimmedField);
1270 line->setText(trimmedValue);
1271 line->setReadOnly(true);
1272 line->setStyleSheet(lineStyle);
1273 line->setContentsMargins(0, 0, 0, 0);
1274 line->setEchoMode(QLineEdit::Password);
1275 auto *showButton = new QPushButtonShowPassword(line, this);
1276 showButton->setStyleSheet(buttonStyle);
1277 showButton->setContentsMargins(0, 0, 0, 0);
1278 frame->layout()->addWidget(showButton);
1279 frame->layout()->addWidget(line);
1280 } else {
1281 auto *line = new QTextBrowser();
1282 line->setOpenExternalLinks(true);
1283 line->setOpenLinks(true);
1284 line->setMaximumHeight(26);
1285 line->setMinimumHeight(26);
1286 line->setSizePolicy(
1287 QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1288 line->setObjectName(trimmedField);
1289 trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
1290 line->setText(trimmedValue);
1291 line->setReadOnly(true);
1292 line->setStyleSheet(lineStyle);
1293 line->setContentsMargins(0, 0, 0, 0);
1294 frame->layout()->addWidget(line);
1295 }
1296
1297 frame->setStyleSheet(
1298 ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1299
1300 // set into the layout
1301 ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
1302 ui->gridLayout->addWidget(frame, position, 1);
1303}
1304
1311void MainWindow::showStatusMessage(const QString &msg, int timeout) {
1312 ui->statusBar->showMessage(msg, timeout);
1313}
1314
1319 setUiElementsEnabled(false);
1320 ui->treeView->setDisabled(true);
1321}
1322
1327
1328void MainWindow::updateGitButtonVisibility() {
1329 if (!QtPassSettings::isUseGit() ||
1330 (QtPassSettings::getGitExecutable().isEmpty() &&
1331 QtPassSettings::getPassExecutable().isEmpty())) {
1332 enableGitButtons(false);
1333 } else {
1334 enableGitButtons(true);
1335 }
1336}
1337
1338void MainWindow::updateOtpButtonVisibility() {
1339#if defined(Q_OS_WIN) || defined(__APPLE__)
1340 ui->actionOtp->setVisible(false);
1341#endif
1342 if (!QtPassSettings::isUseOtp()) {
1343 ui->actionOtp->setEnabled(false);
1344 } else {
1345 ui->actionOtp->setEnabled(true);
1346 }
1347}
1348
1349void MainWindow::enableGitButtons(const bool &state) {
1350 // Following GNOME guidelines is preferable disable buttons instead of hide
1351 ui->actionPush->setEnabled(state);
1352 ui->actionUpdate->setEnabled(state);
1353}
1354
1360void MainWindow::critical(const QString &title, const QString &msg) {
1361 QMessageBox::critical(this, title, msg);
1362}
The ConfigDialog handles the configuration interface.
void emptyClicked()
emptyClicked event
auto getNamedValues() const -> NamedValues
Gets named value pairs from the parsed file.
auto getRemainingDataForDisplay() const -> QString
Gets remaining data for display (excludes hidden fields like OTP).
auto getPassword() const -> QString
Gets the password from the parsed file.
static auto parse(const QString &fileContent, const QStringList &templateFields, bool allFields) -> FileContent
parse parses the given fileContent in a FileContent object. The password is accessible through getPas...
void startReencryptPath()
MainWindow::startReencryptPath disable ui elements and treeview.
void closeEvent(QCloseEvent *event) override
MainWindow::closeEvent hide or quit.
void passShowHandler(const QString &)
~MainWindow() override
void endReencryptPath()
MainWindow::endReencryptPath re-enable ui elements.
void executeWrapperStarted()
void generateKeyPair(const QString &, QDialog *)
MainWindow::generateKeyPair internal gpg keypair generator . .
void changeEvent(QEvent *event) override
MainWindow::changeEvent sets focus to the search box.
void critical(const QString &, const QString &)
MainWindow::critical critical message popup wrapper.
void messageAvailable(const QString &message)
MainWindow::messageAvailable we have some text/message/search to do.
MainWindow(const QString &searchText=QString(), QWidget *parent=nullptr)
MainWindow::MainWindow handles all of the main functionality and also the main window.
void keyPressEvent(QKeyEvent *event) override
MainWindow::keyPressEvent did anyone press return, enter or escape?
void onPush()
MainWindow::onPush do a git push.
void showStatusMessage(const QString &msg, int timeout=2000)
Displays message in status bar.
void passOtpHandler(const QString &)
void passShowHandlerFinished(const QString &output)
void generateGPGKeyPair(const QString &batch)
void restoreWindow()
auto eventFilter(QObject *obj, QEvent *event) -> bool override
MainWindow::eventFilter filter out some events and focus the treeview.
void flashText(const QString &text, const bool isError, const bool isHtml=false)
void userDialog(const QString &="")
MainWindow::userDialog see MainWindow::onUsers().
void setUiElementsEnabled(bool state)
MainWindow::setUiElementsEnabled enable or disable the relevant UI elements.
auto getCurrentTreeViewIndex() -> QModelIndex
void cleanKeygenDialog()
void deselect()
MainWindow::deselect clear the selection, password and copy buffer.
void on_treeView_clicked(const QModelIndex &index)
MainWindow::on_treeView_clicked read the selected password file.
void config()
The NamedValues class is mostly a list of NamedValue but also has a method to take a specific NamedVa...
Definition filecontent.h:23
void finishedShow(const QString &)
Emitted when show finishes.
void clicked(const QString &)
void clicked(const QString &)
void copyTextToClipboard(const QString &text)
MainWindow::copyTextToClipboard copies text to your clipboard.
Definition qtpass.cpp:499
void showTextAsQRCode(const QString &text)
displays the text as qrcode
Definition qtpass.cpp:521
static void setMaximized(const bool &maximized)
Save maximized state.
static auto isStartMinimized(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether application should start minimized.
static auto isUseOtp(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether OTP support is enabled.
static void setProfile(const QString &profile)
Save active profile name.
static auto isNoLineWrapping(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to disable line wrapping.
static void setPassStore(const QString &passStore)
Save password store path.
static auto isHideContent(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to hide content (password + username).
static auto getSize(const QSize &defaultValue=QVariant().toSize()) -> QSize
Get saved window size.
static auto getPass() -> Pass *
Get currently active pass backend instance.
static auto isUseQrencode(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether qrencode support is enabled.
static auto isUseAutoclearPanel(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to use panel autoclear.
static auto isAutoPull(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether automatic pull is enabled.
static auto isUseGit(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether Git integration is enabled.
static auto getClipBoardType(const Enums::clipBoardType &defaultValue=Enums::CLIPBOARD_NEVER) -> Enums::clipBoardType
Get clipboard type as enum.
static auto isTemplateAllFields(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether template applies to all fields.
static void setPassSigningKey(const QString &passSigningKey)
Save GPG signing key.
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
Get password store directory path.
static auto isUseTemplate(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether template usage is enabled.
static auto isUseTrayIcon(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether tray icon support is enabled.
static auto isAddGPGId(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to auto-add GPG ID when receiving files.
static void setPos(const QPoint &pos)
Save window position.
static auto getPassTemplate(const QString &defaultValue=QVariant().toString()) -> QString
Get pass entry template.
static auto isMaximized(const bool &defaultValue=QVariant().toBool()) -> bool
Get maximized state.
static auto getProfile(const QString &defaultValue=QVariant().toString()) -> QString
Get active profile name.
static auto isUseMonospace(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to use monospace font.
static void setUsePass(const bool &usePass)
Save use pass setting.
static auto getAutoclearPanelSeconds(const int &defaultValue=QVariant().toInt()) -> int
Get panel autoclear delay in seconds.
static void setSavestate(const QByteArray &saveState)
Save window state.
static auto isAlwaysOnTop(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether main window should stay always on top.
static auto isHidePassword(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to hide password in UI.
static auto getPassExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get pass executable path.
static auto getPos(const QPoint &defaultValue=QVariant().toPoint()) -> QPoint
Get saved window position.
static auto isHideOnClose(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether closing the window hides the application.
static auto getProfiles() -> QHash< QString, QHash< QString, QString > >
Get all configured profiles.
static void setSize(const QSize &size)
Save window size.
static void setGeometry(const QByteArray &geometry)
Save window geometry.
static auto getGitExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get git executable path.
static auto getGeometry(const QByteArray &defaultValue=QVariant().toByteArray()) -> QByteArray
Get saved window geometry.
static auto isDisplayAsIs(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to display password as-is (no modification).
static auto getSavestate(const QByteArray &defaultValue=QVariant().toByteArray()) -> QByteArray
Get saved window state.
Dialog for selecting GPG recipients for password encryption.
Definition usersdialog.h:28
static auto protocolRegex() -> const QRegularExpression &
Returns a regex to match URL protocols.
Definition util.cpp:266
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
Definition util.cpp:249
static auto findPasswordStore() -> QString
Locate the password store directory.
Definition util.cpp:77
static auto getDir(const QModelIndex &index, bool forPass, const QFileSystemModel &model, const StoreModel &storeModel) -> QString
Get the selected folder path, either relative to the configured pass store or absolute.
Definition util.cpp:231
static auto configIsValid() -> bool
Verify that the required configuration is complete.
Definition util.cpp:188
Debug utilities for QtPass.
#define dbg()
Simple debug macro that includes file and line number.
Definition debughelper.h:21
@ CLIPBOARD_NEVER
Definition enums.h:17
QString name
Definition filecontent.h:11
QString value
Definition filecontent.h:12
constexpr int MS_PER_SECOND
Definition util.h:12