passwordmanager/gui/mainwindow.cpp

1231 lines
44 KiB
C++

#include "./mainwindow.h"
#include "../model/fieldmodel.h"
#include "../model/entrymodel.h"
#include "../model/entryfiltermodel.h"
#include "gui/ui_mainwindow.h"
#include <passwordfile/io/cryptoexception.h>
#include <passwordfile/io/entry.h>
#include <qtutilities/enterpassworddialog/enterpassworddialog.h>
#include <qtutilities/misc/dialogutils.h>
#include <c++utilities/io/path.h>
#include <QFileDialog>
#include <QMessageBox>
#include <QInputDialog>
#include <QDesktopServices>
#include <QAction>
#include <QActionGroup>
#include <QTableWidgetItem>
#include <QClipboard>
#include <QSettings>
#include <QCloseEvent>
#include <QTreeWidgetItem>
#include <QTimerEvent>
#include <QPushButton>
#include <QHeaderView>
#include <QUndoStack>
#include <QUndoView>
#include <QMimeData>
#include <stdexcept>
#include <cassert>
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<int>() << 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<bool (MainWindow::*)(void)>(&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<void (MainWindow::*)(void)>(&MainWindow::setSomethingChanged));
connect(m_fieldModel, &QAbstractItemModel::dataChanged, this, static_cast<void (MainWindow::*)(void)>(&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<QDropEvent *>(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<QAction *> 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<QAction *>(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 <i>%1</i> 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<QAction *> 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<AccountEntry *>(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<int> 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<int>(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. <strong>No encryption</strong> 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<QAction *> 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<NodeEntry *>(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."));
}
}
}