#include "./mainwindow.h" #include "./fielddelegate.h" #include "../model/entryfiltermodel.h" #include "../model/entrymodel.h" #include "../model/fieldmodel.h" #include "ui_mainwindow.h" #include "resources/config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace CppUtilities; using namespace QtUtilities; using namespace Io; namespace QtGui { /*! * \namespace QtGui * \brief Contains all GUI related classes and helper functions. */ /*! * \namespace QtGui::Ui * \brief Contains all classes generated by the Qt User Interface Compiler (uic). */ /*! * \class MainWindow * \brief The MainWindow class provides the main window of the widgets-based GUI of the application. */ /*! * \brief Copies the selected cells to the clipboard. */ void MainWindow::copyFields() { copyFieldsForXMilliSeconds(-1); } /*! * \brief Inserts fields from the clipboard. */ void MainWindow::insertFieldsFromClipboard() { insertFields(QApplication::clipboard()->text()); } /*! * \brief Clears the clipboard. */ void MainWindow::clearClipboard() { QApplication::clipboard()->clear(); } /*! * \brief Flags the current file as being changed since the last save. */ void MainWindow::setSomethingChanged() { setSomethingChanged(true); } /*! * \brief Sets whether the current file has been changed since the last save. */ void MainWindow::setSomethingChanged(bool somethingChanged) { if (m_somethingChanged != somethingChanged) { m_somethingChanged = somethingChanged; updateWindowTitle(); } } /*! * \brief Constructs a new main window. */ MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings, QWidget *parent) : QMainWindow(parent) , m_ui(new Ui::MainWindow) , m_openFlags(PasswordFileOpenFlags::None) , m_clearClipboardTimer(0) , m_aboutDlg(nullptr) , m_settings(settings) , m_qtSettings(qtSettings) , m_settingsDlg(nullptr) { // setup ui m_ui->setupUi(this); #ifdef Q_OS_WIN32 setStyleSheet(QStringLiteral("%1 #splitter QWidget { background-color: palette(base); color: palette(text); } #splitter QWidget *, #splitter " "QWidget * { background-color: none; } #leftWidget { border-right: 1px solid %2; }") .arg(dialogStyle(), windowFrameColor().name())); #endif // set default values setSomethingChanged(false); m_dontUpdateSelection = false; updateUiStatus(); // load settings settings.beginGroup(QStringLiteral("mainwindow")); // init recent menu manager m_recentMgr = new RecentMenuManager(m_ui->menuRecent, this); m_recentMgr->restore(settings.value(QStringLiteral("recententries"), QStringList()).toStringList()); connect(m_recentMgr, &RecentMenuManager::fileSelected, this, static_cast(&MainWindow::openFile)); // set position and size restoreGeometry(settings.value(QStringLiteral("geometry")).toByteArray()); restoreState(settings.value(QStringLiteral("state")).toByteArray()); // setup undo stack and related actions m_undoStack = new QUndoStack(this); m_undoView = nullptr; m_ui->actionUndo->setShortcuts(QKeySequence::Undo); m_ui->actionRedo->setShortcuts(QKeySequence::Redo); // setup models, tree and table view m_ui->treeView->setModel(m_entryFilterModel = new EntryFilterModel(this)); m_ui->tableView->setModel(m_fieldModel = new FieldModel(m_undoStack, this)); m_ui->tableView->setItemDelegate(new FieldDelegate(this)); m_entryFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_entryFilterModel->setSourceModel(m_entryModel = new EntryModel(m_undoStack, this)); #ifdef Q_OS_WIN32 m_ui->treeView->setFrameShape(QFrame::NoFrame); m_ui->tableView->setFrameShape(QFrame::NoFrame); #else m_ui->treeView->setFrameShape(QFrame::StyledPanel); m_ui->tableView->setFrameShape(QFrame::StyledPanel); #endif m_ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // splitter sizes m_ui->splitter->setSizes(QList() << 100 << 800); // password visibility group auto *const passwordVisibilityGroup = new QActionGroup(this); passwordVisibilityGroup->addAction(m_ui->actionShowAlways); passwordVisibilityGroup->addAction(m_ui->actionShowOnlyWhenEditing); passwordVisibilityGroup->addAction(m_ui->actionHideAlways); const QString pwVisibility(settings.value(QStringLiteral("pwvisibility")).toString()); QAction *pwVisibilityAction; if (pwVisibility == QStringLiteral("always")) { pwVisibilityAction = m_ui->actionShowAlways; } else if (pwVisibility == QStringLiteral("hidden")) { pwVisibilityAction = m_ui->actionHideAlways; } else { pwVisibilityAction = m_ui->actionShowOnlyWhenEditing; } pwVisibilityAction->setChecked(true); setPasswordVisibility(pwVisibilityAction); // connect signals and slots // -> file related actions connect(m_ui->actionSave, &QAction::triggered, this, &MainWindow::saveFile); connect(m_ui->actionExport, &QAction::triggered, this, &MainWindow::exportFile); connect(m_ui->actionDetails, &QAction::triggered, this, &MainWindow::showFileDetails); connect(m_ui->actionShowContainingDirectory, &QAction::triggered, this, &MainWindow::showContainingDirectory); connect(m_ui->actionClose, &QAction::triggered, this, &MainWindow::closeFile); connect(m_ui->actionCreate, &QAction::triggered, this, static_cast(&MainWindow::createFile)); connect(m_ui->actionQuit, &QAction::triggered, this, &MainWindow::close); connect(m_ui->actionChangepassword, &QAction::triggered, this, &MainWindow::changePassword); // -> showing dialogs connect(m_ui->actionPasswordGenerator, &QAction::triggered, this, &MainWindow::showPassowrdGeneratorDialog); connect(m_ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutDialog); connect(m_ui->actionOpen, &QAction::triggered, this, &MainWindow::showOpenFileDialog); connect(m_ui->actionSaveAs, &QAction::triggered, this, &MainWindow::showSaveFileDialog); connect(m_ui->actionQtSettings, &QAction::triggered, this, &MainWindow::showSettingsDialog); // -> add/remove account connect(m_ui->actionAddAccount, &QAction::triggered, this, &MainWindow::addAccount); connect(m_ui->actionAddCategory, &QAction::triggered, this, &MainWindow::addCategory); connect(m_ui->actionRemoveAccount, &QAction::triggered, this, &MainWindow::removeEntry); // -> insert/remove fields connect(m_ui->actionInsertRow, &QAction::triggered, this, &MainWindow::insertRow); connect(m_ui->actionRemoveRows, &QAction::triggered, this, &MainWindow::removeRows); connect(m_ui->actionCopyFields, &QAction::triggered, this, &MainWindow::copyFields); connect(m_ui->actionPasteFields, &QAction::triggered, this, &MainWindow::insertFieldsFromClipboard); // -> undo/redo connect(m_ui->actionUndo, &QAction::triggered, m_undoStack, &QUndoStack::undo); connect(m_ui->actionRedo, &QAction::triggered, m_undoStack, &QUndoStack::redo); connect(m_undoStack, &QUndoStack::canUndoChanged, m_ui->actionUndo, &QAction::setEnabled); connect(m_undoStack, &QUndoStack::canRedoChanged, m_ui->actionRedo, &QAction::setEnabled); // -> view connect(passwordVisibilityGroup, &QActionGroup::triggered, this, &MainWindow::setPasswordVisibility); connect(m_ui->actionShowUndoStack, &QAction::triggered, this, &MainWindow::showUndoView); // -> models connect(m_ui->treeView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::accountSelected); connect(m_entryModel, &QAbstractItemModel::dataChanged, this, static_cast(&MainWindow::setSomethingChanged)); connect(m_fieldModel, &QAbstractItemModel::dataChanged, this, static_cast(&MainWindow::setSomethingChanged)); // -> context menus connect(m_ui->treeView, &QTableView::customContextMenuRequested, this, &MainWindow::showTreeViewContextMenu); connect(m_ui->tableView, &QTableView::customContextMenuRequested, this, &MainWindow::showTableViewContextMenu); // -> filter connect(m_ui->accountFilterLineEdit, &QLineEdit::textChanged, this, &MainWindow::applyFilter); // setup other controls m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), false).toBool()); m_ui->accountFilterLineEdit->setText(settings.value(QStringLiteral("accountfilter"), QString()).toString()); m_ui->centralWidget->installEventFilter(this); settings.endGroup(); } /*! * \brief Destroys the main window. */ MainWindow::~MainWindow() { } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == m_undoView) { switch (event->type()) { case QEvent::Hide: m_ui->actionShowUndoStack->setChecked(false); break; default:; } } else if (obj == m_ui->centralWidget) { switch (event->type()) { case QEvent::DragEnter: case QEvent::Drop: if (const QDropEvent *const dropEvent = static_cast(event)) { QString data; const QMimeData *mimeData = dropEvent->mimeData(); if (mimeData->hasUrls()) { const QUrl url = mimeData->urls().front(); if (url.scheme() == QLatin1String("file")) { data = url.path(); } } else if (mimeData->hasText()) { data = mimeData->text(); } if (!data.isEmpty()) { event->accept(); if (event->type() == QEvent::Drop) { openFile(data); } } return true; } [[fallthrough]]; default:; } } return QMainWindow::eventFilter(obj, event); } void MainWindow::closeEvent(QCloseEvent *event) { // ask if file is opened if (m_file.hasRootEntry() && !closeFile()) { event->ignore(); return; } // close undow view if (m_undoView) { m_undoView->close(); } // save settings m_settings.beginGroup(QStringLiteral("mainwindow")); m_settings.setValue(QStringLiteral("geometry"), QVariant(saveGeometry())); m_settings.setValue(QStringLiteral("state"), QVariant(saveState())); m_settings.setValue(QStringLiteral("recententries"), QVariant(m_recentMgr->save())); m_settings.setValue(QStringLiteral("accountfilter"), QVariant(m_ui->accountFilterLineEdit->text())); m_settings.setValue(QStringLiteral("alwayscreatebackup"), m_ui->actionAlwaysCreateBackup->isChecked()); QString pwVisibility; if (m_ui->actionShowAlways->isChecked()) { pwVisibility = QStringLiteral("always"); } else if (m_ui->actionHideAlways->isChecked()) { pwVisibility = QStringLiteral("hidden"); } else { pwVisibility = QStringLiteral("editing"); } m_settings.setValue(QStringLiteral("pwvisibility"), QVariant(pwVisibility)); m_settings.endGroup(); if (m_qtSettings) { m_qtSettings->save(m_settings); } } void MainWindow::timerEvent(QTimerEvent *event) { if (event->timerId() == m_clearClipboardTimer) { clearClipboard(); m_clearClipboardTimer = 0; } } /*! * \brief Shows the settings dialog (which currently only consists of the Qt settings category). */ void MainWindow::showSettingsDialog() { if (!m_settingsDlg) { m_settingsDlg = new SettingsDialog(this); if (m_qtSettings) { m_settingsDlg->setWindowTitle(tr("Qt settings")); m_settingsDlg->setSingleCategory(m_qtSettings->category()); } } if (m_settingsDlg->isHidden()) { m_settingsDlg->showNormal(); } else { m_settingsDlg->activateWindow(); } } /*! * \brief Shows the about dialog. */ void MainWindow::showAboutDialog() { if (!m_aboutDlg) { m_aboutDlg = new AboutDialog(this, QStringLiteral(APP_URL), tr("A simple password store using AES-256-CBC encryption via OpenSSL."), QImage(":/icons/hicolor/128x128/apps/passwordmanager.png")); } m_aboutDlg->show(); } /*! * \brief Shows the password generator dialog. */ void MainWindow::showPassowrdGeneratorDialog() { PasswordGeneratorDialog *const pwgDialog = new PasswordGeneratorDialog(this); pwgDialog->show(); } /*! * \brief Shows the open file dialog and opens the selected file. */ void MainWindow::showOpenFileDialog() { if (m_file.hasRootEntry() && !closeFile()) { return; } const QString fileName = QFileDialog::getOpenFileName(this, tr("Select a password list"), QString(), tr("Password Manager files (*.pwmgr);;All files (*)")); if (!fileName.isEmpty()) { openFile(fileName); } } /*! * \brief Shows the save file dialog and saves the file at the selected location. */ void MainWindow::showSaveFileDialog() { if (showNoFileOpened()) { return; } if (askForCreatingFile()) { saveFile(); } } /*! * \brief Shows the undo view. */ void MainWindow::showUndoView() { if (m_ui->actionShowUndoStack->isChecked()) { if (!m_undoView) { m_undoView = new QUndoView(m_undoStack); m_undoView->setWindowTitle(tr("Undo stack")); m_undoView->setWindowFlags(Qt::Tool); m_undoView->setAttribute(Qt::WA_QuitOnClose); m_undoView->setWindowIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); m_undoView->installEventFilter(this); } m_undoView->show(); } else if (m_undoView) { m_undoView->hide(); } } /*! * \brief Opens a file with the specified \a path and updates all widgets to show its contents. * \returns Returns true on success; otherwise false */ bool MainWindow::openFile(const QString &path, PasswordFileOpenFlags openFlags) { // close previous file if (m_file.hasRootEntry() && !closeFile()) { return false; } // set path and open file m_file.setPath(path.toStdString()); try { m_file.open(m_openFlags = openFlags); } catch (const std::ios_base::failure &failure) { // try read-only if (!(m_openFlags & PasswordFileOpenFlags::ReadOnly)) { return openFile(path, m_openFlags | PasswordFileOpenFlags::ReadOnly); } // show error message const QString errmsg = tr("An IO error occurred when opening the specified file \"%1\": %2").arg(path, QString::fromLocal8Bit(failure.what())); m_ui->statusBar->showMessage(errmsg, 5000); QMessageBox::critical(this, QApplication::applicationName(), errmsg); return false; } // warn before loading a very big file if (m_file.size() > 10485760 && QMessageBox::warning(this, QApplication::applicationName(), tr("The file you want to load seems to be very big. Do you really want to open it?"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { m_file.clear(); return false; } // ask for a password if required if (m_file.isEncryptionUsed()) { EnterPasswordDialog pwDlg(this); pwDlg.setWindowTitle(tr("Opening file") + QStringLiteral(" - " APP_NAME)); pwDlg.setInstruction(tr("Enter the password to open the file \"%1\"").arg(path)); pwDlg.setPasswordRequired(true); switch (pwDlg.exec()) { case QDialog::Accepted: if (pwDlg.password().isEmpty()) { m_ui->statusBar->showMessage(tr("A password is needed to open the file."), 5000); QMessageBox::warning(this, QApplication::applicationName(), tr("A password is needed to open the file.")); m_file.clear(); return false; } else { break; } case QDialog::Rejected: m_file.clear(); return false; default:; } m_file.setPassword(pwDlg.password().toStdString()); } // load the contents of the file QString msg; try { m_file.load(); } catch (const CryptoException &e) { msg = tr("The file couldn't be decrypted.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(e.what())); } catch (const std::ios_base::failure &failure) { try { msg = QString::fromLocal8Bit(failure.what()); } catch (const runtime_error &e) { msg = tr("Unable to parse the file. %1").arg(QString::fromLocal8Bit(e.what())); } } // show a message in the error case if (msg.isEmpty()) { // show contents return showFile(); } m_file.clear(); m_ui->statusBar->showMessage(msg, 5000); if (QMessageBox::critical(this, QApplication::applicationName(), msg, QMessageBox::Cancel, QMessageBox::Retry) == QMessageBox::Retry) { return openFile(path, openFlags); // retry } else { return false; } } /*! * \brief Creates a new file. * \returns Returns true on success; otherwise false */ bool MainWindow::createFile() { // close previous file if (m_file.hasRootEntry() && !closeFile()) { return false; } m_file.generateRootEntry(); return showFile(); } /*! * \brief Creates a new file with the specified \a path. * \returns Returns true on success; otherwise false */ void MainWindow::createFile(const QString &path) { createFile(path, QString()); } /*! * \brief Creates a new file with the specified \a path and \a password. * \returns Returns true on success; otherwise false */ void MainWindow::createFile(const QString &path, const QString &password) { // close previous file if (m_file.hasRootEntry() && !closeFile()) { return; } // set path and password m_file.setPath(path.toStdString()); m_file.setPassword(password.toStdString()); // create the file and show it try { m_openFlags = PasswordFileOpenFlags::Default; m_file.create(); } catch (const std::ios_base::failure &failure) { QMessageBox::critical(this, QApplication::applicationName(), tr("The file %1 couldn't be created: %2").arg(path, QString::fromLocal8Bit(failure.what()))); return; } m_file.generateRootEntry(); showFile(); } /*! * \brief Shows the previously opened file. Called within openFile() and createFile(). * \returns Returns true on success; otherwise false */ bool MainWindow::showFile() { m_fieldModel->reset(); m_entryModel->setRootEntry(m_file.rootEntry()); applyDefaultExpanding(QModelIndex()); if (m_file.path().empty()) { m_ui->statusBar->showMessage(tr("A new password list has been created."), 5000); } else { m_recentMgr->addEntry(QString::fromStdString(m_file.path())); m_ui->statusBar->showMessage(tr("The password list has been load."), 5000); } updateWindowTitle(); updateUiStatus(); applyFilter(m_ui->accountFilterLineEdit->text()); setSomethingChanged(false); return true; } /*! * \brief Updates the status of the UI elements. */ void MainWindow::updateUiStatus() { const bool fileOpened = m_file.hasRootEntry(); m_ui->actionCreate->setEnabled(true); m_ui->actionOpen->setEnabled(true); m_ui->actionSave->setEnabled(fileOpened); m_ui->actionSaveAs->setEnabled(fileOpened); m_ui->actionExport->setEnabled(fileOpened); m_ui->actionDetails->setEnabled(fileOpened); m_ui->actionShowContainingDirectory->setEnabled(fileOpened); m_ui->actionClose->setEnabled(fileOpened); m_ui->actionChangepassword->setEnabled(fileOpened); m_ui->menuEdit->setEnabled(fileOpened); m_ui->accountFilterLineEdit->setEnabled(true); } /*! * \brief Updates the window title. */ void MainWindow::updateWindowTitle() { DocumentStatus docStatus; if (m_file.hasRootEntry()) { if (m_somethingChanged) { docStatus = DocumentStatus::Unsaved; } else { docStatus = DocumentStatus::Saved; } } else { docStatus = DocumentStatus::NoDocument; } auto documentPath(QString::fromStdString(m_file.path())); if (m_openFlags & PasswordFileOpenFlags::ReadOnly) { documentPath += tr(" [read-only]"); } setWindowTitle(generateWindowTitle(docStatus, documentPath)); } void MainWindow::applyDefaultExpanding(const QModelIndex &parent) { for (int row = 0, rows = m_entryFilterModel->rowCount(parent); row < rows; ++row) { const QModelIndex index = m_entryFilterModel->index(row, 0, parent); if (!index.isValid()) { return; } applyDefaultExpanding(index); m_ui->treeView->setExpanded(index, m_entryFilterModel->data(index, DefaultExpandedRole).toBool()); } } /*! * \brief Returns the save options (to be) used when saving the file next time. */ PasswordFileSaveFlags MainWindow::saveOptions() const { auto options = PasswordFileSaveFlags::Compression | PasswordFileSaveFlags::PasswordHashing; if (!m_file.password().empty()) { options |= PasswordFileSaveFlags::Encryption; } return options; } /*! * \brief Returns a string with the values of all selected fields. * \remarks Columns are sparated with \\t, rows with \\n. */ QString MainWindow::selectedFieldsString() const { const QModelIndexList selectedIndexes(m_ui->tableView->selectionModel()->selectedIndexes()); if (selectedIndexes.isEmpty()) { return QString(); } if (selectedIndexes.size() == 1) { return selectedIndexes.front().data(Qt::EditRole).toString(); } QString text; int maxRow = m_fieldModel->rowCount() - 1; int firstRow = maxRow, lastRow = 0; int maxCol = m_fieldModel->columnCount() - 1; int firstCol = maxCol, lastCol = 0; for (const QModelIndex &index : selectedIndexes) { if (index.row() < firstRow) { firstRow = index.row(); } if (index.row() > lastRow) { lastRow = index.row(); } if (index.column() < firstCol) { firstCol = index.column(); } if (index.column() > lastCol) { lastCol = index.column(); } } for (int row = firstRow; row <= lastRow; ++row) { for (int col = firstCol; col <= lastCol; ++col) { const QModelIndex index(m_fieldModel->index(row, col)); if (selectedIndexes.contains(index)) { text.append(index.data(Qt::EditRole).toString()); } text.append('\t'); } text.append('\n'); } return text; } /*! * \brief Inserts fields from the specified \a fieldsString. */ void MainWindow::insertFields(const QString &fieldsString) { const auto selectedIndexes(m_ui->tableView->selectionModel()->selectedIndexes()); if (selectedIndexes.size() != 1) { QMessageBox::warning(this, QApplication::applicationName(), tr("Exactly one field needs to be selected (top-left corner for insertion).")); return; } const auto cols = m_fieldModel->columnCount(); const auto initCol = selectedIndexes.front().column(); const auto rowValues = [&] { QStringList lines = fieldsString.split('\n'); if (lines.back().isEmpty()) { lines.pop_back(); } return lines; }(); auto row = selectedIndexes.front().row(); m_fieldModel->insertRows(row, rowValues.size(), QModelIndex()); for (const auto &rowValue : rowValues) { int col = initCol; for (const auto &cellValue : rowValue.split('\t')) { if (col < cols) { m_fieldModel->setData(m_fieldModel->index(row, col), cellValue, Qt::EditRole); ++col; } else { break; } } ++row; } } /*! * \brief Asks the user to create a new file. */ bool MainWindow::askForCreatingFile() { if (showNoFileOpened()) { return false; } const QString fileName = QFileDialog::getSaveFileName( this, tr("Select where you want to save the password list"), QString(), tr("Password Manager files (*.pwmgr);;All files (*)")); if (fileName.isEmpty()) { m_ui->statusBar->showMessage(tr("The file was not be saved."), 7000); return false; } else { m_file.setPath(fileName.toStdString()); try { m_file.create(); updateWindowTitle(); } catch (const std::ios_base::failure &failure) { QMessageBox::critical(this, QApplication::applicationName(), QString::fromLocal8Bit(failure.what())); return false; } } return true; } /*! * \brief Shows an warning if no file is opened. * \returns Returns whether the warning has been shown. */ bool MainWindow::showNoFileOpened() { if (!m_file.hasRootEntry()) { QMessageBox::warning(this, QApplication::applicationName(), tr("There is no password list opened.")); return true; } return false; } /*! * \brief Shows an warning if no account is selected. * \returns Returns whether the warning has been shown. */ bool MainWindow::showNoAccount() { if (!m_fieldModel->fields()) { QMessageBox::warning(this, QApplication::applicationName(), tr("There's no account selected.")); return true; } return false; } /*! * \brief Closes the currently opened file. Asks the user to save changes if the file has been modified. * \returns Returns whether the file has been closed. */ bool MainWindow::closeFile() { if (showNoFileOpened()) { return false; } if (m_somethingChanged) { QMessageBox msg(this); msg.setText(tr("The password file has been modified.")); msg.setInformativeText(tr("Do you want to save the changes before closing?")); msg.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); msg.setDefaultButton(QMessageBox::Save); msg.setIcon(QMessageBox::Warning); switch (msg.exec()) { case QMessageBox::Save: if (saveFile()) { break; } else { return false; } case QMessageBox::Cancel: return false; default:; } } m_fieldModel->reset(); m_entryModel->reset(); m_file.clear(); m_ui->statusBar->showMessage(tr("The password list has been closed.")); updateWindowTitle(); updateUiStatus(); setSomethingChanged(false); return true; } /*! * \brief Saves the currently opened file. * \returns Returns whether the file could be saved. */ bool MainWindow::saveFile() { if (showNoFileOpened()) { return false; } // create backup if (!m_file.path().empty() && QFile::exists(QString::fromStdString(m_file.path()))) { if (m_ui->actionAlwaysCreateBackup->isChecked()) { try { m_file.doBackup(); } catch (const std::ios_base::failure &failure) { const QString message(tr("An IO error occurred when making the backup file: %1").arg(QString::fromLocal8Bit(failure.what()))); QMessageBox::critical(this, QApplication::applicationName(), message); m_ui->statusBar->showMessage(message, 7000); return false; } } } else if (!askForCreatingFile()) { return false; } // ask for a password if none is set if (m_file.password().empty()) { EnterPasswordDialog pwDlg(this); pwDlg.setWindowTitle(tr("Saving file") + QStringLiteral(" - " APP_NAME)); pwDlg.setInstruction(tr("Enter a password to save the file")); pwDlg.setVerificationRequired(true); switch (pwDlg.exec()) { case QDialog::Accepted: m_file.setPassword(pwDlg.password().toStdString()); break; default: m_ui->statusBar->showMessage(tr("The file hasn't been saved."), 7000); return false; } } // save the file QString msg; try { m_file.save(saveOptions()); } catch (const CryptoException &ex) { msg = tr("The password list couldn't be saved due to encryption failure.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(ex.what())); } catch (const std::ios_base::failure &failure) { msg = QString::fromLocal8Bit(failure.what()); } // show status if (!msg.isEmpty()) { m_ui->statusBar->showMessage(msg, 5000); QMessageBox::critical(this, QApplication::applicationName(), msg); return false; } if ((m_openFlags & PasswordFileOpenFlags::ReadOnly) || m_somethingChanged) { m_openFlags = PasswordFileOpenFlags::Default; m_somethingChanged = false; updateWindowTitle(); } m_recentMgr->addEntry(QString::fromStdString(m_file.path())); m_ui->statusBar->showMessage(tr("The password list has been saved."), 5000); return true; } /*! * \brief Exports the files contents to a plain text file. */ void MainWindow::exportFile() { if (showNoFileOpened()) { return; } const QString targetPath = QFileDialog::getSaveFileName(this, QApplication::applicationName(), QString(), tr("Plain text document (*.txt);;All files (*.*)")); if (targetPath.isEmpty()) { return; } QString errmsg; try { m_file.exportToTextfile(targetPath.toStdString()); } catch (const std::ios_base::failure &failure) { errmsg = tr("An IO error occurred when exporting the password list: %1").arg(QString::fromLocal8Bit(failure.what())); } if (errmsg.isEmpty()) { m_ui->statusBar->showMessage(tr("The password list has been exported."), 5000); } else { m_ui->statusBar->showMessage(errmsg, 5000); QMessageBox::critical(this, QApplication::applicationName(), errmsg); } } /*! * \brief Shows the containing directory for the currently opened file. */ void MainWindow::showContainingDirectory() { if (showNoFileOpened()) { return; } if (m_file.path().empty()) { QMessageBox::warning(this, QApplication::applicationName(), tr("The currently opened file hasn't been saved yet.")); return; } const QFileInfo file(QString::fromStdString(m_file.path())); if (file.dir().exists()) { openLocalFileOrDir(file.dir().absolutePath()); } } /*! * \brief Adds a new account entry to the selected category. */ void MainWindow::addAccount() { addEntry(EntryType::Account); } /*! * \brief Adds a new category/node entry to the selected category. */ void MainWindow::addCategory() { addEntry(EntryType::Node); } /*! * \brief Adds a new entry to the selected category. * \param type Specifies the type of the entry to be created. * \param title Specifies the title of the user prompt which will be shown to ask for the entry label. */ void MainWindow::addEntry(EntryType type) { if (showNoFileOpened()) { return; } const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); const QModelIndex selected = selectedIndexes.size() == 1 ? m_entryFilterModel->mapToSource(selectedIndexes.at(0)) : QModelIndex(); if (!selected.isValid() || !m_entryModel->isNode(selected)) { QMessageBox::warning(this, QApplication::applicationName(), tr("No node element selected.")); return; } bool result; const QString text = QInputDialog::getText(this, type == EntryType::Account ? tr("Add account") : tr("Add category"), tr("Enter the entry name"), QLineEdit::Normal, tr("new entry"), &result); if (!result) { return; } if (text.isEmpty()) { QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter text.")); return; } int row = m_entryModel->rowCount(selected); m_entryModel->setInsertType(type); if (!m_entryModel->insertRow(row, selected)) { QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to create new entry.")); return; } m_entryModel->setData(m_entryModel->index(row, 0, selected), QVariant(text), Qt::DisplayRole); setSomethingChanged(true); } /*! * \brief Removes the selected entry. */ void MainWindow::removeEntry() { if (showNoFileOpened()) { return; } const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); if (selectedIndexes.size() != 1) { QMessageBox::warning(this, QApplication::applicationName(), tr("No entry selected.")); return; } const QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0)); if (!m_entryModel->removeRow(selected.row(), selected.parent())) { QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to remove the entry.")); } } /*! * \brief Applies the entered filter. * \remarks Called when the textChanged signal of m_ui->accountFilterLineEdit is emittet. */ void MainWindow::applyFilter(const QString &filterText) { m_entryFilterModel-> #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) setFilterRegularExpression(QRegularExpression(filterText, QRegularExpression::CaseInsensitiveOption)) #else setFilterRegExp(filterText) #endif ; if (filterText.isEmpty()) { applyDefaultExpanding(QModelIndex()); } else { m_ui->treeView->expandAll(); } } /*! * \brief Called when the user \a selected an entry. */ void MainWindow::accountSelected(const QModelIndex &selected, const QModelIndex &) { if (Entry *entry = m_entryModel->entry(m_entryFilterModel->mapToSource(selected))) { if (entry->type() == EntryType::Account) { m_fieldModel->setAccountEntry(static_cast(entry)); m_ui->tableView->resizeRowsToContents(); return; } } m_fieldModel->setAccountEntry(nullptr); } /*! * \brief Inserts an empty row before the selected one. */ void MainWindow::insertRow() { if (showNoFileOpened() || showNoAccount()) { return; } const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if (selectedIndexes.empty()) { QMessageBox::warning( this, windowTitle(), tr("A field has to be selected since new fields are always inserted before the currently selected field.")); return; } int row = m_fieldModel->rowCount(); for (const QModelIndex &index : selectedIndexes) { if (index.row() < row) { row = index.row(); } } if (row < m_fieldModel->rowCount() - 1) { m_fieldModel->insertRow(row); } } /*! * \brief Removes the selected rows. */ void MainWindow::removeRows() { if (showNoFileOpened() || showNoAccount()) { return; } const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); QList rows; for (const QModelIndex &index : selectedIndexes) { rows << index.row(); } if (rows.empty()) { QMessageBox::warning(this, windowTitle(), tr("No fields have been removed since there are currently no fields selected.")); return; } for (int i = m_fieldModel->rowCount() - 1; i >= 0; --i) { if (rows.contains(i)) { m_fieldModel->removeRow(i); } } } /*! * \brief Marks the selected field as password field. */ void MainWindow::markAsPasswordField() { setFieldType(FieldType::Password); } /*! * \brief Marks the selected field as normal field. */ void MainWindow::markAsNormalField() { setFieldType(FieldType::Normal); } /*! * \brief Sets the type of the selected field to the specified \a fieldType. */ void MainWindow::setFieldType(FieldType fieldType) { if (showNoFileOpened() || showNoAccount()) { return; } const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if (selectedIndexes.isEmpty()) { QMessageBox::warning(this, windowTitle(), tr("No fields have been changed since there are currently no fields selected.")); return; } const QVariant typeVariant(static_cast(fieldType)); for (const QModelIndex &index : selectedIndexes) { m_fieldModel->setData(index, typeVariant, FieldTypeRole); } } /*! * \brief Sets the password visibility of m_fieldModel depending on which action has been chosen. * * This private slot is connected to the triggered signal of the passwordVisibility QActionGroup. */ void MainWindow::setPasswordVisibility(QAction *selectedAction) { if (selectedAction == m_ui->actionShowAlways) { m_fieldModel->setPasswordVisibility(PasswordVisibility::Always); } else if (selectedAction == m_ui->actionShowOnlyWhenEditing) { m_fieldModel->setPasswordVisibility(PasswordVisibility::OnlyWhenEditing); } else if (selectedAction == m_ui->actionHideAlways) { m_fieldModel->setPasswordVisibility(PasswordVisibility::Never); } } /*! * \brief Asks the user to change the password which will be used when calling saveFile() next time. */ void MainWindow::changePassword() { if (showNoFileOpened()) { return; } EnterPasswordDialog pwDlg(this); pwDlg.setWindowTitle(tr("Changing password") + QStringLiteral(" - " APP_NAME)); pwDlg.setVerificationRequired(true); switch (pwDlg.exec()) { case QDialog::Accepted: if (pwDlg.password().isEmpty()) { m_file.clearPassword(); setSomethingChanged(true); QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter a password. No encryption will be used when saving the file next time.")); } else { m_file.setPassword(pwDlg.password().toStdString()); setSomethingChanged(true); QMessageBox::warning(this, QApplication::applicationName(), tr("The new password will be used next time you save the file.")); } break; default: QMessageBox::warning( this, QApplication::applicationName(), tr("You aborted. The old password will still be used when saving the file next time.")); } } /*! * \brief Shows the tree view context menu. */ void MainWindow::showTreeViewContextMenu(const QPoint &pos) { if (!m_file.hasRootEntry()) { return; } const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); if (selectedIndexes.size() != 1) { return; } QMenu contextMenu(this); const QModelIndex selected(m_entryFilterModel->mapToSource(selectedIndexes.at(0))); const Entry *const entry = m_entryModel->entry(selected); if (entry->type() == EntryType::Node) { contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add account"), this, &MainWindow::addAccount); contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add category"), this, &MainWindow::addCategory); } contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), tr("Remove entry"), this, &MainWindow::removeEntry); if (entry->type() == EntryType::Node) { const auto *const nodeEntry = static_cast(entry); contextMenu.addSeparator(); auto *const action = new QAction(&contextMenu); action->setCheckable(true); action->setText(tr("Expanded by default")); action->setChecked(nodeEntry->isExpandedByDefault()); connect(action, &QAction::triggered, std::bind(&EntryModel::setData, m_entryModel, std::cref(selected), QVariant(!nodeEntry->isExpandedByDefault()), DefaultExpandedRole)); contextMenu.addAction(action); } contextMenu.exec(m_ui->treeView->viewport()->mapToGlobal(pos)); } /*! * \brief Shows the table view context menu. */ void MainWindow::showTableViewContextMenu(const QPoint &pos) { // check whether there is a selection at all const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if (!m_file.hasRootEntry() || !m_fieldModel->fields() || selectedIndexes.isEmpty()) { return; } // check what kind of fields have been selected auto firstType = FieldType::Normal; bool allOfSameType = true; bool hasFirstFieldType = false; int row = selectedIndexes.front().row(); int multipleRows = 1; QUrl url; static const string protocols[] = { "http:", "https:", "file:" }; for (const QModelIndex &index : selectedIndexes) { if (!index.isValid()) { continue; } if (const Field *field = m_fieldModel->field(static_cast(index.row()))) { if (url.isEmpty() && field->type() != FieldType::Password) { for (const string &protocol : protocols) { if (startsWith(field->value(), protocol)) { url = QString::fromUtf8(field->value().data()); } } } if (hasFirstFieldType) { if (firstType != field->type()) { allOfSameType = false; break; } } else { firstType = field->type(); hasFirstFieldType = true; } } if (multipleRows == 1 && index.row() != row) { ++multipleRows; } } // create context menu QMenu contextMenu(this); // -> insertion and removal contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Insert field"), this, &MainWindow::insertRow); contextMenu.addAction( QIcon::fromTheme(QStringLiteral("list-remove")), tr("Remove field(s)", nullptr, multipleRows), this, &MainWindow::removeRows); // -> show the "Mark as ..." action only when all selected indexes are of the same type if (hasFirstFieldType && allOfSameType) { switch (firstType) { case FieldType::Normal: contextMenu.addAction( QIcon::fromTheme(QStringLiteral("flag-black")), tr("Mark as password field"), this, &MainWindow::markAsPasswordField); break; case FieldType::Password: contextMenu.addAction(QIcon::fromTheme(QStringLiteral("flag-blue")), tr("Mark as normal field"), this, &MainWindow::markAsNormalField); break; } } // -> insert copy & paste contextMenu.addSeparator(); contextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"), this, &MainWindow::copyFields); contextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy for 5 seconds"), this, &MainWindow::copyFieldsForXMilliSeconds); const auto *const mimeData = QGuiApplication::clipboard()->mimeData(); if (mimeData && mimeData->hasText()) { contextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-paste")), tr("Paste"), this, &MainWindow::insertFieldsFromClipboard); } // -> insert open URL if (multipleRows == 1 && !url.isEmpty()) { auto *openUrlAction = new QAction(QIcon::fromTheme(QStringLiteral("applications-internet")), tr("Open URL"), &contextMenu); connect(openUrlAction, &QAction::triggered, bind(&QDesktopServices::openUrl, url)); contextMenu.addAction(openUrlAction); } contextMenu.exec(m_ui->tableView->viewport()->mapToGlobal(pos)); } void MainWindow::showFileDetails() { if (!m_file.isOpen()) { return; } QMessageBox msgBox; msgBox.setWindowTitle(tr("File details")); msgBox.setText(QString::fromStdString(m_file.summary(saveOptions()))); msgBox.setIcon(QMessageBox::NoIcon); msgBox.exec(); } /*! * \brief Copies the selected cells to the clipboard and clears the clipboard after \a x milli seconds again. */ void MainWindow::copyFieldsForXMilliSeconds(int x) { const QString text(selectedFieldsString()); if (text.isEmpty()) { QMessageBox::warning(this, QApplication::applicationName(), tr("The selection is empty.")); return; } if (m_clearClipboardTimer) { killTimer(m_clearClipboardTimer); } QApplication::clipboard()->setText(text); if (x > 0) { m_clearClipboardTimer = startTimer(x, Qt::CoarseTimer); } } } // namespace QtGui