#include "mainwindow.h" #include "ui_mainwindow.h" #include "model/fieldmodel.h" #include "model/entrymodel.h" #include "model/entryfiltermodel.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 using namespace std; using namespace IoUtilities; 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(QWidget *parent) : QMainWindow(parent), m_ui(new Ui::MainWindow), m_clearClipboardTimer(0) { // setup ui m_ui->setupUi(this); #ifdef Q_OS_WIN32 setStyleSheet(QStringLiteral("* { font: 9pt \"Segoe UI\", \"Sans\"; } QMessageBox QLabel, QInputDialog QLabel { font-size: 12pt; color: #003399; } #statusBar { border-top: 1px solid #919191; padding-top: 1px; } #splitter QWidget { background-color: #FFF; } #assumePushButton { font-weight: bold; } #splitter #treeButtonsWidget, #splitter #listButtonsWidget { background-color: #F0F0F0; border-top: 1px solid #DFDFDF; } #leftWidget { border-right: 1px solid #DFDFDF; } #splitter QWidget *, #splitter QWidget * { background-color: none; }")); #endif // set default values setSomethingChanged(false); m_dontUpdateSelection = false; updateUiStatus(); // load settings QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); settings.beginGroup(QStringLiteral("mainwindow")); QStringList recentEntries = settings.value(QStringLiteral("recententries"), QStringList()).toStringList(); QAction *action = nullptr; m_ui->actionSepRecent->setSeparator(true); for(const QString &path : recentEntries) { if(!path.isEmpty()) { action = new QAction(path, this); action->setProperty("file_path", path); m_ui->menuRecent->insertAction(m_ui->actionSepRecent, action); connect(action, &QAction::triggered, this, &MainWindow::openRecentFile); } } m_ui->menuRecent->setEnabled(action); // set position and size resize(settings.value("size", size()).toSize()); move(settings.value("pos", QPoint(300, 200)).toPoint()); // 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_fieldModel->setHidePasswords(settings.value("hidepasswords", true).toBool()); 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); // 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->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); // recent menu connect(m_ui->actionClearRecent, &QAction::triggered, this, &MainWindow::clearRecent); // add/remove account connect(m_ui->actionAddAccount, &QAction::triggered, this, &MainWindow::addAccount); connect(m_ui->actionAddCategory, &QAction::triggered, this, &MainWindow::addCategory); connect(m_ui->actionRemoveRows, &QAction::triggered, this, &MainWindow::removeEntry); // insert/remove fields connect(m_ui->actionInsertRow, &QAction::triggered, this, &MainWindow::insertRow); connect(m_ui->actionRemoveAccount, &QAction::triggered, this, &MainWindow::removeRows); // 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(m_ui->actionHidePasswords, &QAction::triggered, m_fieldModel, &FieldModel::setHidePasswords); 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, m_entryFilterModel, &QSortFilterProxyModel::setFilterFixedString); 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->actionHidePasswords->setChecked(m_fieldModel->hidePasswords()); 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(QDropEvent *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; } default: ; } } return QMainWindow::eventFilter(obj, event); } void MainWindow::closeEvent(QCloseEvent *event) { // ask if file is opened if(m_file.hasRootEntry()) { if(!closeFile()) { event->ignore(); return; } } // close undow view if(m_undoView) { m_undoView->close(); } // save settings QSettings settings(QSettings::IniFormat, QSettings::UserScope, QApplication::organizationName(), QApplication::applicationName()); settings.beginGroup(QStringLiteral("mainwindow")); settings.setValue(QStringLiteral("size"), size()); settings.setValue(QStringLiteral("pos"), pos()); QStringList existingEntires; QList entryActions = m_ui->menuRecent->actions(); existingEntires.reserve(entryActions.size()); for(const QAction *action : entryActions) { QVariant path = action->property("file_path"); if(!path.isNull()) { existingEntires << path.toString(); } } settings.setValue(QStringLiteral("recententries"), existingEntires); settings.setValue(QStringLiteral("accountfilter"), m_ui->accountFilterLineEdit->text()); settings.setValue(QStringLiteral("alwayscreatebackup"), m_ui->actionAlwaysCreateBackup->isChecked()); settings.setValue(QStringLiteral("hidepasswords"), m_ui->actionHidePasswords->isChecked()); settings.endGroup(); } void MainWindow::timerEvent(QTimerEvent *event) { if(event->timerId() == m_clearClipboardTimer) { clearClipboard(); m_clearClipboardTimer = 0; } } /*! * \brief Shows the about dialog. */ void MainWindow::showAboutDialog() { using namespace Dialogs; AboutDialog* aboutDlg = new AboutDialog(this, tr("A simple password store using AES-256-CBC encryption via OpenSSL."), QImage(":/icons/hicolor/128x128/apps/passwordmanager.png")); aboutDlg->show(); } /*! * \brief Shows the password generator dialog. */ void MainWindow::showPassowrdGeneratorDialog() { PasswordGeneratorDialog* 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; } QString fileName = QFileDialog::getOpenFileName(this, tr("Select a password list")); if(!fileName.isEmpty()) { openFile(fileName); } } /*! * \brief Opens a file from the "recently opened" list. * * This private slot is directly called when the corresponding QAction is triggered. */ void MainWindow::openRecentFile() { if(QAction* action = qobject_cast(sender())) { QString path = action->property("file_path").toString(); if(!path.isEmpty()) { if(QFile::exists(path)) { openFile(path); } else { QMessageBox msg(this); msg.setWindowTitle(QApplication::applicationName()); msg.setText(tr("The selected file can't be found anymore. Do you want to delete the obsolete entry from the list?")); msg.setIcon(QMessageBox::Warning); QPushButton *keepEntryButton = msg.addButton(tr("keep entry"), QMessageBox::NoRole); QPushButton *deleteEntryButton = msg.addButton(tr("delete entry"), QMessageBox::YesRole); msg.setEscapeButton(keepEntryButton); msg.exec(); if(msg.clickedButton() == deleteEntryButton) { delete action; } } } } } /*! * \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) { using namespace Dialogs; // close previous file if(m_file.hasRootEntry() && !closeFile()) { return false; } // set path and open file m_file.setPath(path.toStdString()); try { m_file.open(); } catch (ios_base::failure &ex) { QString errmsg = tr("An IO error occured when opening the specified file \"%1\".\n\n(%2)").arg(path, QString::fromLocal8Bit(ex.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) { if(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(QApplication::applicationName()); pwDlg.setInstruction(tr("Enter the password to open the file")); 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(CryptoException &ex) { msg = tr("The file couldn't be decrypted.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(ex.what())); } catch(ios_base::failure &ex) { msg = QString::fromLocal8Bit(ex.what()); } catch(runtime_error &ex) { msg = tr("Unable to parse the file. %1").arg(QString::fromLocal8Bit(ex.what())); } // show a message in the error case if(!msg.isEmpty()) { 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); // retry } else { return false; } } else { // show contents return showFile(); } } /*! * \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_file.create(); } catch (ios_base::failure) { QMessageBox::critical(this, QApplication::applicationName(), tr("The file %1 couldn't be created.").arg(path)); 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 { addRecentEntry(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 Adds a recent entry for the specified \a path. Called within showFile(). */ void MainWindow::addRecentEntry(const QString &path) { // check if the path already exists QList existingEntries = m_ui->menuRecent->actions(); QAction *entry = nullptr; for(QAction *existingEntry : existingEntries) { if(existingEntry->property("file_path").toString() == path) { entry = existingEntry; break; } } if(!entry) { // remove old entries to have never more then 8 entries for(int i = existingEntries.size(); i > 9; --i) { delete existingEntries.back(); } existingEntries = m_ui->menuRecent->actions(); // create new action entry = new QAction(path, this); entry->setProperty("file_path", path); connect(entry, &QAction::triggered, this, &MainWindow::openRecentFile); } else { // remove existing action (will be inserted again as first action) m_ui->menuRecent->removeAction(entry); } // ensure menu is enabled m_ui->menuRecent->setEnabled(true); // add action as first action in the recent menu m_ui->menuRecent->insertAction(m_ui->menuRecent->isEmpty() ? nullptr : m_ui->menuRecent->actions().front(), entry); } /*! * \brief Updates the status of the UI elements. */ void MainWindow::updateUiStatus() { 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->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() { Dialogs::DocumentStatus docStatus; if(m_file.hasRootEntry()) { if(m_somethingChanged) { docStatus = Dialogs::DocumentStatus::Unsaved; } else { docStatus = Dialogs::DocumentStatus::Saved; } } else { docStatus = Dialogs::DocumentStatus::NoDocument; } setWindowTitle(Dialogs::generateWindowTitle(docStatus, QString::fromStdString(m_file.path()))); } void MainWindow::applyDefaultExpanding(const QModelIndex &parent) { for(int row = 0, rows = m_entryFilterModel->rowCount(parent); row < rows; ++row) { 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 a string with the values of all selected fields. * \remarks Columns are sparated with \t, rows with \n. */ QString MainWindow::selectedFieldsString() const { QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); QString text; if(!selectedIndexes.isEmpty()) { if(selectedIndexes.size() > 1) { 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) { QModelIndex index = m_fieldModel->index(row, col); if(selectedIndexes.contains(index)) { text.append(index.data(Qt::EditRole).toString()); } text.append('\t'); } text.append('\n'); } } else { text = selectedIndexes.front().data(Qt::EditRole).toString(); } } return text; } /*! * \brief Inserts fields from the specified \a fieldsString. */ void MainWindow::insertFields(const QString &fieldsString) { QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if(selectedIndexes.size() == 1) { int rows = m_fieldModel->rowCount(), cols = m_fieldModel->columnCount(); int row = selectedIndexes.front().row(); int initCol = selectedIndexes.front().column(); assert(row < rows); QStringList rowValues = fieldsString.split('\n'); if(rowValues.back().isEmpty()) { rowValues.pop_back(); } m_fieldModel->insertRows(row, rowValues.size(), QModelIndex()); for(const QString &rowValue : rowValues) { int col = initCol; for(const QString &cellValue : rowValue.split('\t')) { if(col < cols) { m_fieldModel->setData(m_fieldModel->index(row, col), cellValue, Qt::EditRole); ++col; } else { break; } } ++row; } } else { QMessageBox::warning(this, QApplication::applicationName(), tr("Exactly one fields needs to be selected (top-left corner for insertion).")); } } /*! * \brief Asks the user to create a new file. */ bool MainWindow::askForCreatingFile() { if(showNoFileOpened()) { return false; } QString fileName = QFileDialog::getSaveFileName( this, tr("Select where you want to save the password list"), QString(), tr("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(); } catch (ios_base::failure &ex) { QMessageBox::critical(this, QApplication::applicationName(), QString::fromLocal8Bit(ex.what())); return false; } } return true; } /*! * \brief Shows an warning if no file is opened. * \retruns 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. * \retruns 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() { using namespace Dialogs; 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(ios_base::failure &ex) { QString message(tr("The backup file couldn't be created. %1").arg(QString::fromLocal8Bit(ex.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()[0] == 0) { EnterPasswordDialog pwDlg(this); pwDlg.setWindowTitle(QApplication::applicationName()); 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(m_file.password()[0] != 0); } catch (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(ios_base::failure &ex) { msg = QString::fromLocal8Bit(ex.what()); } // show status if(!msg.isEmpty()) { m_ui->statusBar->showMessage(msg, 5000); QMessageBox::critical(this, QApplication::applicationName(), msg); return false; } else { setSomethingChanged(false); addRecentEntry(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; } QString targetPath = QFileDialog::getSaveFileName(this, QApplication::applicationName()); if(!targetPath.isNull()) { QString errmsg; try { m_file.exportToTextfile(targetPath.toStdString()); } catch (ios_base::failure &ex) { errmsg = tr("The password list couldn't be exported. %1").arg(QString::fromLocal8Bit(ex.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; } else if(m_file.path().empty()) { QMessageBox::warning(this, QApplication::applicationName(), tr("The currently opened file hasn't been saved yet.")); } else { QFileInfo file(QString::fromStdString(m_file.path())); if(file.dir().exists()) { QDesktopServices::openUrl(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; } QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); if(selectedIndexes.size() == 1) { QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0)); if(m_entryModel->isNode(selected)) { bool result; 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) { if(!text.isEmpty()) { int row = m_entryModel->rowCount(selected); m_entryModel->setInsertType(type); if(m_entryModel->insertRow(row, selected)) { m_entryModel->setData(m_entryModel->index(row, 0, selected), text, Qt::DisplayRole); setSomethingChanged(true); } else { QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to create new entry.")); } } else { QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter text.")); } } return; } } QMessageBox::warning(this, QApplication::applicationName(), tr("No node element selected.")); } /*! * \brief Removes the selected entry. */ void MainWindow::removeEntry() { if(showNoFileOpened()) { return; } QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); if(selectedIndexes.size() == 1) { 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.")); } } else { QMessageBox::warning(this, QApplication::applicationName(), tr("No entry selected.")); } } /*! * \brief Applies the entered filter. * \remarks Called when the textChanged signal of m_ui->accountFilterLineEdit is emittet. */ void MainWindow::applyFilter(const QString &filterText) { if(filterText.isEmpty()) { applyDefaultExpanding(QModelIndex()); } else { m_ui->treeView->expandAll(); } m_entryFilterModel->setFilterRegExp(filterText); } /*! * \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)); return; } } m_fieldModel->setAccountEntry(nullptr); } /*! * \brief Inserts an empty row before the selected one. */ void MainWindow::insertRow() { if(showNoFileOpened() || showNoAccount()) { return; } QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if(selectedIndexes.size()) { int row = m_fieldModel->rowCount(); foreach(const QModelIndex &index, selectedIndexes) { if(index.row() < row) { row = index.row(); } } if(row < m_fieldModel->rowCount() - 1) { m_fieldModel->insertRow(row); } } else { QMessageBox::warning(this, windowTitle(), tr("A field has to be selected since new fields are always inserted before the currently selected field.")); } } /*! * \brief Removes the selected rows. */ void MainWindow::removeRows() { if(showNoFileOpened() || showNoAccount()) { return; } QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); QList rows; foreach(const QModelIndex &index, selectedIndexes) { rows << index.row(); } if(rows.size()) { for(int i = m_fieldModel->rowCount() - 1; i >= 0; --i) { if(rows.contains(i)) { m_fieldModel->removeRow(i); } } } else { QMessageBox::warning(this, windowTitle(), tr("No fields have been removed since there are currently no fields selected.")); } } /*! * \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; } QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if(!selectedIndexes.isEmpty()) { QVariant typeVariant(static_cast(fieldType)); foreach(const QModelIndex &index, selectedIndexes) { m_fieldModel->setData(index, typeVariant, FieldTypeRole); } } else { QMessageBox::warning(this, windowTitle(), tr("No fields have been changed since there are currently no fields selected.")); } } /*! * \brief Asks the user to change the password which will be used when calling saveFile() next time. */ void MainWindow::changePassword() { using namespace Dialogs; if(showNoFileOpened()) { return; } EnterPasswordDialog pwDlg(this); pwDlg.setWindowTitle(QApplication::applicationName()); 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 Clears all entries in the "recently opened" list. */ void MainWindow::clearRecent() { QList entries = m_ui->menuRecent->actions(); for(auto i = entries.begin(), end = entries.end() - 2; i != end; ++i) { if(*i != m_ui->actionClearRecent) { delete *i; } } m_ui->menuRecent->setEnabled(false); } /*! * \brief Shows the tree view context menu. */ void MainWindow::showTreeViewContextMenu() { if(!m_file.hasRootEntry()) { return; } QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0); if(selectedIndexes.size() == 1) { QMenu contextMenu(this); QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0)); Entry *entry = m_entryModel->entry(selected); if(entry->type() == EntryType::Node) { contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add account"), this, SLOT(addAccount())); contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add category"), this, SLOT(addCategory())); } contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), tr("Remove entry"), this, SLOT(removeEntry())); if(entry->type() == EntryType::Node) { NodeEntry *nodeEntry = static_cast(entry); contextMenu.addSeparator(); QAction *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(QCursor::pos()); } } /*! * \brief Shows the table view context menu. */ void MainWindow::showTableViewContextMenu() { QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes(); if(!m_file.hasRootEntry() || !m_fieldModel->fields() || selectedIndexes.isEmpty()) { return; } QMenu contextMenu(this); FieldType firstType = FieldType::Normal; bool allOfSameType = true; bool hasOneFieldType = false; int row = selectedIndexes.front().row(); int multipleRows = 1; foreach(const QModelIndex &index, selectedIndexes) { if(const Field *field = m_fieldModel->field(index.row())) { if(hasOneFieldType) { if(firstType != field->type()) { allOfSameType = false; break; } } else { firstType = field->type(); hasOneFieldType = true; } } if(multipleRows == 1 && index.row() != row) { ++multipleRows; } } // insertion and removal contextMenu.addAction(QIcon::fromTheme("list-add"), tr("Insert field(s)", nullptr, multipleRows), this, SLOT(insertRow())); contextMenu.addAction(QIcon::fromTheme("list-remove"), tr("Remove field(s)", nullptr, multipleRows), this, SLOT(removeRows())); // show the "Mark as ..." action only when all selected indexes are of the same type if(hasOneFieldType && allOfSameType) { switch(firstType) { case FieldType::Normal: contextMenu.addAction(tr("Mark as password field"), this, SLOT(markAsPasswordField())); break; case FieldType::Password: contextMenu.addAction(tr("Mark as normal field"), this, SLOT(markAsNormalField())); break; } } contextMenu.addSeparator(); contextMenu.addAction(QIcon::fromTheme("edit-copy"), tr("Copy"), this, SLOT(copyFields())); contextMenu.addAction(QIcon::fromTheme("edit-copy"), tr("Copy for 5 seconds"), this, SLOT(copyFieldsForXMilliSeconds())); if(QApplication::clipboard()->mimeData()->hasText()) { contextMenu.addAction(QIcon::fromTheme("edit-paste"), tr("Paste"), this, SLOT(insertFieldsFromClipboard())); } contextMenu.exec(QCursor::pos()); } /*! * \brief Copies the selected cells to the clipboard and clears the clipboard after \a x milli seconds again. */ void MainWindow::copyFieldsForXMilliSeconds(int x) { QString text = selectedFieldsString(); if(!text.isEmpty()) { if(m_clearClipboardTimer) { killTimer(m_clearClipboardTimer); } QApplication::clipboard()->setText(text); if(x > 0) { m_clearClipboardTimer = startTimer(x, Qt::CoarseTimer); } } else { QMessageBox::warning(this, QApplication::applicationName(), tr("The selection is empty.")); } } }