#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 <passwordfile/util/openssl.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>
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
#include <qtutilities/settingsdialog/optioncategory.h>
#endif

#include <c++utilities/chrono/datetime.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 Computes the TOTP for the selected fields and sets it as clipboard text.
 */
void MainWindow::copyFieldsTOTP()
{
    const auto text = selectedFieldsString();
    if (text.startsWith(QLatin1String("otpauth:"))) {
        copyTOTP(text.toStdString());
    } else {
        QMessageBox::warning(this, QApplication::applicationName(), tr("The selection does not contain an \"otpauth:\" URL."));
    }
}

/*!
 * \brief Computes the TOTP for the specified \a url and sets it as clipboard text.
 */
void MainWindow::copyTOTP(std::string url)
{
    try {
        const auto totp = Util::OpenSsl::computeTOTP(url, DateTime::gmtNow());
        QGuiApplication::clipboard()->setText(QString::fromStdString(totp.digits));
        statusBar()->showMessage(
            tr("Copied TOTP, valid for %1").arg(QString::fromStdString(totp.remaining.toString(TimeSpanOutputFormat::WithMeasures))),
            static_cast<int>(totp.remaining.totalMilliseconds()));
    } catch (const std::runtime_error &e) {
        QMessageBox::warning(this, QCoreApplication::applicationName(), tr("Unable to compute TOTP: %1").arg(QString::fromUtf8(e.what())));
    }
}

/*!
 * \brief Inserts fields from the clipboard.
 */
void MainWindow::insertFieldsFromClipboard()
{
    insertFields(QApplication::clipboard()->text());
}

/*!
 * \brief Clears the clipboard.
 */
void MainWindow::clearClipboard()
{
#ifdef Q_OS_LINUX
    if (QApplication::platformName() == QStringLiteral("wayland")) {
        QApplication::clipboard()->setText(QString());
        return;
    }
#endif
    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 Updates the style sheet.
 */
void MainWindow::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
    const auto p = palette();
    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(dialogStyleForPalette(p), windowFrameColorForPalette(p).name()));
#endif
}

/*!
 * \brief Updates the columns sizing according to the checked action/option.
 */
void MainWindow::updateColumnSizing()
{
    auto *const header = m_ui->tableView->horizontalHeader();
    if (m_ui->actionColumnWidthCustom->isChecked()) {
        header->setStretchLastSection(true);
        header->setCascadingSectionResizes(true);
        header->setSectionResizeMode(QHeaderView::Interactive);
    } else {
        header->setSectionResizeMode(QHeaderView::Stretch);
    }
}

/*!
 * \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)
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
    , m_updateSettingsDlg(nullptr)
#endif
{
    // setup ui
    updateStyleSheet();
    m_ui->setupUi(this);

    // 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

    // setup column sizing
    auto *const columnSizingGroup = new QActionGroup(this);
    columnSizingGroup->addAction(m_ui->actionColumnWidthAuto);
    columnSizingGroup->addAction(m_ui->actionColumnWidthCustom);
    if (settings.value(QStringLiteral("interactivecolumns")).toBool()) {
        m_ui->actionColumnWidthCustom->setChecked(true);
    } else {
        m_ui->actionColumnWidthAuto->setChecked(true);
    }
    updateColumnSizing();

    // 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);

    // add action for updater if setup tools are enabled
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
    auto *const updaterAction = m_ui->menu->addAction(QIcon::fromTheme(QStringLiteral("install")), tr("Check for updates"));
#endif

    // 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);
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
    connect(updaterAction, &QAction::triggered, this, &MainWindow::showUpdaterDialog);
#endif
    // -> 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->actionCopyTOTP, &QAction::triggered, this, &MainWindow::copyFieldsTOTP);
    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(columnSizingGroup, &QActionGroup::triggered, this, &MainWindow::updateColumnSizing);
    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"), true).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::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::PaletteChange:
        if (m_qtSettings) {
            m_qtSettings->reevaluatePaletteAndDefaultIconTheme();
        }
        updateStyleSheet();
        break;
    default:;
    }
    return QMainWindow::event(event);
}

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 auto urls = mimeData->urls();
                    const auto &url = urls.front();
                    if (url.scheme() == QLatin1String("file")) {
                        data = url.path();
                    }
                } else if (mimeData->hasText()) {
                    data = mimeData->text();
                }
                if (!data.isEmpty()) {
                    event->accept();
                    if (event->type() == QEvent::Drop) {
                        openFile(data);
                    }
                }
                return true;
            }
            [[fallthrough]];
        default:;
        }
    }
    return QMainWindow::eventFilter(obj, event);
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    // ask if file is opened
    if (m_file.hasRootEntry() && !closeFile()) {
        event->ignore();
        return;
    }

    // close undow view
    if (m_undoView) {
        m_undoView->close();
    }

    // save settings
    m_settings.beginGroup(QStringLiteral("mainwindow"));
    m_settings.setValue(QStringLiteral("geometry"), QVariant(saveGeometry()));
    m_settings.setValue(QStringLiteral("state"), QVariant(saveState()));
    m_settings.setValue(QStringLiteral("recententries"), QVariant(m_recentMgr->save()));
    m_settings.setValue(QStringLiteral("accountfilter"), QVariant(m_ui->accountFilterLineEdit->text()));
    m_settings.setValue(QStringLiteral("alwayscreatebackup"), m_ui->actionAlwaysCreateBackup->isChecked());
    QString pwVisibility;
    if (m_ui->actionShowAlways->isChecked()) {
        pwVisibility = QStringLiteral("always");
    } else if (m_ui->actionHideAlways->isChecked()) {
        pwVisibility = QStringLiteral("hidden");
    } else {
        pwVisibility = QStringLiteral("editing");
    }
    m_settings.setValue(QStringLiteral("pwvisibility"), QVariant(pwVisibility));
    m_settings.setValue(QStringLiteral("interactivecolumns"), m_ui->actionColumnWidthCustom->isChecked());
    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());
            connect(m_settingsDlg, &SettingsDialog::applied, this, [this] { m_qtSettings->apply(); });
        }
    }
    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();
}

#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
/*!
 * \brief Shows the updater dialog.
 */
void MainWindow::showUpdaterDialog()
{
    if (!m_updateSettingsDlg) {
        m_updateSettingsDlg = new UpdateDialog(this);
        m_updateSettingsDlg->page()->setRestartHandler(m_restartHandler.requester());
    }
    if (m_updateSettingsDlg->isHidden()) {
        m_updateSettingsDlg->showNormal();
    } else {
        m_updateSettingsDlg->activateWindow();
    }
}
#endif

/*!
 * \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 (askForCreatingFile()) {
        saveFile();
    }
}

/*!
 * \brief Shows the undo view.
 */
void MainWindow::showUndoView()
{
    if (m_ui->actionShowUndoStack->isChecked()) {
        if (!m_undoView) {
            m_undoView = new QUndoView(m_undoStack);
            m_undoView->setWindowTitle(tr("Undo stack"));
            m_undoView->setWindowFlags(Qt::Tool);
            m_undoView->setAttribute(Qt::WA_QuitOnClose);
            m_undoView->setWindowIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
            m_undoView->installEventFilter(this);
        }
        m_undoView->show();
    } else if (m_undoView) {
        m_undoView->hide();
    }
}

/*!
 * \brief Opens a file with the specified \a path and updates all widgets to show its contents.
 * \returns Returns true on success; otherwise false
 */
bool MainWindow::openFile(const QString &path, PasswordFileOpenFlags openFlags)
{
    // close previous file
    if (m_file.hasRootEntry() && !closeFile()) {
        return false;
    }

    // set path and open file
    m_file.setPath(path.toStdString());
    try {
        m_file.open(m_openFlags = openFlags);
    } catch (const std::ios_base::failure &failure) {
        // try read-only
        if (!(m_openFlags & PasswordFileOpenFlags::ReadOnly)) {
            return openFile(path, m_openFlags | PasswordFileOpenFlags::ReadOnly);
        }

        // show error message
        const QString errmsg
            = tr("An IO error occurred when opening the specified file \"%1\": %2").arg(path, QString::fromLocal8Bit(failure.what()));
        m_ui->statusBar->showMessage(errmsg, 5000);
        QMessageBox::critical(this, QApplication::applicationName(), errmsg);
        return false;
    }

    // warn before loading a very big file
    if (m_file.size() > 10485760
        && QMessageBox::warning(this, QApplication::applicationName(),
               tr("The file you want to load seems to be very big. Do you really want to open it?"), QMessageBox::Yes, QMessageBox::No)
            == QMessageBox::No) {
        m_file.clear();
        return false;
    }

    // ask for a password if required
    if (m_file.isEncryptionUsed()) {
        EnterPasswordDialog pwDlg(this);
        pwDlg.setWindowTitle(tr("Opening file") + QStringLiteral(" - " APP_NAME));
        pwDlg.setInstruction(tr("Enter the password to open the file \"%1\"").arg(path));
        pwDlg.setPasswordRequired(true);
        switch (pwDlg.exec()) {
        case QDialog::Accepted:
            if (pwDlg.password().isEmpty()) {
                m_ui->statusBar->showMessage(tr("A password is needed to open the file."), 5000);
                QMessageBox::warning(this, QApplication::applicationName(), tr("A password is needed to open the file."));
                m_file.clear();
                return false;
            } else {
                break;
            }
        case QDialog::Rejected:
            m_file.clear();
            return false;
        default:;
        }
        m_file.setPassword(pwDlg.password().toStdString());
    }

    // load the contents of the file
    QString msg;
    try {
        m_file.load();
    } catch (const CryptoException &e) {
        msg = tr("The file couldn't be decrypted.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(e.what()));
    } catch (const std::ios_base::failure &failure) {
        try {
            msg = QString::fromLocal8Bit(failure.what());
        } catch (const runtime_error &e) {
            msg = tr("Unable to parse the file. %1").arg(QString::fromLocal8Bit(e.what()));
        }
    }

    // show warning if a file is encrypted but no auth tag is present
    if ((m_file.saveOptions() & PasswordFileSaveFlags::Encryption) && !(m_file.saveOptions() & PasswordFileSaveFlags::AuthenticationTag)) {
        QMessageBox::warning(this, windowTitle(),
            tr("The encrypted file could be opened but its authenticity could not be verified because it was written with an old version of the "
               "password manager that did not include an authentication tag."));
    }

    // show contents
    if (msg.isEmpty()) {
        return showFile();
    }

    // clear file and show a message in the error case
    m_file.clear();
    m_ui->statusBar->showMessage(msg, 5000);
    if (QMessageBox::critical(this, QApplication::applicationName(), msg, QMessageBox::Cancel | QMessageBox::Retry, 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;
    if (!m_file.password().empty()) {
        options |= PasswordFileSaveFlags::Encryption | PasswordFileSaveFlags::PasswordHashing | PasswordFileSaveFlags::AuthenticationTag;
    }
    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, static_cast<int>(rowValues.size()), QModelIndex());

    for (const auto &rowValue : rowValues) {
        int col = initCol;
        for (const auto &cellValue : rowValue.split('\t')) {
            if (col < cols) {
                m_fieldModel->setData(m_fieldModel->index(row, col), cellValue, Qt::EditRole);
                ++col;
            } else {
                break;
            }
        }
        ++row;
    }
}

/*!
 * \brief Asks the user to create a new file.
 */
bool MainWindow::askForCreatingFile()
{
    if (showNoFileOpened()) {
        return false;
    }

    const QString fileName = QFileDialog::getSaveFileName(
        this, tr("Select where you want to save the password list"), QString(), tr("Password Manager files (*.pwmgr);;All files (*)"));
    if (fileName.isEmpty()) {
        m_ui->statusBar->showMessage(tr("The file was not be saved."), 7000);
        return false;
    } else {
        m_file.setPath(fileName.toStdString());
        try {
            m_file.create();
            updateWindowTitle();
        } catch (const std::ios_base::failure &failure) {
            QMessageBox::critical(this, QApplication::applicationName(), QString::fromLocal8Bit(failure.what()));
            return false;
        }
    }
    return true;
}

/*!
 * \brief Shows an warning if no file is opened.
 * \returns Returns whether the warning has been shown.
 */
bool MainWindow::showNoFileOpened()
{
    if (!m_file.hasRootEntry()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("There is no password list opened."));
        return true;
    }
    return false;
}

/*!
 * \brief Shows an warning if no account is selected.
 * \returns Returns whether the warning has been shown.
 */
bool MainWindow::showNoAccount()
{
    if (!m_fieldModel->fields()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("There's no account selected."));
        return true;
    }
    return false;
}

/*!
 * \brief Closes the currently opened file. Asks the user to save changes if the file has been modified.
 * \returns Returns whether the file has been closed.
 */
bool MainWindow::closeFile()
{
    if (showNoFileOpened()) {
        return false;
    }
    if (m_somethingChanged) {
        QMessageBox msg(this);
        msg.setText(tr("The password file has been modified."));
        msg.setInformativeText(tr("Do you want to save the changes before closing?"));
        msg.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
        msg.setDefaultButton(QMessageBox::Save);
        msg.setIcon(QMessageBox::Warning);
        switch (msg.exec()) {
        case QMessageBox::Save:
            if (saveFile()) {
                break;
            } else {
                return false;
            }
        case QMessageBox::Cancel:
            return false;
        default:;
        }
    }
    m_fieldModel->reset();
    m_entryModel->reset();
    m_file.clear();
    m_ui->statusBar->showMessage(tr("The password list has been closed."));
    updateWindowTitle();
    updateUiStatus();
    setSomethingChanged(false);
    return true;
}

/*!
 * \brief Saves the currently opened file.
 * \returns Returns whether the file could be saved.
 */
bool MainWindow::saveFile()
{
    if (showNoFileOpened()) {
        return false;
    }

    // create backup
    if (!m_file.path().empty() && QFile::exists(QString::fromStdString(m_file.path()))) {
        if (m_ui->actionAlwaysCreateBackup->isChecked()) {
            try {
                m_file.doBackup();
            } catch (const std::ios_base::failure &failure) {
                const QString message(tr("An IO error occurred when making the backup file: %1").arg(QString::fromLocal8Bit(failure.what())));
                QMessageBox::critical(this, QApplication::applicationName(), message);
                m_ui->statusBar->showMessage(message, 7000);
                return false;
            }
        }
    } else if (!askForCreatingFile()) {
        return false;
    }

    // ask for a password if none is set
    if (m_file.password().empty()) {
        EnterPasswordDialog pwDlg(this);
        pwDlg.setWindowTitle(tr("Saving file") + QStringLiteral(" - " APP_NAME));
        pwDlg.setInstruction(tr("Enter a password to save the file"));
        pwDlg.setVerificationRequired(true);

        switch (pwDlg.exec()) {
        case QDialog::Accepted:
            m_file.setPassword(pwDlg.password().toStdString());
            break;
        default:
            m_ui->statusBar->showMessage(tr("The file hasn't been saved."), 7000);
            return false;
        }
    }

    // save the file
    QString msg;
    try {
        m_file.save(saveOptions());
    } catch (const CryptoException &ex) {
        msg = tr("The password list couldn't be saved due to encryption failure.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(ex.what()));
    } catch (const std::ios_base::failure &failure) {
        msg = QString::fromLocal8Bit(failure.what());
    }
    // show status
    if (!msg.isEmpty()) {
        m_ui->statusBar->showMessage(msg, 5000);
        QMessageBox::critical(this, QApplication::applicationName(), msg);
        return false;
    }
    if ((m_openFlags & PasswordFileOpenFlags::ReadOnly) || m_somethingChanged) {
        m_openFlags = PasswordFileOpenFlags::Default;
        m_somethingChanged = false;
        updateWindowTitle();
    }
    m_recentMgr->addEntry(QString::fromStdString(m_file.path()));
    m_ui->statusBar->showMessage(tr("The password list has been saved."), 5000);
    return true;
}

/*!
 * \brief Exports the files contents to a plain text file.
 */
void MainWindow::exportFile()
{
    if (showNoFileOpened()) {
        return;
    }
    const QString targetPath
        = QFileDialog::getSaveFileName(this, QApplication::applicationName(), QString(), tr("Plain text document (*.txt);;All files (*.*)"));
    if (targetPath.isEmpty()) {
        return;
    }
    QString errmsg;
    try {
        m_file.exportToTextfile(targetPath.toStdString());
    } catch (const std::ios_base::failure &failure) {
        errmsg = tr("An IO error occurred when exporting the password list: %1").arg(QString::fromLocal8Bit(failure.what()));
    }
    if (errmsg.isEmpty()) {
        m_ui->statusBar->showMessage(tr("The password list has been exported."), 5000);
    } else {
        m_ui->statusBar->showMessage(errmsg, 5000);
        QMessageBox::critical(this, QApplication::applicationName(), errmsg);
    }
}

/*!
 * \brief Shows the containing directory for the currently opened file.
 */
void MainWindow::showContainingDirectory()
{
    if (showNoFileOpened()) {
        return;
    }
    if (m_file.path().empty()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("The currently opened file hasn't been saved yet."));
        return;
    }
    const QFileInfo file(QString::fromStdString(m_file.path()));
    if (file.dir().exists()) {
        openLocalFileOrDir(file.dir().absolutePath());
    }
}

/*!
 * \brief Adds a new account entry to the selected category.
 */
void MainWindow::addAccount()
{
    addEntry(EntryType::Account);
}

/*!
 * \brief Adds a new category/node entry to the selected category.
 */
void MainWindow::addCategory()
{
    addEntry(EntryType::Node);
}

/*!
 * \brief Adds a new entry to the selected category.
 * \param type Specifies the type of the entry to be created.
 * \param title Specifies the title of the user prompt which will be shown to ask for the entry label.
 */
void MainWindow::addEntry(EntryType type)
{
    if (showNoFileOpened()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0);
    const QModelIndex selected = selectedIndexes.size() == 1 ? m_entryFilterModel->mapToSource(selectedIndexes.at(0)) : QModelIndex();
    if (!selected.isValid() || !m_entryModel->isNode(selected)) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("No node element selected."));
        return;
    }
    bool result;
    const QString text = QInputDialog::getText(this, type == EntryType::Account ? tr("Add account") : tr("Add category"), tr("Enter the entry name"),
        QLineEdit::Normal, tr("new entry"), &result);
    if (!result) {
        return;
    }
    if (text.isEmpty()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter text."));
        return;
    }
    int row = m_entryModel->rowCount(selected);
    m_entryModel->setInsertType(type);
    if (!m_entryModel->insertRow(row, selected)) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to create new entry."));
        return;
    }
    m_entryModel->setData(m_entryModel->index(row, 0, selected), QVariant(text), Qt::DisplayRole);
    setSomethingChanged(true);
}

/*!
 * \brief Removes the selected entry.
 */
void MainWindow::removeEntry()
{
    if (showNoFileOpened()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0);
    if (selectedIndexes.size() != 1) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("No entry selected."));
        return;
    }
    const QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0));
    if (!m_entryModel->removeRow(selected.row(), selected.parent())) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to remove the entry."));
    }
}

/*!
 * \brief Applies the entered filter.
 * \remarks Called when the textChanged signal of m_ui->accountFilterLineEdit is emittet.
 */
void MainWindow::applyFilter(const QString &filterText)
{
    m_entryFilterModel->
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
        setFilterRegularExpression(QRegularExpression(filterText, QRegularExpression::CaseInsensitiveOption))
#else
        setFilterRegExp(filterText)
#endif
        ;
    if (filterText.isEmpty()) {
        applyDefaultExpanding(QModelIndex());
    } else {
        m_ui->treeView->expandAll();
    }
}

/*!
 * \brief Called when the user \a selected an entry.
 */
void MainWindow::accountSelected(const QModelIndex &selected, const QModelIndex &)
{
    if (Entry *entry = m_entryModel->entry(m_entryFilterModel->mapToSource(selected))) {
        if (entry->type() == EntryType::Account) {
            m_fieldModel->setAccountEntry(static_cast<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 serviceUrl;
    auto totpUrl = std::string_view();
    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 (serviceUrl.isEmpty() && field->type() != FieldType::Password) {
                for (const string &protocol : protocols) {
                    if (startsWith(field->value(), protocol)) {
                        serviceUrl = QString::fromUtf8(field->value().data());
                        break;
                    }
                }
            }
            if (totpUrl.empty() && startsWith(field->value(), "otpauth:")) {
                totpUrl = field->value();
            }
            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,
        std::bind(&MainWindow::copyFieldsForXMilliSeconds, this, 5000));
    if (multipleRows == 1 && !totpUrl.empty()) {
        auto *copyTotpAction = new QAction(QIcon::fromTheme(QStringLiteral("clock-symbolic")), tr("Copy TOTP"), &contextMenu);
        connect(copyTotpAction, &QAction::triggered, this, std::bind(&MainWindow::copyTOTP, this, std::string(totpUrl)));
        contextMenu.addAction(copyTotpAction);
    }
    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 && !serviceUrl.isEmpty()) {
        auto *openUrlAction = new QAction(QIcon::fromTheme(QStringLiteral("applications-internet")), tr("Open URL"), &contextMenu);
        connect(openUrlAction, &QAction::triggered, bind(&QDesktopServices::openUrl, serviceUrl));
        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.setWindowIcon(windowIcon());
    msgBox.setWindowIconText(windowIconText());
    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 auto 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);
        statusBar()->showMessage(tr("Copied values, clearing clipboard in %1 s").arg(x / 1000), x);
    } else {
        statusBar()->showMessage(tr("Copied values"), 10000);
    }
}
} // namespace QtGui
