1304 lines
44 KiB
C++
1304 lines
44 KiB
C++
#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 <passwordfile/io/cryptoexception.h>
|
|
#include <passwordfile/io/entry.h>
|
|
|
|
#include <qtutilities/aboutdialog/aboutdialog.h>
|
|
#include <qtutilities/enterpassworddialog/enterpassworddialog.h>
|
|
#include <qtutilities/misc/desktoputils.h>
|
|
#include <qtutilities/misc/dialogutils.h>
|
|
#include <qtutilities/misc/recentmenumanager.h>
|
|
#include <qtutilities/settingsdialog/optioncategorymodel.h>
|
|
#include <qtutilities/settingsdialog/qtsettings.h>
|
|
#include <qtutilities/settingsdialog/settingsdialog.h>
|
|
|
|
#include <c++utilities/conversion/stringconversion.h>
|
|
#include <c++utilities/io/path.h>
|
|
|
|
#include <QActionGroup>
|
|
#include <QClipboard>
|
|
#include <QCloseEvent>
|
|
#include <QDesktopServices>
|
|
#include <QFileDialog>
|
|
#include <QInputDialog>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#include <QSettings>
|
|
#include <QTimerEvent>
|
|
#include <QUndoStack>
|
|
#include <QUndoView>
|
|
|
|
#include <cassert>
|
|
#include <functional>
|
|
#include <stdexcept>
|
|
|
|
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<bool (MainWindow::*)(const QString &)>(&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<int>() << 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<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);
|
|
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<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, 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<const 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;
|
|
}
|
|
[[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"), saveGeometry());
|
|
m_settings.setValue(QStringLiteral("state"), saveState());
|
|
m_settings.setValue(QStringLiteral("recententries"), m_recentMgr->save());
|
|
m_settings.setValue(QStringLiteral("accountfilter"), 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"), 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 occured 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 <i>%1</i> 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 occured 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), 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->setFilterRegExp(filterText);
|
|
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<AccountEntry *>(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<int> 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<int>(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. <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 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<const NodeEntry *>(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<size_t>(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
|